You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
421 lines
15 KiB
421 lines
15 KiB
// twister_io.js |
|
// 2013 Miguel Freitas |
|
// |
|
// low-level twister i/o. |
|
// implements requests of dht resources. multiple pending requests to the same resource are joined. |
|
// cache results (profile, avatar, etc) in memory. |
|
// avatars are cached in localstored (expiration = 24 hours) |
|
|
|
// main json rpc method. receives callbacks for success and error |
|
function twisterRpc(method, params, resultFunc, resultArg, errorFunc, errorArg) { |
|
var foo = new $.JsonRpcClient({ ajaxUrl: '/', username: 'user', password: 'pwd'}); |
|
foo.call(method, params, |
|
function(ret) { resultFunc(resultArg, ret); }, |
|
function(ret) { if(ret != null) errorFunc(errorArg, ret); } |
|
); |
|
} |
|
|
|
// join multiple dhtgets to the same resources in this map |
|
var _dhtgetPendingMap = {}; |
|
|
|
// memory cache for profile and avatar |
|
var _profileMap = {}; |
|
var _avatarMap = {}; |
|
|
|
// number of dhtgets in progress (requests to the daemon) |
|
var _dhtgetsInProgress = 0; |
|
|
|
// keep _maxDhtgets smaller than the number of daemon/browser sockets |
|
// most browsers limit to 6 per domain (see http://www.browserscope.org/?category=network) |
|
var _maxDhtgets = 5; |
|
|
|
// requests not yet sent to the daemon due to _maxDhtgets limit |
|
var _queuedDhtgets = []; |
|
|
|
// private function to define a key in _dhtgetPendingMap |
|
function _dhtgetLocator(username, resource, multi) { |
|
return username+";"+resource+";"+multi; |
|
} |
|
|
|
function _dhtgetAddPending(locator, cbFunc, cbArg) |
|
{ |
|
if( !(locator in _dhtgetPendingMap) ) { |
|
_dhtgetPendingMap[locator] = []; |
|
} |
|
_dhtgetPendingMap[locator].push( {cbFunc:cbFunc, cbArg:cbArg} ); |
|
} |
|
|
|
function _dhtgetProcessPending(locator, multi, ret) |
|
{ |
|
if( locator in _dhtgetPendingMap ) { |
|
for( var i = 0; i < _dhtgetPendingMap[locator].length; i++) { |
|
var cbFunc = _dhtgetPendingMap[locator][i].cbFunc; |
|
var cbArg = _dhtgetPendingMap[locator][i].cbArg; |
|
|
|
if( multi == 's' ) { |
|
if( ret[0] != undefined ) { |
|
cbFunc(cbArg, ret[0]["p"]["v"], ret); |
|
} else { |
|
cbFunc(cbArg, null); |
|
} |
|
} else { |
|
var multiret = []; |
|
for (var j = 0; j < ret.length; j++) { |
|
multiret.push(ret[j]["p"]["v"]); |
|
} |
|
cbFunc(cbArg, multiret, ret); |
|
} |
|
} |
|
delete _dhtgetPendingMap[locator]; |
|
} else { |
|
console.log("warning: _dhtgetProcessPending with unknown locator "+locator); |
|
} |
|
} |
|
|
|
function _dhtgetAbortPending(locator) |
|
{ |
|
if( locator in _dhtgetPendingMap ) { |
|
for( var i = 0; i < _dhtgetPendingMap[locator].length; i++) { |
|
var cbFunc = _dhtgetPendingMap[locator][i].cbFunc; |
|
var cbArg = _dhtgetPendingMap[locator][i].cbArg; |
|
cbFunc(cbArg, null); |
|
} |
|
delete _dhtgetPendingMap[locator]; |
|
} else { |
|
console.log("warning: _dhtgetAbortPending with unknown locator "+locator); |
|
} |
|
} |
|
|
|
// get data from dht resource |
|
// the value ["v"] is extracted from response and returned to callback |
|
// null is passed to callback in case of an error |
|
function dhtget( username, resource, multi, cbFunc, cbArg, timeoutArgs ) { |
|
var locator = _dhtgetLocator(username, resource, multi); |
|
if( locator in _dhtgetPendingMap) { |
|
_dhtgetAddPending(locator, cbFunc, cbArg); |
|
} else { |
|
_dhtgetAddPending(locator, cbFunc, cbArg); |
|
// limit the number of simultaneous dhtgets. |
|
// this should leave some sockets for other non-blocking daemon requests. |
|
if( _dhtgetsInProgress < _maxDhtgets ) { |
|
_dhtgetInternal( username, resource, multi, timeoutArgs ); |
|
} else { |
|
// just queue the locator. it will be unqueue when some dhtget completes. |
|
_queuedDhtgets.push(locator); |
|
} |
|
} |
|
} |
|
|
|
function _dhtgetInternal( username, resource, multi, timeoutArgs ) { |
|
var locator = _dhtgetLocator(username, resource, multi); |
|
_dhtgetsInProgress++; |
|
argsList = [username,resource,multi]; |
|
if( typeof timeoutArgs !== 'undefined' ) { |
|
argsList = argsList.concat(timeoutArgs); |
|
} |
|
twisterRpc("dhtget", argsList, |
|
function(args, ret) { |
|
_dhtgetsInProgress--; |
|
_dhtgetProcessPending(args.locator, args.multi, ret); |
|
_dhtgetDequeue(); |
|
}, {locator:locator,multi:multi}, |
|
function(cbArg, ret) { |
|
console.log("ajax error:" + ret); |
|
_dhtgetsInProgress--; |
|
_dhtgetAbortPending(locator); |
|
_dhtgetDequeue(); |
|
}, locator); |
|
} |
|
|
|
function _dhtgetDequeue() { |
|
if( _queuedDhtgets.length ) { |
|
var locatorSplit = _queuedDhtgets.pop().split(";"); |
|
_dhtgetInternal(locatorSplit[0], locatorSplit[1], locatorSplit[2]); |
|
} |
|
} |
|
|
|
// removes queued dhtgets (requests that have not been made to the daemon) |
|
// this is used by user search dropdown to discard old users we are not interested anymore |
|
function removeUserFromDhtgetQueue(username) { |
|
var resources = ["profile","avatar"] |
|
for (var i = 0; i < resources.length; i++) { |
|
var locator = _dhtgetLocator(username,resources[i],"s"); |
|
var locatorIndex = _queuedDhtgets.indexOf(locator); |
|
if( locatorIndex > -1 ) { |
|
_queuedDhtgets.splice(locatorIndex,1); |
|
delete _dhtgetPendingMap[locator]; |
|
} |
|
} |
|
} |
|
|
|
function removeUsersFromDhtgetQueue(users) { |
|
for (var i = 0; i < users.length; i++ ) { |
|
removeUserFromDhtgetQueue( users[i] ); |
|
} |
|
} |
|
|
|
// store value at the dht resource |
|
function dhtput( username, resource, multi, value, sig_user, seq, cbFunc, cbArg ) { |
|
twisterRpc("dhtput", [username,resource,multi, value, sig_user, seq], |
|
function(args, ret) { |
|
if( args.cbFunc ) |
|
args.cbFunc(args.cbArg, true); |
|
}, {cbFunc:cbFunc, cbArg:cbArg}, |
|
function(args, ret) { |
|
console.log("ajax error:" + ret); |
|
if( args.cbFunc ) |
|
args.cbFunc(args.cbArg, false); |
|
}, cbArg); |
|
} |
|
|
|
// get something from profile and store it in item.text or do callback |
|
function getProfileResource( username, resource, item, cbFunc, cbArg ){ |
|
var profile = undefined; |
|
if( username in _profileMap ) { |
|
profile = _profileMap[username]; |
|
} else { |
|
profile = _getResourceFromStorage("profile:" + username); |
|
} |
|
if( profile ) { |
|
_profileMap[username] = profile; |
|
if( item ) |
|
item.text(profile[resource]); |
|
if( cbFunc ) |
|
cbFunc(cbArg, profile[resource]); |
|
} else { |
|
dhtget( username, "profile", "s", |
|
function(args, profile) { |
|
if( profile ) { |
|
_profileMap[args.username] = profile; |
|
_putResourceIntoStorage("profile:" + username, profile); |
|
if( args.item ) |
|
args.item.text(profile[resource]); |
|
if( args.cbFunc ) |
|
args.cbFunc(args.cbArg, profile[resource]); |
|
} else { |
|
if( args.cbFunc ) |
|
args.cbFunc(args.cbArg, null); |
|
} |
|
}, {username:username,item:item,cbFunc:cbFunc,cbArg:cbArg}); |
|
} |
|
} |
|
|
|
// get fullname and store it in item.text |
|
function getFullname( username, item ){ |
|
// Set the username first in case the profile has no fullname |
|
item.text(username); |
|
getProfileResource( username, "fullname", undefined, |
|
function(args, value) { |
|
if( value ) { |
|
value.replace(/^\s+|\s+$/g, ''); |
|
if( value.length ) |
|
args.item.text(value); |
|
} |
|
}, {item: item} ); |
|
if (typeof(twisterFollowingO) !== 'undefined' && |
|
($.Options.getIsFollowingMeOpt() === 'everywhere' || item.hasClass('profile-name'))) { |
|
if (twisterFollowingO.knownFollowers.indexOf(username) > -1) { |
|
item.addClass('isFollowing'); |
|
item.attr("title", polyglot.t("follows you")); |
|
} else if (twisterFollowingO.notFollowers.indexOf(username) > -1) |
|
item.addClass("notFollowing"); |
|
else { |
|
loadFollowingFromDht(username, 1, [], 0, function (args, following, seqNum) { |
|
if (following.indexOf(args.user) > -1) { |
|
item.addClass('isFollowing'); |
|
item.attr("title", polyglot.t("follows you")); |
|
if (twisterFollowingO.knownFollowers.indexOf(args.username) < 0) |
|
twisterFollowingO.knownFollowers.push(args.username); |
|
} else { |
|
item.addClass('notFollowing'); |
|
if (twisterFollowingO.notFollowers.indexOf(args.username) < 0) |
|
twisterFollowingO.notFollowers.push(args.username); |
|
} |
|
|
|
//storeFollowingSessionData(); |
|
twisterFollowingO.save(); |
|
}, {"user": defaultScreenName, "item": item, "username": username}); |
|
} |
|
|
|
$(".open-followers").attr("title", twisterFollowingO.knownFollowers.length.toString()); |
|
} |
|
} |
|
|
|
// get bio and store it in item.text |
|
function getBio( username, item ){ |
|
getProfileResource( username, "bio", item); |
|
} |
|
|
|
// get tox address and store it in item.text |
|
function getTox( username, item ){ |
|
getProfileResource( username, "tox", false, function(item, text){ |
|
item.empty(); |
|
if(text) { |
|
item.attr('href', 'tox:'+text); |
|
item.next().attr('data', text); |
|
item.parent().show().parent().show(); |
|
} |
|
}, item); |
|
} |
|
|
|
// get bitmessage address and store it in item.text |
|
function getBitmessage( username, item ){ |
|
getProfileResource( username, "bitmessage", false, function(item, text){ |
|
item.empty(); |
|
if(text) { |
|
item.attr('href', 'bitmsg:'+text+'?action=add&label='+username); |
|
item.next().attr('data', text); |
|
item.parent().show().parent().show(); |
|
} |
|
}, item); |
|
} |
|
|
|
// get location and store it in item.text |
|
function getLocation( username, item ){ |
|
getProfileResource( username, "location", item); |
|
} |
|
|
|
// get location and store it in item.text |
|
function getWebpage( username, item ){ |
|
getProfileResource( username, "url", item, |
|
function(args, val) { |
|
if(typeof(val) !== 'undefined') { |
|
if (val.indexOf("://") < 0) { |
|
val = "http://" + val; |
|
} |
|
args.item.attr("href", val); |
|
} |
|
}, {item:item} ); |
|
} |
|
|
|
// we must cache avatar results to disk to lower bandwidth on |
|
// other peers. dht server limits udp rate so requesting too much |
|
// data will only cause new requests to fail. |
|
function _getResourceFromStorage(locator) { |
|
var storage = $.localStorage; |
|
if( storage.isSet(locator) ) { |
|
var storedResource = storage.get(locator); |
|
var curTime = new Date().getTime() / 1000; |
|
// avatar is downloaded once per day |
|
if( storedResource.time + 3600*24 > curTime ) { |
|
return storedResource.data; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
function _putResourceIntoStorage(locator, data) { |
|
var curTime = new Date().getTime() / 1000; |
|
var storedResource = {time: curTime, data: data}; |
|
|
|
var storage = $.localStorage; |
|
storage.set(locator, storedResource); |
|
} |
|
|
|
// get avatar and set it in img.attr("src") |
|
function getAvatar( username, img ){ |
|
if( username == "nobody" ) { |
|
img.attr('src', "img/tornado_avatar.png"); |
|
return; |
|
} |
|
|
|
if( username in _avatarMap ) { |
|
//img.attr('src', "data:image/jpg;base64,"+avatarMap[username]); |
|
img.attr('src', _avatarMap[username]); |
|
} else { |
|
var data = _getResourceFromStorage("avatar:" + username); |
|
if( data ) { |
|
_avatarMap[username] = data; |
|
img.attr('src', data); |
|
} else { |
|
dhtget( username, "avatar", "s", |
|
function(args, imagedata) { |
|
if( imagedata && imagedata.length ) { |
|
_avatarMap[args.username] = imagedata; |
|
_putResourceIntoStorage("avatar:" + username, imagedata); |
|
args.img.attr('src', imagedata); |
|
} |
|
}, {username:username,img:img} ); |
|
} |
|
} |
|
} |
|
|
|
function clearAvatarAndProfileCache(username) { |
|
var storage = $.localStorage; |
|
storage.remove("avatar:" + username); |
|
storage.remove("profile:" + username); |
|
if( username in _avatarMap ) { |
|
delete _avatarMap[username]; |
|
} |
|
if( username in _profileMap ) { |
|
delete _profileMap[username]; |
|
} |
|
} |
|
|
|
// get estimative for number of followers (use known peers of torrent tracker) |
|
function getFollowers( username, item ) { |
|
dhtget( username, "tracker", "m", |
|
function(args, ret) { |
|
if( ret && ret.length && ret[0]["followers"] ) { |
|
args.item.text(ret[0]["followers"]) |
|
} |
|
}, {username:username,item:item} ); |
|
} |
|
|
|
function getPostsCount( username, item ) { |
|
dhtget( username, "status", "s", |
|
function(args, v) { |
|
var count = 0; |
|
if( v && v["userpost"] ) { |
|
count = v["userpost"]["k"]+1; |
|
} |
|
var oldCount = parseInt(args.item.text()); |
|
if( !oldCount || count > oldCount ) { |
|
args.item.text(count); |
|
} |
|
if( username == defaultScreenName && count ) { |
|
incLastPostId( v["userpost"]["k"] ); |
|
} |
|
}, {username:username,item:item} ); |
|
} |
|
|
|
|
|
function checkPubkeyExists(username, cbFunc, cbArg) { |
|
// pubkey is checked in block chain db. |
|
// so only accepted registrations are reported (local wallet users are not) |
|
twisterRpc("dumppubkey", [username], |
|
function(args, ret) { |
|
args.cbFunc(args.cbArg, ret.length > 0); |
|
}, {cbFunc:cbFunc, cbArg:cbArg}, |
|
function(args, ret) { |
|
alert(polyglot.t("error_connecting_to_daemon")); |
|
}, {cbFunc:cbFunc, cbArg:cbArg}); |
|
} |
|
|
|
// pubkey is obtained from block chain db. |
|
// so only accepted registrations are reported (local wallet users are not) |
|
// cbFunc is called as cbFunc(cbArg, pubkey) |
|
// if user doesn't exist then pubkey.length == 0 |
|
function dumpPubkey(username, cbFunc, cbArg) { |
|
twisterRpc("dumppubkey", [username], |
|
function(args, ret) { |
|
args.cbFunc(args.cbArg, ret); |
|
}, {cbFunc:cbFunc, cbArg:cbArg}, |
|
function(args, ret) { |
|
alert(polyglot.t("error_connecting_to_daemon")); |
|
}, {cbFunc:cbFunc, cbArg:cbArg}); |
|
} |
|
|
|
// privkey is obtained from wallet db |
|
// so privkey is returned even for unsent transactions |
|
function dumpPrivkey(username, cbFunc, cbArg) { |
|
twisterRpc("dumpprivkey", [username], |
|
function(args, ret) { |
|
args.cbFunc(args.cbArg, ret); |
|
}, {cbFunc:cbFunc, cbArg:cbArg}, |
|
function(args, ret) { |
|
args.cbFunc(args.cbArg, ""); |
|
console.log("dumpprivkey: user unknown"); |
|
}, {cbFunc:cbFunc, cbArg:cbArg}); |
|
} |
|
|
|
|