ActivityObject.ensureObject = function(obj, callback) { var type = obj.objectType, Cls, id = obj.id, url = obj.url, tryDiscover = function(obj, cb) { Step( function() { ActivityObject.discover(obj, this); }, function(err, remote) { if (err) { tryCreate(obj, cb); } else { tryCreate(remote, cb); } } ); }, tryCreate = function(obj, cb) { Step( function() { Cls.create(obj, this); }, function(err, result) { if (err && err.name == "AlreadyExistsError") { ActivityObject.ensureObject(obj, cb); } else if (err) { cb(err, null); } else { cb(null, result); } } ); }, softGet = function(Cls, id, cb) { Step( function() { Cls.get(id, this); }, function(err, result) { if (err && err.name == "NoSuchThingError") { cb(null, null); } else if (!err) { cb(null, result); } else { cb(err, null); } } ); }, findOne = function(Cls, criteria, cb) { Step( function() { Cls.search(criteria, this); }, function(err, results) { if (err) throw err; if (!results || results.length === 0) { cb(null, null); } else { cb(null, results[0]); } } ); }; // Since this is a major entry point, check our arguments if (!_.isString(id) && !_.isUndefined(id)) { callback(new TypeError("ID is not a string: " + id), null); return; } if (!_.isString(url) && !_.isUndefined(url)) { callback(new TypeError("URL is not a string: " + url), null); return; } if (!_.isString(type)) { callback(new TypeError("Type is not a string: " + type), null); return; } Cls = ActivityObject.toClass(type); Step( function() { if (_.isString(id)) { softGet(Cls, id, this); } else if (_.isString(url)) { // XXX: we could use other fields here to guide search findOne(Cls, {url: url}, this); } else { // XXX: without a unique identifier, just punt this(null, null); } }, function(err, result) { var delta; if (err) throw err; if (!result) { if (!ActivityObject.isLocal(obj)) { tryDiscover(obj, callback); } else { // XXX: Log this; it's unusual tryCreate(obj, callback); } } else if (!ActivityObject.isLocal(obj) && !ActivityObject.isReference(obj) && (ActivityObject.isReference(result) || obj.updated > result.updated)) { delta = ActivityObject.delta(result, obj); result.update(delta, function(err) { if (err) { callback(err, null); } else { callback(null, result); } }); } else { callback(null, result); } } ); };
ActivityObject.ensureObject = function(obj, callback) { var type = obj.objectType, Cls, id = obj.id, url = obj.url, getRemoteObject = function(url, cb) { var Host = require("./host").Host; Step( function() { var hostname = urlparse(url).hostname; Host.ensureHost(hostname, this); }, function(err, host) { if (err) throw err; host.getOAuth(this); }, function(err, oa) { if (err) throw err; oa.get(url, null, null, this); }, function(err, body, resp) { var parsed; if (err) throw err; if (resp.statusCode != 200) { throw new Error("Bad status code: " + resp.statusCode); } if (!resp.headers["content-type"] || resp.headers["content-type"].substr(0, 16) != "application/json") { throw new Error("Bad content type: " + resp.headers["content-type"]); } parsed = JSON.parse(body); this(null, parsed); }, cb ); }, mergeLinks = function(jrd, obj) { var feeds = ["followers", "following", "links", "favorites", "likes", "replies", "shares"]; _.each(jrd.links, function(link) { var rel = link.rel; if (_.contains(feeds, rel)) { obj[rel] = {url: link.href}; } else { if (!obj.links) { obj.links = {}; } obj.links[rel] = {href: link.href}; } }); }, tryDiscover = function(obj, cb) { var proto = ActivityObject.protocolOf(obj.id), wf; if (!_.contains(["http", "https", "acct"], proto) || ActivityObject.isDomainToSkip(ActivityObject.domainOf(obj.id))) { tryCreate(obj, cb); return; } wf = require("webfinger"); Step( function() { wf.webfinger(obj.id, this); }, function(err, jrd) { var selfies; if (err) throw err; mergeLinks(jrd, obj); selfies = _.filter(jrd.links, function(link) { return link.rel == "self"; }); if (selfies.length > 0) { getRemoteObject(selfies[0].href, this); } else { this(null, obj); } }, function(err, remote) { if (err) { tryCreate(obj, cb); } else { tryCreate(remote, cb); } } ); }, tryCreate = function(obj, cb) { Step( function() { Cls.create(obj, this); }, function(err, result) { if (err && err.name == "AlreadyExistsError") { ActivityObject.ensureObject(obj, cb); } else if (err) { cb(err, null); } else { cb(null, result); } } ); }, softGet = function(Cls, id, cb) { Step( function() { Cls.get(id, this); }, function(err, result) { if (err && err.name == "NoSuchThingError") { cb(null, null); } else if (!err) { cb(null, result); } else { cb(err, null); } } ); }, findOne = function(Cls, criteria, cb) { Step( function() { Cls.search(criteria, this); }, function(err, results) { if (err) throw err; if (!results || results.length == 0) { cb(null, null); } else { cb(null, results[0]); } } ); }; // Since this is a major entry point, check our arguments if (!_.isString(id) && !_.isUndefined(id)) { callback(new TypeError("ID is not a string: " + id), null); return; } if (!_.isString(url) && !_.isUndefined(url)) { callback(new TypeError("URL is not a string: " + url), null); return; } if (!_.isString(type)) { callback(new TypeError("Type is not a string: " + type), null); return; } Cls = ActivityObject.toClass(type); Step( function() { if (_.isString(id)) { softGet(Cls, id, this); } else if (_.isString(url)) { // XXX: we could use other fields here to guide search findOne(Cls, {url: url}, this); } else { // XXX: without a unique identifier, just punt this(null, null); } }, function(err, result) { var delta; if (err) throw err; if (!result) { if (!ActivityObject.isLocal(obj)) { tryDiscover(obj, callback); } else { // XXX: Log this; it's unusual tryCreate(obj, callback); } } else if (!ActivityObject.isLocal(obj) && !ActivityObject.isReference(obj) && (ActivityObject.isReference(result) || obj.updated > result.updated)) { delta = ActivityObject.delta(result, obj); result.update(delta, function(err) { if (err) { callback(err, null); } else { callback(null, result); } }); } else { callback(null, result); } } ); };