/**
* @file Manages CypherPoker.JS connectivity by providing a bridge between {@link P2PRouter},
* {@link APIRouter}, {@link CypherPoker}, and {@link CypherPokerUI}.
*
* @version 0.4.1
*/
/**
* @class Manages network connectivity for CypherPoker.JS.
* @extends EventDispatcher
* @see {@link APIRouter}
* @see {@link P2PRouter}
*/
class ConnectivityManager extends EventDispatcher {
/**
* Creates an instance of ConnectivityManager.
*
* @param {CypherPoker} cypherpokerRef The CypherPoker instance for which
* this class is controlling connectivity.
*/
constructor(cypherpokerRef) {
super();
this._cypherpoker = cypherpokerRef;
}
/**
* @property {Object} selectors Name/value pairs for UI elements and their
* associated CSS-style DOM selectors within the connectivity management template <code>connectivityManage.html</code>
* @property {String} selectors.gatewaysList="#gatewaysList" Pulldown selection list of available server gateways.
* @property {String} selectors.serverSDBTypeRadioGroup="#serverSDBTypeRadioGroup" Radio group containing options for the SDB variant
* to generate when starting a gateway.
* @property {String} selectors.serverSDB="#serverSDB" The generated SDB textfield element for the server when it's started.
* @property {String} selectors.serverAccessGroup="#serverAccessGroup" Checkbox button group for determining the inclusion
* of entity types (e.g. "api", "p2p", etc.), in the SDB.
* @property {String} selectors.connectSDB="#connectSDB" Textarea containing SDB to use for establishing connection(s).
* @property {String} selectors.apiConnectionList="#apiConnectionList" Pulldown selection list of saved API connections.
* @property {String} selectors.apiConnectionURL="#apiConnectionURL" The URL input field for the API connection.
* @property {String} selectors.apiConnectionCreate="#apiConnectionCreate" The API creation JavaScript snippet input field.
* @property {String} selectors.p2pConnectionList="#p2pConnectionList" Pulldown selection list of saved P2P connections.
* @property {String} selectors.p2pConnectionURL="#p2pConnectionURL" The URL input field for the P2P connection.
* @property {String} selectors.p2pConnectionCreate="#p2pConnectionCreate" The P2P creation JavaScript snippet input field.
* @property {String} selectors.manualConnectionData="#manualConnectionData" A TextArea input for establishing manual
* peer to peer connections input field.
*/
get selectors() {
return({
"gatewaysList":"#gatewaysList",
"serverSDBTypeRadioGroup":"#serverSDBTypeRadioGroup",
"serverSDB":"#serverSDB",
"serverAccessGroup":"#serverAccessGroup",
"connectSDB":"#connectSDB",
"apiConnectionList":"#apiConnectionList",
"apiConnectionURL":"#apiConnectionURL",
"apiConnectionCreate":"#apiConnectionCreate",
"p2pConnectionList":"#p2pConnectionList",
"p2pConnectionURL":"#p2pConnectionURL",
"p2pConnectionCreate":"#p2pConnectionCreate",
"manualConnectionData":"#manualConnectionData"
});
}
/**
* @property {CypherPoker} cypherpoker The main CypherPoker instance for
* which this class is managing connectivity, as set at instantiation.
* @readonly
*/
get cypherpoker() {
return (this._cypherpoker);
}
/**
* @property {Object} p2p=null Reference to a peer-to-peer multi-network routing interface
* supporting a dynamic private ID, direct send (single or multi), and broadcast functionality.
* For example, {@link P2PRouter}
*/
get p2p() {
if (this._p2p == undefined) {
this._p2p = null;
}
return (this._p2p);
}
set p2p(p2pSet) {
this._p2p = p2pSet;
}
/**
* @property {Object} api=null Reference to a multi-network routing interface over which RPC API functions
* are invoked. For example, {@link APIRouter}.
*/
get api() {
if (this._api == undefined) {
this._api = null;
}
return (this._api);
}
set api(apiSet) {
this._api = apiSet;
}
/**
* @property {Boolean} apiConnected=false True if the instance is connected to the
* API services provider and ready to accept requests.
* @readonly
*/
get apiConnected() {
return (this._apiConnected);
}
/**
* @property {Boolean} p2pConnected=false True if the instance is connected to the peer-to-peer
* network and ready to accept requests.
* @readonly
*/
get p2pConnected() {
return (this._p2pConnected);
}
/**
* Registers an event listener for an event handled by this class. Any changes to connectivity
* will also include updates to registered listeners.<br/>
* Currently supported listeners include:<br/>
* <ul>
* <li><code>"message"<code> - dispatched by the [p2p]{@link ConnectivityManager#p2p instance}</li>
* <ul>
*
* @param {String} eventType The type of event to register a listener for.
* @param {String} router The router type to register the listener with. Valid types
* include <code>"api"</code> and <code>"p2p"</code>.
* @param {Function} funcRef The handler function to register for the event.
* @param {*} context The context in which to execute <code>funcRef</code>.
*/
registerListener(eventType, router, funcRef, context) {
if (this._registeredListeners == undefined) {
this._registeredListeners = new Object();
this._registeredListeners.api = new Array();
this._registeredListeners.p2p = new Array();
}
var eventObj = new Object();
eventObj.type = eventType;
eventObj.func = funcRef;
eventObj.context = context;
this._registeredListeners[router].push(eventObj);
}
/**
* Adds any listeners registered with [registerListener]{@link ConnectivityManager#registerListener}
* to the specified router.
*
* @param {String} router The router type, either <code>"api"</code> and <code>"p2p"</code>, to which
* to add the registered listener(s) to.
*
* @private
*/
addRegisteredListeners(router) {
if (this[router] == null) {
return;
}
var listenersArr = this._registeredListeners[router];
for (var count=0; count < listenersArr.length; count++) {
var listenerObj = listenersArr[count];
this[router].removeEventListener(listenerObj.type, listenerObj.func);
this[router].addEventListener(listenerObj.type, listenerObj.func, listenerObj.context);
}
}
/**
* Populates the list of enabled gateways in the server portion of the
* connectivity management interface,
*/
populateGatewaysList() {
if (isDesktop() == false) {
return;
}
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
var listElement = manageElement.querySelector(this.selectors.gatewaysList);
//clear the list
listElement.innerHTML = "";
var result = IPCSend("get-gateways"); //get gateways data array
for (var count=0; count < result.data.length; count++) {
var gatewayObj = result.data[count];
var gatewayName = gatewayObj.name; //name from gateway class
var gatewayConfgName = gatewayObj.configName; //name in app config
var gatewayConfg = gatewayObj.config; //config data for gateway
var newOptionElement = document.createElement("option");
newOptionElement.setAttribute("value", gatewayName);
newOptionElement.innerHTML = gatewayName;
newOptionElement.gateway = gatewayObj;
listElement.appendChild(newOptionElement);
}
}
/**
* Creates and starts the [api]{@link CypherPoker#api} and [p2p]{@link CypherPoker#p2p}
* connections by calling [connectAPI]{@link ConnectivityManager#connectAPI} and
* [connectP2P]{@link ConnectivityManager#connectP2P}.
*
* @param {Boolean} [apiFatalFail=false] If true, a connection failure to the API
* services server is considered fatal and will throw an exception, otherwise only
* a connection failure warning is displayed in the console.
* @param {Boolean} [p2pFatalFail=false] If true, a connection failure to the
* peer-to-peer network is considered fatal and will throw an exception, otherwise
* only a connection failure warning is displayed in the console.
*
* @async
*/
async startConnections(apiFatalFail=false, p2pFatalFail=false) {
if (this.api != null) {
this.api.destroy();
this.api = null;
}
if (this.p2p != null) {
this.p2p.destroy();
this.p2p = null;
}
var result = await this.connectAPI();
if (result == false) {
this._apiConnected = false;
if (apiFatalFail == true) {
throw (new Error("Could not establish API services connection."));
} else {
console.warn("Could not establish API services connection.");
}
} else {
this._apiConnected = true;
}
if (this.canShareConnections(cypherpoker.settings.api.connectInfo, cypherpoker.settings.p2p.connectInfo) == true) {
if (this._apiConnected == true) {
result = await this.connectP2P(this.api.connection);
} else {
//already failed during API attempt
result = false;
}
} else {
result = await this.connectP2P();
}
if (result == false) {
this._p2pConnected = false;
if (p2pFatalFail == true) {
throw (new Error("Could not establish peer-to-peer networking connection."));
} else {
console.warn ("Could not establish peer-to-peer networking connection.");
}
} else {
this._p2pConnected = true;
}
return (true);
}
/**
* Determines if two connection and transport settings objects are the same
* and can therefore be shared.
*
* @param {Object} connectObj1 The first connection info object to compare.
* @param {Object} connectObj2 The second connection info object to compare.
*
* @return {Boolean} True if both the URLs and transports specified
* by the settings are the same, false otherwise.
*/
canShareConnections(connectObj1, connectObj2) {
var conn1URL = connectObj1.url;
var conn1Type = connectObj1.transport;
conn1URL = conn1URL.trim();
conn1Type = conn1Type.trim();
var conn2URL = connectObj2.url;
var conn2Type = connectObj2.transport;
conn2URL = conn2URL.trim();
conn2Type = conn2Type.trim();
if ((conn1URL == conn2URL) && (conn1Type == conn2Type)) {
return (true);
} else {
return (false);
}
}
/**
* Establishes connection(s) using information from a Services Descriptor Bundle.
* This may result in new [api]{@link ConnectivityManager#api} and/or [p2p]{@link ConnectivityManager#p2p}
* instances being created and connected. Related <code>connectInfo</code> objects in the
* [settings]{@link CypherPoker#settings} object are updated accordingly.
*
* @param {String|Array} sdb Either Base85 / Ascii85 (including <code>-s</code> variant), or Base64 encoded string, or
* a native JavaScript array containing SDB entities.
* @param {String} [entityType="*"] The type of SDB entity to connect to, if
* contained in the <code>sdb</code>. Valid types are "api" or "p2p". If "*"
* is supplied, all included entities are connected.
* @param {Function} [statusCB=null] Optional callback function that tracks
* the status of the connection(s) progress. This function will receive
* a single object parameter with properties <code>entity</code> (the entity
* for which the status is being reported), <code>status</code> (either "connecting",
* "connected", or "failed"), and <code>url</code> (the assembled connection URL).
*
*
* @return {Promise} The returned promise will resolve with the true if
* all the specified connections connected successfuly, or false if one
* or more failed.
* @async
*/
async connectFromSDB(sdb, entityType="*", statusCB=null) {
var allSuccess = true;
if (typeof(sdb) == "string") {
var sdbInst = new SDB();
var failedFirst = false;
try {
var result = await sdbInst.decode(sdb);
if (sdbInst.data == null) {
failedFirst = true;
}
} catch (err) {
failedFirst = true;
}
try {
if (failedFirst == true) {
//may bew SDB-s data
sdb = "<~"+sdb+"~>";
sdbInst = new SDB();
result = await sdbInst.decode(sdb);
}
if (sdbInst.data == null) {
return (false);
}
} catch (err) {
return (false)
}
var sdbArr = sdbInst.data;
} else if (typeof(sdb) == "object") {
if (typeof(sdb.length) == "number") {
sdbArr = sdb;
} else {
return (false);
}
} else {
return (false);
}
for (var count=0; count < sdbArr.length; count++) {
var sdbEntity = sdbArr[count];
var sdbEntType = sdbEntity.entity;
if (typeof(sdbEntity.url) != "string") {
//construct URL, assume port is present (we should probably check for this)
if (sdbEntity.port > 0) {
var url = sdbEntity.protocol +"://"+ sdbEntity.host + ":" + sdbEntity.port;
} else {
url = sdbEntity.protocol +"://"+ sdbEntity.host;
}
sdbEntity.url = url;
}
}
//first disconnect if connected and specified
var connectArr = new Array();
for (count=0; count < sdbArr.length; count++) {
var sdbEntity = sdbArr[count];
switch (sdbEntity.entity) {
case "api":
if (this.api != null) {
result = await this.api.destroy();
}
this._api = null;
this._apiConnected = false;
sdbEntity.create = cypherpoker.settings.api.connectInfo.create; //copy create property
cypherpoker.settings.api.connectInfo = sdbEntity;
connectArr.unshift (sdbEntity); //ensure this is first
break;
case "p2p":
if (this.p2p != null) {
result = await this.p2p.destroy();
}
this._p2p = null;
this._p2pConnected = false;
sdbEntity.create = cypherpoker.settings.p2p.connectInfo.create; //copy create property
cypherpoker.settings.p2p.connectInfo = sdbEntity;
connectArr.push (sdbEntity); //ensure this is second
break;
}
}
for (count=0; count < connectArr.length; count++) {
var sdbEntity = connectArr[count];
switch (sdbEntity.entity) {
case "api":
var url = cypherpoker.settings.api.connectInfo.url;
if (statusCB != null) {
statusCB({entity:"api", status:"connecting", url:url});
}
var result = await this.connectAPI();
if (result == false) {
this._apiConnected = false;
if (apiFatalFail == true) {
throw (new Error("Could not establish API services connection."));
} else {
console.warn("Could not establish API services connection.");
}
if (statusCB != null) {
statusCB({entity:"api", status:"failed", url:url});
}
} else {
this._apiConnected = true;
if (statusCB != null) {
statusCB({entity:"api", status:"connected", url:url});
}
}
break;
case "p2p":
url = cypherpoker.settings.p2p.connectInfo.url;
if (statusCB != null) {
statusCB({entity:"p2p", status:"connecting", url:url});
}
if (this.canShareConnections(cypherpoker.settings.api.connectInfo, cypherpoker.settings.p2p.connectInfo) == true) {
if (this._apiConnected == true) {
result = await this.connectP2P(this.api.connection);
} else {
allSuccess = false;
result = false;
}
} else {
result = await this.connectP2P();
}
if (result == false) {
this._p2pConnected = false;
allSuccess = false;
if (statusCB != null) {
statusCB({entity:"p2p", status:"failed", url:url});
}
} else {
this._p2pConnected = true;
if (statusCB != null) {
statusCB({entity:"p2p", status:"connected", url:url});
}
}
break;
}
}
return (allSuccess);
}
/**
* Creates a new API connection specified in the <code>api</code> definition
* of the [settings]{@link CypherPoker#settings} object, and assigns it to the
* [api]{@link CypherPoker#p2p} reference.
* If a connection aready exists, it's destroyed and removed first.
*
* @param {Object} [sharedConnection=null] An optional shared / multiplexed
* connection already established (for example, the connection used for the
* [p2p]{@link CypherPoker#api});
*
* @return {Promise} The returned promise will resolve with the true if
* the connection was successfully established and false if the connection
* attempt failed.
* @async
*/
async connectAPI(sharedConnection=null) {
try {
if (this.api != null) {
var result = await this.api.destroy();
this.api = null;
}
//create API networking interface
this.api = Function(cypherpoker.settings.api.connectInfo.create)();
this.addRegisteredListeners("api");
if (sharedConnection) {
//shared P2P / API connection
this.api.connection = sharedConnection;
} else {
//independent API connection
result = await this.api.connectAPI(cypherpoker.settings.api.connectInfo);
}
this._connected = true;
} catch (err) {
result = null;
this._connected = false;
console.error (err);
return (false);
}
return (true);
}
/**
* Creates a new peer-to-peer rendezvous / signalling connection specified
* in the <code>p2p</code> definition of the [settings]{@link CypherPoker#settings}
* object, and assigns it to the [p2p]{@link CypherPoker#p2p} reference.
* If a connection aready exists, it's destroyed and removed first.
*
* @param {Object} [sharedConnection=null] An optional shared / multiplexed
* connection already established (for example, the connection used for the
* [api]{@link CypherPoker#api});
*
* @return {Promise} The returned promise will resolve with the true if
* the connection was successfully established and false if the connection
* attempt failed.
* @async
*/
async connectP2P(sharedConnection=null) {
try {
if (this.p2p != null) {
var result = await this.p2p.destroy();
this.p2p = null;
}
//create peer-to-peer routing interface
this.p2p = Function(cypherpoker.settings.p2p.connectInfo.create)();
this.addRegisteredListeners("p2p");
//this.p2p.addEventListener("message", cypherpoker.handleP2PMessage, cypherpoker);
if (sharedConnection != null) {
//shared API / P2P connection
this.p2p.rendezvous = sharedConnection;
} else {
//independent P2P connection
result = await this.p2p.connectRendezvous(cypherpoker.settings.p2p.connectInfo);
}
} catch (err) {
result = null;
this._connected = false;
console.error (err);
return (false);
}
return (true);
}
/**
* Invoked by the toggle switch used to enable external access to API / P2P functionality.
*
* @param {HTMLElement} cbRef The styled checkbox (sliding toggle), that trigerred this function.
*
* @async
*/
async onEnableServerAccessClick(cbRef) {
if (isDesktop() == false) {
throw (new Error("Can't enable server access in non-desktop mode."));
}
ui.disable(cbRef);
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
var serverSDBElement = manageElement.querySelector(this.selectors.serverSDB);
var restoreSDB = serverSDBElement.value;
var listElement = manageElement.querySelector(this.selectors.gatewaysList);
ui.disable(listElement);
var selectedOption = listElement.options[listElement.selectedIndex];
var gatewayObj = selectedOption.gateway;
var gatewayStarted = gatewayObj.started;
var requestObj = new Object();
requestObj.gateway = gatewayObj;
if (cbRef.checked == true) {
if (gatewayStarted == true) {
//gateway is already started, UI is probably mismatched
console.error ("Can't start \""+gatewayObj.configName+"\". Gateway is already started.");
serverSDBElement.innerHTML = restoreSDB;
ui.enable(listElement);
} else {
serverSDBElement.innerHTML = "[ Starting ]";
var result = await IPCSend("start-gateway", requestObj, true);
if (result.data == "ok") {
gatewayObj.started = true;
}
serverSDBElement.innerHTML = "[ Started ]";
this.populateServerSDB(gatewayObj);
}
} else {
if (gatewayStarted == false) {
//gateway isn't started, UI is probably mismatched
console.error ("Can't stop \""+gatewayObj.configName+"\". Gateway isn't started.");
serverSDBElement.innerHTML = restoreSDB;
ui.enable(listElement);
} else {
serverSDBElement.innerHTML = "[ Stopping ]";
result = await IPCSend("stop-gateway", gatewayObj, true);
if (result.data == "ok") {
gatewayObj.started = false;
serverSDBElement.innerHTML = "[ Stopped ]";
ui.enable(listElement);
ui.enable(cbRef);
}
}
}
ui.enable(cbRef);
}
/**
* Populates the server-generated SDB <code>textarea</code> element in the user interface when server
* connectivity is enabled, or when the SDB format or options change.
*
* @param {Object} gatewayObj Object containing information about the gateway such
* as would be added in during {@link populateGatewaysList}.
*/
populateServerSDB(gatewayObj) {
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
var serverSDBElement = manageElement.querySelector(this.selectors.serverSDB);
serverSDBElement.innerHTML = "[ Generating SDB ]";
var radioGroupElement = manageElement.querySelector(this.selectors.serverSDBTypeRadioGroup);
var checkboxGroupElement = manageElement.querySelector(this.selectors.serverAccessGroup);
var selectedOptions = ui.getGroupSelections(radioGroupElement);
var selectedValue = selectedOptions[0].value; //this is a group so it should only allow one selection
var valueSplit = selectedValue.split("-");
var requestObj = new Object();
requestObj.gateway = gatewayObj;
var format = valueSplit[0];
var subFormat = valueSplit[1];
requestObj.format = format;
requestObj.entityTypes = new Array();
selectedOptions = ui.getGroupSelections(checkboxGroupElement);
for (var count = 0; count < selectedOptions.length; count++) {
var entityType = selectedOptions[count].value;
requestObj.entityTypes.push(entityType)
}
var result = IPCSend("get-gateway-sdb", requestObj);
var sdbString = result.data.sdb;
if (subFormat == "s") {
//create *-s variant
if (format == "base85") {
sdbString = sdbString.substring(2, sdbString.length-2);
}
}
var serverSDBElement = manageElement.querySelector(this.selectors.serverSDB);
serverSDBElement.innerHTML = sdbString;
return (true);
}
/**
* Invoked when any of the SDb generation options (radio buttons or checkboxes), are
* clicked in the server portion of the connectivity management interface.
*/
onServerSDBOptionClick() {
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
var listElement = manageElement.querySelector(this.selectors.gatewaysList);
var selectedOption = listElement.options[listElement.selectedIndex];
var gatewayObj = selectedOption.gateway;
var gatewayStarted = gatewayObj.started;
if (gatewayStarted == false) {
return(false);
}
this.populateServerSDB(gatewayObj);
}
/**
* Copies the current contents of the server-generated SDB <code>textarea</code> element
* to the system clipboard.
*/
serverSDBToClipboard() {
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
var serverSDBElement = manageElement.querySelector(this.selectors.serverSDB);
var sdbStr = serverSDBElement.value; //doesn't convert characters to HTML entities unlike innerHTML
ui.copyToClipboard(sdbStr);
}
/**
* Invoked by the SDB connect button in the connectivity options of the user
* interface (the alternative to manual address entry).
*/
async onConnectUsingSDBClick() {
for (var count=0; count < this.cypherpoker.games.length; count++) {
var currentGame = this.cypherpoker.games[count];
if ((currentGame.gameStarted == true) || (currentGame.gameEnding == true)) {
var confirmElement = ui.getTemplateByName("logOutConfirm").elements[0];
ui.show(confirmElement.querySelector("#gameSDBChange"));
ui.show(confirmElement);
ui.showDialog();
return;
}
}
if (this.cypherpoker.joinedTables.length > 0) {
confirmElement = ui.getTemplateByName("logOutConfirm").elements[0];
ui.show(confirmElement.querySelector("#lobbySDBChange"));
ui.show(confirmElement);
ui.showDialog();
return;
}
this.confirmConnectSDB(false, false);
}
/**
* Displays connection status using the main dialog when a connection attempt
* has been started using a SDB.
*/
showSDBConnectStatus(statusObj) {
switch (statusObj.entity) {
case "api":
var entity = "API";
break;
case "p2p":
entity = "Peer-to-Peer";
break;
default:
entity = "unknown";
break;
}
var statusString = new String();
switch (statusObj.status) {
case "connecting":
statusString = "Establishing "+entity+" connection to: "+statusObj.url;
break;
case "connected":
statusString = "Successfully established "+entity+" connection to: "+statusObj.url;
break;
case "failed":
statusString = "Failed "+entity+" connection to: "+statusObj.url;
break;
default:
statusString = "Unrecognized connection status.";
break;
}
ui.showDialog(statusString);
ui.hideDialog(5000);
}
/**
* Loads any saved connections stored by the user.
*
* @param {String} [connectionType=null] The type of connection information
* to retrieve, either <code>api</code> or <code>p2p</code>
*
* @return {Array|Object} Indexed array of objects containing saved connection
* information, or an object containing all saved connections contained in
* named indexed arrays.
*/
getSavedConnections(connectionType=null) {
var storage = window.localStorage;
var connectionsObjStr = storage.getItem("connectivity");
if (connectionsObjStr == null) {
return (new Array());
}
var connectionsObj = JSON.parse(connectionsObjStr);
if (connectionType == null) {
return (connectionsObj);
}
if ((connectionsObj[connectionType] == undefined) || (connectionsObj[connectionType] == null)) {
return (new Array());
}
return (connectionsObj[connectionType]);
}
/**
* Saves a connection info object for later retieval.
*
* @param {Object} connectionInfo An object containing information on how to establish
* the connection.
* @param {String} connectionType The type of connection that <code>connectionInfo</code>
* represents, either "api" or "p2p".
*
* @return {Boolean} True if the information was successfully saved, false otherwise.
*
* @async
*/
async saveConnection(connectionInfo, connectionType) {
var storage = window.localStorage;
var connectionsObjStr = storage.getItem("connectivity");
if (connectionsObjStr == null) {
var connectionsObj = new Object();
} else {
connectionsObj = JSON.parse(connectionsObjStr);
}
if ((connectionsObj[connectionType] == undefined) || (connectionsObj[connectionType] == null)) {
connectionsObj[connectionType] = new Array();
}
connectionsObj[connectionType].push (connectionInfo);
storage.setItem("connectivity", JSON.stringify(connectionsObj));
}
/**
* Returns a descriptive, human-readable name for a connection type.
*
* @param {String|Object} connectionInfo Either a connection type to evaluate directly
* or an object containing a <code>type</code> property to evaluate.
*
* @return {String} A descriptive name for the connection type. A blank string is returned
* for any unrecignized type.
*/
getConnectionName(connectionInfo) {
if (typeof(connectionInfo) == "object") {
var connectionType = connectionInfo.transport;
} else {
connectionType = connectionInfo;
}
connectionType = connectionType.trim().toLowerCase();
switch (connectionType) {
case "wss":
return ("WebSocket Sessions");
break;
case "webrtc":
return ("WebRTC");
break;
default:
return ("");
break;
}
}
/**
* Returns a human-readable security descriptor (e.g. "Secure") for a connection type.
*
* @param {String|Object} connectionInfo Either a connection url to evaluate directly
* or an object containing a <code>url</code> property to evaluate.
* @param {Boolean} [addSpace=true] If true, an extra space is added to the end of the
* descriptor unless it's an empty string.
*
* @return {String} The security descriptor, either "Secure" or an empty string for
* insecure.
*/
getConnectionSecurity(connectionInfo, addSpace=true) {
if (typeof(connectionInfo) == "object") {
var connectionURL = connectionInfo.url;
} else {
connectionURL = connectionInfo;
}
connectionURL = connectionURL.trim().toLowerCase();
connectionURL = connectionURL.split("://")[0];
var descriptor = "";
switch (connectionURL) {
case "wss":
descriptor = "Secure";
break;
case "https":
descriptor = "Secure";
break;
default:
return ("");
break;
}
if ((addSpace == true) && (descriptor != "")) {
descriptor += " ";
}
return (descriptor);
}
/**
* Returns the transport type based on a supplied URL.
*
* @param {String} connectionURL The URL from which to determine the transport type.
*
* @return {String} The transport type represented by the URL. <code>null</code>
* is returned if the transport type can't be determined.
*/
getTransportType(connectionURL) {
var protocol = connectionURL.split(":")[0];
protocol = protocol.toLowerCase().trim();
switch (protocol) {
case "ws":
return ("wss");
break;
case "wss":
return ("wss");
break;
case "tunnel@ws":
return ("wsst");
break;
case "tunnel@wss":
return ("wsst");
break;
default:
return (null);
break;
}
}
/**
* Adjusts a URL that contains additional information that would be considered invalid
* (such as a "tunnel@" protocol prefix, for example).
*
* @param {String} connectionURL The URL to adjust.
*
* @return {String} The adjusted URL.
*/
adjustURL(connectionURL) {
var urlSplit = connectionURL.split(":");
var protocol = urlSplit[0];
var protocolSplit = protocol.split("@");
if (protocolSplit.length > 1) {
urlSplit.splice(0, 1);
var urlRemainder = urlSplit.join(":");
connectionURL = protocolSplit[1] + ":" + urlRemainder;
}
return (connectionURL);
}
/**
* Returns the parameters included with a supplied URL.
*
* @param {String} connectionURL The URL from which to determine the parameters.
*
* @return {URLSearchParams} The parameters included with the URL
*/
getConnectionParameters(connectionURL) {
var decodedURL = decodeURI(connectionURL);
var urlObj = new URL(decodedURL);
return (urlObj.searchParams);
}
/**
* Populates a pulldown selection list (HTML <code>select</code> element), with
* connection options. The existing / default connection information specified
* in the [CypherPoker.settings]{@link CypherPoker#settings} is added to the
* list first so that there will always be at least one option.
*
* @param {String} connectionType The type of connection list to populate, either
* "api" or "p2p".
* @param {String} [defaultSelection=null] The default selection to set the list to.
* If <code>null</code> the first (current / default) option is selected.
*/
populateConnectionsList(connectionType, defaultSelection=null) {
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
if (connectionType == "api") {
var listElement = manageElement.querySelector(this.selectors.apiConnectionList);
} else if (connectionType == "p2p") {
listElement = manageElement.querySelector(this.selectors.p2pConnectionList);
} else {
return;
}
var connList = this.getSavedConnections(connectionType);
var defaultConnection = cypherpoker.settings[connectionType].connectInfo;
//clear the list
listElement.innerHTML = "";
var found = connList.some(arrElement => {
return ((arrElement.url == defaultConnection.url) && (arrElement.transport == defaultConnection.transport));
})
if (found == false) {
//current / default connection does not exist in saved list so add it to the top
connList.splice(0, 0, defaultConnection);
}
for (var count = 0; count < connList.length; count++) {
var connObj = connList[count];
var newOptionElement = document.createElement("option");
newOptionElement.setAttribute("value", connObj.url);
newOptionElement.innerHTML = this.getConnectionSecurity(connObj)+this.getConnectionName(connObj)+" → "+connObj.url;
newOptionElement.connection = connObj;
listElement.appendChild(newOptionElement);
}
if (this.defaultSelection == null) {
listElement.value = defaultConnection.url; //set default item as current selection
} else {
listElement.value = defaultSelection;
}
}
/**
* Populates the API connection input fields (url and create script) in the user interface,
* using the currently selected option in the API connections list (HTML <code>select</code> element).
*/
populateAPIConnectionInputs() {
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
var listElement = manageElement.querySelector(this.selectors.apiConnectionList);
var urlInputElement = manageElement.querySelector(this.selectors.apiConnectionURL);
var connectionCreateElement = manageElement.querySelector(this.selectors.apiConnectionCreate);
var selectedOption = listElement.options[listElement.selectedIndex];
urlInputElement.value = selectedOption.connection.url;
connectionCreateElement.value = selectedOption.connection.create;
}
/**
* Invoked by a button on the API connectivity form to trigger a connection
* to the currently selected (or manually entered), connection. Prior to triggering
* this action, this function displays warning dialogs if a game or a table advertisement /
* join request is currently active.
*
* @async
*/
async onConnectAPIClick() {
ui.hide(ui.getTemplateByName("accountCreate").elements[0]);
ui.hide(ui.getTemplateByName("accountLogin").elements[0]);
ui.hide(ui.getTemplateByName("accountManage").elements[0]);
for (var count=0; count < this.cypherpoker.games.length; count++) {
var currentGame = this.cypherpoker.games[count];
if ((currentGame.gameStarted == true) || (currentGame.gameEnding == true)) {
var confirmElement = ui.getTemplateByName("logOutConfirm").elements[0];
ui.show(confirmElement.querySelector("#gameAPIChange"));
ui.show(confirmElement);
ui.showDialog();
return;
}
}
if (this.cypherpoker.joinedTables.length > 0) {
confirmElement = ui.getTemplateByName("logOutConfirm").elements[0];
ui.show(confirmElement.querySelector("#lobbyAPIChange"));
ui.show(confirmElement);
ui.showDialog();
return;
}
this.confirmConnectAPI(false);
}
/**
* Trigerred after [onConnectUsingSDBClick]{@link ConnectivityManager#onConnectUsingSDBClick} either
* directly or as a result of a confirmation by the plyer. New connection(s) is/are
* determined from the SDB, closing any current connections, and establishing new ones.
*
* @param {Boolean} [gameActive=false] If true, any existing games are destroyed
* prior to establishing the new connection.
* @param {Boolean} [joinActive=false] If true, any existing table advertisement
* or join requests are cancelled prior to establishing the new connection.
*
* @async
*/
async confirmConnectSDB(gameActive=false, joinActive=false) {
if (joinActive) {
//lobby is active
this.cypherpoker.removeAllTables(true, true);
} else if (gameActive) {
//game(s) active
this.cypherpoker.removeAllGames(true);
}
var confirmElement = ui.getTemplateByName("logOutConfirm").elements[0];
ui.hide(confirmElement.querySelector("#gameSDBChange"));
ui.hide(confirmElement.querySelector("#lobbySDBChange"));
ui.hide(confirmElement);
ui.hideDialog();
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
var connectSDBElement = manageElement.querySelector(this.selectors.connectSDB);
var sdbStr = connectSDBElement.value; //doesn't convert characters to HTML entities unlike innerHTML
if (sdbStr.trim() == "") {
return (false);
}
ui.hide(manageElement);
ui.hide(ui.getTemplateByName("accountCreate").elements[0]);
ui.hide(ui.getTemplateByName("accountLogin").elements[0]);
ui.hide(ui.getTemplateByName("accountManage").elements[0]);
ui.hide(ui.getTemplateByName("lobby").elements[0]);
//this._apiConnected = false; //this must be set after account list is emptied
this.cypherpoker.clearAccounts();
ui.clearAccountsUI();
var result = await this.connectFromSDB(sdbStr, "*", this.showSDBConnectStatus);
if (result == false) {
ui.showDialog ("Failed to establish any connection using SDB.");
ui.hideDialog(4000);
} else {
ui.show(ui.getTemplateByName("accountLogin").elements[0]);
this.cypherpoker.restoreAccounts(this.cypherpoker.settings.api.connectInfo.url);
result = await this.p2p.changePrivateID(this.api.privateID);
ui.updateAccountsUI();
}
}
/**
* Trigerred after [onConnectAPIClick]{@link ConnectivityManager#onConnectAPIClick} either
* directly or as a result of a confirmation by the plyer. A new API connection is
* established by gathering the url, create script, and generating a connection type
* via [getTransportType]{@link ConnectivityManager#getTransportType}, creating
* a new <code>connectInfo</code> object, and using it to replace the current
* <code>cypherpoker.settings.api.connectInfo</code> setting.
*
* @param {Boolean} [gameActive=false] If true, any existing games are destroyed
* prior to establishing the new connection.
* @param {Boolean} [joinActive=false] If true, any existing table advertisement
* or join requests are cancelled prior to establishing the new connection.
*
* @async
*/
async confirmConnectAPI(gameActive=false, joinActive=false) {
if (joinActive) {
//lobby is active
this.cypherpoker.removeAllTables(true, true);
} else if (gameActive) {
//game(s) active
this.cypherpoker.removeAllGames(true);
}
var confirmElement = ui.getTemplateByName("logOutConfirm").elements[0];
ui.hide(confirmElement.querySelector("#gameAPIChange"));
ui.hide(confirmElement);
ui.hideDialog();
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
var urlInputElement = manageElement.querySelector(this.selectors.apiConnectionURL);
var connectionCreateElement = manageElement.querySelector(this.selectors.apiConnectionCreate);
var url = urlInputElement.value;
var create = connectionCreateElement.value;
var connectInfo = new Object();
connectInfo.url = this.adjustURL(url);
connectInfo.transport = this.getTransportType(url);
connectInfo.parameters = this.getConnectionParameters(this.adjustURL(url));
connectInfo.create = create;
this.cypherpoker.settings.api.connectInfo = connectInfo;
this._apiConnected = false; //this must be set after account list is emptied
this.cypherpoker.clearAccounts();
ui.clearAccountsUI();
ui.hide(ui.getTemplateByName("lobby").elements[0]);
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
ui.hide(manageElement);
ui.showDialog("Establishing new API connection to: "+url);
var result = await this.connectAPI();
if (result == true) {
this._apiConnected = true;
this.cypherpoker.restoreAccounts(this.cypherpoker.settings.api.connectInfo.url);
ui.showDialog("Connected to new Services API: "+url);
ui.hideDialog(5000);
result = await this.p2p.changePrivateID(this.api.privateID);
if (result == true) {
ui.showDialog("Peer-to-peer identity updated.");
ui.hideDialog(5000);
}
ui.updateAccountsUI();
}
}
/**
* Invoked by a button on the API connectivity form to trigger the saving of
* information currently enetered into the form as a new entry which will
* subsequently be included in the population of the API connections.
* list.
*
* @async
*/
async onSaveAPIClick() {
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
var urlInputElement = manageElement.querySelector(this.selectors.apiConnectionURL);
var connectionCreateElement = manageElement.querySelector(this.selectors.apiConnectionCreate);
var url = urlInputElement.value;
var create = connectionCreateElement.value;
var connectInfo = new Object();
connectInfo.url = this.adjustURL(url);
connectInfo.transport = this.getTransportType(url);
connectInfo.parameters = this.getConnectionParameters(this.adjustURL(url));
connectInfo.create = create;
this.saveConnection(connectInfo, "api");
this.populateConnectionsList("api", url);
}
/**
* Populates the P2P connection input fields (url and create script) in the user interface,
* using the currently selected option in the P2P connections list (HTML <code>select</code> element).
*/
populateP2PConnectionInputs() {
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
var listElement = manageElement.querySelector(this.selectors.p2pConnectionList);
var urlInputElement = manageElement.querySelector(this.selectors.p2pConnectionURL);
var connectionCreateElement = manageElement.querySelector(this.selectors.p2pConnectionCreate);
var selectedOption = listElement.options[listElement.selectedIndex];
urlInputElement.value = selectedOption.connection.url;
connectionCreateElement.value = selectedOption.connection.create;
}
/**
* Invoked by a button on the P2P connectivity form to trigger a connection
* to the currently selected (or manually entered), connection. Prior to triggering
* this action, this function displays warning dialogs if a game or a table advertisement /
* join request is currently active.
*
* @async
*/
async onConnectP2PClick() {
ui.hide(ui.getTemplateByName("accountCreate").elements[0]);
ui.hide(ui.getTemplateByName("accountLogin").elements[0]);
ui.hide(ui.getTemplateByName("accountManage").elements[0]);
for (var count=0; count < this.cypherpoker.games.length; count++) {
var currentGame = this.cypherpoker.games[count];
if ((currentGame.gameStarted == true) || (currentGame.gameEnding == true)) {
var confirmElement = ui.getTemplateByName("logOutConfirm").elements[0];
ui.show(confirmElement.querySelector("#gameP2PChange"));
ui.show(confirmElement);
ui.showDialog();
return;
}
}
if (this.cypherpoker.joinedTables.length > 0) {
confirmElement = ui.getTemplateByName("logOutConfirm").elements[0];
ui.show(confirmElement.querySelector("#lobbyP2PChange"));
ui.show(confirmElement);
ui.showDialog();
return;
}
this.confirmConnectP2P(false);
}
/**
* Trigerred after [onConnectP2PClick]{@link ConnectivityManager#onConnectP2PClick} either
* directly or as a result of a confirmation by the plyer. A new P2P connection is
* established by gathering the url, create script, and generating a connection type
* via [getTransportType]{@link ConnectivityManager#getTransportType}, creating
* a new <code>connectInfo</code> object, and using it to replace the current
* <code>cypherpoker.settings.p2p.connectInfo</code> setting.
*
* @param {Boolean} [gameActive=false] If true, any existing games are destroyed
* prior to establishing the new connection.
* @param {Boolean} [joinActive=false] If true, any existing table advertisement
* or join requests are cancelled prior to establishing the new connection.
*
* @async
*/
async confirmConnectP2P(gameActive=false, joinActive=false) {
if (joinActive) {
//lobby is active
this.cypherpoker.removeAllTables(true, true);
} else if (gameActive) {
//game(s) active
this.cypherpoker.removeAllGames(true);
}
var confirmElement = ui.getTemplateByName("logOutConfirm").elements[0];
ui.hide(confirmElement.querySelector("#gameP2PChange"));
ui.hide(confirmElement);
ui.hideDialog();
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
var urlInputElement = manageElement.querySelector(this.selectors.p2pConnectionURL);
var connectionCreateElement = manageElement.querySelector(this.selectors.p2pConnectionCreate);
var url = urlInputElement.value;
var create = connectionCreateElement.value;
var connectInfo = new Object();
connectInfo.url = this.adjustURL(url);
connectInfo.transport = this.getTransportType(url);
connectInfo.parameters = this.getConnectionParameters(this.adjustURL(url));
connectInfo.create = create;
this.cypherpoker.settings.p2p.connectInfo = connectInfo;
this._p2pConnected = false;
//Accounts are not cleared since they're associate with the API connection
//(i.e. switching P2P connections only requites that the P2P private ID be
// set to match the API private ID).
ui.hide(ui.getTemplateByName("lobby").elements[0]);
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
ui.hide(manageElement);
ui.showDialog("Establishing new peer-to-peer rendezvous connection to: "+url);
var result = await this.connectP2P();
if (result == true) {
this._p2pConnected = true;
ui.show(ui.getTemplateByName("accountLogin").elements[0]);
ui.showDialog("Connected to new peer-to-peer rendezvous: "+url);
ui.hideDialog(5000);
result = await this.p2p.changePrivateID(this.api.privateID);
if (result == true) {
ui.showDialog("Peer-to-peer identity updated.");
ui.hideDialog(5000);
}
}
}
/**
* Invoked by a button on the P2P connectivity form to trigger the saving of
* information currently enetered into the form as a new entry which will
* subsequently be included in the population of the P2P connections.
* list.
*
* @async
*/
async onSaveP2PClick() {
var manageElement = ui.getTemplateByName("connectivityManage").elements[0];
var urlInputElement = manageElement.querySelector(this.selectors.p2pConnectionURL);
var connectionCreateElement = manageElement.querySelector(this.selectors.p2pConnectionCreate);
var url = urlInputElement.value;
var create = connectionCreateElement.value;
var connectInfo = new Object();
connectInfo.url = this.adjustURL(url);
connectInfo.transport = this.getTransportType(url);
connectInfo.parameters = this.getConnectionParameters(this.adjustURL(url));
connectInfo.create = create;
this.saveConnection(connectInfo, "p2p");
this.populateConnectionsList("p2p", url);
}
}