mirror of
https://github.com/RetroShare/RSNewWebUI.git
synced 2026-09-13 18:45:49 +05:00
499 lines
13 KiB
JavaScript
499 lines
13 KiB
JavaScript
const m = require('mithril');
|
|
|
|
const RsEventsType = {
|
|
NONE: 0, // Used internally to detect invalid event type passed
|
|
|
|
// @see RsBroadcastDiscovery
|
|
BROADCAST_DISCOVERY: 1,
|
|
|
|
// @see RsDiscPendingPgpReceivedEvent
|
|
GOSSIP_DISCOVERY: 2,
|
|
|
|
// @see AuthSSL
|
|
AUTHSSL_CONNECTION_AUTENTICATION: 3,
|
|
|
|
// @see pqissl
|
|
PEER_STATE: 4,
|
|
|
|
// @see RsGxsChanges, used also in @see RsGxsBroadcast
|
|
GXS_CHANGES: 5,
|
|
|
|
// Emitted when a peer state changes, @see RsPeers
|
|
_________UNUSED___001_: 6,
|
|
|
|
// @see RsMailStatusEvent
|
|
MAIL_STATUS: 7,
|
|
|
|
// @see RsGxsCircleEvent
|
|
GXS_CIRCLES: 8,
|
|
|
|
// @see RsGxsChannelEvent
|
|
GXS_CHANNELS: 9,
|
|
|
|
// @see RsGxsForumEvent
|
|
GXS_FORUMS: 10,
|
|
|
|
// @see RsGxsPostedEvent
|
|
GXS_POSTED: 11,
|
|
|
|
// @see RsGxsPostedEvent
|
|
GXS_IDENTITY: 12,
|
|
|
|
// @see RsFiles @deprecated
|
|
SHARED_DIRECTORIES: 13,
|
|
|
|
// @see RsFiles
|
|
FILE_TRANSFER: 14,
|
|
|
|
// @see RsChats
|
|
CHAT_SERVICE: 15,
|
|
|
|
// @see rspeers.h
|
|
NETWORK: 16,
|
|
|
|
// @see RsMailTagEvent
|
|
MAIL_TAG: 17,
|
|
|
|
/** Emitted to update library clients about file hashing being completed */
|
|
_________UNUSED___002_: 20,
|
|
|
|
// @see rspeers.h
|
|
TOR_MANAGER: 21,
|
|
|
|
// @see rsfriendserver.h
|
|
FRIEND_SERVER: 22,
|
|
|
|
// _MAX //used internally, keep last
|
|
};
|
|
|
|
const API_URL = 'http://127.0.0.1:9092';
|
|
const loginKey = {
|
|
username: sessionStorage.getItem('rs_username') || '',
|
|
passwd: sessionStorage.getItem('rs_passwd') || '',
|
|
isVerified: sessionStorage.getItem('rs_isVerified') === 'true',
|
|
url: sessionStorage.getItem('rs_url') || API_URL,
|
|
};
|
|
|
|
// Make this as object property?
|
|
function setKeys(username, password, url = API_URL, verified = true) {
|
|
loginKey.username = username;
|
|
loginKey.passwd = password;
|
|
loginKey.url = url;
|
|
loginKey.isVerified = verified;
|
|
|
|
if (verified) {
|
|
sessionStorage.setItem('rs_username', username);
|
|
sessionStorage.setItem('rs_passwd', password);
|
|
sessionStorage.setItem('rs_url', url);
|
|
sessionStorage.setItem('rs_isVerified', 'true');
|
|
} else {
|
|
sessionStorage.removeItem('rs_isVerified');
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
setKeys('', '', loginKey.url, false);
|
|
m.route.set('/');
|
|
}
|
|
|
|
const connectionState = {
|
|
status: true,
|
|
};
|
|
|
|
function rsJsonApiRequest(
|
|
path,
|
|
data = {},
|
|
callback = () => { },
|
|
async = true,
|
|
headers = {},
|
|
handleDeserialize = JSON.parse,
|
|
handleSerialize = JSON.stringify,
|
|
config = null
|
|
) {
|
|
headers['Accept'] = 'application/json';
|
|
if (loginKey.isVerified) {
|
|
if (loginKey.username && loginKey.passwd) {
|
|
headers['Authorization'] = 'Basic ' + btoa(loginKey.username + ':' + loginKey.passwd);
|
|
}
|
|
}
|
|
// NOTE: After upgrading to mithrilv2, options.extract is no longer required
|
|
// since the status will become part of return value and then
|
|
// handleDeserialize can also be simply passed as options.deserialize
|
|
return m
|
|
.request({
|
|
method: 'POST',
|
|
url: loginKey.url + path,
|
|
async,
|
|
extract: (xhr) => {
|
|
// Empty string is not valid json and fails on parse
|
|
const response = xhr.responseText || '""';
|
|
return {
|
|
status: xhr.status,
|
|
statusText: xhr.statusText,
|
|
body: handleDeserialize(response),
|
|
};
|
|
},
|
|
serialize: handleSerialize,
|
|
headers,
|
|
body: data,
|
|
|
|
xhr: config,
|
|
})
|
|
.then((result) => {
|
|
if (result.status === 200) {
|
|
connectionState.status = true;
|
|
try {
|
|
callback(result.body, true);
|
|
} catch (e) {
|
|
console.error('[RS] Error in success callback for path:', path, e);
|
|
}
|
|
} else {
|
|
connectionState.status = false;
|
|
if (result.status === 401 || result.status === 403) {
|
|
setKeys(loginKey.username, loginKey.passwd, loginKey.url, false);
|
|
m.route.set('/');
|
|
} else if (result.status === 0) {
|
|
console.error('[RS] Retroshare-jsonapi not available.');
|
|
} else {
|
|
console.error('[RS] HTTP error:', result.status, result.statusText);
|
|
}
|
|
try {
|
|
callback(result, false);
|
|
} catch (e) {
|
|
console.error('[RS] Error in error callback for path:', path, e);
|
|
}
|
|
}
|
|
return result;
|
|
})
|
|
.catch(function (e) {
|
|
connectionState.status = false;
|
|
try {
|
|
callback(e, false);
|
|
} catch (cbErr) {
|
|
// console.error('[RS] Error in catch callback for path:', path, cbErr);
|
|
}
|
|
console.error('[RS] Error: While sending request for path:', path, '\ninfo:', e);
|
|
});
|
|
}
|
|
|
|
function setBackgroundTask(task, interval, taskInScope) {
|
|
// Always use bound(.bind) function when accsssing outside objects
|
|
// to avoid loss of scope
|
|
task();
|
|
let taskId = setTimeout(function caller() {
|
|
if (taskInScope()) {
|
|
task();
|
|
taskId = setTimeout(caller, interval);
|
|
} else {
|
|
clearTimeout(taskId);
|
|
}
|
|
}, interval);
|
|
return taskId;
|
|
}
|
|
|
|
function computeIfMissing(map, key, missing = () => ({})) {
|
|
if (!Object.prototype.hasOwnProperty.call(map, key)) {
|
|
map[key] = missing();
|
|
}
|
|
return map[key];
|
|
}
|
|
|
|
function deeperIfExist(map, key, action) {
|
|
if (Object.prototype.hasOwnProperty.call(map, key)) {
|
|
action(map[key]);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const eventQueue = {
|
|
events: {
|
|
[RsEventsType.CHAT_SERVICE]: {
|
|
// Chat-Messages
|
|
types: {
|
|
// #define RS_CHAT_TYPE_PUBLIC 1
|
|
// #define RS_CHAT_TYPE_PRIVATE 2
|
|
|
|
1: (cid) => hexId(cid),
|
|
2: (cid) => hexId(cid),
|
|
3: (cid) => hexId(cid),
|
|
4: (cid) => hexId(cid),
|
|
},
|
|
messages: {},
|
|
chatMessages: (chatId, owner, action) => {
|
|
if (
|
|
!deeperIfExist(owner.types, chatId.type, (keyfn) =>
|
|
action(
|
|
computeIfMissing(
|
|
computeIfMissing(owner.messages, chatId.type),
|
|
keyfn(chatId),
|
|
|
|
() => []
|
|
)
|
|
)
|
|
)
|
|
) {
|
|
if (chatId) {
|
|
// Silent match
|
|
}
|
|
}
|
|
},
|
|
handler: (event, owner) => {
|
|
if (event && event.mChatMessage && event.mChatMessage.chat_id) {
|
|
owner.chatMessages(event.mChatMessage.chat_id, owner, (r) => {
|
|
r.push(event.mChatMessage);
|
|
owner.notify(event.mChatMessage);
|
|
});
|
|
} else if (event && event.mCid) {
|
|
// Administrative chat event (e.g. lobby info change, peer join/leave)
|
|
// Silent for now to avoid console spam, as actual messages use mChatMessage
|
|
}
|
|
},
|
|
notify: () => { },
|
|
},
|
|
[RsEventsType.GXS_CIRCLES]: {
|
|
// Circles (ignore in the meantime)
|
|
handler: (event, owner) => { },
|
|
},
|
|
[RsEventsType.SHARED_DIRECTORIES]: {
|
|
// Deprecated/Administrative (ignore quietly)
|
|
handler: (event, owner) => { },
|
|
},
|
|
},
|
|
handler: (event) => {
|
|
if (!deeperIfExist(eventQueue.events, event.mType, (owner) => owner.handler(event, owner))) {
|
|
// Ignore unhandled events silently
|
|
}
|
|
},
|
|
};
|
|
|
|
const userList = {
|
|
users: [],
|
|
userMap: {},
|
|
pendingIds: new Set(),
|
|
fetchTimer: null,
|
|
|
|
triggerFetch: () => {
|
|
if (userList.fetchTimer) return;
|
|
userList.fetchTimer = setTimeout(() => {
|
|
userList.fetchTimer = null;
|
|
if (userList.pendingIds.size === 0) return;
|
|
|
|
const ids = Array.from(userList.pendingIds);
|
|
userList.pendingIds.clear();
|
|
|
|
userList.fetchBulk(ids);
|
|
}, 1000);
|
|
},
|
|
|
|
fetchBulk: (ids) => {
|
|
// Chunk requests to avoid too large payloads if necessary, but for now 100 is safe
|
|
const chunkSize = 100;
|
|
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
const chunk = ids.slice(i, i + chunkSize);
|
|
rsJsonApiRequest('/rsIdentity/getIdentitiesInfo', { ids: chunk }, (data, success) => {
|
|
if (success && data.idsInfo) {
|
|
data.idsInfo.forEach((info) => {
|
|
const gid = info.mMeta && info.mMeta.mGroupId;
|
|
if (gid) {
|
|
userList.userMap[gid] = {
|
|
name: info.mMeta.mGroupName,
|
|
isContact: info.mIsAContact,
|
|
};
|
|
}
|
|
});
|
|
m.redraw();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
loadUsers: () => {
|
|
rsJsonApiRequest('/rsIdentity/getIdentitiesSummaries', {}, (list) => {
|
|
if (list !== undefined && list.ids) {
|
|
userList.users = list.ids;
|
|
userList.userMap = list.ids.reduce((a, c) => {
|
|
a[c.mGroupId] = { name: c.mGroupName, isContact: false };
|
|
return a;
|
|
}, {});
|
|
|
|
// Fetch contact status and details in bulk immediately
|
|
userList.fetchBulk(list.ids.map((u) => u.mGroupId));
|
|
}
|
|
});
|
|
},
|
|
username: (id) => {
|
|
if (!id) return '';
|
|
const entry = userList.userMap[id];
|
|
const name = typeof entry === 'object' ? entry.name : entry;
|
|
|
|
if (!name && id.length > 10) {
|
|
if (!userList.pendingIds.has(id)) {
|
|
userList.pendingIds.add(id);
|
|
userList.triggerFetch();
|
|
}
|
|
return id;
|
|
}
|
|
return name || id;
|
|
},
|
|
};
|
|
|
|
/*
|
|
path,
|
|
data = {},
|
|
callback = () => {},
|
|
async = true,
|
|
headers = {},
|
|
handleDeserialize = JSON.parse,
|
|
handleSerialize = JSON.stringify
|
|
config
|
|
*/
|
|
function startEventQueue(
|
|
info,
|
|
loginHeader = {},
|
|
displayAuthError = () => { },
|
|
displayErrorMessage = () => { },
|
|
successful = () => { }
|
|
) {
|
|
const xhr = new window.XMLHttpRequest();
|
|
let lastIndex = 0;
|
|
xhr.open('POST', loginKey.url + '/rsEvents/registerEventsHandler', true);
|
|
|
|
// Set headers for authentication
|
|
const headers = {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
...loginHeader,
|
|
};
|
|
|
|
if (loginKey.isVerified && !headers['Authorization']) {
|
|
if (loginKey.username && loginKey.passwd) {
|
|
headers['Authorization'] = 'Basic ' + btoa(loginKey.username + ':' + loginKey.passwd);
|
|
}
|
|
}
|
|
|
|
Object.keys(headers).forEach((key) => {
|
|
xhr.setRequestHeader(key, headers[key]);
|
|
});
|
|
|
|
xhr.onreadystatechange = () => {
|
|
if (xhr.readyState === 4) {
|
|
if (xhr.status === 401) {
|
|
displayAuthError('Incorrect login/password.');
|
|
}
|
|
}
|
|
};
|
|
|
|
xhr.onprogress = (ev) => {
|
|
const currIndex = xhr.responseText.length;
|
|
if (currIndex > lastIndex) {
|
|
const parts = xhr.responseText.substring(lastIndex, currIndex);
|
|
lastIndex = currIndex;
|
|
parts
|
|
.trim()
|
|
.split('\n\n')
|
|
.filter((e) => e.trim().length > 0)
|
|
.forEach((e) => {
|
|
if (e.startsWith('data: {')) {
|
|
try {
|
|
const data = JSON.parse(e.substr(6));
|
|
if (Object.prototype.hasOwnProperty.call(data, 'retval')) {
|
|
if (data.retval.errorNumber !== 0) {
|
|
displayErrorMessage(
|
|
`${info} failed: [${data.retval.errorCategory}] ${data.retval.errorMessage}`
|
|
);
|
|
} else {
|
|
successful();
|
|
}
|
|
} else if (Object.prototype.hasOwnProperty.call(data, 'event')) {
|
|
data.event.queueSize = currIndex;
|
|
try {
|
|
eventQueue.handler(data.event);
|
|
} catch (err) {
|
|
console.error('[RS] Error in event handler:', err, data.event);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('[RS] JSON parse error for part:', e, err);
|
|
}
|
|
}
|
|
});
|
|
if (currIndex > 1e6) {
|
|
// max 1 MB eventQueue
|
|
startEventQueue('restart queue');
|
|
xhr.abort();
|
|
}
|
|
}
|
|
};
|
|
|
|
xhr.onload = () => { };
|
|
|
|
xhr.onerror = (err) => {
|
|
console.error('[RS] Event Queue XHR error occurred:', err);
|
|
// Retry after 5 seconds to avoid silent event loss
|
|
setTimeout(() => {
|
|
console.log('[RS] Retrying event queue connection...');
|
|
startEventQueue(info, loginHeader, displayAuthError, displayErrorMessage, successful);
|
|
}, 5000);
|
|
};
|
|
|
|
// We need to send an eventType to registerEventsHandler
|
|
// 0 means all events
|
|
xhr.send(JSON.stringify({ eventType: 0 }));
|
|
return xhr;
|
|
}
|
|
|
|
function logon(loginHeader, displayAuthError, displayErrorMessage, successful) {
|
|
startEventQueue('login', loginHeader, displayAuthError, displayErrorMessage, () => {
|
|
successful();
|
|
userList.loadUsers();
|
|
});
|
|
}
|
|
|
|
function formatBytes(bytes, decimals = 2) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function hexId(id) {
|
|
if (!id) return '';
|
|
if (typeof id === 'string') return id;
|
|
if (typeof id === 'number') return String(id);
|
|
if (typeof id === 'object') {
|
|
// 1. Check for xstr64 (64-bit wrapped ID)
|
|
if (id.xstr64 && id.xstr64 !== '0') return id.xstr64;
|
|
|
|
// 2. Search for any hex string of appropriate length (128-bit or 64-bit)
|
|
const keys = Object.keys(id);
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const val = id[keys[i]];
|
|
if (typeof val === 'string' && val.length >= 16 && val !== '00000000000000000000000000000000') return val;
|
|
// Search deeper for nested xstr64
|
|
if (val && typeof val === 'object' && val.xstr64 && val.xstr64 !== '0') return val.xstr64;
|
|
}
|
|
// 3. Last resort fallbacks
|
|
if (id.xstr64 !== undefined) return String(id.xstr64);
|
|
}
|
|
return String(id);
|
|
}
|
|
|
|
module.exports = {
|
|
rsJsonApiRequest,
|
|
idToHex: hexId,
|
|
connectionState,
|
|
setKeys,
|
|
setBackgroundTask,
|
|
logon,
|
|
events: eventQueue.events,
|
|
RsEventsType,
|
|
userList,
|
|
loginKey,
|
|
formatBytes,
|
|
logout,
|
|
};
|