fossasia/loklak_webclient

View on GitHub
app/js/components/hello.all.js

Summary

Maintainability
F
1 mo
Test Coverage
/*! hellojs v1.12.0 | (c) 2012-2016 Andrew Dodson | MIT https://adodson.com/hello.js/LICENSE */
// ES5 Object.create
if (!Object.create) {

    // Shim, Object create
    // A shim for Object.create(), it adds a prototype to a new object
    Object.create = (function() {

        function F() {}

        return function(o) {

            if (arguments.length != 1) {
                throw new Error('Object.create implementation only accepts one parameter.');
            }

            F.prototype = o;
            return new F();
        };

    })();

}

// ES5 Object.keys
if (!Object.keys) {
    Object.keys = function(o, k, r) {
        r = [];
        for (k in o) {
            if (r.hasOwnProperty.call(o, k))
                r.push(k);
        }

        return r;
    };
}

// ES5 [].indexOf
if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(s) {

        for (var j = 0; j < this.length; j++) {
            if (this[j] === s) {
                return j;
            }
        }

        return -1;
    };
}

// ES5 [].forEach
if (!Array.prototype.forEach) {
    Array.prototype.forEach = function(fun/*, thisArg*/) {

        if (this === void 0 || this === null) {
            throw new TypeError();
        }

        var t = Object(this);
        var len = t.length >>> 0;
        if (typeof fun !== 'function') {
            throw new TypeError();
        }

        var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
        for (var i = 0; i < len; i++) {
            if (i in t) {
                fun.call(thisArg, t[i], i, t);
            }
        }

        return this;
    };
}

// ES5 [].filter
if (!Array.prototype.filter) {
    Array.prototype.filter = function(fun, thisArg) {

        var a = [];
        this.forEach(function(val, i, t) {
            if (fun.call(thisArg || void 0, val, i, t)) {
                a.push(val);
            }
        });

        return a;
    };
}

// Production steps of ECMA-262, Edition 5, 15.4.4.19
// Reference: http://es5.github.io/#x15.4.4.19
if (!Array.prototype.map) {

    Array.prototype.map = function(fun, thisArg) {

        var a = [];
        this.forEach(function(val, i, t) {
            a.push(fun.call(thisArg || void 0, val, i, t));
        });

        return a;
    };
}

// ES5 isArray
if (!Array.isArray) {

    // Function Array.isArray
    Array.isArray = function(o) {
        return Object.prototype.toString.call(o) === '[object Array]';
    };

}

// Test for location.assign
if (typeof window === 'object' && typeof window.location === 'object' && !window.location.assign) {

    window.location.assign = function(url) {
        window.location = url;
    };

}

// Test for Function.bind
if (!Function.prototype.bind) {

    // MDN
    // Polyfill IE8, does not support native Function.bind
    Function.prototype.bind = function(b) {

        if (typeof this !== 'function') {
            throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
        }

        function C() {}

        var a = [].slice;
        var f = a.call(arguments, 1);
        var _this = this;
        var D = function() {
            return _this.apply(this instanceof C ? this : b || window, f.concat(a.call(arguments)));
        };

        C.prototype = this.prototype;
        D.prototype = new C();

        return D;
    };

}

/**
 * @hello.js
 *
 * HelloJS is a client side Javascript SDK for making OAuth2 logins and subsequent REST calls.
 *
 * @author Andrew Dodson
 * @website https://adodson.com/hello.js/
 *
 * @copyright Andrew Dodson, 2012 - 2015
 * @license MIT: You are free to use and modify this code for any use, on the condition that this copyright notice remains.
 */

var hello = function(name) {
    return hello.use(name);
};

hello.utils = {

    // Extend the first object with the properties and methods of the second
    extend: function(r /*, a[, b[, ...]] */) {

        // Get the arguments as an array but ommit the initial item
        Array.prototype.slice.call(arguments, 1).forEach(function(a) {
            if (Array.isArray(r) && Array.isArray(a)) {
                Array.prototype.push.apply(r, a);
            }
            else if (r instanceof Object && a instanceof Object && r !== a) {
                for (var x in a) {
                    r[x] = hello.utils.extend(r[x], a[x]);
                }
            }
            else {

                if (Array.isArray(a)) {
                    // Clone it
                    a = a.slice(0);
                }

                r = a;
            }
        });

        return r;
    }
};

// Core library
hello.utils.extend(hello, {

    settings: {

        // OAuth2 authentication defaults
        redirect_uri: window.location.href.split('#')[0],
        response_type: 'token',
        display: 'popup',
        state: '',

        // OAuth1 shim
        // The path to the OAuth1 server for signing user requests
        // Want to recreate your own? Checkout https://github.com/MrSwitch/node-oauth-shim
        oauth_proxy: 'https://auth-server.herokuapp.com/proxy',

        // API timeout in milliseconds
        timeout: 20000,

        // Popup Options
        popup: {
            resizable: 1,
            scrollbars: 1,
            width: 500,
            height: 550
        },

        // Default scope
        // Many services require atleast a profile scope,
        // HelloJS automatially includes the value of provider.scope_map.basic
        // If that's not required it can be removed via hello.settings.scope.length = 0;
        scope: ['basic'],

        // Scope Maps
        // This is the default module scope, these are the defaults which each service is mapped too.
        // By including them here it prevents the scope from being applied accidentally
        scope_map: {
            basic: ''
        },

        // Default service / network
        default_service: null,

        // Force authentication
        // When hello.login is fired.
        // (null): ignore current session expiry and continue with login
        // (true): ignore current session expiry and continue with login, ask for user to reauthenticate
        // (false): if the current session looks good for the request scopes return the current session.
        force: null,

        // Page URL
        // When 'display=page' this property defines where the users page should end up after redirect_uri
        // Ths could be problematic if the redirect_uri is indeed the final place,
        // Typically this circumvents the problem of the redirect_url being a dumb relay page.
        page_uri: window.location.href
    },

    // Service configuration objects
    services: {},

    // Use
    // Define a new instance of the HelloJS library with a default service
    use: function(service) {

        // Create self, which inherits from its parent
        var self = Object.create(this);

        // Inherit the prototype from its parent
        self.settings = Object.create(this.settings);

        // Define the default service
        if (service) {
            self.settings.default_service = service;
        }

        // Create an instance of Events
        self.utils.Event.call(self);

        return self;
    },

    // Initialize
    // Define the client_ids for the endpoint services
    // @param object o, contains a key value pair, service => clientId
    // @param object opts, contains a key value pair of options used for defining the authentication defaults
    // @param number timeout, timeout in seconds
    init: function(services, options) {

        var utils = this.utils;

        if (!services) {
            return this.services;
        }

        // Define provider credentials
        // Reformat the ID field
        for (var x in services) {if (services.hasOwnProperty(x)) {
            if (typeof (services[x]) !== 'object') {
                services[x] = {id: services[x]};
            }
        }}

        // Merge services if there already exists some
        utils.extend(this.services, services);

        // Update the default settings with this one.
        if (options) {
            utils.extend(this.settings, options);

            // Do this immediatly incase the browser changes the current path.
            if ('redirect_uri' in options) {
                this.settings.redirect_uri = utils.url(options.redirect_uri).href;
            }
        }

        return this;
    },

    // Login
    // Using the endpoint
    // @param network stringify       name to connect to
    // @param options object    (optional)  {display mode, is either none|popup(default)|page, scope: email,birthday,publish, .. }
    // @param callback  function  (optional)  fired on signin
    login: function() {

        // Create an object which inherits its parent as the prototype and constructs a new event chain.
        var _this = this;
        var utils = _this.utils;
        var error = utils.error;
        var promise = utils.Promise();

        // Get parameters
        var p = utils.args({network: 's', options: 'o', callback: 'f'}, arguments);

        // Local vars
        var url;

        // Get all the custom options and store to be appended to the querystring
        var qs = utils.diffKey(p.options, _this.settings);

        // Merge/override options with app defaults
        var opts = p.options = utils.merge(_this.settings, p.options || {});

        // Merge/override options with app defaults
        opts.popup = utils.merge(_this.settings.popup, p.options.popup || {});

        // Network
        p.network = p.network || _this.settings.default_service;

        // Bind callback to both reject and fulfill states
        promise.proxy.then(p.callback, p.callback);

        // Trigger an event on the global listener
        function emit(s, value) {
            hello.emit(s, value);
        }

        promise.proxy.then(emit.bind(this, 'auth.login auth'), emit.bind(this, 'auth.failed auth'));

        // Is our service valid?
        if (typeof (p.network) !== 'string' || !(p.network in _this.services)) {
            // Trigger the default login.
            // Ahh we dont have one.
            return promise.reject(error('invalid_network', 'The provided network was not recognized'));
        }

        var provider = _this.services[p.network];

        // Create a global listener to capture events triggered out of scope
        var callbackId = utils.globalEvent(function(str) {

            // The responseHandler returns a string, lets save this locally
            var obj;

            if (str) {
                obj = JSON.parse(str);
            }
            else {
                obj = error('cancelled', 'The authentication was not completed');
            }

            // Handle these response using the local
            // Trigger on the parent
            if (!obj.error) {

                // Save on the parent window the new credentials
                // This fixes an IE10 bug i think... atleast it does for me.
                utils.store(obj.network, obj);

                // Fulfill a successful login
                promise.fulfill({
                    network: obj.network,
                    authResponse: obj
                });
            }
            else {
                // Reject a successful login
                promise.reject(obj);
            }
        });

        var redirectUri = utils.url(opts.redirect_uri).href;

        // May be a space-delimited list of multiple, complementary types
        var responseType = provider.oauth.response_type || opts.response_type;

        // Fallback to token if the module hasn't defined a grant url
        if (/\bcode\b/.test(responseType) && !provider.oauth.grant) {
            responseType = responseType.replace(/\bcode\b/, 'token');
        }

        // Query string parameters, we may pass our own arguments to form the querystring
        p.qs = utils.merge(qs, {
            client_id: encodeURIComponent(provider.id),
            response_type: encodeURIComponent(responseType),
            redirect_uri: encodeURIComponent(redirectUri),
            display: opts.display,
            state: {
                client_id: provider.id,
                network: p.network,
                display: opts.display,
                callback: callbackId,
                state: opts.state,
                redirect_uri: redirectUri
            }
        });

        // Get current session for merging scopes, and for quick auth response
        var session = utils.store(p.network);

        // Scopes (authentication permisions)
        // Ensure this is a string - IE has a problem moving Arrays between windows
        // Append the setup scope
        var SCOPE_SPLIT = /[,\s]+/;

        // Include default scope settings (cloned).
        var scope = _this.settings.scope ? [_this.settings.scope.toString()] : [];

        // Extend the providers scope list with the default
        var scopeMap = utils.merge(_this.settings.scope_map, provider.scope || {});

        // Add user defined scopes...
        if (opts.scope) {
            scope.push(opts.scope.toString());
        }

        // Append scopes from a previous session.
        // This helps keep app credentials constant,
        // Avoiding having to keep tabs on what scopes are authorized
        if (session && 'scope' in session && session.scope instanceof String) {
            scope.push(session.scope);
        }

        // Join and Split again
        scope = scope.join(',').split(SCOPE_SPLIT);

        // Format remove duplicates and empty values
        scope = utils.unique(scope).filter(filterEmpty);

        // Save the the scopes to the state with the names that they were requested with.
        p.qs.state.scope = scope.join(',');

        // Map scopes to the providers naming convention
        scope = scope.map(function(item) {
            // Does this have a mapping?
            return (item in scopeMap) ? scopeMap[item] : item;
        });

        // Stringify and Arrayify so that double mapped scopes are given the chance to be formatted
        scope = scope.join(',').split(SCOPE_SPLIT);

        // Again...
        // Format remove duplicates and empty values
        scope = utils.unique(scope).filter(filterEmpty);

        // Join with the expected scope delimiter into a string
        p.qs.scope = scope.join(provider.scope_delim || ',');

        // Is the user already signed in with the appropriate scopes, valid access_token?
        if (opts.force === false) {

            if (session && 'access_token' in session && session.access_token && 'expires' in session && session.expires > ((new Date()).getTime() / 1e3)) {
                // What is different about the scopes in the session vs the scopes in the new login?
                var diff = utils.diff((session.scope || '').split(SCOPE_SPLIT), (p.qs.state.scope || '').split(SCOPE_SPLIT));
                if (diff.length === 0) {

                    // OK trigger the callback
                    promise.fulfill({
                        unchanged: true,
                        network: p.network,
                        authResponse: session
                    });

                    // Nothing has changed
                    return promise;
                }
            }
        }

        // Page URL
        if (opts.display === 'page' && opts.page_uri) {
            // Add a page location, place to endup after session has authenticated
            p.qs.state.page_uri = utils.url(opts.page_uri).href;
        }

        // Bespoke
        // Override login querystrings from auth_options
        if ('login' in provider && typeof (provider.login) === 'function') {
            // Format the paramaters according to the providers formatting function
            provider.login(p);
        }

        // Add OAuth to state
        // Where the service is going to take advantage of the oauth_proxy
        if (!/\btoken\b/.test(responseType) ||
        parseInt(provider.oauth.version, 10) < 2 ||
        (opts.display === 'none' && provider.oauth.grant && session && session.refresh_token)) {

            // Add the oauth endpoints
            p.qs.state.oauth = provider.oauth;

            // Add the proxy url
            p.qs.state.oauth_proxy = opts.oauth_proxy;

        }

        // Convert state to a string
        p.qs.state = encodeURIComponent(JSON.stringify(p.qs.state));

        // URL
        if (parseInt(provider.oauth.version, 10) === 1) {

            // Turn the request to the OAuth Proxy for 3-legged auth
            url = utils.qs(opts.oauth_proxy, p.qs, encodeFunction);
        }

        // Refresh token
        else if (opts.display === 'none' && provider.oauth.grant && session && session.refresh_token) {

            // Add the refresh_token to the request
            p.qs.refresh_token = session.refresh_token;

            // Define the request path
            url = utils.qs(opts.oauth_proxy, p.qs, encodeFunction);
        }
        else {
            url = utils.qs(provider.oauth.auth, p.qs, encodeFunction);
        }

        // Broadcast this event as an auth:init
        emit('auth.init', p);

        // Execute
        // Trigger how we want self displayed
        if (opts.display === 'none') {
            // Sign-in in the background, iframe
            utils.iframe(url, redirectUri);
        }

        // Triggering popup?
        else if (opts.display === 'popup') {

            var popup = utils.popup(url, redirectUri, opts.popup);

            var timer = setInterval(function() {
                if (!popup || popup.closed) {
                    clearInterval(timer);
                    if (!promise.state) {

                        var response = error('cancelled', 'Login has been cancelled');

                        if (!popup) {
                            response = error('blocked', 'Popup was blocked');
                        }

                        response.network = p.network;

                        promise.reject(response);
                    }
                }
            }, 100);
        }

        else {
            window.location = url;
        }

        return promise.proxy;

        function encodeFunction(s) {return s;}

        function filterEmpty(s) {return !!s;}
    },

    // Remove any data associated with a given service
    // @param string name of the service
    // @param function callback
    logout: function() {

        var _this = this;
        var utils = _this.utils;
        var error = utils.error;

        // Create a new promise
        var promise = utils.Promise();

        var p = utils.args({name:'s', options: 'o', callback: 'f'}, arguments);

        p.options = p.options || {};

        // Add callback to events
        promise.proxy.then(p.callback, p.callback);

        // Trigger an event on the global listener
        function emit(s, value) {
            hello.emit(s, value);
        }

        promise.proxy.then(emit.bind(this, 'auth.logout auth'), emit.bind(this, 'error'));

        // Network
        p.name = p.name || this.settings.default_service;
        p.authResponse = utils.store(p.name);

        if (p.name && !(p.name in _this.services)) {

            promise.reject(error('invalid_network', 'The network was unrecognized'));

        }
        else if (p.name && p.authResponse) {

            // Define the callback
            var callback = function(opts) {

                // Remove from the store
                utils.store(p.name, null);

                // Emit events by default
                promise.fulfill(hello.utils.merge({network:p.name}, opts || {}));
            };

            // Run an async operation to remove the users session
            var _opts = {};
            if (p.options.force) {
                var logout = _this.services[p.name].logout;
                if (logout) {
                    // Convert logout to URL string,
                    // If no string is returned, then this function will handle the logout async style
                    if (typeof (logout) === 'function') {
                        logout = logout(callback, p);
                    }

                    // If logout is a string then assume URL and open in iframe.
                    if (typeof (logout) === 'string') {
                        utils.iframe(logout);
                        _opts.force = null;
                        _opts.message = 'Logout success on providers site was indeterminate';
                    }
                    else if (logout === undefined) {
                        // The callback function will handle the response.
                        return promise.proxy;
                    }
                }
            }

            // Remove local credentials
            callback(_opts);
        }
        else {
            promise.reject(error('invalid_session', 'There was no session to remove'));
        }

        return promise.proxy;
    },

    // Returns all the sessions that are subscribed too
    // @param string optional, name of the service to get information about.
    getAuthResponse: function(service) {

        // If the service doesn't exist
        service = service || this.settings.default_service;

        if (!service || !(service in this.services)) {
            return null;
        }

        return this.utils.store(service) || null;
    },

    // Events: placeholder for the events
    events: {}
});

