initial commit
should practically work as long as you know how to copy a url
This commit is contained in:
commit
fbd2648fa6
|
@ -0,0 +1,2 @@
|
||||||
|
tmp/*
|
||||||
|
node_modules/*
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Messenger.js Third Edition
|
||||||
|
A MSN(r) Messenger(tm) Activity API Emulation script client with a WebSocket server.
|
||||||
|
|
||||||
|
## Why?
|
||||||
|
I wanted to play the old MSN(r) Messenger(tm) Activities online.
|
||||||
|
|
||||||
|
## Client compatibility:
|
||||||
|
Server: whatever node.js version uWS supports
|
||||||
|
Client: does it ES6?
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
* Client synchronization should be "less bad and less prone to desynching"
|
||||||
|
|
||||||
|
## Desires
|
||||||
|
* Discover more Activities that aren't Flash-based, and hope they don't use some other foregone extension.
|
||||||
|
* Pray Ruffle finally starts supporting the scripability APIs needed to support Flash-based activities.
|
|
@ -0,0 +1,197 @@
|
||||||
|
const channelSocket = new WebSocket("ws://localhost:9091/connect");
|
||||||
|
let sessionID = null;
|
||||||
|
let imTheRoomOwner = false;
|
||||||
|
let myNickname = "";
|
||||||
|
/** @section guacamole **/
|
||||||
|
const 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; // todo: not overflow
|
||||||
|
sections.push(instruction.slice(length + 1, position)
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(///g, '/')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
);
|
||||||
|
|
||||||
|
if(instruction.slice(position, position + 1) === ';') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
const 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") {
|
||||||
|
argv[argc] = sections[argc].toString();
|
||||||
|
}
|
||||||
|
argv[argc] = argv[argc]
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/\//g, '/' )
|
||||||
|
.replace(/</g, '<' )
|
||||||
|
.replace(/>/g, '>' )
|
||||||
|
.replace(/&/g, '&' )
|
||||||
|
;
|
||||||
|
instruction = instruction.concat(argv[argc].length.toString(), ".", argv[argc])
|
||||||
|
instruction += (argc === sections.length - 1) ? ';' : ',';
|
||||||
|
}
|
||||||
|
return instruction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** window.external **/
|
||||||
|
window.external.CloseApp = function() {
|
||||||
|
channelSocket.close(1000, "called CloseApp");
|
||||||
|
}
|
||||||
|
// this is important because things expect it
|
||||||
|
/** window.external.Channel **/
|
||||||
|
window.external.Channel = {"Data": "", "SendData": null, "Initialize": null}; // "Type" is a defined property
|
||||||
|
window.external.Channel.SendData = function(d) {channelSocket.send(d)}; // this is dumb but trying to redirect this gives you TypeError's
|
||||||
|
window.external.Channel.Initialize = function() {
|
||||||
|
if(new URL(location.href).searchParams.get("channel") === null) {
|
||||||
|
sessionID = Math.random().toString(36).split('.')[1].substring(0,8);
|
||||||
|
history.pushState(null, "", `?channel=${sessionID}`);
|
||||||
|
} else {
|
||||||
|
sessionID = new URL(location.href).searchParams.get("channel");
|
||||||
|
}
|
||||||
|
createWSEventsNowThatImReady();
|
||||||
|
};
|
||||||
|
Object.defineProperty(window.external.Channel, "Type", {
|
||||||
|
get: function Type() {
|
||||||
|
if(channelSocket.readyState === 1) {
|
||||||
|
return 1; // Indirect;
|
||||||
|
} else {
|
||||||
|
return 2; // Disconnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/** window.external.Users **/
|
||||||
|
// This sadly needs to be already configured before an activity tries using it...
|
||||||
|
// We can't just block execution until we've got the session...
|
||||||
|
window.external.yellows111_Users = [
|
||||||
|
{"Name": "Initiator", "Email": "you@messenger.js", "GlobalIP": "0.0.0.0", "LocalIP": "0.0.0.0", "PUID": "0"},
|
||||||
|
{"Name": "Target", "Email": "opponent@messenger.js", "GlobalIP": "0.0.0.0", "LocalIP": "0.0.0.1", "PUID": "1"}
|
||||||
|
];
|
||||||
|
window.external.Users = {"Me": null, "me": null, "Inviter": null, "Item": null}; // "Count" is a defined property
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(new URL(location.href).searchParams.get("channel") === null) {
|
||||||
|
window.external.Users.Me = window.external.yellows111_Users[0];
|
||||||
|
window.external.Users.me = window.external.yellows111_Users[0];
|
||||||
|
window.external.Users.Inviter = window.external.yellows111_Users[0];
|
||||||
|
document.title = "Initiator: " + document.title;
|
||||||
|
} else {
|
||||||
|
window.external.Users.Me = window.external.yellows111_Users[1];
|
||||||
|
window.external.Users.me = window.external.yellows111_Users[1];
|
||||||
|
window.external.Users.Inviter = window.external.yellows111_Users[0];
|
||||||
|
document.title = "Target: " + document.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** websocket stuff **/
|
||||||
|
channelSocket.binaryType = "arraybuffer"; // what's the point of doing Blob.arrayBuffer every binary call... which is awful because IT'S A PROMISE CALL AND PROMISES SUXX
|
||||||
|
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 = ParseCommandString(new TextDecoder("utf8").decode(event.data));
|
||||||
|
switch(args[0]) {
|
||||||
|
case "rename": {
|
||||||
|
myNickname = args[3];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "connect": {
|
||||||
|
switch(args[1]) {
|
||||||
|
case 0: {
|
||||||
|
console.error("room is full or not avaliable");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
console.log("joined room successfully.");
|
||||||
|
if(parseInt(args[2])) {
|
||||||
|
imTheRoomOwner = false;
|
||||||
|
} else {
|
||||||
|
imTheRoomOwner = true;
|
||||||
|
//window.external.Users.Inviter = window.external.Users.Me;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
console.log("disconnecting?");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "adduser": {
|
||||||
|
if(imTheRoomOwner === false) {
|
||||||
|
window.external.yellows111_Users[1].Name = myNickname;
|
||||||
|
}
|
||||||
|
window.external.yellows111_Users[0].Name = args[2];
|
||||||
|
Channel_OnRemoteAppLoaded();
|
||||||
|
Channel_OnTypeChanged();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "deluser": {
|
||||||
|
// todo?
|
||||||
|
Channel_OnTypeChanged();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "sync": {
|
||||||
|
channelSocket.send(new TextEncoder("utf8").encode(EncodeCommandString(["sync", (new Date).getTime()])));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
console.warn("unknown instruction sequence:", args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channelSocket.onerror = function() {
|
||||||
|
Channel_OnDataError();
|
||||||
|
}
|
||||||
|
channelSocket.onclose = function() {
|
||||||
|
Channel_OnAppClose();
|
||||||
|
}
|
||||||
|
function createWSEventsNowThatImReady() {
|
||||||
|
if(channelSocket.readyState === 1) {
|
||||||
|
// if we're already there
|
||||||
|
channelSocket.send(new TextEncoder("utf8").encode(EncodeCommandString(["rename"])));
|
||||||
|
channelSocket.send(new TextEncoder("utf8").encode(EncodeCommandString(["connect", sessionID])));
|
||||||
|
}
|
||||||
|
channelSocket.onopen = function() {
|
||||||
|
channelSocket.send(new TextEncoder("utf8").encode(EncodeCommandString(["rename"])));
|
||||||
|
channelSocket.send(new TextEncoder("utf8").encode(EncodeCommandString(["connect", sessionID])));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "messenger-js",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "messenger-js",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"dependencies": {
|
||||||
|
"uWebSockets.js": "github:uNetworking/uWebSockets.js#semver:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {}
|
||||||
|
},
|
||||||
|
"node_modules/uWebSockets.js": {
|
||||||
|
"version": "20.42.0",
|
||||||
|
"resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#f40213ec0a97d0d8721d9d32d92d6eb6ddcd22e7",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"uWebSockets.js": "github:uNetworking/uWebSockets.js#semver:*"
|
||||||
|
},
|
||||||
|
"name": "messenger-js",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"description": "An emulation library for the MSN Messenger(tm) Activity API, with a server.",
|
||||||
|
"main": "server.js",
|
||||||
|
"devDependencies": {},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "none"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"websocket"
|
||||||
|
],
|
||||||
|
"author": "yellows111",
|
||||||
|
"license": "UNLICENSED"
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
const config = {port: 9091};
|
||||||
|
var uidcounter = 0;
|
||||||
|
Object.freeze(config); // Config should not be modified after initialization!
|
||||||
|
|
||||||
|
const ChannelStorage = new Map();
|
||||||
|
const UserAttachedChannelStorage = new Map();
|
||||||
|
const UserAttachedNameStorage = new Map(); // uid=>username resolve
|
||||||
|
const ReverseNameToUidStorage = new Map();
|
||||||
|
|
||||||
|
/** @section guacamole **/
|
||||||
|
const 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; // todo: not overflow
|
||||||
|
sections.push(instruction.slice(length + 1, position)
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(///g, '/')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
);
|
||||||
|
|
||||||
|
if(instruction.slice(position, position + 1) === ';') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
const 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") {
|
||||||
|
argv[argc] = sections[argc].toString();
|
||||||
|
}
|
||||||
|
argv[argc] = argv[argc]
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/\//g, '/' )
|
||||||
|
.replace(/</g, '<' )
|
||||||
|
.replace(/>/g, '>' )
|
||||||
|
.replace(/&/g, '&' )
|
||||||
|
;
|
||||||
|
instruction = instruction.concat(argv[argc].length.toString(), ".", argv[argc])
|
||||||
|
instruction += (argc === sections.length - 1) ? ';' : ',';
|
||||||
|
}
|
||||||
|
return instruction;
|
||||||
|
}
|
||||||
|
/** @section sv_functions **/
|
||||||
|
const MessageParser = function(webSocket, message, isBinary) {
|
||||||
|
const msg = new TextDecoder("utf-8").decode(message);
|
||||||
|
if(isBinary) {
|
||||||
|
// TODO: command functions
|
||||||
|
try {
|
||||||
|
const args = ParseCommandString(msg);
|
||||||
|
switch(args[0]) {
|
||||||
|
case "rename": {
|
||||||
|
// todo: make users be able to rename based on parameters given to rename
|
||||||
|
var nickname = `guest${Math.floor(Math.random() * 99999)}`;
|
||||||
|
// this should probably be a while or something
|
||||||
|
if(ReverseNameToUidStorage.get(nickname) !== webSocket.getUserData().uid) {
|
||||||
|
nickname = `guest${Math.floor(Math.random() * 99999)}`;
|
||||||
|
}
|
||||||
|
UserAttachedNameStorage.set(webSocket.getUserData().uid, nickname);
|
||||||
|
ReverseNameToUidStorage.set(nickname, webSocket.getUserData().uid);
|
||||||
|
webSocket.send(EncodeCommandString(["rename", 0, "", nickname]), true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "connect": {
|
||||||
|
if(args.length <= 1) {
|
||||||
|
webSocket.send(EncodeCommandString(["connect", 0]), true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let getChannelId = args[1];
|
||||||
|
if(server.numSubscribers(`channels/${getChannelId}`) >= 2) {
|
||||||
|
// this room is full
|
||||||
|
webSocket.send(EncodeCommandString(["connect", 0]), true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(typeof UserAttachedNameStorage.get(webSocket.getUserData().uid) === "undefined") {
|
||||||
|
// user doesn't have a username
|
||||||
|
webSocket.send(EncodeCommandString(["connect", 0]), true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(typeof ChannelStorage.get(getChannelId) === "undefined") {
|
||||||
|
// make the channel right then and there
|
||||||
|
ChannelStorage.set(getChannelId, {"owner": webSocket.getUserData().uid});
|
||||||
|
};
|
||||||
|
if(webSocket.isSubscribed(`channels/${getChannelId}`) === true) {
|
||||||
|
// already in this channel
|
||||||
|
webSocket.send(EncodeCommandString(["connect", 0]), true);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if(typeof UserAttachedChannelStorage.get(webSocket.getUserData().uid) !== "undefined") {
|
||||||
|
// already in a channel that isn't the one we're trying to join
|
||||||
|
webSocket.send(EncodeCommandString(["connect", 0]), true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
webSocket.send(EncodeCommandString(["connect", 1, server.numSubscribers(`channels/${getChannelId}`)]), true);
|
||||||
|
webSocket.subscribe(`channels/${getChannelId}`);
|
||||||
|
UserAttachedChannelStorage.set(webSocket.getUserData().uid, getChannelId);
|
||||||
|
// do we keep adduser?
|
||||||
|
if(ChannelStorage.get(getChannelId).owner !== webSocket.getUserData().uid) {
|
||||||
|
// send other user's presence if we don't own the channel (if we're not owner)
|
||||||
|
webSocket.send(EncodeCommandString(["adduser", 1, UserAttachedNameStorage.get(ChannelStorage.get(getChannelId).owner)]), true);
|
||||||
|
}
|
||||||
|
// send to other user (or nobody) that we joined this channel
|
||||||
|
webSocket.publish(`channels/${getChannelId}`, EncodeCommandString(["adduser", 1, UserAttachedNameStorage.get(webSocket.getUserData().uid)]), true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "sync": {
|
||||||
|
// client is alive...
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
console.warn("unknown command", args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
webSocket.end(1003, new TextEncoder("utf-8").encode("Unknown format."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// send data to channel
|
||||||
|
webSocket.publish(`channels/${UserAttachedChannelStorage.get(webSocket.getUserData().uid)}`, message, false);
|
||||||
|
// ask other user if alive still
|
||||||
|
webSocket.publish(`channels/${UserAttachedChannelStorage.get(webSocket.getUserData().uid)}`, EncodeCommandString(["sync", (new Date).getTime()]), true);
|
||||||
|
}
|
||||||
|
//console.log(`${new TextDecoder("utf-8").decode(webSocket.getRemoteAddressAsText())} message: ${msg}`);
|
||||||
|
}
|
||||||
|
const UserStateRequest = function(webSocket, state, data = null) {
|
||||||
|
switch(state) {
|
||||||
|
case "open": {
|
||||||
|
//console.log("%s client hello", new TextDecoder("utf-8").decode(webSocket.getRemoteAddressAsText()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "close": {
|
||||||
|
// close event gets WS disconnect code and the client's termination message (avaliable if data.code is 1000, 3xxx, or 4xxx, but only 1000 is standard)
|
||||||
|
// but we DO NOT get to access any WS methods, since they've been closed, so we don't actually know who left.
|
||||||
|
if(data.code === 1000) {
|
||||||
|
console.log(`client exiting, reason: ${new TextDecoder("utf-8").decode(data.message)}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** @section creation **/
|
||||||
|
const server = require("uWebSockets.js").App({});
|
||||||
|
/** @section config **/
|
||||||
|
server.ws("/connect", {
|
||||||
|
// options
|
||||||
|
"compression": require("uWebSockets.js").DEDICATED_COMPRESSOR_3KB, // do we need a compressor for this kind of protocol, uWS actively despises compression, so it's worth considering.
|
||||||
|
// here be events
|
||||||
|
"upgrade": function(rs, rq, cx) {
|
||||||
|
rs.upgrade({"uid": (uidcounter++)}, /* very important, this sets a UserData object (which can contain anything we like) to the WSConnection instance. This can only be done here. */
|
||||||
|
rq.getHeader("sec-websocket-key"),
|
||||||
|
rq.getHeader("sec-websocket-protocol"),
|
||||||
|
rq.getHeader("sec-websocket-extensions"),
|
||||||
|
cx);
|
||||||
|
},
|
||||||
|
"message": MessageParser,
|
||||||
|
"open": function(ws) {UserStateRequest(ws, "open")},
|
||||||
|
"close": function(ws, code, message) {UserStateRequest(ws, "close", {code, message})},
|
||||||
|
"drain": function(ws) {console.log(`WS going through back-pressure!: ${ws.getBufferedAmount()}`)}
|
||||||
|
});
|
||||||
|
server.listen(config.port, function(token) {console.log(token ? `open on ${config.port}` : `failed to listen to ${config.port}`)});
|
Loading…
Reference in New Issue