app/js/components/hello.all.js
/*! 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;
}