messenger.js/client.js

368 lines
14 KiB
JavaScript

const __messengerjs__ = {
"sessionID": null,
"imTheChannelOwner": false,
"myNickname": "",
"onloadfunction": null,
"onloadargs": null,
"fade": null,
"channelSocket": null,
"getWSAddress": null,
"callIfExists": null,
"beforeUnloadHandler": null,
"ParseCommandString": null,
"EncodeCommandString": null,
"changeUsernameDialog": null
}
__messengerjs__.getWSAddress = function() {
const ws = "ws://localhost:19180/connect";
const wss = "wss://localhost:19543/connect";
if(window.location.host === "localhost" || window.location.protocol === "file:") {
// local users probably want ws
return ws;
}
if(window.hasOwnProperty("isSecureContext")) {
if(window.isSecureContext) {
// use wss
return wss;
} else {
// use ws
return ws;
}
} else {
// use wss (can't be sure)
return wss;
}
}
__messengerjs__.channelSocket = new WebSocket(__messengerjs__.getWSAddress());
__messengerjs__.fade = document.createElement("div");
__messengerjs__.fade.style.cssText = "position: absolute;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.75);color:white;text-align:center;font-family:sans-serif;";
__messengerjs__.fade.textContent = "connecting to WebSocket server...";
__messengerjs__.callIfExists = function(fnName) {
// THIS function exists because some of the events we call may not exist on the client... why?
if(typeof window[fnName] === "function") {
window[fnName]();
}
}
__messengerjs__.beforeUnloadHandler = function(ev) {
Channel_OnAppClose();
__messengerjs__.channelSocket.send(new TextEncoder("utf8").encode(__messengerjs__.EncodeCommandString(["disconnect"])));
__messengerjs__.channelSocket.close(1000, "unloading");
};
/** @section guacamole **/
__messengerjs__.ParseCommandString = function(instruction) {
if(typeof instruction !== "string") {
return;
}
let position = -1;
let sections = new Array();
for(;;) {
let length = instruction.indexOf('.', position + 1);
if(length === -1) {
break;
}
position = (parseInt(instruction.slice(position + 1, length)) + length) + 1;
if(position > instruction.length) {
return;
}
sections.push(instruction.slice(length + 1, position)
.replace(/'/g, "'")
.replace(/"/g, '"')
.replace(///g, '/')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
);
if(instruction.slice(position, position + 1) === ';') {
break;
}
}
return sections;
}
__messengerjs__.EncodeCommandString = function(sections) {
if(Array.isArray(sections) === false) {
return;
}
let instruction = new String();
const argv = sections;
for (let argc = 0; argc < sections.length ; argc++) {
if(typeof sections[argc] !== "string") {
if(sections[argc] === null || typeof sections[argc] === "undefined") {
argv[argc] = "";
} else {
argv[argc] = sections[argc].toString();
}
}
argv[argc] = argv[argc]
.replace(/&/g, '&amp;' )
.replace(/>/g, '&gt;' )
.replace(/</g, '&lt;' )
.replace(/\//g, '&#x2F' )
.replace(/"/g, '&quot;')
.replace(/'/g, "&#x27;")
;
instruction = instruction.concat(argv[argc].length.toString(), ".", argv[argc])
instruction += (argc === sections.length - 1) ? ';' : ',';
}
return instruction;
}
/** window.external **/
window.external.CloseApp = function() {
Channel_OnAppClose();
__messengerjs__.channelSocket.send(new TextEncoder("utf8").encode(__messengerjs__.EncodeCommandString(["disconnect"])));
__messengerjs__.channelSocket.close(1000, "we called CloseApp");
}
// this is important because things expect it
/** window.external.Channel **/
window.external.Channel = {"Data": "", "SendData": null, "Initialize": null, "Error": {"Data": "", "Type": 0}}; // "Type" is a defined property
// things may want Channel.IM: string and Channel.FileInfo: object {Path: string, Size: number, Progress: number<0-100>, Incoming: bool, Status: num<0-3>}
window.external.Channel.SendData = function(d) {
try {
__messengerjs__.channelSocket.send(d)
} catch(e) {
window.external.Channel.Error.Data = d;
window.external.Channel.Error.Type = 0xBAD50CE7|0; // bad socket, might change this to something else
__messengerjs__.callIfExists("Channel_OnDataError");
__messengerjs__.callIfExists("Channel_OnTypeChanged");
if(__messengerjs__.channelSocket.readyState !== 1) {
__messengerjs__.fade.textContent = "the connection was closed while trying to send data."
__messengerjs__.fade.style.display = "block";
}
}
}; // this is dumb but trying to redirect this gives you TypeError's
window.external.Channel.Initialize = function() {
// no idea we're already pretty initalized by the time most things call
__messengerjs__.callIfExists("Channel_OnTypeChanged"); // make client check if its even connected still
};
Object.defineProperty(window.external.Channel, "Type", {
get: function Type() {
if(__messengerjs__.channelSocket.readyState === 1) {
return 1; // Indirect;
} else {
return 2; // Disconnected;
}
}
});
/** window.external.Users **/
// yes i define these as the default users... should be created a bit more dynamically
// problem with that is that i'm yet to find a 1 user activity that calls any of these... ReplaceIM abuse maybe?
// avaliablity of these properties depends on the permission flags, especially UserProperties being enabled
window.external.yellows111_Users = [
{"Name": "Initiator", "Email": "initiator@messenger.js", "GlobalIP": "0.0.0.0", "LocalIP": "0.0.0.0", "PUID": "0"},
{"Name": "Target", "Email": "target@messenger.js", "GlobalIP": "0.0.0.0", "LocalIP": "0.0.0.1", "PUID": "1"}
];
window.external.Users = {"Me": null, "Inviter": null, "Item": null}; // "Count" is a defined property
/** window.external.Users._NewEnum **/
window.external.Users[Symbol.iterator] = function*() {
yield window.external.yellows111_Users[0];
yield window.external.yellows111_Users[1];
};
window.external.Users.Item = function(which){
return window.external.yellows111_Users[which];
}
Object.defineProperty(window.external.Users, "Count", {
get: function Count() {
return window.external.yellows111_Users.length; // 2
}
});
/** Some early activities use a lower case "Me", support that like this... **/
Object.defineProperty(window.external.Users, "me", {
get: function me() {
return window.external.Users.Me;
}
});
/*
window.external.Messenger exists but all it does is:
* allow you to open the Options dialog (on a certain page: number)
* allow you to open the phone dialer (with a predetermined number: string)
so why would i even bother?
*/
/** websocket stuff **/
__messengerjs__.channelSocket.binaryType = "arraybuffer"; // the alternative is blob.arrayBuffer(); IT'S A PROMISE CALL AND PROMISES SUXX
__messengerjs__.channelSocket.onmessage = function(event) {
switch(event.data.constructor.name) {
case "String": {
// is a Activity message
window.external.Channel.Data = event.data;
Channel_OnDataReceived();
break;
}
case "ArrayBuffer": {
// is a Command message
let args = __messengerjs__.ParseCommandString(new TextDecoder("utf8").decode(event.data));
switch(args[0]) {
case "rename": {
__messengerjs__.myNickname = args[3];
break;
}
case "ready": {
console.log("joined channel successfully.");
// todo: allow targets to change username, not only initiators
__messengerjs__.fade.textContent = "waiting for opponent to join this channel...";
const nameDialog = document.createElement("div");
nameDialog.style.textAlign = "center";
const usernameInput = document.createElement("input");
const usernameButton = document.createElement("button");
const helpText = document.createElement("p");
usernameInput.onkeydown = function() {
usernameInput.setCustomValidity("");
}
usernameInput.style.marginRight = "2px";
nameDialog.appendChild(usernameInput);
usernameButton.textContent = "Change Username";
usernameButton.onclick = function() {
if(/^[\w ]{3,32}$/u.test(usernameInput.value.trim())) {
__messengerjs__.channelSocket.send(
new TextEncoder("utf8").encode(__messengerjs__.EncodeCommandString(["rename", usernameInput.value.trim()]))
);
} else {
usernameInput.setCustomValidity("Username must be between 3 and 32 alphanumeric characters, spaces are allowed.");
usernameInput.reportValidity();
}
}
usernameButton.style.marginLeft = "2px";
nameDialog.appendChild(usernameButton);
helpText.textContent = "Usernames can be between 3 and 32 alphanumeric characters with spaces.";
nameDialog.appendChild(helpText);
__messengerjs__.fade.appendChild(document.createElement("br"));
__messengerjs__.fade.appendChild(document.createElement("br"));
__messengerjs__.fade.appendChild(nameDialog);
window.addEventListener("beforeunload", __messengerjs__.beforeUnloadHandler);
__messengerjs__.imTheChannelOwner = (args[1] !== "1");
break;
}
case "error": {
window.external.Channel.Error.Data = args[1];
window.external.Channel.Error.Type = parseInt(args[2]);
switch(args[2]) {
case "0": { // if we're getting an error with STATUS_SUCCESSFUL... which sounds stupid, but i'm using it as SERVER_CLOSING)
__messengerjs__.fade.textContent = "the channel server was closed. please try again later.";
__messengerjs__.fade.style.display = "block";
__messengerjs__.channelSocket.close(1000, "server shutdown acknoledged.");
__messengerjs__.callIfExists("Channel_OnTypeChanged");
Channel_OnRemoteAppClosed(); // they're gone
Channel_OnAppClose(); // we're gone too
break;
}
case "513": { // SERVER_BUSY = channel was full
console.warn("channel was full, generating new room");
__messengerjs__.sessionID = Math.random().toString(36).split('.')[1].substring(0,8);
history.pushState(null, "", "#channel=" + __messengerjs__.sessionID);
__messengerjs__.channelSocket.send(new TextEncoder("utf8").encode(__messengerjs__.EncodeCommandString(["connect", __messengerjs__.sessionID, window.GameCode])));
break;
}
default: {
__messengerjs__.fade.textContent = "server error: " + args[1] + " code (" + args[2] + ")";
__messengerjs__.fade.style.display = "block";
__messengerjs__.channelSocket.close(1000, "server sent non-zero error");
__messengerjs__.callIfExists("Channel_OnTypeChanged");
__messengerjs__.callIfExists("Channel_OnDataError"); // this may just attempt to retry to send the failed data... when we're already closed;
}
}
break;
}
case "adduser": {
__messengerjs__.fade.style.display = "none";
if(__messengerjs__.imTheChannelOwner === false) {
window.external.yellows111_Users[1].Name = __messengerjs__.myNickname;
window.external.yellows111_Users[0].Name = args[2];
} else {
window.external.yellows111_Users[1].Name = args[2];
window.external.yellows111_Users[0].Name = __messengerjs__.myNickname;
}
if(__messengerjs__.imTheChannelOwner === true) {
window.external.Users.Me = window.external.yellows111_Users[0];
window.external.Users.Inviter = window.external.yellows111_Users[0];
} else {
window.external.Users.Me = window.external.yellows111_Users[1];
window.external.Users.Inviter = window.external.yellows111_Users[0];
}
document.title = __messengerjs__.myNickname + ": " + document.title;
/** beyond even hack territory **/
if(typeof __messengerjs__.onloadfunction === "function") {
__messengerjs__.onloadfunction(__messengerjs__.onloadargs);
}
Channel_OnRemoteAppLoaded();
__messengerjs__.callIfExists("Channel_OnTypeChanged");
break;
}
case "remuser": {
__messengerjs__.fade.textContent = "opponent (" + args[1] + ") disconnected. refresh to get a new session!";
__messengerjs__.fade.style.display = "block";
window.removeEventListener("beforeunload", __messengerjs__.beforeUnloadHandler);
Channel_OnRemoteAppClosed();
__messengerjs__.channelSocket.send(new TextEncoder("utf8").encode(__messengerjs__.EncodeCommandString(["disconnect"])));
__messengerjs__.channelSocket.close(1000, "opponent left");
break;
}
case "sync": {
__messengerjs__.channelSocket.send(new TextEncoder("utf8").encode(__messengerjs__.EncodeCommandString(["sync", (new Date).getTime()])));
break;
}
default: {
console.warn("unknown instruction sequence:", args);
}
}
break;
}
}
}
__messengerjs__.channelSocket.onerror = function(ev) {
window.external.Channel.Error.Data = "WebSocket generic error";
__messengerjs__.fade.textContent = "encountered a WebSocket error"
__messengerjs__.fade.style.display = "block";
__messengerjs__.callIfExists("Channel_OnDataError");
}
__messengerjs__.channelSocket.onclose = function() {
__messengerjs__.callIfExists("Channel_OnTypeChanged");
}
function createWSEventsNowThatImReady() {
if(__messengerjs__.channelSocket.readyState === 1) {
// if we're already there
__messengerjs__.channelSocket.send(new TextEncoder("utf8").encode(__messengerjs__.EncodeCommandString(["rename"])));
__messengerjs__.channelSocket.send(new TextEncoder("utf8").encode(__messengerjs__.EncodeCommandString(["connect", __messengerjs__.sessionID, window.GameCode])));
}
__messengerjs__.channelSocket.onopen = function() {
__messengerjs__.channelSocket.send(new TextEncoder("utf8").encode(__messengerjs__.EncodeCommandString(["rename"])));
__messengerjs__.channelSocket.send(new TextEncoder("utf8").encode(__messengerjs__.EncodeCommandString(["connect", __messengerjs__.sessionID, window.GameCode])));
}
}
/** pure evil **/
__messengerjs__.OnLoadInterrupt = function(ev) {
document.body.appendChild(__messengerjs__.fade);
if(new URL(location.href).hash.substring(1).split("=")[0] === "channel") {
__messengerjs__.sessionID = new URL(location.href).hash.substring(1).split("=")[1];
} else {
__messengerjs__.sessionID = Math.random().toString(36).split('.')[1].substring(0,8);
history.pushState(null, "", "#channel=" + __messengerjs__.sessionID);
}
createWSEventsNowThatImReady();
__messengerjs__.onloadargs = ev;
}
document.addEventListener("DOMContentLoaded", function(event) {
// do document.body.onload too since some things suck and like doing that
if(typeof document.body.onload === "function") {
__messengerjs__.onloadfunction = document.body.onload;
}
window.onload = __messengerjs__.OnLoadInterrupt;
Object.defineProperty(window, "onload", {
set: function onload(fn) {
__messengerjs__.onloadfunction = fn;
}
});
});