// Core utilities
hello.utils.extend(hello.utils, {

    // Error
    error: function(code, message) {
        return {
            error: {
                code: code,
                message: message
            }
        };
    },

    // Append the querystring to a url
    // @param string url
    // @param object parameters
    qs: function(url, params, formatFunction) {

        if (params) {

            // Set default formatting function
            formatFunction = formatFunction || encodeURIComponent;

            // Override the items in the URL which already exist
            for (var x in params) {
                var str = '([\\?\\&])' + x + '=[^\\&]*';
                var reg = new RegExp(str);
                if (url.match(reg)) {
                    url = url.replace(reg, '$1' + x + '=' + formatFunction(params[x]));
                    delete params[x];
                }
            }
        }

        if (!this.isEmpty(params)) {
            return url + (url.indexOf('?') > -1 ? '&' : '?') + this.param(params, formatFunction);
        }

        return url;
    },

    // Param
    // Explode/encode the parameters of an URL string/object
    // @param string s, string to decode
    param: function(s, formatFunction) {
        var b;
        var a = {};
        var m;

        if (typeof (s) === 'string') {

            formatFunction = formatFunction || decodeURIComponent;

            m = s.replace(/^[\#\?]/, '').match(/([^=\/\&]+)=([^\&]+)/g);
            if (m) {
                for (var i = 0; i < m.length; i++) {
                    b = m[i].match(/([^=]+)=(.*)/);
                    a[b[1]] = formatFunction(b[2]);
                }
            }

            return a;
        }
        else {

            formatFunction = formatFunction || encodeURIComponent;

            var o = s;

            a = [];

            for (var x in o) {if (o.hasOwnProperty(x)) {
                if (o.hasOwnProperty(x)) {
                    a.push([x, o[x] === '?' ? '?' : formatFunction(o[x])].join('='));
                }
            }}

            return a.join('&');
        }
    },

    // Local storage facade
    store: (function() {

        var a = ['localStorage', 'sessionStorage'];
        var i = -1;
        var prefix = 'test';

        // Set LocalStorage
        var localStorage;

        while (a[++i]) {
            try {
                // In Chrome with cookies blocked, calling localStorage throws an error
                localStorage = window[a[i]];
                localStorage.setItem(prefix + i, i);
                localStorage.removeItem(prefix + i);
                break;
            }
            catch (e) {
                localStorage = null;
            }
        }

        if (!localStorage) {

            var cache = null;

            localStorage = {
                getItem: function(prop) {
                    prop = prop + '=';
                    var m = document.cookie.split(';');
                    for (var i = 0; i < m.length; i++) {
                        var _m = m[i].replace(/(^\s+|\s+$)/, '');
                        if (_m && _m.indexOf(prop) === 0) {
                            return _m.substr(prop.length);
                        }
                    }

                    return cache;
                },

                setItem: function(prop, value) {
                    cache = value;
                    document.cookie = prop + '=' + value;
                }
            };

            // Fill the cache up
            cache = localStorage.getItem('hello');
        }

        function get() {
            var json = {};
            try {
                json = JSON.parse(localStorage.getItem('hello')) || {};
            }
            catch (e) {}

            return json;
        }

        function set(json) {
            localStorage.setItem('hello', JSON.stringify(json));
        }

        // Check if the browser support local storage
        return function(name, value, days) {

            // Local storage
            var json = get();

            if (name && value === undefined) {
                return json[name] || null;
            }
            else if (name && value === null) {
                try {
                    delete json[name];
                }
                catch (e) {
                    json[name] = null;
                }
            }
            else if (name) {
                json[name] = value;
            }
            else {
                return json;
            }

            set(json);

            return json || null;
        };

    })(),

    // Create and Append new DOM elements
    // @param node string
    // @param attr object literal
    // @param dom/string
    append: function(node, attr, target) {

        var n = typeof (node) === 'string' ? document.createElement(node) : node;

        if (typeof (attr) === 'object') {
            if ('tagName' in attr) {
                target = attr;
            }
            else {
                for (var x in attr) {if (attr.hasOwnProperty(x)) {
                    if (typeof (attr[x]) === 'object') {
                        for (var y in attr[x]) {if (attr[x].hasOwnProperty(y)) {
                            n[x][y] = attr[x][y];
                        }}
                    }
                    else if (x === 'html') {
                        n.innerHTML = attr[x];
                    }

                    // IE doesn't like us setting methods with setAttribute
                    else if (!/^on/.test(x)) {
                        n.setAttribute(x, attr[x]);
                    }
                    else {
                        n[x] = attr[x];
                    }
                }}
            }
        }

        if (target === 'body') {
            (function self() {
                if (document.body) {
                    document.body.appendChild(n);
                }
                else {
                    setTimeout(self, 16);
                }
            })();
        }
        else if (typeof (target) === 'object') {
            target.appendChild(n);
        }
        else if (typeof (target) === 'string') {
            document.getElementsByTagName(target)[0].appendChild(n);
        }

        return n;
    },

    // An easy way to create a hidden iframe
    // @param string src
    iframe: function(src) {
        this.append('iframe', {src: src, style: {position:'absolute', left: '-1000px', bottom: 0, height: '1px', width: '1px'}}, 'body');
    },

    // Recursive merge two objects into one, second parameter overides the first
    // @param a array
    merge: function(/* Args: a, b, c, .. n */) {
        var args = Array.prototype.slice.call(arguments);
        args.unshift({});
        return this.extend.apply(null, args);
    },

    // Makes it easier to assign parameters, where some are optional
    // @param o object
    // @param a arguments
    args: function(o, args) {

        var p = {};
        var i = 0;
        var t = null;
        var x = null;

        // 'x' is the first key in the list of object parameters
        for (x in o) {if (o.hasOwnProperty(x)) {
            break;
        }}

        // Passing in hash object of arguments?
        // Where the first argument can't be an object
        if ((args.length === 1) && (typeof (args[0]) === 'object') && o[x] != 'o!') {

            // Could this object still belong to a property?
            // Check the object keys if they match any of the property keys
            for (x in args[0]) {if (o.hasOwnProperty(x)) {
                // Does this key exist in the property list?
                if (x in o) {
                    // Yes this key does exist so its most likely this function has been invoked with an object parameter
                    // Return first argument as the hash of all arguments
                    return args[0];
                }
            }}
        }

        // Else loop through and account for the missing ones.
        for (x in o) {if (o.hasOwnProperty(x)) {

            t = typeof (args[i]);

            if ((typeof (o[x]) === 'function' && o[x].test(args[i])) || (typeof (o[x]) === 'string' && (
            (o[x].indexOf('s') > -1 && t === 'string') ||
            (o[x].indexOf('o') > -1 && t === 'object') ||
            (o[x].indexOf('i') > -1 && t === 'number') ||
            (o[x].indexOf('a') > -1 && t === 'object') ||
            (o[x].indexOf('f') > -1 && t === 'function')
            ))
            ) {
                p[x] = args[i++];
            }

            else if (typeof (o[x]) === 'string' && o[x].indexOf('!') > -1) {
                return false;
            }
        }}

        return p;
    },

    // Returns a URL instance
    url: function(path) {

        // If the path is empty
        if (!path) {
            return window.location;
        }

        // Chrome and FireFox support new URL() to extract URL objects
        else if (window.URL && URL instanceof Function && URL.length !== 0) {
            return new URL(path, window.location);
        }

        // Ugly shim, it works!
        else {
            var a = document.createElement('a');
            a.href = path;
            return a.cloneNode(false);
        }
    },

    diff: function(a, b) {
        return b.filter(function(item) {
            return a.indexOf(item) === -1;
        });
    },

    // Get the different hash of properties unique to `a`, and not in `b`
    diffKey: function(a, b) {
        if (a || !b) {
            var r = {};
            for (var x in a) {
                // Does the property not exist?
                if (!(x in b)) {
                    r[x] = a[x];
                }
            }

            return r;
        }

        return a;
    },

    // Unique
    // Remove duplicate and null values from an array
    // @param a array
    unique: function(a) {
        if (!Array.isArray(a)) { return []; }

        return a.filter(function(item, index) {
            // Is this the first location of item
            return a.indexOf(item) === index;
        });
    },

    isEmpty: function(obj) {

        // Scalar
        if (!obj)
            return true;

        // Array
        if (Array.isArray(obj)) {
            return !obj.length;
        }
        else if (typeof (obj) === 'object') {
            // Object
            for (var key in obj) {
                if (obj.hasOwnProperty(key)) {
                    return false;
                }
            }
        }

        return true;
    },

    //jscs:disable

    /*!
     **  Thenable -- Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable
     **  Copyright (c) 2013-2014 Ralf S. Engelschall <http://engelschall.com>
     **  Licensed under The MIT License <http://opensource.org/licenses/MIT>
     **  Source-Code distributed on <http://github.com/rse/thenable>
     */
    Promise: (function(){
        /*  promise states [Promises/A+ 2.1]  */
        var STATE_PENDING   = 0;                                         /*  [Promises/A+ 2.1.1]  */
        var STATE_FULFILLED = 1;                                         /*  [Promises/A+ 2.1.2]  */
        var STATE_REJECTED  = 2;                                         /*  [Promises/A+ 2.1.3]  */

        /*  promise object constructor  */
        var api = function (executor) {
            /*  optionally support non-constructor/plain-function call  */
            if (!(this instanceof api))
                return new api(executor);

            /*  initialize object  */
            this.id           = "Thenable/1.0.6";
            this.state        = STATE_PENDING; /*  initial state  */
            this.fulfillValue = undefined;     /*  initial value  */     /*  [Promises/A+ 1.3, 2.1.2.2]  */
            this.rejectReason = undefined;     /*  initial reason */     /*  [Promises/A+ 1.5, 2.1.3.2]  */
            this.onFulfilled  = [];            /*  initial handlers  */
            this.onRejected   = [];            /*  initial handlers  */

            /*  provide optional information-hiding proxy  */
            this.proxy = {
                then: this.then.bind(this)
            };

            /*  support optional executor function  */
            if (typeof executor === "function")
                executor.call(this, this.fulfill.bind(this), this.reject.bind(this));
        };

        /*  promise API methods  */
        api.prototype = {
            /*  promise resolving methods  */
            fulfill: function (value) { return deliver(this, STATE_FULFILLED, "fulfillValue", value); },
            reject:  function (value) { return deliver(this, STATE_REJECTED,  "rejectReason", value); },

            /*  "The then Method" [Promises/A+ 1.1, 1.2, 2.2]  */
            then: function (onFulfilled, onRejected) {
                var curr = this;
                var next = new api();                                    /*  [Promises/A+ 2.2.7]  */
                curr.onFulfilled.push(
                    resolver(onFulfilled, next, "fulfill"));             /*  [Promises/A+ 2.2.2/2.2.6]  */
                curr.onRejected.push(
                    resolver(onRejected,  next, "reject" ));             /*  [Promises/A+ 2.2.3/2.2.6]  */
                execute(curr);
                return next.proxy;                                       /*  [Promises/A+ 2.2.7, 3.3]  */
            }
        };

        /*  deliver an action  */
        var deliver = function (curr, state, name, value) {
            if (curr.state === STATE_PENDING) {
                curr.state = state;                                      /*  [Promises/A+ 2.1.2.1, 2.1.3.1]  */
                curr[name] = value;                                      /*  [Promises/A+ 2.1.2.2, 2.1.3.2]  */
                execute(curr);
            }
            return curr;
        };

        /*  execute all handlers  */
        var execute = function (curr) {
            if (curr.state === STATE_FULFILLED)
                execute_handlers(curr, "onFulfilled", curr.fulfillValue);
            else if (curr.state === STATE_REJECTED)
                execute_handlers(curr, "onRejected",  curr.rejectReason);
        };

        /*  execute particular set of handlers  */
        var execute_handlers = function (curr, name, value) {
            /* global process: true */
            /* global setImmediate: true */
            /* global setTimeout: true */

            /*  short-circuit processing  */
            if (curr[name].length === 0)
                return;

            /*  iterate over all handlers, exactly once  */
            var handlers = curr[name];
            curr[name] = [];                                             /*  [Promises/A+ 2.2.2.3, 2.2.3.3]  */
            var func = function () {
                for (var i = 0; i < handlers.length; i++)
                    handlers[i](value);                                  /*  [Promises/A+ 2.2.5]  */
            };

            /*  execute procedure asynchronously  */                     /*  [Promises/A+ 2.2.4, 3.1]  */
            if (typeof process === "object" && typeof process.nextTick === "function")
                process.nextTick(func);
            else if (typeof setImmediate === "function")
                setImmediate(func);
            else
                setTimeout(func, 0);
        };

        /*  generate a resolver function  */
        var resolver = function (cb, next, method) {
            return function (value) {
                if (typeof cb !== "function")                            /*  [Promises/A+ 2.2.1, 2.2.7.3, 2.2.7.4]  */
                    next[method].call(next, value);                      /*  [Promises/A+ 2.2.7.3, 2.2.7.4]  */
                else {
                    var result;
                    try { result = cb(value); }                          /*  [Promises/A+ 2.2.2.1, 2.2.3.1, 2.2.5, 3.2]  */
                    catch (e) {
                        next.reject(e);                                  /*  [Promises/A+ 2.2.7.2]  */
                        return;
                    }
                    resolve(next, result);                               /*  [Promises/A+ 2.2.7.1]  */
                }
            };
        };

        /*  "Promise Resolution Procedure"  */                           /*  [Promises/A+ 2.3]  */
        var resolve = function (promise, x) {
            /*  sanity check arguments  */                               /*  [Promises/A+ 2.3.1]  */
            if (promise === x || promise.proxy === x) {
                promise.reject(new TypeError("cannot resolve promise with itself"));
                return;
            }

            /*  surgically check for a "then" method
                (mainly to just call the "getter" of "then" only once)  */
            var then;
            if ((typeof x === "object" && x !== null) || typeof x === "function") {
                try { then = x.then; }                                   /*  [Promises/A+ 2.3.3.1, 3.5]  */
                catch (e) {
                    promise.reject(e);                                   /*  [Promises/A+ 2.3.3.2]  */
                    return;
                }
            }

            /*  handle own Thenables    [Promises/A+ 2.3.2]
                and similar "thenables" [Promises/A+ 2.3.3]  */
            if (typeof then === "function") {
                var resolved = false;
                try {
                    /*  call retrieved "then" method */                  /*  [Promises/A+ 2.3.3.3]  */
                    then.call(x,
                        /*  resolvePromise  */                           /*  [Promises/A+ 2.3.3.3.1]  */
                        function (y) {
                            if (resolved) return; resolved = true;       /*  [Promises/A+ 2.3.3.3.3]  */
                            if (y === x)                                 /*  [Promises/A+ 3.6]  */
                                promise.reject(new TypeError("circular thenable chain"));
                            else
                                resolve(promise, y);
                        },

                        /*  rejectPromise  */                            /*  [Promises/A+ 2.3.3.3.2]  */
                        function (r) {
                            if (resolved) return; resolved = true;       /*  [Promises/A+ 2.3.3.3.3]  */
                            promise.reject(r);
                        }
                    );
                }
                catch (e) {
                    if (!resolved)                                       /*  [Promises/A+ 2.3.3.3.3]  */
                        promise.reject(e);                               /*  [Promises/A+ 2.3.3.3.4]  */
                }
                return;
            }

            /*  handle other values  */
            promise.fulfill(x);                                          /*  [Promises/A+ 2.3.4, 2.3.3.4]  */
        };

        /*  export API  */
        return api;
    })(),

    //jscs:enable

    // Event
    // A contructor superclass for adding event menthods, on, off, emit.
    Event: function() {

        var separator = /[\s\,]+/;

        // If this doesn't support getPrototype then we can't get prototype.events of the parent
        // So lets get the current instance events, and add those to a parent property
        this.parent = {
            events: this.events,
            findEvents: this.findEvents,
            parent: this.parent,
            utils: this.utils
        };

        this.events = {};

        // On, subscribe to events
        // @param evt   string
        // @param callback  function
        this.on = function(evt, callback) {

            if (callback && typeof (callback) === 'function') {
                var a = evt.split(separator);
                for (var i = 0; i < a.length; i++) {

                    // Has this event already been fired on this instance?
                    this.events[a[i]] = [callback].concat(this.events[a[i]] || []);
                }
            }

            return this;
        };

        // Off, unsubscribe to events
        // @param evt   string
        // @param callback  function
        this.off = function(evt, callback) {

            this.findEvents(evt, function(name, index) {
                if (!callback || this.events[name][index] === callback) {
                    this.events[name][index] = null;
                }
            });

            return this;
        };

        // Emit
        // Triggers any subscribed events
        this.emit = function(evt /*, data, ... */) {

            // Get arguments as an Array, knock off the first one
            var args = Array.prototype.slice.call(arguments, 1);
            args.push(evt);

            // Handler
            var handler = function(name, index) {

                // Replace the last property with the event name
                args[args.length - 1] = (name === '*' ? evt : name);

                // Trigger
                this.events[name][index].apply(this, args);
            };

            // Find the callbacks which match the condition and call
            var _this = this;
            while (_this && _this.findEvents) {

                // Find events which match
                _this.findEvents(evt + ',*', handler);
                _this = _this.parent;
            }

            return this;
        };

        //
        // Easy functions
        this.emitAfter = function() {
            var _this = this;
            var args = arguments;
            setTimeout(function() {
                _this.emit.apply(_this, args);
            }, 0);

            return this;
        };

        this.findEvents = function(evt, callback) {

            var a = evt.split(separator);

            for (var name in this.events) {if (this.events.hasOwnProperty(name)) {

                if (a.indexOf(name) > -1) {

                    for (var i = 0; i < this.events[name].length; i++) {

                        // Does the event handler exist?
                        if (this.events[name][i]) {
                            // Emit on the local instance of this
                            callback.call(this, name, i);
                        }
                    }
                }
            }}
        };

        return this;
    },

    // Global Events
    // Attach the callback to the window object
    // Return its unique reference
    globalEvent: function(callback, guid) {
        // If the guid has not been supplied then create a new one.
        guid = guid || '_hellojs_' + parseInt(Math.random() * 1e12, 10).toString(36);

        // Define the callback function
        window[guid] = function() {
            // Trigger the callback
            try {
                if (callback.apply(this, arguments)) {
                    delete window[guid];
                }
            }
            catch (e) {
                console.error(e);
            }
        };

        return guid;
    },

    // Trigger a clientside popup
    // This has been augmented to support PhoneGap
    popup: function(url, redirectUri, options) {

        var documentElement = document.documentElement;

        // Multi Screen Popup Positioning (http://stackoverflow.com/a/16861050)
        // Credit: http://www.xtf.dk/2011/08/center-new-popup-window-even-on.html
        // Fixes dual-screen position                         Most browsers      Firefox

        if (options.height) {
            var dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top;
            var height = screen.height || window.innerHeight || documentElement.clientHeight;
            options.top = parseInt((height - options.height) / 2, 10) + dualScreenTop;
        }

        if (options.width) {
            var dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left;
            var width = screen.width || window.innerWidth || documentElement.clientWidth;
            options.left = parseInt((width - options.width) / 2, 10) + dualScreenLeft;
        }

        // Convert options into an array
        var optionsArray = [];
        Object.keys(options).forEach(function(name) {
            var value = options[name];
            optionsArray.push(name + (value !== null ? '=' + value : ''));
        });

        // Call the open() function with the initial path
        //
        // OAuth redirect, fixes URI fragments from being lost in Safari
        // (URI Fragments within 302 Location URI are lost over HTTPS)
        // Loading the redirect.html before triggering the OAuth Flow seems to fix it.
        //
        // Firefox  decodes URL fragments when calling location.hash.
        //  - This is bad if the value contains break points which are escaped
        //  - Hence the url must be encoded twice as it contains breakpoints.
        if (navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1) {
            url = redirectUri + '#oauth_redirect=' + encodeURIComponent(encodeURIComponent(url));
        }

        var popup = window.open(
            url,
            '_blank',
            optionsArray.join(',')
        );

        if (popup && popup.focus) {
            popup.focus();
        }

        return popup;
    },

    // OAuth and API response handler
    responseHandler: function(window, parent) {

        var _this = this;
        var p;
        var location = window.location;

        // Is this an auth relay message which needs to call the proxy?
        p = _this.param(location.search);

        // OAuth2 or OAuth1 server response?
        if (p && p.state && (p.code || p.oauth_token)) {

            var state = JSON.parse(p.state);

            // Add this path as the redirect_uri
            p.redirect_uri = state.redirect_uri || location.href.replace(/[\?\#].*$/, '');

            // Redirect to the host
            var path = state.oauth_proxy + '?' + _this.param(p);

            location.assign(path);

            return;
        }

        // Save session, from redirected authentication
        // #access_token has come in?
        //
        // FACEBOOK is returning auth errors within as a query_string... thats a stickler for consistency.
        // SoundCloud is the state in the querystring and the token in the hashtag, so we'll mix the two together

        p = _this.merge(_this.param(location.search || ''), _this.param(location.hash || ''));

        // If p.state
        if (p && 'state' in p) {

            // Remove any addition information
            // E.g. p.state = 'facebook.page';
            try {
                var a = JSON.parse(p.state);
                _this.extend(p, a);
            }
            catch (e) {
                console.error('Could not decode state parameter');
            }

            // Access_token?
            if (('access_token' in p && p.access_token) && p.network) {

                if (!p.expires_in || parseInt(p.expires_in, 10) === 0) {
                    // If p.expires_in is unset, set to 0
                    p.expires_in = 0;
                }

                p.expires_in = parseInt(p.expires_in, 10);
                p.expires = ((new Date()).getTime() / 1e3) + (p.expires_in || (60 * 60 * 24 * 365));

                // Lets use the "state" to assign it to one of our networks
                authCallback(p, window, parent);
            }

            // Error=?
            // &error_description=?
            // &state=?
            else if (('error' in p && p.error) && p.network) {

                p.error = {
                    code: p.error,
                    message: p.error_message || p.error_description
                };

                // Let the state handler handle it
                authCallback(p, window, parent);
            }

            // API call, or a cancelled login
            // Result is serialized JSON string
            else if (p.callback && p.callback in parent) {

                // Trigger a function in the parent
                var res = 'result' in p && p.result ? JSON.parse(p.result) : false;

                // Trigger the callback on the parent
                parent[p.callback](res);
                closeWindow();
            }

            // If this page is still open
            if (p.page_uri) {
                location.assign(p.page_uri);
            }
        }

        // OAuth redirect, fixes URI fragments from being lost in Safari
        // (URI Fragments within 302 Location URI are lost over HTTPS)
        // Loading the redirect.html before triggering the OAuth Flow seems to fix it.
        else if ('oauth_redirect' in p) {

            location.assign(decodeURIComponent(p.oauth_redirect));
            return;
        }

        // Trigger a callback to authenticate
        function authCallback(obj, window, parent) {

            var cb = obj.callback;
            var network = obj.network;

            // Trigger the callback on the parent
            _this.store(network, obj);

            // If this is a page request it has no parent or opener window to handle callbacks
            if (('display' in obj) && obj.display === 'page') {
                return;
            }

            // Remove from session object
            if (parent && cb && cb in parent) {

                try {
                    delete obj.callback;
                }
                catch (e) {}

                // Update store
                _this.store(network, obj);

                // Call the globalEvent function on the parent
                // It's safer to pass back a string to the parent,
                // Rather than an object/array (better for IE8)
                var str = JSON.stringify(obj);

                try {
                    parent[cb](str);
                }
                catch (e) {
                    // Error thrown whilst executing parent callback
                }
            }

            closeWindow();
        }

        function closeWindow() {

            if (window.frameElement) {
                // Inside an iframe, remove from parent
                parent.document.body.removeChild(window.frameElement);
            }
            else {
                // Close this current window
                try {
                    window.close();
                }
                catch (e) {}

                // IOS bug wont let us close a popup if still loading
                if (window.addEventListener) {
                    window.addEventListener('load', function() {
                        window.close();
                    });
                }
            }

        }
    }
});

// Events
// Extend the hello object with its own event instance
hello.utils.Event.call(hello);

///////////////////////////////////
// Monitoring session state
// Check for session changes
///////////////////////////////////

(function(hello) {

    // Monitor for a change in state and fire
    var oldSessions = {};

    // Hash of expired tokens
    var expired = {};

    // Listen to other triggers to Auth events, use these to update this
    hello.on('auth.login, auth.logout', function(auth) {
        if (auth && typeof (auth) === 'object' && auth.network) {
            oldSessions[auth.network] = hello.utils.store(auth.network) || {};
        }
    });

    (function self() {

        var CURRENT_TIME = ((new Date()).getTime() / 1e3);
        var emit = function(eventName) {
            hello.emit('auth.' + eventName, {
                network: name,
                authResponse: session
            });
        };

        // Loop through the services
        for (var name in hello.services) {if (hello.services.hasOwnProperty(name)) {

            if (!hello.services[name].id) {
                // We haven't attached an ID so dont listen.
                continue;
            }

            // Get session
            var session = hello.utils.store(name) || {};
            var provider = hello.services[name];
            var oldSess = oldSessions[name] || {};

            // Listen for globalEvents that did not get triggered from the child
            if (session && 'callback' in session) {

                // To do remove from session object...
                var cb = session.callback;
                try {
                    delete session.callback;
                }
                catch (e) {}

                // Update store
                // Removing the callback
                hello.utils.store(name, session);

                // Emit global events
                try {
                    window[cb](session);
                }
                catch (e) {}
            }

            // Refresh token
            if (session && ('expires' in session) && session.expires < CURRENT_TIME) {

                // If auto refresh is possible
                // Either the browser supports
                var refresh = provider.refresh || session.refresh_token;

                // Has the refresh been run recently?
                if (refresh && (!(name in expired) || expired[name] < CURRENT_TIME)) {
                    // Try to resignin
                    hello.emit('notice', name + ' has expired trying to resignin');
                    hello.login(name, {display: 'none', force: false});

                    // Update expired, every 10 minutes
                    expired[name] = CURRENT_TIME + 600;
                }

                // Does this provider not support refresh
                else if (!refresh && !(name in expired)) {
                    // Label the event
                    emit('expired');
                    expired[name] = true;
                }

                // If session has expired then we dont want to store its value until it can be established that its been updated
                continue;
            }

            // Has session changed?
            else if (oldSess.access_token === session.access_token &&
            oldSess.expires === session.expires) {
                continue;
            }

            // Access_token has been removed
            else if (!session.access_token && oldSess.access_token) {
                emit('logout');
            }

            // Access_token has been created
            else if (session.access_token && !oldSess.access_token) {
                emit('login');
            }

            // Access_token has been updated
            else if (session.expires !== oldSess.expires) {
                emit('update');
            }

            // Updated stored session
            oldSessions[name] = session;

            // Remove the expired flags
            if (name in expired) {
                delete expired[name];
            }
        }}

        // Check error events
        setTimeout(self, 1000);
    })();

})(hello);

// EOF CORE lib
//////////////////////////////////

/////////////////////////////////////////
// API
// @param path    string
// @param query   object (optional)
// @param method  string (optional)
// @param data    object (optional)
// @param timeout integer (optional)
// @param callback  function (optional)

hello.api = function() {

    // Shorthand
    var _this = this;
    var utils = _this.utils;
    var error = utils.error;

    // Construct a new Promise object
    var promise = utils.Promise();

    // Arguments
    var p = utils.args({path: 's!', query: 'o', method: 's', data: 'o', timeout: 'i', callback: 'f'}, arguments);

    // Method
    p.method = (p.method || 'get').toLowerCase();

    // Headers
    p.headers = p.headers || {};

    // Query
    p.query = p.query || {};

    // If get, put all parameters into query
    if (p.method === 'get' || p.method === 'delete') {
        utils.extend(p.query, p.data);
        p.data = {};
    }

    var data = p.data = p.data || {};

    // Completed event callback
    promise.then(p.callback, p.callback);

    // Remove the network from path, e.g. facebook:/me/friends
    // Results in { network : facebook, path : me/friends }
    if (!p.path) {
        return promise.reject(error('invalid_path', 'Missing the path parameter from the request'));
    }

    p.path = p.path.replace(/^\/+/, '');
    var a = (p.path.split(/[\/\:]/, 2) || [])[0].toLowerCase();

    if (a in _this.services) {
        p.network = a;
        var reg = new RegExp('^' + a + ':?\/?');
        p.path = p.path.replace(reg, '');
    }

    // Network & Provider
    // Define the network that this request is made for
    p.network = _this.settings.default_service = p.network || _this.settings.default_service;
    var o = _this.services[p.network];

    // INVALID
    // Is there no service by the given network name?
    if (!o) {
        return promise.reject(error('invalid_network', 'Could not match the service requested: ' + p.network));
    }

    // PATH
    // As long as the path isn't flagged as unavaiable, e.g. path == false

    if (!(!(p.method in o) || !(p.path in o[p.method]) || o[p.method][p.path] !== false)) {
        return promise.reject(error('invalid_path', 'The provided path is not available on the selected network'));
    }

    // PROXY
    // OAuth1 calls always need a proxy

    if (!p.oauth_proxy) {
        p.oauth_proxy = _this.settings.oauth_proxy;
    }

    if (!('proxy' in p)) {
        p.proxy = p.oauth_proxy && o.oauth && parseInt(o.oauth.version, 10) === 1;
    }

    // TIMEOUT
    // Adopt timeout from global settings by default

    if (!('timeout' in p)) {
        p.timeout = _this.settings.timeout;
    }

    // Format response
    // Whether to run the raw response through post processing.
    if (!('formatResponse' in p)) {
        p.formatResponse = true;
    }

    // Get the current session
    // Append the access_token to the query
    p.authResponse = _this.getAuthResponse(p.network);
    if (p.authResponse && p.authResponse.access_token) {
        p.query.access_token = p.authResponse.access_token;
    }

    var url = p.path;
    var m;

    // Store the query as options
    // This is used to populate the request object before the data is augmented by the prewrap handlers.
    p.options = utils.clone(p.query);

    // Clone the data object
    // Prevent this script overwriting the data of the incoming object.
    // Ensure that everytime we run an iteration the callbacks haven't removed some data
    p.data = utils.clone(data);

    // URL Mapping
    // Is there a map for the given URL?
    var actions = o[{'delete': 'del'}[p.method] || p.method] || {};

    // Extrapolate the QueryString
    // Provide a clean path
    // Move the querystring into the data
    if (p.method === 'get') {

        var query = url.split(/[\?#]/)[1];
        if (query) {
            utils.extend(p.query, utils.param(query));

            // Remove the query part from the URL
            url = url.replace(/\?.*?(#|$)/, '$1');
        }
    }

    // Is the hash fragment defined
    if ((m = url.match(/#(.+)/, ''))) {
        url = url.split('#')[0];
        p.path = m[1];
    }
    else if (url in actions) {
        p.path = url;
        url = actions[url];
    }
    else if ('default' in actions) {
        url = actions['default'];
    }

    // Redirect Handler
    // This defines for the Form+Iframe+Hash hack where to return the results too.
    p.redirect_uri = _this.settings.redirect_uri;

    // Define FormatHandler
    // The request can be procesed in a multitude of ways
    // Here's the options - depending on the browser and endpoint
    p.xhr = o.xhr;
    p.jsonp = o.jsonp;
    p.form = o.form;

    // Make request
    if (typeof (url) === 'function') {
        // Does self have its own callback?
        url(p, getPath);
    }
    else {
        // Else the URL is a string
        getPath(url);
    }

    return promise.proxy;

    // If url needs a base
    // Wrap everything in
    function getPath(url) {

        // Format the string if it needs it
        url = url.replace(/\@\{([a-z\_\-]+)(\|.*?)?\}/gi, function(m, key, defaults) {
            var val = defaults ? defaults.replace(/^\|/, '') : '';
            if (key in p.query) {
                val = p.query[key];
                delete p.query[key];
            }
            else if (p.data && key in p.data) {
                val = p.data[key];
                delete p.data[key];
            }
            else if (!defaults) {
                promise.reject(error('missing_attribute', 'The attribute ' + key + ' is missing from the request'));
            }

            return val;
        });

        // Add base
        if (!url.match(/^https?:\/\//)) {
            url = o.base + url;
        }

        // Define the request URL
        p.url = url;

        // Make the HTTP request with the curated request object
        // CALLBACK HANDLER
        // @ response object
        // @ statusCode integer if available
        utils.request(p, function(r, headers) {

            // Is this a raw response?
            if (!p.formatResponse) {
                // Bad request? error statusCode or otherwise contains an error response vis JSONP?
                if (typeof headers === 'object' ? (headers.statusCode >= 400) : (typeof r === 'object' && 'error' in r)) {
                    promise.reject(r);
                }
                else {
                    promise.fulfill(r);
                }

                return;
            }

            // Should this be an object
            if (r === true) {
                r = {success:true};
            }
            else if (!r) {
                r = {};
            }

            // The delete callback needs a better response
            if (p.method === 'delete') {
                r = (!r || utils.isEmpty(r)) ? {success:true} : r;
            }

            // FORMAT RESPONSE?
            // Does self request have a corresponding formatter
            if (o.wrap && ((p.path in o.wrap) || ('default' in o.wrap))) {
                var wrap = (p.path in o.wrap ? p.path : 'default');
                var time = (new Date()).getTime();

                // FORMAT RESPONSE
                var b = o.wrap[wrap](r, headers, p);

                // Has the response been utterly overwritten?
                // Typically self augments the existing object.. but for those rare occassions
                if (b) {
                    r = b;
                }
            }

            // Is there a next_page defined in the response?
            if (r && 'paging' in r && r.paging.next) {

                // Add the relative path if it is missing from the paging/next path
                if (r.paging.next[0] === '?') {
                    r.paging.next = p.path + r.paging.next;
                }

                // The relative path has been defined, lets markup the handler in the HashFragment
                else {
                    r.paging.next += '#' + p.path;
                }
            }

            // Dispatch to listeners
            // Emit events which pertain to the formatted response
            if (!r || 'error' in r) {
                promise.reject(r);
            }
            else {
                promise.fulfill(r);
            }
        });
    }
};

// API utilities
hello.utils.extend(hello.utils, {

    // Make an HTTP request
    request: function(p, callback) {

        var _this = this;
        var error = _this.error;

        // This has to go through a POST request
        if (!_this.isEmpty(p.data) && !('FileList' in window) && _this.hasBinary(p.data)) {

            // Disable XHR and JSONP
            p.xhr = false;
            p.jsonp = false;
        }

        // Check if the browser and service support CORS
        var cors = this.request_cors(function() {
            // If it does then run this...
            return ((p.xhr === undefined) || (p.xhr && (typeof (p.xhr) !== 'function' || p.xhr(p, p.query))));
        });

        if (cors) {

            formatUrl(p, function(url) {

                var x = _this.xhr(p.method, url, p.headers, p.data, callback);
                x.onprogress = p.onprogress || null;

                // Windows Phone does not support xhr.upload, see #74
                // Feature detect
                if (x.upload && p.onuploadprogress) {
                    x.upload.onprogress = p.onuploadprogress;
                }

            });

            return;
        }

        // Clone the query object
        // Each request modifies the query object and needs to be tared after each one.
        var _query = p.query;

        p.query = _this.clone(p.query);

        // Assign a new callbackID
        p.callbackID = _this.globalEvent();

        // JSONP
        if (p.jsonp !== false) {

            // Clone the query object
            p.query.callback = p.callbackID;

            // If the JSONP is a function then run it
            if (typeof (p.jsonp) === 'function') {
                p.jsonp(p, p.query);
            }

            // Lets use JSONP if the method is 'get'
            if (p.method === 'get') {

                formatUrl(p, function(url) {
                    _this.jsonp(url, callback, p.callbackID, p.timeout);
                });

                return;
            }
            else {
                // It's not compatible reset query
                p.query = _query;
            }

        }

        // Otherwise we're on to the old school, iframe hacks and JSONP
        if (p.form !== false) {

            // Add some additional query parameters to the URL
            // We're pretty stuffed if the endpoint doesn't like these
            p.query.redirect_uri = p.redirect_uri;
            p.query.state = JSON.stringify({callback:p.callbackID});

            var opts;

            if (typeof (p.form) === 'function') {

                // Format the request
                opts = p.form(p, p.query);
            }

            if (p.method === 'post' && opts !== false) {

                formatUrl(p, function(url) {
                    _this.post(url, p.data, opts, callback, p.callbackID, p.timeout);
                });

                return;
            }
        }

        // None of the methods were successful throw an error
        callback(error('invalid_request', 'There was no mechanism for handling this request'));

        return;

        // Format URL
        // Constructs the request URL, optionally wraps the URL through a call to a proxy server
        // Returns the formatted URL
        function formatUrl(p, callback) {

            // Are we signing the request?
            var sign;

            // OAuth1
            // Remove the token from the query before signing
            if (p.authResponse && p.authResponse.oauth && parseInt(p.authResponse.oauth.version, 10) === 1) {

                // OAUTH SIGNING PROXY
                sign = p.query.access_token;

                // Remove the access_token
                delete p.query.access_token;

                // Enfore use of Proxy
                p.proxy = true;
            }

            // POST body to querystring
            if (p.data && (p.method === 'get' || p.method === 'delete')) {
                // Attach the p.data to the querystring.
                _this.extend(p.query, p.data);
                p.data = null;
            }

            // Construct the path
            var path = _this.qs(p.url, p.query);

            // Proxy the request through a server
            // Used for signing OAuth1
            // And circumventing services without Access-Control Headers
            if (p.proxy) {
                // Use the proxy as a path
                path = _this.qs(p.oauth_proxy, {
                    path: path,
                    access_token: sign || '',

                    // This will prompt the request to be signed as though it is OAuth1
                    then: p.proxy_response_type || (p.method.toLowerCase() === 'get' ? 'redirect' : 'proxy'),
                    method: p.method.toLowerCase(),
                    suppress_response_codes: true
                });
            }

            callback(path);
        }
    },

    // Test whether the browser supports the CORS response
    request_cors: function(callback) {
        return 'withCredentials' in new XMLHttpRequest() && callback();
    },

    // Return the type of DOM object
    domInstance: function(type, data) {
        var test = 'HTML' + (type || '').replace(
            /^[a-z]/,
            function(m) {
                return m.toUpperCase();
            }

        ) + 'Element';

        if (!data) {
            return false;
        }

        if (window[test]) {
            return data instanceof window[test];
        }
        else if (window.Element) {
            return data instanceof window.Element && (!type || (data.tagName && data.tagName.toLowerCase() === type));
        }
        else {
            return (!(data instanceof Object || data instanceof Array || data instanceof String || data instanceof Number) && data.tagName && data.tagName.toLowerCase() === type);
        }
    },

    // Create a clone of an object
    clone: function(obj) {
        // Does not clone DOM elements, nor Binary data, e.g. Blobs, Filelists
        if (obj === null || typeof (obj) !== 'object' || obj instanceof Date || 'nodeName' in obj || this.isBinary(obj) || (typeof FormData === 'function' && obj instanceof FormData)) {
            return obj;
        }

        if (Array.isArray(obj)) {
            // Clone each item in the array
            return obj.map(this.clone.bind(this));
        }

        // But does clone everything else.
        var clone = {};
        for (var x in obj) {
            clone[x] = this.clone(obj[x]);
        }

        return clone;
    },

    // XHR: uses CORS to make requests
    xhr: function(method, url, headers, data, callback) {

        var r = new XMLHttpRequest();
        var error = this.error;

        // Binary?
        var binary = false;
        if (method === 'blob') {
            binary = method;
            method = 'GET';
        }

        method = method.toUpperCase();

        // Xhr.responseType 'json' is not supported in any of the vendors yet.
        r.onload = function(e) {
            var json = r.response;
            try {
                json = JSON.parse(r.responseText);
            }
            catch (_e) {
                if (r.status === 401) {
                    json = error('access_denied', r.statusText);
                }
            }

            var headers = headersToJSON(r.getAllResponseHeaders());
            headers.statusCode = r.status;

            callback(json || (method === 'GET' ? error('empty_response', 'Could not get resource') : {}), headers);
        };

        r.onerror = function(e) {
            var json = r.responseText;
            try {
                json = JSON.parse(r.responseText);
            }
            catch (_e) {}

            callback(json || error('access_denied', 'Could not get resource'));
        };

        var x;

        // Should we add the query to the URL?
        if (method === 'GET' || method === 'DELETE') {
            data = null;
        }
        else if (data && typeof (data) !== 'string' && !(data instanceof FormData) && !(data instanceof File) && !(data instanceof Blob)) {
            // Loop through and add formData
            var f = new FormData();
            for (x in data) if (data.hasOwnProperty(x)) {
                if (data[x] instanceof HTMLInputElement) {
                    if ('files' in data[x] && data[x].files.length > 0) {
                        f.append(x, data[x].files[0]);
                    }
                }
                else if (data[x] instanceof Blob) {
                    f.append(x, data[x], data.name);
                }
                else {
                    f.append(x, data[x]);
                }
            }

            data = f;
        }

        // Open the path, async
        r.open(method, url, true);

        if (binary) {
            if ('responseType' in r) {
                r.responseType = binary;
            }
            else {
                r.overrideMimeType('text/plain; charset=x-user-defined');
            }
        }

        // Set any bespoke headers
        if (headers) {
            for (x in headers) {
                r.setRequestHeader(x, headers[x]);
            }
        }

        r.send(data);

        return r;

        // Headers are returned as a string
        function headersToJSON(s) {
            var r = {};
            var reg = /([a-z\-]+):\s?(.*);?/gi;
            var m;
            while ((m = reg.exec(s))) {
                r[m[1]] = m[2];
            }

            return r;
        }
    },

    // JSONP
    // Injects a script tag into the DOM to be executed and appends a callback function to the window object
    // @param string/function pathFunc either a string of the URL or a callback function pathFunc(querystringhash, continueFunc);
    // @param function callback a function to call on completion;
    jsonp: function(url, callback, callbackID, timeout) {

        var _this = this;
        var error = _this.error;

        // Change the name of the callback
        var bool = 0;
        var head = document.getElementsByTagName('head')[0];
        var operaFix;
        var result = error('server_error', 'server_error');
        var cb = function() {
            if (!(bool++)) {
                window.setTimeout(function() {
                    callback(result);
                    head.removeChild(script);
                }, 0);
            }

        };

        // Add callback to the window object
        callbackID = _this.globalEvent(function(json) {
            result = json;
            return true;

            // Mark callback as done
        }, callbackID);

        // The URL is a function for some cases and as such
        // Determine its value with a callback containing the new parameters of this function.
        url = url.replace(new RegExp('=\\?(&|$)'), '=' + callbackID + '$1');

        // Build script tag
        var script = _this.append('script', {
            id: callbackID,
            name: callbackID,
            src: url,
            async: true,
            onload: cb,
            onerror: cb,
            onreadystatechange: function() {
                if (/loaded|complete/i.test(this.readyState)) {
                    cb();
                }
            }
        });

        // Opera fix error
        // Problem: If an error occurs with script loading Opera fails to trigger the script.onerror handler we specified
        //
        // Fix:
        // By setting the request to synchronous we can trigger the error handler when all else fails.
        // This action will be ignored if we've already called the callback handler "cb" with a successful onload event
        if (window.navigator.userAgent.toLowerCase().indexOf('opera') > -1) {
            operaFix = _this.append('script', {
                text: 'document.getElementById(\'' + callbackID + '\').onerror();'
            });
            script.async = false;
        }

        // Add timeout
        if (timeout) {
            window.setTimeout(function() {
                result = error('timeout', 'timeout');
                cb();
            }, timeout);
        }

        // TODO: add fix for IE,
        // However: unable recreate the bug of firing off the onreadystatechange before the script content has been executed and the value of "result" has been defined.
        // Inject script tag into the head element
        head.appendChild(script);

        // Append Opera Fix to run after our script
        if (operaFix) {
            head.appendChild(operaFix);
        }
    },

    // Post
    // Send information to a remote location using the post mechanism
    // @param string uri path
    // @param object data, key value data to send
    // @param function callback, function to execute in response
    post: function(url, data, options, callback, callbackID, timeout) {

        var _this = this;
        var error = _this.error;
        var doc = document;

        // This hack needs a form
        var form = null;
        var reenableAfterSubmit = [];
        var newform;
        var i = 0;
        var x = null;
        var bool = 0;
        var cb = function(r) {
            if (!(bool++)) {
                callback(r);
            }
        };

        // What is the name of the callback to contain
        // We'll also use this to name the iframe
        _this.globalEvent(cb, callbackID);

        // Build the iframe window
        var win;
        try {
            // IE7 hack, only lets us define the name here, not later.
            win = doc.createElement('<iframe name="' + callbackID + '">');
        }
        catch (e) {
            win = doc.createElement('iframe');
        }

        win.name = callbackID;
        win.id = callbackID;
        win.style.display = 'none';

        // Override callback mechanism. Triggger a response onload/onerror
        if (options && options.callbackonload) {
            // Onload is being fired twice
            win.onload = function() {
                cb({
                    response: 'posted',
                    message: 'Content was posted'
                });
            };
        }

        if (timeout) {
            setTimeout(function() {
                cb(error('timeout', 'The post operation timed out'));
            }, timeout);
        }

        doc.body.appendChild(win);

        // If we are just posting a single item
        if (_this.domInstance('form', data)) {
            // Get the parent form
            form = data.form;

            // Loop through and disable all of its siblings
            for (i = 0; i < form.elements.length; i++) {
                if (form.elements[i] !== data) {
                    form.elements[i].setAttribute('disabled', true);
                }
            }

            // Move the focus to the form
            data = form;
        }

        // Posting a form
        if (_this.domInstance('form', data)) {
            // This is a form element
            form = data;

            // Does this form need to be a multipart form?
            for (i = 0; i < form.elements.length; i++) {
                if (!form.elements[i].disabled && form.elements[i].type === 'file') {
                    form.encoding = form.enctype = 'multipart/form-data';
                    form.elements[i].setAttribute('name', 'file');
                }
            }
        }
        else {
            // Its not a form element,
            // Therefore it must be a JSON object of Key=>Value or Key=>Element
            // If anyone of those values are a input type=file we shall shall insert its siblings into the form for which it belongs.
            for (x in data) if (data.hasOwnProperty(x)) {
                // Is this an input Element?
                if (_this.domInstance('input', data[x]) && data[x].type === 'file') {
                    form = data[x].form;
                    form.encoding = form.enctype = 'multipart/form-data';
                }
            }

            // Do If there is no defined form element, lets create one.
            if (!form) {
                // Build form
                form = doc.createElement('form');
                doc.body.appendChild(form);
                newform = form;
            }

            var input;

            // Add elements to the form if they dont exist
            for (x in data) if (data.hasOwnProperty(x)) {

                // Is this an element?
                var el = (_this.domInstance('input', data[x]) || _this.domInstance('textArea', data[x]) || _this.domInstance('select', data[x]));

                // Is this not an input element, or one that exists outside the form.
                if (!el || data[x].form !== form) {

                    // Does an element have the same name?
                    var inputs = form.elements[x];
                    if (input) {
                        // Remove it.
                        if (!(inputs instanceof NodeList)) {
                            inputs = [inputs];
                        }

                        for (i = 0; i < inputs.length; i++) {
                            inputs[i].parentNode.removeChild(inputs[i]);
                        }

                    }

                    // Create an input element
                    input = doc.createElement('input');
                    input.setAttribute('type', 'hidden');
                    input.setAttribute('name', x);

                    // Does it have a value attribute?
                    if (el) {
                        input.value = data[x].value;
                    }
                    else if (_this.domInstance(null, data[x])) {
                        input.value = data[x].innerHTML || data[x].innerText;
                    }
                    else {
                        input.value = data[x];
                    }

                    form.appendChild(input);
                }

                // It is an element, which exists within the form, but the name is wrong
                else if (el && data[x].name !== x) {
                    data[x].setAttribute('name', x);
                    data[x].name = x;
                }
            }

            // Disable elements from within the form if they weren't specified
            for (i = 0; i < form.elements.length; i++) {

                input = form.elements[i];

                // Does the same name and value exist in the parent
                if (!(input.name in data) && input.getAttribute('disabled') !== true) {
                    // Disable
                    input.setAttribute('disabled', true);

                    // Add re-enable to callback
                    reenableAfterSubmit.push(input);
                }
            }
        }

        // Set the target of the form
        form.setAttribute('method', 'POST');
        form.setAttribute('target', callbackID);
        form.target = callbackID;

        // Update the form URL
        form.setAttribute('action', url);

        // Submit the form
        // Some reason this needs to be offset from the current window execution
        setTimeout(function() {
            form.submit();

            setTimeout(function() {
                try {
                    // Remove the iframe from the page.
                    //win.parentNode.removeChild(win);
                    // Remove the form
                    if (newform) {
                        newform.parentNode.removeChild(newform);
                    }
                }
                catch (e) {
                    try {
                        console.error('HelloJS: could not remove iframe');
                    }
                    catch (ee) {}
                }

                // Reenable the disabled form
                for (var i = 0; i < reenableAfterSubmit.length; i++) {
                    if (reenableAfterSubmit[i]) {
                        reenableAfterSubmit[i].setAttribute('disabled', false);
                        reenableAfterSubmit[i].disabled = false;
                    }
                }
            }, 0);
        }, 100);
    },

    // Some of the providers require that only multipart is used with non-binary forms.
    // This function checks whether the form contains binary data
    hasBinary: function(data) {
        for (var x in data) if (data.hasOwnProperty(x)) {
            if (this.isBinary(data[x])) {
                return true;
            }
        }

        return false;
    },

    // Determines if a variable Either Is or like a FormInput has the value of a Blob

    isBinary: function(data) {

        return data instanceof Object && (
        (this.domInstance('input', data) && data.type === 'file') ||
        ('FileList' in window && data instanceof window.FileList) ||
        ('File' in window && data instanceof window.File) ||
        ('Blob' in window && data instanceof window.Blob));

    },

    // Convert Data-URI to Blob string
    toBlob: function(dataURI) {
        var reg = /^data\:([^;,]+(\;charset=[^;,]+)?)(\;base64)?,/i;
        var m = dataURI.match(reg);
        if (!m) {
            return dataURI;
        }

        var binary = atob(dataURI.replace(reg, ''));
        var array = [];
        for (var i = 0; i < binary.length; i++) {
            array.push(binary.charCodeAt(i));
        }

        return new Blob([new Uint8Array(array)], {type: m[1]});
    }

});

// EXTRA: Convert FormElement to JSON for POSTing
// Wrappers to add additional functionality to existing functions
(function(hello) {

    // Copy original function
    var api = hello.api;
    var utils = hello.utils;

    utils.extend(utils, {

        // DataToJSON
        // This takes a FormElement|NodeList|InputElement|MixedObjects and convers the data object to JSON.
        dataToJSON: function(p) {

            var _this = this;
            var w = window;
            var data = p.data;

            // Is data a form object
            if (_this.domInstance('form', data)) {
                data = _this.nodeListToJSON(data.elements);
            }
            else if ('NodeList' in w && data instanceof NodeList) {
                data = _this.nodeListToJSON(data);
            }
            else if (_this.domInstance('input', data)) {
                data = _this.nodeListToJSON([data]);
            }

            // Is data a blob, File, FileList?
            if (('File' in w && data instanceof w.File) ||
                ('Blob' in w && data instanceof w.Blob) ||
                ('FileList' in w && data instanceof w.FileList)) {
                data = {file: data};
            }

            // Loop through data if it's not form data it must now be a JSON object
            if (!('FormData' in w && data instanceof w.FormData)) {

                for (var x in data) if (data.hasOwnProperty(x)) {

                    if ('FileList' in w && data[x] instanceof w.FileList) {
                        if (data[x].length === 1) {
                            data[x] = data[x][0];
                        }
                    }
                    else if (_this.domInstance('input', data[x]) && data[x].type === 'file') {
                        continue;
                    }
                    else if (_this.domInstance('input', data[x]) ||
                        _this.domInstance('select', data[x]) ||
                        _this.domInstance('textArea', data[x])) {
                        data[x] = data[x].value;
                    }
                    else if (_this.domInstance(null, data[x])) {
                        data[x] = data[x].innerHTML || data[x].innerText;
                    }
                }
            }

            p.data = data;
            return data;
        },

        // NodeListToJSON
        // Given a list of elements extrapolate their values and return as a json object
        nodeListToJSON: function(nodelist) {

            var json = {};

            // Create a data string
            for (var i = 0; i < nodelist.length; i++) {

                var input = nodelist[i];

                // If the name of the input is empty or diabled, dont add it.
                if (input.disabled || !input.name) {
                    continue;
                }

                // Is this a file, does the browser not support 'files' and 'FormData'?
                if (input.type === 'file') {
                    json[input.name] = input;
                }
                else {
                    json[input.name] = input.value || input.innerHTML;
                }
            }

            return json;
        }
    });

    // Replace it
    hello.api = function() {

        // Get arguments
        var p = utils.args({path: 's!', method: 's', data:'o', timeout: 'i', callback: 'f'}, arguments);

        // Change for into a data object
        if (p.data) {
            utils.dataToJSON(p);
        }

        return api.call(this, p);
    };

})(hello);

/////////////////////////////////////
//
// Save any access token that is in the current page URL
// Handle any response solicited through iframe hash tag following an API request
//
/////////////////////////////////////

hello.utils.responseHandler(window, window.opener || window.parent);

// Script to support ChromeApps
// This overides the hello.utils.popup method to support chrome.identity.launchWebAuthFlow
// See https://developer.chrome.com/apps/app_identity#non

// Is this a chrome app?

if (typeof chrome === 'object' && typeof chrome.identity === 'object' && chrome.identity.launchWebAuthFlow) {

    (function() {

        // Swap the popup method
        hello.utils.popup = function(url) {

            return _open(url, true);

        };

        // Swap the hidden iframe method
        hello.utils.iframe = function(url) {

            _open(url, false);

        };

        // Swap the request_cors method
        hello.utils.request_cors = function(callback) {

            callback();

            // Always run as CORS

            return true;
        };

        // Swap the storage method
        var _cache = {};
        chrome.storage.local.get('hello', function(r) {
            // Update the cache
            _cache = r.hello || {};
        });

        hello.utils.store = function(name, value) {

            // Get all
            if (arguments.length === 0) {
                return _cache;
            }

            // Get
            if (arguments.length === 1) {
                return _cache[name] || null;
            }

            // Set
            if (value) {
                _cache[name] = value;
                chrome.storage.local.set({hello: _cache});
                return value;
            }

            // Delete
            if (value === null) {
                delete _cache[name];
                chrome.storage.local.set({hello: _cache});
                return null;
            }
        };

        // Open function
        function _open(url, interactive) {

            // Launch
            var ref = {
                closed: false
            };

            // Launch the webAuthFlow
            chrome.identity.launchWebAuthFlow({
                url: url,
                interactive: interactive
            }, function(responseUrl) {

                // Did the user cancel this prematurely
                if (responseUrl === undefined) {
                    ref.closed = true;
                    return;
                }

                // Split appart the URL
                var a = hello.utils.url(responseUrl);

                // The location can be augmented in to a location object like so...
                // We dont have window operations on the popup so lets create some
                var _popup = {
                    location: {

                        // Change the location of the popup
                        assign: function(url) {

                            // If there is a secondary reassign
                            // In the case of OAuth1
                            // Trigger this in non-interactive mode.
                            _open(url, false);
                        },

                        search: a.search,
                        hash: a.hash,
                        href: a.href
                    },
                    close: function() {}
                };

                // Then this URL contains information which HelloJS must process
                // URL string
                // Window - any action such as window relocation goes here
                // Opener - the parent window which opened this, aka this script

                hello.utils.responseHandler(_popup, window);
            });

            // Return the reference
            return ref;
        }

    })();
}

// Phonegap override for hello.phonegap.js
(function() {

    // Is this a phonegap implementation?
    if (!(/^file:\/{3}[^\/]/.test(window.location.href) && window.cordova)) {
        // Cordova is not included.
        return;
    }

    // Augment the hidden iframe method
    hello.utils.iframe = function(url, redirectUri) {
        hello.utils.popup(url, redirectUri, {hidden: 'yes'});
    };

    // Augment the popup
    var utilPopup = hello.utils.popup;

    // Replace popup
    hello.utils.popup = function(url, redirectUri, options) {

        // Run the standard
        var popup = utilPopup.call(this, url, redirectUri, options);

        // Create a function for reopening the popup, and assigning events to the new popup object
        // PhoneGap support
        // Add an event listener to listen to the change in the popup windows URL
        // This must appear before popup.focus();
        try {
            if (popup && popup.addEventListener) {

                // Get the origin of the redirect URI

                var a = hello.utils.url(redirectUri);
                var redirectUriOrigin = a.origin || (a.protocol + '//' + a.hostname);

                // Listen to changes in the InAppBrowser window

                popup.addEventListener('loadstart', function(e) {

                    var url = e.url;

                    // Is this the path, as given by the redirectUri?
                    // Check the new URL agains the redirectUriOrigin.
                    // According to #63 a user could click 'cancel' in some dialog boxes ....
                    // The popup redirects to another page with the same origin, yet we still wish it to close.

                    if (url.indexOf(redirectUriOrigin) !== 0) {
                        return;
                    }

                    // Split appart the URL
                    var a = hello.utils.url(url);

                    // We dont have window operations on the popup so lets create some
                    // The location can be augmented in to a location object like so...

                    var _popup = {
                        location: {
                            // Change the location of the popup
                            assign: function(location) {

                                // Unfourtunatly an app is may not change the location of a InAppBrowser window.
                                // So to shim this, just open a new one.
                                popup.executeScript({code: 'window.location.href = "' + location + ';"'});
                            },

                            search: a.search,
                            hash: a.hash,
                            href: a.href
                        },
                        close: function() {
                            if (popup.close) {
                                popup.close();
                                try {
                                    popup.closed = true;
                                }
                                catch (_e) {}
                            }
                        }
                    };

                    // Then this URL contains information which HelloJS must process
                    // URL string
                    // Window - any action such as window relocation goes here
                    // Opener - the parent window which opened this, aka this script

                    hello.utils.responseHandler(_popup, window);

                });
            }
        }
        catch (e) {}

        return popup;
    };

})();

(function(hello) {

    // OAuth1
    var OAuth1Settings = {
        version: '1.0',
        auth: 'https://www.dropbox.com/1/oauth/authorize',
        request: 'https://api.dropbox.com/1/oauth/request_token',
        token: 'https://api.dropbox.com/1/oauth/access_token'
    };

    // OAuth2 Settings
    var OAuth2Settings = {
        version: 2,
        auth: 'https://www.dropbox.com/1/oauth2/authorize',
        grant: 'https://api.dropbox.com/1/oauth2/token'
    };

    // Initiate the Dropbox module
    hello.init({

        dropbox: {

            name: 'Dropbox',

            oauth: OAuth2Settings,

            login: function(p) {
                // OAuth2 non-standard adjustments
                p.qs.scope = '';
                delete p.qs.display;

                // Should this be run as OAuth1?
                // If the redirect_uri is is HTTP (non-secure) then its required to revert to the OAuth1 endpoints
                var redirect = decodeURIComponent(p.qs.redirect_uri);
                if (redirect.indexOf('http:') === 0 && redirect.indexOf('http://localhost/') !== 0) {

                    // Override the dropbox OAuth settings.
                    hello.services.dropbox.oauth = OAuth1Settings;
                }
                else {
                    // Override the dropbox OAuth settings.
                    hello.services.dropbox.oauth = OAuth2Settings;
                }

                // The dropbox login window is a different size
                p.options.popup.width = 1000;
                p.options.popup.height = 1000;
            },

            /*
                Dropbox does not allow insecure HTTP URI's in the redirect_uri field
                ...otherwise I'd love to use OAuth2

                Follow request https://forums.dropbox.com/topic.php?id=106505

                p.qs.response_type = 'code';
                oauth: {
                    version: 2,
                    auth: 'https://www.dropbox.com/1/oauth2/authorize',
                    grant: 'https://api.dropbox.com/1/oauth2/token'
                }
            */

            // API Base URL
            base: 'https://api.dropbox.com/1/',

            // Bespoke setting: this is states whether to use the custom environment of Dropbox or to use their own environment
            // Because it's notoriously difficult for Dropbox too provide access from other webservices, this defaults to Sandbox
            root: 'sandbox',

            // Map GET requests
            get: {
                me: 'account/info',

                // Https://www.dropbox.com/developers/core/docs#metadata
                'me/files': req('metadata/auto/@{parent|}'),
                'me/folder': req('metadata/auto/@{id}'),
                'me/folders': req('metadata/auto/'),

                'default': function(p, callback) {
                    if (p.path.match('https://api-content.dropbox.com/1/files/')) {
                        // This is a file, return binary data
                        p.method = 'blob';
                    }

                    callback(p.path);
                }
            },

            post: {
                'me/files': function(p, callback) {

                    var path = p.data.parent;
                    var fileName = p.data.name;

                    p.data = {
                        file: p.data.file
                    };

                    // Does this have a data-uri to upload as a file?
                    if (typeof (p.data.file) === 'string') {
                        p.data.file = hello.utils.toBlob(p.data.file);
                    }

                    callback('https://api-content.dropbox.com/1/files_put/auto/' + path + '/' + fileName);
                },

                'me/folders': function(p, callback) {

                    var name = p.data.name;
                    p.data = {};

                    callback('fileops/create_folder?root=@{root|sandbox}&' + hello.utils.param({
                        path: name
                    }));
                }
            },

            // Map DELETE requests
            del: {
                'me/files': 'fileops/delete?root=@{root|sandbox}&path=@{id}',
                'me/folder': 'fileops/delete?root=@{root|sandbox}&path=@{id}'
            },

            wrap: {
                me: function(o) {
                    formatError(o);
                    if (!o.uid) {
                        return o;
                    }

                    o.name = o.display_name;
                    var m = o.name.split(' ');
                    o.first_name = m.shift();
                    o.last_name = m.join(' ');
                    o.id = o.uid;
                    delete o.uid;
                    delete o.display_name;
                    return o;
                },

                'default': function(o, headers, req) {
                    formatError(o);
                    if (o.is_dir && o.contents) {
                        o.data = o.contents;
                        delete o.contents;

                        o.data.forEach(function(item) {
                            item.root = o.root;
                            formatFile(item, headers, req);
                        });
                    }

                    formatFile(o, headers, req);

                    if (o.is_deleted) {
                        o.success = true;
                    }

                    return o;
                }
            },

            // Doesn't return the CORS headers
            xhr: function(p) {

                // The proxy supports allow-cross-origin-resource
                // Alas that's the only thing we're using.
                if (p.data && p.data.file) {
                    var file = p.data.file;
                    if (file) {
                        if (file.files) {
                            p.data = file.files[0];
                        }
                        else {
                            p.data = file;
                        }
                    }
                }

                if (p.method === 'delete') {
                    p.method = 'post';
                }

                return true;
            },

            form: function(p, qs) {
                delete qs.state;
                delete qs.redirect_uri;
            }
        }
    });

    function formatError(o) {
        if (o && 'error' in o) {
            o.error = {
                code: 'server_error',
                message: o.error.message || o.error
            };
        }
    }

    function formatFile(o, headers, req) {

        if (typeof o !== 'object' ||
            (typeof Blob !== 'undefined' && o instanceof Blob) ||
            (typeof ArrayBuffer !== 'undefined' && o instanceof ArrayBuffer)) {
            // This is a file, let it through unformatted
            return;
        }

        if ('error' in o) {
            return;
        }

        var path = (o.root !== 'app_folder' ? o.root : '') + o.path.replace(/\&/g, '%26');
        path = path.replace(/^\//, '');
        if (o.thumb_exists) {
            o.thumbnail = req.oauth_proxy + '?path=' +
            encodeURIComponent('https://api-content.dropbox.com/1/thumbnails/auto/' + path + '?format=jpeg&size=m') + '&access_token=' + req.options.access_token;
        }

        o.type = (o.is_dir ? 'folder' : o.mime_type);
        o.name = o.path.replace(/.*\//g, '');
        if (o.is_dir) {
            o.files = path.replace(/^\//, '');
        }
        else {
            o.downloadLink = hello.settings.oauth_proxy + '?path=' +
            encodeURIComponent('https://api-content.dropbox.com/1/files/auto/' + path) + '&access_token=' + req.options.access_token;
            o.file = 'https://api-content.dropbox.com/1/files/auto/' + path;
        }

        if (!o.id) {
            o.id = o.path.replace(/^\//, '');
        }

        // O.media = 'https://api-content.dropbox.com/1/files/' + path;
    }

    function req(str) {
        return function(p, cb) {
            delete p.query.limit;
            cb(str);
        };
    }

})(hello);

(function(hello) {

    hello.init({

        facebook: {

            name: 'Facebook',

            // SEE https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.1
            oauth: {
                version: 2,
                auth: 'https://www.facebook.com/dialog/oauth/',
                grant: 'https://graph.facebook.com/oauth/access_token'
            },

            // Authorization scopes
            scope: {
                basic: 'public_profile',
                email: 'email',
                share: 'user_posts',
                birthday: 'user_birthday',
                events: 'user_events',
                photos: 'user_photos',
                videos: 'user_videos',
                friends: 'user_friends',
                files: 'user_photos,user_videos',
                publish_files: 'user_photos,user_videos,publish_actions',
                publish: 'publish_actions',

                // Deprecated in v2.0
                // Create_event    : 'create_event',

                offline_access: ''
            },

            // Refresh the access_token
            refresh: true,

            login: function(p) {

                // Reauthenticate
                // https://developers.facebook.com/docs/facebook-login/reauthentication
                if (p.options.force) {
                    p.qs.auth_type = 'reauthenticate';
                }

                // The facebook login window is a different size.
                p.options.popup.width = 580;
                p.options.popup.height = 400;
            },

            logout: function(callback, options) {
                // Assign callback to a global handler
                var callbackID = hello.utils.globalEvent(callback);
                var redirect = encodeURIComponent(hello.settings.redirect_uri + '?' + hello.utils.param({callback:callbackID, result: JSON.stringify({force:true}), state: '{}'}));
                var token = (options.authResponse || {}).access_token;
                hello.utils.iframe('https://www.facebook.com/logout.php?next=' + redirect + '&access_token=' + token);

                // Possible responses:
                // String URL    - hello.logout should handle the logout
                // Undefined    - this function will handle the callback
                // True - throw a success, this callback isn't handling the callback
                // False - throw a error
                if (!token) {
                    // If there isn't a token, the above wont return a response, so lets trigger a response
                    return false;
                }
            },

            // API Base URL
            base: 'https://graph.facebook.com/v2.4/',

            // Map GET requests
            get: {
                me: 'me?fields=email,first_name,last_name,name,timezone,verified',
                'me/friends': 'me/friends',
                'me/following': 'me/friends',
                'me/followers': 'me/friends',
                'me/share': 'me/feed',
                'me/like': 'me/likes',
                'me/files': 'me/albums',
                'me/albums': 'me/albums?fields=cover_photo,name',
                'me/album': '@{id}/photos?fields=picture',
                'me/photos': 'me/photos',
                'me/photo': '@{id}',
                'friend/albums': '@{id}/albums',
                'friend/photos': '@{id}/photos'

                // Pagination
                // Https://developers.facebook.com/docs/reference/api/pagination/
            },

            // Map POST requests
            post: {
                'me/share': 'me/feed',
                'me/photo': '@{id}'

                // Https://developers.facebook.com/docs/graph-api/reference/v2.2/object/likes/
            },

            wrap: {
                me: formatUser,
                'me/friends': formatFriends,
                'me/following': formatFriends,
                'me/followers': formatFriends,
                'me/albums': format,
                'me/photos': format,
                'me/files': format,
                'default': format
            },

            // Special requirements for handling XHR
            xhr: function(p, qs) {
                if (p.method === 'get' || p.method === 'post') {
                    qs.suppress_response_codes = true;
                }

                // Is this a post with a data-uri?
                if (p.method === 'post' && p.data && typeof (p.data.file) === 'string') {
                    // Convert the Data-URI to a Blob
                    p.data.file = hello.utils.toBlob(p.data.file);
                }

                return true;
            },

            // Special requirements for handling JSONP fallback
            jsonp: function(p, qs) {
                var m = p.method;
                if (m !== 'get' && !hello.utils.hasBinary(p.data)) {
                    p.data.method = m;
                    p.method = 'get';
                }
                else if (p.method === 'delete') {
                    qs.method = 'delete';
                    p.method = 'post';
                }
            },

            // Special requirements for iframe form hack
            form: function(p) {
                return {
                    // Fire the callback onload
                    callbackonload: true
                };
            }
        }
    });

    var base = 'https://graph.facebook.com/';

    function formatUser(o) {
        if (o.id) {
            o.thumbnail = o.picture = 'https://graph.facebook.com/' + o.id + '/picture';
        }

        return o;
    }

    function formatFriends(o) {
        if ('data' in o) {
            o.data.forEach(formatUser);
        }

        return o;
    }

    function format(o, headers, req) {
        if (typeof o === 'boolean') {
            o = {success: o};
        }

        if (o && 'data' in o) {
            var token = req.query.access_token;

            if (!(o.data instanceof Array)) {
                var data = o.data;
                delete o.data;
                o.data = [data];
            }

            o.data.forEach(function(d) {

                if (d.picture) {
                    d.thumbnail = d.picture;
                }

                d.pictures = (d.images || [])
                    .sort(function(a, b) {
                        return a.width - b.width;
                    });

                if (d.cover_photo && d.cover_photo.id) {
                    d.thumbnail = base + d.cover_photo.id + '/picture?access_token=' + token;
                }

                if (d.type === 'album') {
                    d.files = d.photos = base + d.id + '/photos';
                }

                if (d.can_upload) {
                    d.upload_location = base + d.id + '/photos';
                }
            });
        }

        return o;
    }

})(hello);

(function(hello) {

    hello.init({

        flickr: {

            name: 'Flickr',

            // Ensure that you define an oauth_proxy
            oauth: {
                version: '1.0a',
                auth: 'https://www.flickr.com/services/oauth/authorize?perms=read',
                request: 'https://www.flickr.com/services/oauth/request_token',
                token: 'https://www.flickr.com/services/oauth/access_token'
            },

            // API base URL
            base: 'https://api.flickr.com/services/rest',

            // Map GET resquests
            get: {
                me: sign('flickr.people.getInfo'),
                'me/friends': sign('flickr.contacts.getList', {per_page:'@{limit|50}'}),
                'me/following': sign('flickr.contacts.getList', {per_page:'@{limit|50}'}),
                'me/followers': sign('flickr.contacts.getList', {per_page:'@{limit|50}'}),
                'me/albums': sign('flickr.photosets.getList', {per_page:'@{limit|50}'}),
                'me/album': sign('flickr.photosets.getPhotos', {photoset_id: '@{id}'}),
                'me/photos': sign('flickr.people.getPhotos', {per_page:'@{limit|50}'})
            },

            wrap: {
                me: function(o) {
                    formatError(o);
                    o = checkResponse(o, 'person');
                    if (o.id) {
                        if (o.realname) {
                            o.name = o.realname._content;
                            var m = o.name.split(' ');
                            o.first_name = m.shift();
                            o.last_name = m.join(' ');
                        }

                        o.thumbnail = getBuddyIcon(o, 'l');
                        o.picture = getBuddyIcon(o, 'l');
                    }

                    return o;
                },

                'me/friends': formatFriends,
                'me/followers': formatFriends,
                'me/following': formatFriends,
                'me/albums': function(o) {
                    formatError(o);
                    o = checkResponse(o, 'photosets');
                    paging(o);
                    if (o.photoset) {
                        o.data = o.photoset;
                        o.data.forEach(function(item) {
                            item.name = item.title._content;
                            item.photos = 'https://api.flickr.com/services/rest' + getApiUrl('flickr.photosets.getPhotos', {photoset_id: item.id}, true);
                        });

                        delete o.photoset;
                    }

                    return o;
                },

                'me/photos': function(o) {
                    formatError(o);
                    return formatPhotos(o);
                },

                'default': function(o) {
                    formatError(o);
                    return formatPhotos(o);
                }
            },

            xhr: false,

            jsonp: function(p, qs) {
                if (p.method == 'get') {
                    delete qs.callback;
                    qs.jsoncallback = p.callbackID;
                }
            }
        }
    });

    function getApiUrl(method, extraParams, skipNetwork) {
        var url = ((skipNetwork) ? '' : 'flickr:') +
            '?method=' + method +
            '&api_key=' + hello.services.flickr.id +
            '&format=json';
        for (var param in extraParams) {
            if (extraParams.hasOwnProperty(param)) {
                url += '&' + param + '=' + extraParams[param];
            }
        }

        return url;
    }

    // This is not exactly neat but avoid to call
    // The method 'flickr.test.login' for each api call

    function withUser(cb) {
        var auth = hello.getAuthResponse('flickr');
        cb(auth && auth.user_nsid ? auth.user_nsid : null);
    }

    function sign(url, params) {
        if (!params) {
            params = {};
        }

        return function(p, callback) {
            withUser(function(userId) {
                params.user_id = userId;
                callback(getApiUrl(url, params, true));
            });
        };
    }

    function getBuddyIcon(profile, size) {
        var url = 'https://www.flickr.com/images/buddyicon.gif';
        if (profile.nsid && profile.iconserver && profile.iconfarm) {
            url = 'https://farm' + profile.iconfarm + '.staticflickr.com/' +
                profile.iconserver + '/' +
                'buddyicons/' + profile.nsid +
                ((size) ? '_' + size : '') + '.jpg';
        }

        return url;
    }

    // See: https://www.flickr.com/services/api/misc.urls.html
    function createPhotoUrl(id, farm, server, secret, size) {
        size = (size) ? '_' + size : '';
        return 'https://farm' + farm + '.staticflickr.com/' + server + '/' + id + '_' + secret + size + '.jpg';
    }

    function formatUser(o) {
    }

    function formatError(o) {
        if (o && o.stat && o.stat.toLowerCase() != 'ok') {
            o.error = {
                code: 'invalid_request',
                message: o.message
            };
        }
    }

    function formatPhotos(o) {
        if (o.photoset || o.photos) {
            var set = ('photoset' in o) ? 'photoset' : 'photos';
            o = checkResponse(o, set);
            paging(o);
            o.data = o.photo;
            delete o.photo;
            for (var i = 0; i < o.data.length; i++) {
                var photo = o.data[i];
                photo.name = photo.title;
                photo.picture = createPhotoUrl(photo.id, photo.farm, photo.server, photo.secret, '');
                photo.pictures = createPictures(photo.id, photo.farm, photo.server, photo.secret);
                photo.source = createPhotoUrl(photo.id, photo.farm, photo.server, photo.secret, 'b');
                photo.thumbnail = createPhotoUrl(photo.id, photo.farm, photo.server, photo.secret, 'm');
            }
        }

        return o;
    }

    // See: https://www.flickr.com/services/api/misc.urls.html
    function createPictures(id, farm, server, secret) {

        var NO_LIMIT = 2048;
        var sizes = [
            {id: 't', max: 100},
            {id: 'm', max: 240},
            {id: 'n', max: 320},
            {id: '', max: 500},
            {id: 'z', max: 640},
            {id: 'c', max: 800},
            {id: 'b', max: 1024},
            {id: 'h', max: 1600},
            {id: 'k', max: 2048},
            {id: 'o', max: NO_LIMIT}
        ];

        return sizes.map(function(size) {
            return {
                source: createPhotoUrl(id, farm, server, secret, size.id),

                // Note: this is a guess that's almost certain to be wrong (unless square source)
                width: size.max,
                height: size.max
            };
        });
    }

    function checkResponse(o, key) {

        if (key in o) {
            o = o[key];
        }
        else if (!('error' in o)) {
            o.error = {
                code: 'invalid_request',
                message: o.message || 'Failed to get data from Flickr'
            };
        }

        return o;
    }

    function formatFriends(o) {
        formatError(o);
        if (o.contacts) {
            o = checkResponse(o, 'contacts');
            paging(o);
            o.data = o.contact;
            delete o.contact;
            for (var i = 0; i < o.data.length; i++) {
                var item = o.data[i];
                item.id = item.nsid;
                item.name = item.realname || item.username;
                item.thumbnail = getBuddyIcon(item, 'm');
            }
        }

        return o;
    }

    function paging(res) {
        if (res.page && res.pages && res.page !== res.pages) {
            res.paging = {
                next: '?page=' + (++res.page)
            };
        }
    }

})(hello);

(function(hello) {

    hello.init({

        foursquare: {

            name: 'Foursquare',

            oauth: {
                // See: https://developer.foursquare.com/overview/auth
                version: 2,
                auth: 'https://foursquare.com/oauth2/authenticate',
                grant: 'https://foursquare.com/oauth2/access_token'
            },

            // Refresh the access_token once expired
            refresh: true,

            base: 'https://api.foursquare.com/v2/',

            get: {
                me: 'users/self',
                'me/friends': 'users/self/friends',
                'me/followers': 'users/self/friends',
                'me/following': 'users/self/friends'
            },

            wrap: {
                me: function(o) {
                    formatError(o);
                    if (o && o.response) {
                        o = o.response.user;
                        formatUser(o);
                    }

                    return o;
                },

                'default': function(o) {
                    formatError(o);

                    // Format friends
                    if (o && 'response' in o && 'friends' in o.response && 'items' in o.response.friends) {
                        o.data = o.response.friends.items;
                        o.data.forEach(formatUser);
                        delete o.response;
                    }

                    return o;
                }
            },

            xhr: formatRequest,
            jsonp: formatRequest
        }
    });

    function formatError(o) {
        if (o.meta && (o.meta.code === 400 || o.meta.code === 401)) {
            o.error = {
                code: 'access_denied',
                message: o.meta.errorDetail
            };
        }
    }

    function formatUser(o) {
        if (o && o.id) {
            o.thumbnail = o.photo.prefix + '100x100' + o.photo.suffix;
            o.name = o.firstName + ' ' + o.lastName;
            o.first_name = o.firstName;
            o.last_name = o.lastName;
            if (o.contact) {
                if (o.contact.email) {
                    o.email = o.contact.email;
                }
            }
        }
    }

    function formatRequest(p, qs) {
        var token = qs.access_token;
        delete qs.access_token;
        qs.oauth_token = token;
        qs.v = 20121125;
        return true;
    }

})(hello);

(function(hello) {

    hello.init({

        github: {

            name: 'GitHub',

            oauth: {
                version: 2,
                auth: 'https://github.com/login/oauth/authorize',
                grant: 'https://github.com/login/oauth/access_token',
                response_type: 'code'
            },

            scope: {
                email: 'user:email'
            },

            base: 'https://api.github.com/',

            get: {
                me: 'user',
                'me/friends': 'user/following?per_page=@{limit|100}',
                'me/following': 'user/following?per_page=@{limit|100}',
                'me/followers': 'user/followers?per_page=@{limit|100}',
                'me/like': 'user/starred?per_page=@{limit|100}'
            },

            wrap: {
                me: function(o, headers) {

                    formatError(o, headers);
                    formatUser(o);

                    return o;
                },

                'default': function(o, headers, req) {

                    formatError(o, headers);

                    if (Array.isArray(o)) {
                        o = {data:o};
                    }

                    if (o.data) {
                        paging(o, headers, req);
                        o.data.forEach(formatUser);
                    }

                    return o;
                }
            },

            xhr: function(p) {

                if (p.method !== 'get' && p.data) {

                    // Serialize payload as JSON
                    p.headers = p.headers || {};
                    p.headers['Content-Type'] = 'application/json';
                    if (typeof (p.data) === 'object') {
                        p.data = JSON.stringify(p.data);
                    }
                }

                return true;
            }
        }
    });

    function formatError(o, headers) {
        var code = headers ? headers.statusCode : (o && 'meta' in o && 'status' in o.meta && o.meta.status);
        if ((code === 401 || code === 403)) {
            o.error = {
                code: 'access_denied',
                message: o.message || (o.data ? o.data.message : 'Could not get response')
            };
            delete o.message;
        }
    }

    function formatUser(o) {
        if (o.id) {
            o.thumbnail = o.picture = o.avatar_url;
            o.name = o.login;
        }
    }

    function paging(res, headers, req) {
        if (res.data && res.data.length && headers && headers.Link) {
            var next = headers.Link.match(/<(.*?)>;\s*rel=\"next\"/);
            if (next) {
                res.paging = {
                    next: next[1]
                };
            }
        }
    }

})(hello);

(function(hello) {

    var contactsUrl = 'https://www.google.com/m8/feeds/contacts/default/full?v=3.0&alt=json&max-results=@{limit|1000}&start-index=@{start|1}';

    hello.init({

        google: {

            name: 'Google Plus',

            // See: http://code.google.com/apis/accounts/docs/OAuth2UserAgent.html
            oauth: {
                version: 2,
                auth: 'https://accounts.google.com/o/oauth2/auth',
                grant: 'https://accounts.google.com/o/oauth2/token'
            },

            // Authorization scopes
            scope: {
                basic: 'https://www.googleapis.com/auth/plus.me profile',
                email: 'email',
                birthday: '',
                events: '',
                photos: 'https://picasaweb.google.com/data/',
                videos: 'http://gdata.youtube.com',
                friends: 'https://www.google.com/m8/feeds, https://www.googleapis.com/auth/plus.login',
                files: 'https://www.googleapis.com/auth/drive.readonly',
                publish: '',
                publish_files: 'https://www.googleapis.com/auth/drive',
                share: '',
                create_event: '',
                offline_access: ''
            },

            scope_delim: ' ',

            login: function(p) {
                if (p.qs.display === 'none') {
                    // Google doesn't like display=none
                    p.qs.display = '';
                }

                if (p.qs.response_type === 'code') {

                    // Let's set this to an offline access to return a refresh_token
                    p.qs.access_type = 'offline';
                }

                // Reauthenticate
                // https://developers.google.com/identity/protocols/
                if (p.options.force) {
                    p.qs.approval_prompt = 'force';
                }
            },

            // API base URI
            base: 'https://www.googleapis.com/',

            // Map GET requests
            get: {
                me: 'plus/v1/people/me',

                // Deprecated Sept 1, 2014
                //'me': 'oauth2/v1/userinfo?alt=json',

                // See: https://developers.google.com/+/api/latest/people/list
                'me/friends': 'plus/v1/people/me/people/visible?maxResults=@{limit|100}',
                'me/following': contactsUrl,
                'me/followers': contactsUrl,
                'me/contacts': contactsUrl,
                'me/share': 'plus/v1/people/me/activities/public?maxResults=@{limit|100}',
                'me/feed': 'plus/v1/people/me/activities/public?maxResults=@{limit|100}',
                'me/albums': 'https://picasaweb.google.com/data/feed/api/user/default?alt=json&max-results=@{limit|100}&start-index=@{start|1}',
                'me/album': function(p, callback) {
                    var key = p.query.id;
                    delete p.query.id;
                    callback(key.replace('/entry/', '/feed/'));
                },

                'me/photos': 'https://picasaweb.google.com/data/feed/api/user/default?alt=json&kind=photo&max-results=@{limit|100}&start-index=@{start|1}',

                // See: https://developers.google.com/drive/v2/reference/files/list
                'me/file': 'drive/v2/files/@{id}',
                'me/files': 'drive/v2/files?q=%22@{parent|root}%22+in+parents+and+trashed=false&maxResults=@{limit|100}',

                // See: https://developers.google.com/drive/v2/reference/files/list
                'me/folders': 'drive/v2/files?q=%22@{id|root}%22+in+parents+and+mimeType+=+%22application/vnd.google-apps.folder%22+and+trashed=false&maxResults=@{limit|100}',

                // See: https://developers.google.com/drive/v2/reference/files/list
                'me/folder': 'drive/v2/files?q=%22@{id|root}%22+in+parents+and+trashed=false&maxResults=@{limit|100}'
            },

            // Map POST requests
            post: {

                // Google Drive
                'me/files': uploadDrive,
                'me/folders': function(p, callback) {
                    p.data = {
                        title: p.data.name,
                        parents: [{id: p.data.parent || 'root'}],
                        mimeType: 'application/vnd.google-apps.folder'
                    };
                    callback('drive/v2/files');
                }
            },

            // Map PUT requests
            put: {
                'me/files': uploadDrive
            },

            // Map DELETE requests
            del: {
                'me/files': 'drive/v2/files/@{id}',
                'me/folder': 'drive/v2/files/@{id}'
            },

            // Map PATCH requests
            patch: {
                'me/file': 'drive/v2/files/@{id}'
            },

            wrap: {
                me: function(o) {
                    if (o.id) {
                        o.last_name = o.family_name || (o.name ? o.name.familyName : null);
                        o.first_name = o.given_name || (o.name ? o.name.givenName : null);

                        if (o.emails && o.emails.length) {
                            o.email = o.emails[0].value;
                        }

                        formatPerson(o);
                    }

                    return o;
                },

                'me/friends': function(o) {
                    if (o.items) {
                        paging(o);
                        o.data = o.items;
                        o.data.forEach(formatPerson);
                        delete o.items;
                    }

                    return o;
                },

                'me/contacts': formatFriends,
                'me/followers': formatFriends,
                'me/following': formatFriends,
                'me/share': formatFeed,
                'me/feed': formatFeed,
                'me/albums': gEntry,
                'me/photos': formatPhotos,
                'default': gEntry
            },

            xhr: function(p) {

                if (p.method === 'post' || p.method === 'put') {
                    toJSON(p);
                }
                else if (p.method === 'patch') {
                    hello.utils.extend(p.query, p.data);
                    p.data = null;
                }

                return true;
            },

            // Don't even try submitting via form.
            // This means no POST operations in <=IE9
            form: false
        }
    });

    function toInt(s) {
        return parseInt(s, 10);
    }

    function formatFeed(o) {
        paging(o);
        o.data = o.items;
        delete o.items;
        return o;
    }

    // Format: ensure each record contains a name, id etc.
    function formatItem(o) {
        if (o.error) {
            return;
        }

        if (!o.name) {
            o.name = o.title || o.message;
        }

        if (!o.picture) {
            o.picture = o.thumbnailLink;
        }

        if (!o.thumbnail) {
            o.thumbnail = o.thumbnailLink;
        }

        if (o.mimeType === 'application/vnd.google-apps.folder') {
            o.type = 'folder';
            o.files = 'https://www.googleapis.com/drive/v2/files?q=%22' + o.id + '%22+in+parents';
        }

        return o;
    }

    function formatImage(image) {
        return {
            source: image.url,
            width: image.width,
            height: image.height
        };
    }

    function formatPhotos(o) {
        o.data = o.feed.entry.map(formatEntry);
        delete o.feed;
    }

    // Google has a horrible JSON API
    function gEntry(o) {
        paging(o);

        if ('feed' in o && 'entry' in o.feed) {
            o.data = o.feed.entry.map(formatEntry);
            delete o.feed;
        }

        // Old style: Picasa, etc.
        else if ('entry' in o) {
            return formatEntry(o.entry);
        }

        // New style: Google Drive & Plus
        else if ('items' in o) {
            o.data = o.items.map(formatItem);
            delete o.items;
        }
        else {
            formatItem(o);
        }

        return o;
    }

    function formatPerson(o) {
        o.name = o.displayName || o.name;
        o.picture = o.picture || (o.image ? o.image.url : null);
        o.thumbnail = o.picture;
    }

    function formatFriends(o, headers, req) {
        paging(o);
        var r = [];
        if ('feed' in o && 'entry' in o.feed) {
            var token = req.query.access_token;
            for (var i = 0; i < o.feed.entry.length; i++) {
                var a = o.feed.entry[i];

                a.id    = a.id.$t;
                a.name    = a.title.$t;
                delete a.title;
                if (a.gd$email) {
                    a.email    = (a.gd$email && a.gd$email.length > 0) ? a.gd$email[0].address : null;
                    a.emails = a.gd$email;
                    delete a.gd$email;
                }

                if (a.updated) {
                    a.updated = a.updated.$t;
                }

                if (a.link) {

                    var pic = (a.link.length > 0) ? a.link[0].href : null;
                    if (pic && a.link[0].gd$etag) {
                        pic += (pic.indexOf('?') > -1 ? '&' : '?') + 'access_token=' + token;
                        a.picture = pic;
                        a.thumbnail = pic;
                    }

                    delete a.link;
                }

                if (a.category) {
                    delete a.category;
                }
            }

            o.data = o.feed.entry;
            delete o.feed;
        }

        return o;
    }

    function formatEntry(a) {

        var group = a.media$group;
        var photo = group.media$content.length ? group.media$content[0] : {};
        var mediaContent = group.media$content || [];
        var mediaThumbnail = group.media$thumbnail || [];

        var pictures = mediaContent
            .concat(mediaThumbnail)
            .map(formatImage)
            .sort(function(a, b) {
                return a.width - b.width;
            });

        var i = 0;
        var _a;
        var p = {
            id: a.id.$t,
            name: a.title.$t,
            description: a.summary.$t,
            updated_time: a.updated.$t,
            created_time: a.published.$t,
            picture: photo ? photo.url : null,
            pictures: pictures,
            images: [],
            thumbnail: photo ? photo.url : null,
            width: photo.width,
            height: photo.height
        };

        // Get feed/children
        if ('link' in a) {
            for (i = 0; i < a.link.length; i++) {
                var d = a.link[i];
                if (d.rel.match(/\#feed$/)) {
                    p.upload_location = p.files = p.photos = d.href;
                    break;
                }
            }
        }

        // Get images of different scales
        if ('category' in a && a.category.length) {
            _a = a.category;
            for (i = 0; i < _a.length; i++) {
                if (_a[i].scheme && _a[i].scheme.match(/\#kind$/)) {
                    p.type = _a[i].term.replace(/^.*?\#/, '');
                }
            }
        }

        // Get images of different scales
        if ('media$thumbnail' in group && group.media$thumbnail.length) {
            _a = group.media$thumbnail;
            p.thumbnail = _a[0].url;
            p.images = _a.map(formatImage);
        }

        _a = group.media$content;

        if (_a && _a.length) {
            p.images.push(formatImage(_a[0]));
        }

        return p;
    }

    function paging(res) {

        // Contacts V2
        if ('feed' in res && res.feed.openSearch$itemsPerPage) {
            var limit = toInt(res.feed.openSearch$itemsPerPage.$t);
            var start = toInt(res.feed.openSearch$startIndex.$t);
            var total = toInt(res.feed.openSearch$totalResults.$t);

            if ((start + limit) < total) {
                res.paging = {
                    next: '?start=' + (start + limit)
                };
            }
        }
        else if ('nextPageToken' in res) {
            res.paging = {
                next: '?pageToken=' + res.nextPageToken
            };
        }
    }

    // Construct a multipart message
    function Multipart() {

        // Internal body
        var body = [];
        var boundary = (Math.random() * 1e10).toString(32);
        var counter = 0;
        var lineBreak = '\r\n';
        var delim = lineBreak + '--' + boundary;
        var ready = function() {};

        var dataUri = /^data\:([^;,]+(\;charset=[^;,]+)?)(\;base64)?,/i;

        // Add file
        function addFile(item) {
            var fr = new FileReader();
            fr.onload = function(e) {
                addContent(btoa(e.target.result), item.type + lineBreak + 'Content-Transfer-Encoding: base64');
            };

            fr.readAsBinaryString(item);
        }

        // Add content
        function addContent(content, type) {
            body.push(lineBreak + 'Content-Type: ' + type + lineBreak + lineBreak + content);
            counter--;
            ready();
        }

        // Add new things to the object
        this.append = function(content, type) {

            // Does the content have an array
            if (typeof (content) === 'string' || !('length' in Object(content))) {
                // Converti to multiples
                content = [content];
            }

            for (var i = 0; i < content.length; i++) {

                counter++;

                var item = content[i];

                // Is this a file?
                // Files can be either Blobs or File types
                if (
                    (typeof (File) !== 'undefined' && item instanceof File) ||
                    (typeof (Blob) !== 'undefined' && item instanceof Blob)
                ) {
                    // Read the file in
                    addFile(item);
                }

                // Data-URI?
                // Data:[<mime type>][;charset=<charset>][;base64],<encoded data>
                // /^data\:([^;,]+(\;charset=[^;,]+)?)(\;base64)?,/i
                else if (typeof (item) === 'string' && item.match(dataUri)) {
                    var m = item.match(dataUri);
                    addContent(item.replace(dataUri, ''), m[1] + lineBreak + 'Content-Transfer-Encoding: base64');
                }

                // Regular string
                else {
                    addContent(item, type);
                }
            }
        };

        this.onready = function(fn) {
            ready = function() {
                if (counter === 0) {
                    // Trigger ready
                    body.unshift('');
                    body.push('--');
                    fn(body.join(delim), boundary);
                    body = [];
                }
            };

            ready();
        };
    }

    // Upload to Drive
    // If this is PUT then only augment the file uploaded
    // PUT https://developers.google.com/drive/v2/reference/files/update
    // POST https://developers.google.com/drive/manage-uploads
    function uploadDrive(p, callback) {

        var data = {};

        // Test for DOM element
        if (p.data &&
            (typeof (HTMLInputElement) !== 'undefined' && p.data instanceof HTMLInputElement)
        ) {
            p.data = {file: p.data};
        }

        if (!p.data.name && Object(Object(p.data.file).files).length && p.method === 'post') {
            p.data.name = p.data.file.files[0].name;
        }

        if (p.method === 'post') {
            p.data = {
                title: p.data.name,
                parents: [{id: p.data.parent || 'root'}],
                file: p.data.file
            };
        }
        else {

            // Make a reference
            data = p.data;
            p.data = {};

            // Add the parts to change as required
            if (data.parent) {
                p.data.parents = [{id: p.data.parent || 'root'}];
            }

            if (data.file) {
                p.data.file = data.file;
            }

            if (data.name) {
                p.data.title = data.name;
            }
        }

        // Extract the file, if it exists from the data object
        // If the File is an INPUT element lets just concern ourselves with the NodeList
        var file;
        if ('file' in p.data) {
            file = p.data.file;
            delete p.data.file;

            if (typeof (file) === 'object' && 'files' in file) {
                // Assign the NodeList
                file = file.files;
            }

            if (!file || !file.length) {
                callback({
                    error: {
                        code: 'request_invalid',
                        message: 'There were no files attached with this request to upload'
                    }
                });
                return;
            }
        }

        // Set type p.data.mimeType = Object(file[0]).type || 'application/octet-stream';

        // Construct a multipart message
        var parts = new Multipart();
        parts.append(JSON.stringify(p.data), 'application/json');

        // Read the file into a  base64 string... yep a hassle, i know
        // FormData doesn't let us assign our own Multipart headers and HTTP Content-Type
        // Alas GoogleApi need these in a particular format
        if (file) {
            parts.append(file);
        }

        parts.onready(function(body, boundary) {

            p.headers['content-type'] = 'multipart/related; boundary="' + boundary + '"';
            p.data = body;

            callback('upload/drive/v2/files' + (data.id ? '/' + data.id : '') + '?uploadType=multipart');
        });

    }

    function toJSON(p) {
        if (typeof (p.data) === 'object') {
            // Convert the POST into a javascript object
            try {
                p.data = JSON.stringify(p.data);
                p.headers['content-type'] = 'application/json';
            }
            catch (e) {}
        }
    }

})(hello);

(function(hello) {

    hello.init({

        instagram: {

            name: 'Instagram',

            oauth: {
                // See: http://instagram.com/developer/authentication/
                version: 2,
                auth: 'https://instagram.com/oauth/authorize/',
                grant: 'https://api.instagram.com/oauth/access_token'
            },

            // Refresh the access_token once expired
            refresh: true,

            scope: {
                basic: 'basic',
                photos: '',
                friends: 'relationships',
                publish: 'likes comments',
                email: '',
                share: '',
                publish_files: '',
                files: '',
                videos: '',
                offline_access: ''
            },

            scope_delim: ' ',

            login: function(p) {
                // Instagram throws errors like 'JavaScript API is unsupported' if the display is 'popup'.
                // Make the display anything but 'popup'
                p.qs.display = '';
            },

            base: 'https://api.instagram.com/v1/',

            get: {
                me: 'users/self',
                'me/feed': 'users/self/feed?count=@{limit|100}',
                'me/photos': 'users/self/media/recent?min_id=0&count=@{limit|100}',
                'me/friends': 'users/self/follows?count=@{limit|100}',
                'me/following': 'users/self/follows?count=@{limit|100}',
                'me/followers': 'users/self/followed-by?count=@{limit|100}',
                'friend/photos': 'users/@{id}/media/recent?min_id=0&count=@{limit|100}'
            },

            post: {
                'me/like': function(p, callback) {
                    var id = p.data.id;
                    p.data = {};
                    callback('media/' + id + '/likes');
                }
            },

            del: {
                'me/like': 'media/@{id}/likes'
            },

            wrap: {
                me: function(o) {

                    formatError(o);

                    if ('data' in o) {
                        o.id = o.data.id;
                        o.thumbnail = o.data.profile_picture;
                        o.name = o.data.full_name || o.data.username;
                    }

                    return o;
                },

                'me/friends': formatFriends,
                'me/following': formatFriends,
                'me/followers': formatFriends,
                'me/photos': function(o) {

                    formatError(o);
                    paging(o);

                    if ('data' in o) {
                        o.data = o.data.filter(function(d) {
                            return d.type === 'image';
                        });

                        o.data.forEach(function(d) {
                            d.name = d.caption ? d.caption.text : null;
                            d.thumbnail = d.images.thumbnail.url;
                            d.picture = d.images.standard_resolution.url;
                            d.pictures = Object.keys(d.images)
                                .map(function(key) {
                                    var image = d.images[key];
                                    return formatImage(image);
                                })
                                .sort(function(a, b) {
                                    return a.width - b.width;
                                });
                        });
                    }

                    return o;
                },

                'default': function(o) {
                    o = formatError(o);
                    paging(o);
                    return o;
                }
            },

            // Instagram does not return any CORS Headers
            // So besides JSONP we're stuck with proxy
            xhr: function(p, qs) {

                var method = p.method;
                var proxy = method !== 'get';

                if (proxy) {

                    if ((method === 'post' || method === 'put') && p.query.access_token) {
                        p.data.access_token = p.query.access_token;
                        delete p.query.access_token;
                    }

                    // No access control headers
                    // Use the proxy instead
                    p.proxy = proxy;
                }

                return proxy;
            },

            // No form
            form: false
        }
    });

    function formatImage(image) {
        return {
            source: image.url,
            width: image.width,
            height: image.height
        };
    }

    function formatError(o) {
        if (typeof o === 'string') {
            return {
                error: {
                    code: 'invalid_request',
                    message: o
                }
            };
        }

        if (o && 'meta' in o && 'error_type' in o.meta) {
            o.error = {
                code: o.meta.error_type,
                message: o.meta.error_message
            };
        }

        return o;
    }

    function formatFriends(o) {
        paging(o);
        if (o && 'data' in o) {
            o.data.forEach(formatFriend);
        }

        return o;
    }

    function formatFriend(o) {
        if (o.id) {
            o.thumbnail = o.profile_picture;
            o.name = o.full_name || o.username;
        }
    }

    // See: http://instagram.com/developer/endpoints/
    function paging(res) {
        if ('pagination' in res) {
            res.paging = {
                next: res.pagination.next_url
            };
            delete res.pagination;
        }
    }

})(hello);

(function(hello) {

    hello.init({

        joinme: {

            name: 'join.me',

            oauth: {
                version: 2,
                auth: 'https://secure.join.me/api/public/v1/auth/oauth2',
                grant: 'https://secure.join.me/api/public/v1/auth/oauth2'
            },

            refresh: false,

            scope: {
                basic: 'user_info',
                user: 'user_info',
                scheduler: 'scheduler',
                start: 'start_meeting',
                email: '',
                friends: '',
                share: '',
                publish: '',
                photos: '',
                publish_files: '',
                files: '',
                videos: '',
                offline_access: ''
            },

            scope_delim: ' ',

            login: function(p) {
                p.options.popup.width = 400;
                p.options.popup.height = 700;
            },

            base: 'https://api.join.me/v1/',

            get: {
                me: 'user',
                meetings: 'meetings',
                'meetings/info': 'meetings/@{id}'
            },

            post: {
                'meetings/start/adhoc': function(p, callback) {
                    callback('meetings/start');
                },

                'meetings/start/scheduled': function(p, callback) {
                    var meetingId = p.data.meetingId;
                    p.data = {};
                    callback('meetings/' + meetingId + '/start');
                },

                'meetings/schedule': function(p, callback) {
                    callback('meetings');
                }
            },

            patch: {
                'meetings/update': function(p, callback) {
                    callback('meetings/' + p.data.meetingId);
                }
            },

            del: {
                'meetings/delete': 'meetings/@{id}'
            },

            wrap: {
                me: function(o, headers) {
                    formatError(o, headers);

                    if (!o.email) {
                        return o;
                    }

                    o.name = o.fullName;
                    o.first_name = o.name.split(' ')[0];
                    o.last_name = o.name.split(' ')[1];
                    o.id = o.email;

                    return o;
                },

                'default': function(o, headers) {
                    formatError(o, headers);

                    return o;
                }
            },

            xhr: formatRequest

        }
    });

    function formatError(o, headers) {
        var errorCode;
        var message;
        var details;

        if (o && ('Message' in o)) {
            message = o.Message;
            delete o.Message;

            if ('ErrorCode' in o) {
                errorCode = o.ErrorCode;
                delete o.ErrorCode;
            }
            else {
                errorCode = getErrorCode(headers);
            }

            o.error = {
                code: errorCode,
                message: message,
                details: o
            };
        }

        return o;
    }

    function formatRequest(p, qs) {
        // Move the access token from the request body to the request header
        var token = qs.access_token;
        delete qs.access_token;
        p.headers.Authorization = 'Bearer ' + token;

        // Format non-get requests to indicate json body
        if (p.method !== 'get' && p.data) {
            p.headers['Content-Type'] = 'application/json';
            if (typeof (p.data) === 'object') {
                p.data = JSON.stringify(p.data);
            }
        }

        if (p.method === 'put') {
            p.method = 'patch';
        }

        return true;
    }

    function getErrorCode(headers) {
        switch (headers.statusCode) {
            case 400:
                return 'invalid_request';
            case 403:
                return 'stale_token';
            case 401:
                return 'invalid_token';
            case 500:
                return 'server_error';
            default:
                return 'server_error';
        }
    }

}(hello));

(function(hello) {

    hello.init({

        linkedin: {

            oauth: {
                version: 2,
                response_type: 'code',
                auth: 'https://www.linkedin.com/uas/oauth2/authorization',
                grant: 'https://www.linkedin.com/uas/oauth2/accessToken'
            },

            // Refresh the access_token once expired
            refresh: true,

            scope: {
                basic: 'r_basicprofile',
                email: 'r_emailaddress',
                files: '',
                friends: '',
                photos: '',
                publish: 'w_share',
                publish_files: 'w_share',
                share: '',
                videos: '',
                offline_access: ''
            },
            scope_delim: ' ',

            base: 'https://api.linkedin.com/v1/',

            get: {
                me: 'people/~:(picture-url,first-name,last-name,id,formatted-name,email-address)',

                // See: http://developer.linkedin.com/documents/get-network-updates-and-statistics-api
                'me/share': 'people/~/network/updates?count=@{limit|250}'
            },

            post: {

                // See: https://developer.linkedin.com/documents/api-requests-json
                'me/share': function(p, callback) {
                    var data = {
                        visibility: {
                            code: 'anyone'
                        }
                    };

                    if (p.data.id) {

                        data.attribution = {
                            share: {
                                id: p.data.id
                            }
                        };

                    }
                    else {
                        data.comment = p.data.message;
                        if (p.data.picture && p.data.link) {
                            data.content = {
                                'submitted-url': p.data.link,
                                'submitted-image-url': p.data.picture
                            };
                        }
                    }

                    p.data = JSON.stringify(data);

                    callback('people/~/shares?format=json');
                },

                'me/like': like
            },

            del:{
                'me/like': like
            },

            wrap: {
                me: function(o) {
                    formatError(o);
                    formatUser(o);
                    return o;
                },

                'me/friends': formatFriends,
                'me/following': formatFriends,
                'me/followers': formatFriends,
                'me/share': function(o) {
                    formatError(o);
                    paging(o);
                    if (o.values) {
                        o.data = o.values.map(formatUser);
                        o.data.forEach(function(item) {
                            item.message = item.headline;
                        });

                        delete o.values;
                    }

                    return o;
                },

                'default': function(o, headers) {
                    formatError(o);
                    empty(o, headers);
                    paging(o);
                }
            },

            jsonp: function(p, qs) {
                formatQuery(qs);
                if (p.method === 'get') {
                    qs.format = 'jsonp';
                    qs['error-callback'] = p.callbackID;
                }
            },

            xhr: function(p, qs) {
                if (p.method !== 'get') {
                    formatQuery(qs);
                    p.headers['Content-Type'] = 'application/json';

                    // Note: x-li-format ensures error responses are not returned in XML
                    p.headers['x-li-format'] = 'json';
                    p.proxy = true;
                    return true;
                }

                return false;
            }
        }
    });

    function formatError(o) {
        if (o && 'errorCode' in o) {
            o.error = {
                code: o.status,
                message: o.message
            };
        }
    }

    function formatUser(o) {
        if (o.error) {
            return;
        }

        o.first_name = o.firstName;
        o.last_name = o.lastName;
        o.name = o.formattedName || (o.first_name + ' ' + o.last_name);
        o.thumbnail = o.pictureUrl;
        o.email = o.emailAddress;
        return o;
    }

    function formatFriends(o) {
        formatError(o);
        paging(o);
        if (o.values) {
            o.data = o.values.map(formatUser);
            delete o.values;
        }

        return o;
    }

    function paging(res) {
        if ('_count' in res && '_start' in res && (res._count + res._start) < res._total) {
            res.paging = {
                next: '?start=' + (res._start + res._count) + '&count=' + res._count
            };
        }
    }

    function empty(o, headers) {
        if (JSON.stringify(o) === '{}' && headers.statusCode === 200) {
            o.success = true;
        }
    }

    function formatQuery(qs) {
        // LinkedIn signs requests with the parameter 'oauth2_access_token'
        // ... yeah another one who thinks they should be different!
        if (qs.access_token) {
            qs.oauth2_access_token = qs.access_token;
            delete qs.access_token;
        }
    }

    function like(p, callback) {
        p.headers['x-li-format'] = 'json';
        var id = p.data.id;
        p.data = (p.method !== 'delete').toString();
        p.method = 'put';
        callback('people/~/network/updates/key=' + id + '/is-liked');
    }

})(hello);

// See: https://developers.soundcloud.com/docs/api/reference
(function(hello) {

    hello.init({

        soundcloud: {
            name: 'SoundCloud',

            oauth: {
                version: 2,
                auth: 'https://soundcloud.com/connect',
                grant: 'https://soundcloud.com/oauth2/token'
            },

            // Request path translated
            base: 'https://api.soundcloud.com/',
            get: {
                me: 'me.json',

                // Http://developers.soundcloud.com/docs/api/reference#me
                'me/friends': 'me/followings.json',
                'me/followers': 'me/followers.json',
                'me/following': 'me/followings.json',

                // See: http://developers.soundcloud.com/docs/api/reference#activities
                'default': function(p, callback) {

                    // Include '.json at the end of each request'
                    callback(p.path + '.json');
                }
            },

            // Response handlers
            wrap: {
                me: function(o) {
                    formatUser(o);
                    return o;
                },

                'default': function(o) {
                    if (Array.isArray(o)) {
                        o = {
                            data: o.map(formatUser)
                        };
                    }

                    paging(o);
                    return o;
                }
            },

            xhr: formatRequest,
            jsonp: formatRequest
        }
    });

    function formatRequest(p, qs) {
        // Alter the querystring
        var token = qs.access_token;
        delete qs.access_token;
        qs.oauth_token = token;
        qs['_status_code_map[302]'] = 200;
        return true;
    }

    function formatUser(o) {
        if (o.id) {
            o.picture = o.avatar_url;
            o.thumbnail = o.avatar_url;
            o.name = o.username || o.full_name;
        }

        return o;
    }

    // See: http://developers.soundcloud.com/docs/api/reference#activities
    function paging(res) {
        if ('next_href' in res) {
            res.paging = {
                next: res.next_href
            };
        }
    }

})(hello);

(function(hello) {

    var base = 'https://api.twitter.com/';

    hello.init({

        twitter: {

            // Ensure that you define an oauth_proxy
            oauth: {
                version: '1.0a',
                auth: base + 'oauth/authenticate',
                request: base + 'oauth/request_token',
                token: base + 'oauth/access_token'
            },

            login: function(p) {
                // Reauthenticate
                // https://dev.twitter.com/oauth/reference/get/oauth/authenticate
                var prefix = '?force_login=true';
                this.oauth.auth = this.oauth.auth.replace(prefix, '') + (p.options.force ? prefix : '');
            },

            base: base + '1.1/',

            get: {
                me: 'account/verify_credentials.json',
                'me/friends': 'friends/list.json?count=@{limit|200}',
                'me/following': 'friends/list.json?count=@{limit|200}',
                'me/followers': 'followers/list.json?count=@{limit|200}',

                // Https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline
                'me/share': 'statuses/user_timeline.json?count=@{limit|200}',

                // Https://dev.twitter.com/rest/reference/get/favorites/list
                'me/like': 'favorites/list.json?count=@{limit|200}'
            },

            post: {
                'me/share': function(p, callback) {

                    var data = p.data;
                    p.data = null;

                    var status = [];

                    // Change message to status
                    if (data.message) {
                        status.push(data.message);
                        delete data.message;
                    }

                    // If link is given
                    if (data.link) {
                        status.push(data.link);
                        delete data.link;
                    }

                    if (data.picture) {
                        status.push(data.picture);
                        delete data.picture;
                    }

                    // Compound all the components
                    if (status.length) {
                        data.status = status.join(' ');
                    }

                    // Tweet media
                    if (data.file) {
                        data['media[]'] = data.file;
                        delete data.file;
                        p.data = data;
                        callback('statuses/update_with_media.json');
                    }

                    // Retweet?
                    else if ('id' in data) {
                        callback('statuses/retweet/' + data.id + '.json');
                    }

                    // Tweet
                    else {
                        // Assign the post body to the query parameters
                        hello.utils.extend(p.query, data);
                        callback('statuses/update.json?include_entities=1');
                    }
                },

                // See: https://dev.twitter.com/rest/reference/post/favorites/create
                'me/like': function(p, callback) {
                    var id = p.data.id;
                    p.data = null;
                    callback('favorites/create.json?id=' + id);
                }
            },

            del: {

                // See: https://dev.twitter.com/rest/reference/post/favorites/destroy
                'me/like': function() {
                    p.method = 'post';
                    var id = p.data.id;
                    p.data = null;
                    callback('favorites/destroy.json?id=' + id);
                }
            },

            wrap: {
                me: function(res) {
                    formatError(res);
                    formatUser(res);
                    return res;
                },

                'me/friends': formatFriends,
                'me/followers': formatFriends,
                'me/following': formatFriends,

                'me/share': function(res) {
                    formatError(res);
                    paging(res);
                    if (!res.error && 'length' in res) {
                        return {data: res};
                    }

                    return res;
                },

                'default': function(res) {
                    res = arrayToDataResponse(res);
                    paging(res);
                    return res;
                }
            },
            xhr: function(p) {

                // Rely on the proxy for non-GET requests.
                return (p.method !== 'get');
            }
        }
    });

    function formatUser(o) {
        if (o.id) {
            if (o.name) {
                var m = o.name.split(' ');
                o.first_name = m.shift();
                o.last_name = m.join(' ');
            }

            // See: https://dev.twitter.com/overview/general/user-profile-images-and-banners
            o.thumbnail = o.profile_image_url_https || o.profile_image_url;
        }

        return o;
    }

    function formatFriends(o) {
        formatError(o);
        paging(o);
        if (o.users) {
            o.data = o.users.map(formatUser);
            delete o.users;
        }

        return o;
    }

    function formatError(o) {
        if (o.errors) {
            var e = o.errors[0];
            o.error = {
                code: 'request_failed',
                message: e.message
            };
        }
    }

    // Take a cursor and add it to the path
    function paging(res) {
        // Does the response include a 'next_cursor_string'
        if ('next_cursor_str' in res) {
            // See: https://dev.twitter.com/docs/misc/cursoring
            res.paging = {
                next: '?cursor=' + res.next_cursor_str
            };
        }
    }

    function arrayToDataResponse(res) {
        return Array.isArray(res) ? {data: res} : res;
    }

    /**
    // The documentation says to define user in the request
    // Although its not actually required.

    var user_id;

    function withUserId(callback){
        if(user_id){
            callback(user_id);
        }
        else{
            hello.api('twitter:/me', function(o){
                user_id = o.id;
                callback(o.id);
            });
        }
    }

    function sign(url){
        return function(p, callback){
            withUserId(function(user_id){
                callback(url+'?user_id='+user_id);
            });
        };
    }
    */

})(hello);

// Vkontakte (vk.com)
(function(hello) {

    hello.init({

        vk: {
            name: 'Vk',

            // See https://vk.com/dev/oauth_dialog
            oauth: {
                version: 2,
                auth: 'https://oauth.vk.com/authorize',
                grant: 'https://oauth.vk.com/access_token'
            },

            // Authorization scopes
            // See https://vk.com/dev/permissions
            scope: {
                email: 'email',
                friends: 'friends',
                photos: 'photos',
                videos: 'video',
                share: 'share',
                offline_access: 'offline'
            },

            // Refresh the access_token
            refresh: true,

            login: function(p) {
                p.qs.display = window.navigator &&
                    window.navigator.userAgent &&
                    /ipad|phone|phone|android/.test(window.navigator.userAgent.toLowerCase()) ? 'mobile' : 'popup';
            },

            // API Base URL
            base: 'https://api.vk.com/method/',

            // Map GET requests
            get: {
                me: function(p, callback) {
                    p.query.fields = 'id,first_name,last_name,photo_max';
                    callback('users.get');
                }
            },

            wrap: {
                me: function(res, headers, req) {
                    formatError(res);
                    return formatUser(res, req);
                }
            },

            // No XHR
            xhr: false,

            // All requests should be JSONP as of missing CORS headers in https://api.vk.com/method/*
            jsonp: true,

            // No form
            form: false
        }
    });

    function formatUser(o, req) {

        if (o !== null && 'response' in o && o.response !== null && o.response.length) {
            o = o.response[0];
            o.id = o.uid;
            o.thumbnail = o.picture = o.photo_max;
            o.name = o.first_name + ' ' + o.last_name;

            if (req.authResponse && req.authResponse.email !== null)
                o.email = req.authResponse.email;
        }

        return o;
    }

    function formatError(o) {

        if (o.error) {
            var e = o.error;
            o.error = {
                code: e.error_code,
                message: e.error_msg
            };
        }
    }

})(hello);

(function(hello) {

    hello.init({
        windows: {
            name: 'Windows live',

            // REF: http://msdn.microsoft.com/en-us/library/hh243641.aspx
            oauth: {
                version: 2,
                auth: 'https://login.live.com/oauth20_authorize.srf',
                grant: 'https://login.live.com/oauth20_token.srf'
            },

            // Refresh the access_token once expired
            refresh: true,

            logout: function() {
                return 'http://login.live.com/oauth20_logout.srf?ts=' + (new Date()).getTime();
            },

            // Authorization scopes
            scope: {
                basic: 'wl.signin,wl.basic',
                email: 'wl.emails',
                birthday: 'wl.birthday',
                events: 'wl.calendars',
                photos: 'wl.photos',
                videos: 'wl.photos',
                friends: 'wl.contacts_emails',
                files: 'wl.skydrive',
                publish: 'wl.share',
                publish_files: 'wl.skydrive_update',
                share: 'wl.share',
                create_event: 'wl.calendars_update,wl.events_create',
                offline_access: 'wl.offline_access'
            },

            // API base URL
            base: 'https://apis.live.net/v5.0/',

            // Map GET requests
            get: {

                // Friends
                me: 'me',
                'me/friends': 'me/friends',
                'me/following': 'me/contacts',
                'me/followers': 'me/friends',
                'me/contacts': 'me/contacts',

                'me/albums': 'me/albums',

                // Include the data[id] in the path
                'me/album': '@{id}/files',
                'me/photo': '@{id}',

                // Files
                'me/files': '@{parent|me/skydrive}/files',
                'me/folders': '@{id|me/skydrive}/files',
                'me/folder': '@{id|me/skydrive}/files'
            },

            // Map POST requests
            post: {
                'me/albums': 'me/albums',
                'me/album': '@{id}/files/',

                'me/folders': '@{id|me/skydrive/}',
                'me/files': '@{parent|me/skydrive}/files'
            },

            // Map DELETE requests
            del: {
                // Include the data[id] in the path
                'me/album': '@{id}',
                'me/photo': '@{id}',
                'me/folder': '@{id}',
                'me/files': '@{id}'
            },

            wrap: {
                me: formatUser,

                'me/friends': formatFriends,
                'me/contacts': formatFriends,
                'me/followers': formatFriends,
                'me/following': formatFriends,
                'me/albums': formatAlbums,
                'me/photos': formatDefault,
                'default': formatDefault
            },

            xhr: function(p) {
                if (p.method !== 'get' && p.method !== 'delete' && !hello.utils.hasBinary(p.data)) {

                    // Does this have a data-uri to upload as a file?
                    if (typeof (p.data.file) === 'string') {
                        p.data.file = hello.utils.toBlob(p.data.file);
                    }
                    else {
                        p.data = JSON.stringify(p.data);
                        p.headers = {
                            'Content-Type': 'application/json'
                        };
                    }
                }

                return true;
            },

            jsonp: function(p) {
                if (p.method !== 'get' && !hello.utils.hasBinary(p.data)) {
                    p.data.method = p.method;
                    p.method = 'get';
                }
            }
        }
    });

    function formatDefault(o) {
        if ('data' in o) {
            o.data.forEach(function(d) {
                if (d.picture) {
                    d.thumbnail = d.picture;
                }

                if (d.images) {
                    d.pictures = d.images
                        .map(formatImage)
                        .sort(function(a, b) {
                            return a.width - b.width;
                        });
                }
            });
        }

        return o;
    }

    function formatImage(image) {
        return {
            width: image.width,
            height: image.height,
            source: image.source
        };
    }

    function formatAlbums(o) {
        if ('data' in o) {
            o.data.forEach(function(d) {
                d.photos = d.files = 'https://apis.live.net/v5.0/' + d.id + '/photos';
            });
        }

        return o;
    }

    function formatUser(o, headers, req) {
        if (o.id) {
            var token = req.query.access_token;
            if (o.emails) {
                o.email = o.emails.preferred;
            }

            // If this is not an non-network friend
            if (o.is_friend !== false) {
                // Use the id of the user_id if available
                var id = (o.user_id || o.id);
                o.thumbnail = o.picture = 'https://apis.live.net/v5.0/' + id + '/picture?access_token=' + token;
            }
        }

        return o;
    }

    function formatFriends(o, headers, req) {
        if ('data' in o) {
            o.data.forEach(function(d) {
                formatUser(d, headers, req);
            });
        }

        return o;
    }

})(hello);

(function(hello) {

    hello.init({

        yahoo: {

            // Ensure that you define an oauth_proxy
            oauth: {
                version: '1.0a',
                auth: 'https://api.login.yahoo.com/oauth/v2/request_auth',
                request: 'https://api.login.yahoo.com/oauth/v2/get_request_token',
                token: 'https://api.login.yahoo.com/oauth/v2/get_token'
            },

            // Login handler
            login: function(p) {
                // Change the default popup window to be at least 560
                // Yahoo does dynamically change it on the fly for the signin screen (only, what if your already signed in)
                p.options.popup.width = 560;

                // Yahoo throws an parameter error if for whatever reason the state.scope contains a comma, so lets remove scope
                try {delete p.qs.state.scope;}
                catch (e) {}
            },

            base: 'https://social.yahooapis.com/v1/',

            get: {
                me: yql('select * from social.profile(0) where guid=me'),
                'me/friends': yql('select * from social.contacts(0) where guid=me'),
                'me/following': yql('select * from social.contacts(0) where guid=me')
            },
            wrap: {
                me: formatUser,

                // Can't get IDs
                // It might be better to loop through the social.relationship table with has unique IDs of users.
                'me/friends': formatFriends,
                'me/following': formatFriends,
                'default': paging
            }
        }
    });

    /*
        // Auto-refresh fix: bug in Yahoo can't get this to work with node-oauth-shim
        login : function(o){
            // Is the user already logged in
            var auth = hello('yahoo').getAuthResponse();

            // Is this a refresh token?
            if(o.options.display==='none'&&auth&&auth.access_token&&auth.refresh_token){
                // Add the old token and the refresh token, including path to the query
                // See http://developer.yahoo.com/oauth/guide/oauth-refreshaccesstoken.html
                o.qs.access_token = auth.access_token;
                o.qs.refresh_token = auth.refresh_token;
                o.qs.token_url = 'https://api.login.yahoo.com/oauth/v2/get_token';
            }
        },
    */

    function formatError(o) {
        if (o && 'meta' in o && 'error_type' in o.meta) {
            o.error = {
                code: o.meta.error_type,
                message: o.meta.error_message
            };
        }
    }

    function formatUser(o) {

        formatError(o);
        if (o.query && o.query.results && o.query.results.profile) {
            o = o.query.results.profile;
            o.id = o.guid;
            o.last_name = o.familyName;
            o.first_name = o.givenName || o.nickname;
            var a = [];
            if (o.first_name) {
                a.push(o.first_name);
            }

            if (o.last_name) {
                a.push(o.last_name);
            }

            o.name = a.join(' ');
            o.email = (o.emails && o.emails[0]) ? o.emails[0].handle : null;
            o.thumbnail = o.image ? o.image.imageUrl : null;
        }

        return o;
    }

    function formatFriends(o, headers, request) {
        formatError(o);
        paging(o, headers, request);
        var contact;
        var field;
        if (o.query && o.query.results && o.query.results.contact) {
            o.data = o.query.results.contact;
            delete o.query;

            if (!Array.isArray(o.data)) {
                o.data = [o.data];
            }

            o.data.forEach(formatFriend);
        }

        return o;
    }

    function formatFriend(contact) {
        contact.id = null;

        // #362: Reports of responses returning a single item, rather than an Array of items.
        // Format the contact.fields to be an array.
        if (contact.fields && !(contact.fields instanceof Array)) {
            contact.fields = [contact.fields];
        }

        (contact.fields || []).forEach(function(field) {
            if (field.type === 'email') {
                contact.email = field.value;
            }

            if (field.type === 'name') {
                contact.first_name = field.value.givenName;
                contact.last_name = field.value.familyName;
                contact.name = field.value.givenName + ' ' + field.value.familyName;
            }

            if (field.type === 'yahooid') {
                contact.id = field.value;
            }
        });
    }

    function paging(res, headers, request) {

        // See: http://developer.yahoo.com/yql/guide/paging.html#local_limits
        if (res.query && res.query.count && request.options) {
            res.paging = {
                next: '?start=' + (res.query.count + (+request.options.start || 1))
            };
        }

        return res;
    }

    function yql(q) {
        return 'https://query.yahooapis.com/v1/yql?q=' + (q + ' limit @{limit|100} offset @{start|0}').replace(/\s/g, '%20') + '&format=json';
    }

})(hello);

// Register as anonymous AMD module
if (typeof define === 'function' && define.amd) {
    define(function() {
        return hello;
    });
}

// CommonJS module for browserify
if (typeof module === 'object' && module.exports) {
    module.exports = hello;
}