mirror of
https://thingvellir.net/git/overte
synced 2025-03-27 23:52:03 +01:00
651 lines
22 KiB
JavaScript
651 lines
22 KiB
JavaScript
"use strict";
|
|
//
|
|
// places.js
|
|
//
|
|
// Created by Alezia Kurdis, January 1st, 2022.
|
|
// Copyright 2022-2025 Overte e.V.
|
|
//
|
|
// Generate an explore app based on the differents source of placename data.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
(function() {
|
|
var jsMainFileName = "places.js";
|
|
var ROOT = Script.resolvePath('').split(jsMainFileName)[0];
|
|
|
|
var metaverseServers = [];
|
|
var SETTING_METAVERSE_TO_FETCH = "placesAppMetaverseToFetch";
|
|
var SETTING_PINNED_METAVERSE = "placesAppPinnedMetaverse";
|
|
var SETTING_MATURITY_FILTER = "placesAppMaturityFilter";
|
|
var DEFAULT_MATURITY = ["adult", "mature", "teen", "everyone", "unrated"];
|
|
var REQUEST_TIMEOUT = 10000; //10 seconds
|
|
|
|
var httpRequest = null;
|
|
var placesData;
|
|
var portalList = [];
|
|
|
|
var nbrPlacesNoProtocolMatch = 0;
|
|
var nbrPlaceProtocolKnown = 0;
|
|
|
|
var APP_NAME = "PLACES";
|
|
var APP_URL = ROOT + "places.html";
|
|
var APP_ICON_INACTIVE = ROOT + "icons/appicon_i.png";
|
|
var APP_ICON_ACTIVE = ROOT + "icons/appicon_a.png";
|
|
var appStatus = false;
|
|
var channel = "com.overte.places";
|
|
|
|
var portalChannelName = "com.overte.places.portalRezzer";
|
|
var MAX_DISTANCE_TO_CONSIDER_PORTAL = 100.0; //in meters
|
|
var PORTAL_DURATION_MILLISEC = 45000; //45 sec
|
|
var rezzerPortalCount = 0;
|
|
var MAX_REZZED_PORTAL = 15;
|
|
|
|
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
|
|
|
tablet.screenChanged.connect(onScreenChanged);
|
|
|
|
var button = tablet.addButton({
|
|
text: APP_NAME,
|
|
icon: APP_ICON_INACTIVE,
|
|
activeIcon: APP_ICON_ACTIVE,
|
|
sortOrder: 8
|
|
});
|
|
|
|
var timestamp = 0;
|
|
var INTERCALL_DELAY = 200; //0.3 sec
|
|
var PERSISTENCE_ORDERING_CYCLE = 5 * 24 * 3600 * 1000; //5 days
|
|
|
|
function clicked(){
|
|
if (appStatus === true) {
|
|
tablet.webEventReceived.disconnect(onAppWebEventReceived);
|
|
tablet.gotoHomeScreen();
|
|
appStatus = false;
|
|
} else {
|
|
tablet.gotoWebScreen(APP_URL);
|
|
tablet.webEventReceived.connect(onAppWebEventReceived);
|
|
appStatus = true;
|
|
}
|
|
|
|
button.editProperties({
|
|
isActive: appStatus
|
|
});
|
|
}
|
|
|
|
button.clicked.connect(clicked);
|
|
|
|
|
|
function onAppWebEventReceived(message) {
|
|
var d = new Date();
|
|
var n = d.getTime();
|
|
|
|
var messageObj = JSON.parse(message);
|
|
if (messageObj.channel === channel) {
|
|
if (messageObj.action === "READY_FOR_CONTENT" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
sendPersistedMaturityFilter();
|
|
transmitPortalList();
|
|
sendCurrentLocationToUI();
|
|
|
|
} else if (messageObj.action === "TELEPORT" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
|
|
if (messageObj.address.length > 0) {
|
|
Window.location = messageObj.address;
|
|
}
|
|
|
|
} else if (messageObj.action === "REQUEST_PORTAL" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
var portalPosition = Vec3.sum(MyAvatar.feetPosition, Vec3.multiplyQbyV(MyAvatar.orientation, {"x": 0.0, "y": 0.0, "z": -2.0}));
|
|
var requestToSend = {
|
|
"action": "REZ_PORTAL",
|
|
"position": portalPosition,
|
|
"url": messageObj.address,
|
|
"name": messageObj.name,
|
|
"placeID": messageObj.placeID
|
|
};
|
|
Messages.sendMessage(portalChannelName, JSON.stringify(requestToSend), false);
|
|
|
|
} else if (messageObj.action === "COPY_URL" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
Window.copyToClipboard(messageObj.address);
|
|
Window.displayAnnouncement("Place URL copied.");
|
|
} else if (messageObj.action === "GO_HOME" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
if (LocationBookmarks.getHomeLocationAddress()) {
|
|
location.handleLookupString(LocationBookmarks.getHomeLocationAddress());
|
|
} else {
|
|
Window.location = "file:///~/serverless/tutorial.json";
|
|
}
|
|
} else if (messageObj.action === "GO_BACK" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
location.goBack();
|
|
} else if (messageObj.action === "GO_FORWARD" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
location.goForward();
|
|
} else if (messageObj.action === "PIN_META" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
metaverseServers[messageObj.metaverseIndex].pinned = messageObj.value;
|
|
savePinnedMetaverseSetting();
|
|
} else if (messageObj.action === "FETCH_META" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
metaverseServers[messageObj.metaverseIndex].fetch = messageObj.value;
|
|
saveMetaverseToFetchSetting();
|
|
} else if (messageObj.action === "ADD_MS" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
var newMs = {
|
|
"url": messageObj.metaverseUrl,
|
|
"region": "external",
|
|
"fetch": false,
|
|
"pinned": true,
|
|
"order": "Z"
|
|
}
|
|
metaverseServers.push(newMs);
|
|
savePinnedMetaverseSetting();
|
|
} else if (messageObj.action === "SET_MATURITY_FILTER" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
Settings.setValue(SETTING_MATURITY_FILTER, messageObj.filter);
|
|
}
|
|
}
|
|
}
|
|
|
|
function savePinnedMetaverseSetting() {
|
|
var pinnedServers = [];
|
|
for (var q = 0; q < metaverseServers.length; q++) {
|
|
if (metaverseServers[q].pinned) {
|
|
pinnedServers.push(metaverseServers[q].url);
|
|
}
|
|
}
|
|
Settings.setValue(SETTING_PINNED_METAVERSE, pinnedServers);
|
|
}
|
|
|
|
function saveMetaverseToFetchSetting() {
|
|
var fetchedServers = [];
|
|
for (var q = 0; q < metaverseServers.length; q++) {
|
|
if (metaverseServers[q].fetch) {
|
|
fetchedServers.push(metaverseServers[q].url);
|
|
}
|
|
}
|
|
Settings.setValue(SETTING_METAVERSE_TO_FETCH, fetchedServers);
|
|
}
|
|
|
|
function onHostChanged(host) {
|
|
sendCurrentLocationToUI();
|
|
}
|
|
|
|
location.hostChanged.connect(onHostChanged);
|
|
|
|
function sendCurrentLocationToUI() {
|
|
var currentLocationMessage = {
|
|
"channel": channel,
|
|
"action": "CURRENT_LOCATION",
|
|
"data": location.href
|
|
};
|
|
|
|
tablet.emitScriptEvent(currentLocationMessage);
|
|
}
|
|
|
|
function onScreenChanged(type, url) {
|
|
if (type == "Web" && url.indexOf(APP_URL) != -1) {
|
|
appStatus = true;
|
|
} else {
|
|
appStatus = false;
|
|
}
|
|
|
|
button.editProperties({
|
|
isActive: appStatus
|
|
});
|
|
}
|
|
|
|
function transmitPortalList() {
|
|
metaverseServers = [];
|
|
buildMetaverseServerList();
|
|
|
|
portalList = [];
|
|
nbrPlacesNoProtocolMatch = 0;
|
|
nbrPlaceProtocolKnown = 0;
|
|
var extractedData;
|
|
|
|
for (var i = 0; i < metaverseServers.length; i++ ) {
|
|
if (metaverseServers[i].fetch === true) {
|
|
extractedData = getContent(metaverseServers[i].url + "/api/v1/places?status=online&per_page=1000");
|
|
if (extractedData === "") {
|
|
metaverseServers[i].error = true;
|
|
} else {
|
|
metaverseServers[i].error = false;
|
|
}
|
|
try {
|
|
placesData = JSON.parse(extractedData);
|
|
processData(metaverseServers[i]);
|
|
} catch(e) {
|
|
placesData = {};
|
|
}
|
|
httpRequest = null;
|
|
}
|
|
}
|
|
|
|
addUtilityPortals();
|
|
|
|
portalList.sort(sortOrder);
|
|
|
|
var percentProtocolRejected = Math.floor((nbrPlacesNoProtocolMatch/nbrPlaceProtocolKnown) * 100);
|
|
|
|
var warning = "";
|
|
if (percentProtocolRejected > 50) {
|
|
warning = "WARNING: " + percentProtocolRejected + "% of the places are not listed because they are running under a different protocol. Maybe consider to upgrade.";
|
|
}
|
|
|
|
var message = {
|
|
"channel": channel,
|
|
"action": "PLACE_DATA",
|
|
"data": portalList,
|
|
"warning": warning,
|
|
"metaverseServers": metaverseServers
|
|
};
|
|
|
|
tablet.emitScriptEvent(message);
|
|
|
|
};
|
|
|
|
function sendPersistedMaturityFilter() {
|
|
var messageSent = {
|
|
"channel": channel,
|
|
"action": "MATURITY_FILTER",
|
|
"filter": Settings.getValue(SETTING_MATURITY_FILTER, DEFAULT_MATURITY)
|
|
};
|
|
tablet.emitScriptEvent(messageSent);
|
|
}
|
|
|
|
function getFederationData() {
|
|
/*
|
|
//If federation.json is got from the Metaverse Server (not implemented yet)
|
|
var fedDirectoryUrl = AccountServices.metaverseServerURL + "/federation.json";
|
|
var extractedFedData = getContent(fedDirectoryUrl);
|
|
*/
|
|
|
|
/*
|
|
//If federation.json is got from a web storage
|
|
var fedDirectoryUrl = ROOT + "federation.json"; + "?version=" + Math.floor(Math.random() * 999999);
|
|
var extractedFedData = getContent(fedDirectoryUrl);
|
|
*/
|
|
|
|
//if federation.json is local, on the user installation
|
|
var extractedFedData = JSON.stringify(Script.require("./federation.json"));
|
|
|
|
return extractedFedData;
|
|
|
|
}
|
|
|
|
function buildMetaverseServerList () {
|
|
var extractedFedData = getFederationData();
|
|
|
|
var pinnedMetaverses = Settings.getValue(SETTING_PINNED_METAVERSE, []);
|
|
var metaversesToFetch = Settings.getValue(SETTING_METAVERSE_TO_FETCH, []);
|
|
|
|
var federation = [];
|
|
try {
|
|
federation = JSON.parse(extractedFedData);
|
|
} catch(e) {
|
|
federation = [];
|
|
}
|
|
var currentFound = false;
|
|
var region, pinned, fetch, order, metaverse;
|
|
for (var i=0; i < federation.length; i++) {
|
|
if (federation[i].node === AccountServices.metaverseServerURL) {
|
|
region = "local";
|
|
order = "A";
|
|
fetch = true;
|
|
pinned = false;
|
|
currentFound = true;
|
|
} else {
|
|
region = "federation";
|
|
order = "F";
|
|
fetch = false;
|
|
pinned = false;
|
|
}
|
|
|
|
metaverse = {
|
|
"url": federation[i].node,
|
|
"region": region,
|
|
"fetch": fetch,
|
|
"pinned": pinned,
|
|
"order": order
|
|
};
|
|
metaverseServers.push(metaverse);
|
|
}
|
|
if (!currentFound) {
|
|
metaverse = {
|
|
"url": AccountServices.metaverseServerURL,
|
|
"region": "local",
|
|
"fetch": true,
|
|
"pinned": false,
|
|
"order": "A"
|
|
};
|
|
metaverseServers.push(metaverse);
|
|
}
|
|
|
|
for (i = 0; i < pinnedMetaverses.length; i++) {
|
|
var target = pinnedMetaverses[i];
|
|
var found = false;
|
|
for (var k = 0; k < metaverseServers.length; k++) {
|
|
if (metaverseServers[k].url === target) {
|
|
metaverseServers[k].pinned = true;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
metaverse = {
|
|
"url": target,
|
|
"region": "external",
|
|
"fetch": false,
|
|
"pinned": true,
|
|
"order": "Z"
|
|
};
|
|
metaverseServers.push(metaverse);
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < metaversesToFetch.length; i++) {
|
|
var target = metaversesToFetch[i];
|
|
for (var k = 0; k < metaverseServers.length; k++) {
|
|
if (metaverseServers[k].url === target) {
|
|
metaverseServers[k].fetch = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
metaverseServers.sort(sortOrder);
|
|
}
|
|
|
|
function getContent(url) {
|
|
httpRequest = new XMLHttpRequest();
|
|
httpRequest.open("GET", url, false); // false for synchronous request
|
|
httpRequest.setRequestHeader("Cache-Control", "no-cache");
|
|
httpRequest.timeout = REQUEST_TIMEOUT;
|
|
httpRequest.ontimeout=function(){
|
|
return "";
|
|
};
|
|
httpRequest.send( null );
|
|
return httpRequest.responseText;
|
|
}
|
|
|
|
function processData(metaverseInfo){
|
|
var supportedProtocole = Window.protocolSignature();
|
|
|
|
var places = placesData.data.places;
|
|
for (var i = 0;i < places.length; i++) {
|
|
|
|
var region, category, accessStatus;
|
|
|
|
var description = (places[i].description ? places[i].description : "");
|
|
var thumbnail = (places[i].thumbnail ? places[i].thumbnail : "");
|
|
|
|
if ( places[i].domain.protocol_version === supportedProtocole ) {
|
|
|
|
region = metaverseInfo.order;
|
|
|
|
if ( thumbnail.substr(0, 4).toLocaleLowerCase() !== "http") {
|
|
category = "O"; //Other
|
|
} else {
|
|
category = "A"; //Attraction
|
|
}
|
|
|
|
if (places[i].domain.num_users > 0) {
|
|
if (places[i].domain.num_users >= places[i].domain.capacity && places[i].domain.capacity !== 0) {
|
|
accessStatus = "FULL";
|
|
} else {
|
|
accessStatus = "LIFE";
|
|
}
|
|
} else {
|
|
accessStatus = "NOBODY";
|
|
}
|
|
|
|
var portal = {
|
|
"order": category + "_" + region + "_" + getSeededRandomForString(places[i].id),
|
|
"category": category,
|
|
"accessStatus": accessStatus,
|
|
"name": places[i].name,
|
|
"description": description,
|
|
"thumbnail": thumbnail,
|
|
"maturity": places[i].maturity,
|
|
"address": places[i].address,
|
|
"current_attendance": places[i].domain.num_users,
|
|
"id": places[i].id,
|
|
"visibility": places[i].visibility,
|
|
"capacity": places[i].domain.capacity,
|
|
"tags": getListFromArray(places[i].tags),
|
|
"managers": getListFromArray(places[i].managers),
|
|
"domain": places[i].domain.name,
|
|
"domainOrder": aplphabetize(zeroPad(places[i].domain.num_users, 6)) + "_" + places[i].domain.name + "_" + places[i].name,
|
|
"metaverseServer": metaverseInfo.url,
|
|
"metaverseRegion": metaverseInfo.region
|
|
};
|
|
portalList.push(portal);
|
|
|
|
} else {
|
|
nbrPlacesNoProtocolMatch++;
|
|
}
|
|
}
|
|
|
|
nbrPlaceProtocolKnown = nbrPlaceProtocolKnown + places.length;
|
|
|
|
}
|
|
|
|
function addUtilityPortals() {
|
|
var localHostPortal = {
|
|
"order": "Z_Z_AAAAAA",
|
|
"category": "Z",
|
|
"accessStatus": "NOBODY",
|
|
"name": "localhost",
|
|
"description": "",
|
|
"thumbnail": "",
|
|
"maturity": "unrated",
|
|
"address": "localhost",
|
|
"current_attendance": 0,
|
|
"id": "",
|
|
"visibility": "open",
|
|
"capacity": 0,
|
|
"tags": "",
|
|
"managers": "",
|
|
"domain": "",
|
|
"domainOrder": "ZZZZZZZZZZZZZZA",
|
|
"metaverseServer": "",
|
|
"metaverseRegion": "local"
|
|
};
|
|
portalList.push(localHostPortal);
|
|
|
|
var tutorialPortal = {
|
|
"order": "Z_Z_AAAAAZ",
|
|
"category": "Z",
|
|
"accessStatus": "NOBODY",
|
|
"name": "tutorial",
|
|
"description": "",
|
|
"thumbnail": "",
|
|
"maturity": "unrated",
|
|
"address": "file:///~/serverless/tutorial.json",
|
|
"current_attendance": 0,
|
|
"id": "",
|
|
"visibility": "open",
|
|
"capacity": 0,
|
|
"tags": "",
|
|
"managers": "",
|
|
"domain": "",
|
|
"domainOrder": "ZZZZZZZZZZZZZZZ",
|
|
"metaverseServer": "",
|
|
"metaverseRegion": "local"
|
|
};
|
|
portalList.push(tutorialPortal);
|
|
|
|
}
|
|
|
|
function aplphabetize(num) {
|
|
var numbstring = num.toString();
|
|
var newChar = "JIHGFEDCBA";
|
|
var refChar = "0123456789";
|
|
var processed = "";
|
|
for (var j=0; j < numbstring.length; j++) {
|
|
processed = processed + newChar.substr(refChar.indexOf(numbstring.charAt(j)),1);
|
|
}
|
|
return processed;
|
|
}
|
|
|
|
function getListFromArray(dataArray) {
|
|
var dataList = "";
|
|
if (dataArray !== undefined && dataArray.length > 0) {
|
|
for (var k = 0; k < dataArray.length; k++) {
|
|
if (k !== 0) {
|
|
dataList += ", ";
|
|
}
|
|
dataList += dataArray[k];
|
|
}
|
|
if (dataArray.length > 1){
|
|
dataList += ".";
|
|
}
|
|
}
|
|
|
|
return dataList;
|
|
}
|
|
|
|
function sortOrder(a, b) {
|
|
var orderA = a.order.toUpperCase();
|
|
var orderB = b.order.toUpperCase();
|
|
if (orderA > orderB) {
|
|
return 1;
|
|
} else if (orderA < orderB) {
|
|
return -1;
|
|
}
|
|
if (a.order > b.order) {
|
|
return 1;
|
|
} else if (a.order < b.order) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function zeroPad(num, places) {
|
|
var zero = places - num.toString().length + 1;
|
|
return Array(+(zero > 0 && zero)).join("0") + num;
|
|
}
|
|
|
|
function getFrequentPlaces(list) {
|
|
var count = {};
|
|
list.forEach(function(list) {
|
|
count[list] = (count[list] || 0) + 1;
|
|
});
|
|
return count;
|
|
}
|
|
|
|
//####### seed random library ################
|
|
var seed = 75;
|
|
|
|
var seededRandom = function(max, min) {
|
|
max = max || 1;
|
|
min = min || 0;
|
|
seed = (seed * 9301 + 49297) % 233280;
|
|
var rnd = seed / 233280;
|
|
return min + rnd * (max - min);
|
|
}
|
|
|
|
function getStringScore(str) {
|
|
var score = 0;
|
|
for (var j = 0; j < str.length; j++){
|
|
score += str.charAt(j).charCodeAt(0) + 1;
|
|
}
|
|
return score;
|
|
}
|
|
|
|
function getSeededRandomForString(str) {
|
|
var score = getStringScore(str);
|
|
var d = new Date();
|
|
var n = d.getTime();
|
|
var currentSeed = Math.floor(n / PERSISTENCE_ORDERING_CYCLE);
|
|
seed = score * currentSeed;
|
|
return zeroPad(Math.floor(seededRandom() * 100000),5);
|
|
}
|
|
//####### END of seed random library ################
|
|
|
|
function onMessageReceived(paramChannel, paramMessage, paramSender, paramLocalOnly) {
|
|
if (paramChannel === portalChannelName) {
|
|
var instruction = JSON.parse(paramMessage);
|
|
if (instruction.action === "REZ_PORTAL") {
|
|
generatePortal(instruction.position, instruction.url, instruction.name, instruction.placeID);
|
|
}
|
|
}
|
|
}
|
|
|
|
function generatePortal(position, url, name, placeID) {
|
|
if (rezzerPortalCount <= MAX_REZZED_PORTAL) {
|
|
var TOLERANCE_FACTOR = 1.1;
|
|
if (Vec3.distance(MyAvatar.position, position) < MAX_DISTANCE_TO_CONSIDER_PORTAL) {
|
|
var height = MyAvatar.userHeight * MyAvatar.scale * TOLERANCE_FACTOR;
|
|
|
|
var portalPosition = Vec3.sum(position, {"x": 0.0, "y": height/2, "z": 0.0});
|
|
var dimensions = {"x": height * 0.618, "y": height, "z": height * 0.618};
|
|
var userdata = {
|
|
"url": url,
|
|
"name": name,
|
|
"placeID": placeID
|
|
};
|
|
|
|
var portalID = Entities.addEntity({
|
|
"position": portalPosition,
|
|
"dimensions": dimensions,
|
|
"type": "Shape",
|
|
"shape": "Sphere",
|
|
"name": "Portal to " + name,
|
|
"canCastShadow": false,
|
|
"collisionless": true,
|
|
"userData": JSON.stringify(userdata),
|
|
"script": ROOT + "portal.js",
|
|
"visible": "false",
|
|
"grab": {
|
|
"grabbable": false
|
|
}
|
|
}, "local");
|
|
rezzerPortalCount = rezzerPortalCount + 1;
|
|
|
|
Script.setTimeout(function () {
|
|
Entities.deleteEntity(portalID);
|
|
rezzerPortalCount = rezzerPortalCount - 1;
|
|
if (rezzerPortalCount < 0) {
|
|
rezzerPortalCount = 0;
|
|
}
|
|
}, PORTAL_DURATION_MILLISEC);
|
|
}
|
|
}
|
|
}
|
|
|
|
function cleanup() {
|
|
|
|
if (appStatus) {
|
|
tablet.gotoHomeScreen();
|
|
tablet.webEventReceived.disconnect(onAppWebEventReceived);
|
|
}
|
|
|
|
Messages.messageReceived.disconnect(onMessageReceived);
|
|
Messages.unsubscribe(portalChannelName);
|
|
|
|
tablet.screenChanged.disconnect(onScreenChanged);
|
|
tablet.removeButton(button);
|
|
}
|
|
|
|
Messages.subscribe(portalChannelName);
|
|
Messages.messageReceived.connect(onMessageReceived);
|
|
|
|
Script.scriptEnding.connect(cleanup);
|
|
}());
|