|
|
|
// 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} );
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.append($('<a></a>').attr('href', 'tox:'+text).text(text));
|
|
|
|
}
|
|
|
|
}, 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.append($('<a></a>').attr('href', 'bitmsg:'+text).text(text));
|
|
|
|
}
|
|
|
|
}, 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( 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});
|
|
|
|
}
|
|
|
|
|