* Handle room join and leave events + case sensitive userlist sort; fix #2307 * Webclient: add autoscroll, but only if already at the bottom; fix #2306
486 lines
15 KiB
JavaScript
Executable file
486 lines
15 KiB
JavaScript
Executable file
var StatusEnum = {
|
|
DISCONNECTED : 0,
|
|
CONNECTING : 1,
|
|
CONNECTED : 2,
|
|
LOGGINGIN : 3,
|
|
LOGGEDIN : 4,
|
|
DISCONNECTING : 99
|
|
};
|
|
|
|
var WebClient = {
|
|
status : StatusEnum.DISCONNECTED,
|
|
protocolVersion : 14,
|
|
socket : 0,
|
|
keepalivecb: null,
|
|
lastPingPending: false,
|
|
cmdId : 0,
|
|
initialized: false,
|
|
pendingCommands : {},
|
|
options : {
|
|
host: "",
|
|
port: "",
|
|
user: "",
|
|
pass: "",
|
|
debug: false,
|
|
autojoinrooms: false,
|
|
keepalive: 5000
|
|
},
|
|
|
|
pb : null,
|
|
pbfiles : [
|
|
// commands
|
|
"pb/commands.proto",
|
|
"pb/session_commands.proto",
|
|
"pb/room_commands.proto",
|
|
// replies
|
|
"pb/server_message.proto",
|
|
"pb/response.proto",
|
|
"pb/response_login.proto",
|
|
"pb/session_event.proto",
|
|
"pb/event_server_message.proto",
|
|
"pb/event_server_identification.proto",
|
|
"pb/event_server_shutdown.proto",
|
|
"pb/event_notify_user.proto",
|
|
"pb/event_connection_closed.proto",
|
|
"pb/event_list_rooms.proto",
|
|
"pb/response_join_room.proto",
|
|
"pb/room_event.proto",
|
|
"pb/event_room_say.proto",
|
|
"pb/event_join_room.proto",
|
|
"pb/event_leave_room.proto",
|
|
"pb/event_list_games.proto",
|
|
"pb/serverinfo_user.proto",
|
|
"pb/serverinfo_game.proto"
|
|
],
|
|
|
|
initialize : function()
|
|
{
|
|
this.pb = new protobuf.Root();
|
|
|
|
this.pb.load(this.pbfiles, { keepCase: false }, function(err, root) {
|
|
if (err)
|
|
throw err;
|
|
});
|
|
|
|
this.initialized=true;
|
|
},
|
|
|
|
guid : function(options)
|
|
{
|
|
function s4() {
|
|
return Math.floor((1 + Math.random()) * 0x10000)
|
|
.toString(16)
|
|
.substring(1);
|
|
}
|
|
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
|
},
|
|
|
|
setStatus : function(status, desc)
|
|
{
|
|
this.status = status;
|
|
if(this.options.debug)
|
|
console.log("Stats change:", status, desc)
|
|
if(this.options.statusCallback)
|
|
this.options.statusCallback(status, desc);
|
|
},
|
|
|
|
resetConnectionvars : function () {
|
|
this.cmdId = 0;
|
|
this.pendingCommands = {};
|
|
},
|
|
|
|
sendCommand : function (cmd, callback)
|
|
{
|
|
this.cmdId++;
|
|
cmd["cmdId"] = this.cmdId;
|
|
this.pendingCommands[this.cmdId] = callback;
|
|
|
|
if (this.socket.readyState == WebSocket.OPEN) {
|
|
this.socket.send(WebClient.pb.CommandContainer.encode(cmd).finish());
|
|
if(this.options.debug)
|
|
console.log("Sent: " + cmd.toString());
|
|
} else {
|
|
if(this.options.debug)
|
|
console.log("Send: Not connected");
|
|
}
|
|
},
|
|
|
|
sendRoomCommand : function(roomId, roomCmd, callback)
|
|
{
|
|
var cmd = WebClient.pb.CommandContainer.create({
|
|
"roomId" : roomId,
|
|
"roomCommand" : [ roomCmd ]
|
|
});
|
|
WebClient.sendCommand(cmd, callback);
|
|
},
|
|
|
|
sendSessionCommand : function(ses, callback)
|
|
{
|
|
var cmd = WebClient.pb.CommandContainer.create({
|
|
"sessionCommand" : [ ses ]
|
|
});
|
|
WebClient.sendCommand(cmd, callback);
|
|
},
|
|
|
|
startPingLoop : function()
|
|
{
|
|
keepalivecb = setInterval(function() {
|
|
// check if the previous ping got no reply
|
|
if(WebClient.lastPingPending)
|
|
{
|
|
WebClient.socket.close();
|
|
WebClient.setStatus(StatusEnum.DISCONNECTED, 'Connection timeout');
|
|
}
|
|
|
|
// stop the ping loop if we're disconnected
|
|
if(WebClient.status != StatusEnum.LOGGEDIN)
|
|
{
|
|
clearInterval(keepalivecb);
|
|
keepalivecb = null;
|
|
return;
|
|
}
|
|
|
|
// send a ping
|
|
var CmdPing = WebClient.pb.Command_Ping.create();
|
|
|
|
var sc = WebClient.pb.SessionCommand.create({
|
|
".Command_Ping.ext" : CmdPing
|
|
});
|
|
|
|
WebClient.lastPingPending = true;
|
|
WebClient.sendSessionCommand(sc, function() {
|
|
WebClient.lastPingPending = false;
|
|
});
|
|
|
|
}, WebClient.options.keepalive);
|
|
},
|
|
|
|
doLogin : function()
|
|
{
|
|
var CmdLogin = WebClient.pb.Command_Login.create({
|
|
"userName" : this.options.user,
|
|
"password" : this.options.pass,
|
|
"clientid" : this.guid(),
|
|
"clientver" : "webclient-0.3 (2017-05-26)",
|
|
"clientfeatures" : [
|
|
"client_id",
|
|
"client_ver",
|
|
"feature_set",
|
|
"room_chat_history",
|
|
"client_warnings",
|
|
/* unimplemented features */
|
|
"forgot_password",
|
|
"idle_client",
|
|
"mod_log_lookup",
|
|
"user_ban_history"
|
|
]
|
|
});
|
|
|
|
var sc = WebClient.pb.SessionCommand.create({
|
|
".Command_Login.ext" : CmdLogin
|
|
});
|
|
|
|
this.sendSessionCommand(sc, function(raw) {
|
|
var resp = raw[".Response_Login.ext"];
|
|
switch(raw.responseCode)
|
|
{
|
|
case WebClient.pb.Response.ResponseCode.RespOk:
|
|
WebClient.setStatus(StatusEnum.LOGGEDIN, 'Logged in.');
|
|
|
|
if(WebClient.options.userInfoCallback)
|
|
WebClient.options.userInfoCallback(resp);
|
|
|
|
WebClient.startPingLoop();
|
|
WebClient.doListRooms();
|
|
break;
|
|
case WebClient.pb.Response.ResponseCode.RespClientUpdateRequired:
|
|
WebClient.setStatus(StatusEnum.DISCONNECTING, 'Login failed: missing features');
|
|
break;
|
|
case WebClient.pb.Response.ResponseCode.RespWrongPassword:
|
|
WebClient.setStatus(StatusEnum.DISCONNECTING, 'Login failed: incorrect username or password');
|
|
break;
|
|
case WebClient.pb.Response.ResponseCode.RespWouldOverwriteOldSession:
|
|
WebClient.setStatus(StatusEnum.DISCONNECTING, 'Login failed: duplicated user session');
|
|
break;
|
|
case WebClient.pb.Response.ResponseCode.RespUserIsBanned:
|
|
WebClient.setStatus(StatusEnum.DISCONNECTING, 'Login failed: banned user');
|
|
break;
|
|
case WebClient.pb.Response.ResponseCode.RespUsernameInvalid:
|
|
WebClient.setStatus(StatusEnum.DISCONNECTING, 'Login failed: invalid username');
|
|
break;
|
|
case WebClient.pb.Response.ResponseCode.RespRegistrationRequired:
|
|
WebClient.setStatus(StatusEnum.DISCONNECTING, 'Login failed: registration required');
|
|
break;
|
|
case WebClient.pb.Response.ResponseCode.RespClientIdRequired:
|
|
WebClient.setStatus(StatusEnum.DISCONNECTING, 'Login failed: missing client ID');
|
|
break;
|
|
case WebClient.pb.Response.ResponseCode.RespContextError:
|
|
WebClient.setStatus(StatusEnum.DISCONNECTING, 'Login failed: server error');
|
|
break;
|
|
case WebClient.pb.Response.ResponseCode.RespAccountNotActivated:
|
|
WebClient.setStatus(StatusEnum.DISCONNECTING, 'Login failed: account not activated');
|
|
break;
|
|
case WebClient.pb.Response.ResponseCode.RespClientUpdateRequired:
|
|
WebClient.setStatus(StatusEnum.DISCONNECTING, 'Login failed: missing features');
|
|
break;
|
|
default:
|
|
WebClient.setStatus(StatusEnum.DISCONNECTING, 'Login failed: unknown error ' + raw.responseCode);
|
|
break;
|
|
}
|
|
});
|
|
},
|
|
|
|
doListRooms : function()
|
|
{
|
|
var CmdListRooms = WebClient.pb.Command_ListRooms.create();
|
|
|
|
var sc = WebClient.pb.SessionCommand.create({
|
|
".Command_ListRooms.ext" : CmdListRooms
|
|
});
|
|
|
|
this.sendSessionCommand(sc, function(raw) {
|
|
// Command_ListRooms 's response will be received inside a sessionEvent
|
|
});
|
|
},
|
|
|
|
processSessionEvent : function (raw)
|
|
{
|
|
if(raw[".Event_ConnectionClosed.ext"]) {
|
|
var message = '';
|
|
switch(raw[".Event_ConnectionClosed.ext"]["reason"])
|
|
{
|
|
case WebClient.pb.Event_ConnectionClosed.CloseReason.USER_LIMIT_REACHED:
|
|
message = 'The server has reached its maximum user capacity';
|
|
break;
|
|
case WebClient.pb.Event_ConnectionClosed.CloseReason.TOO_MANY_CONNECTIONS:
|
|
message = 'There are too many concurrent connections from your address';
|
|
break;
|
|
case WebClient.pb.Event_ConnectionClosed.CloseReason.BANNED:
|
|
message = 'You are banned';
|
|
break;
|
|
case WebClient.pb.Event_ConnectionClosed.CloseReason.SERVER_SHUTDOWN:
|
|
message = 'Scheduled server shutdown';
|
|
break;
|
|
case WebClient.pb.Event_ConnectionClosed.CloseReason.USERNAMEINVALID:
|
|
message = 'Invalid username';
|
|
break;
|
|
case WebClient.pb.Event_ConnectionClosed.CloseReason.LOGGEDINELSEWERE:
|
|
message = 'You have been logged out due to logging in at another location';
|
|
break;
|
|
case WebClient.pb.Event_ConnectionClosed.CloseReason.OTHER:
|
|
default:
|
|
message = 'Unknown reason';
|
|
break;
|
|
}
|
|
|
|
if(this.options.connectionClosedCallback)
|
|
this.options.connectionClosedCallback(raw[".Event_ConnectionClosed.ext"]["reason"], message);
|
|
|
|
return;
|
|
}
|
|
|
|
if(raw[".Event_ServerMessage.ext"]) {
|
|
if(this.options.serverMessageCallback)
|
|
this.options.serverMessageCallback(raw[".Event_ServerMessage.ext"]["message"]);
|
|
return;
|
|
}
|
|
|
|
if(raw[".Event_ListRooms.ext"]) {
|
|
var roomsList = raw[".Event_ListRooms.ext"]["roomList"];
|
|
if(this.options.listRoomsCallback)
|
|
this.options.listRoomsCallback(roomsList);
|
|
if(this.options.autojoinrooms)
|
|
{
|
|
$.each(roomsList, function(index, room) {
|
|
if(room.autoJoin)
|
|
{
|
|
var CmdJoinRoom = WebClient.pb.Command_JoinRoom.create({
|
|
"roomId" : room.roomId
|
|
});
|
|
|
|
var sc = WebClient.pb.SessionCommand.create({
|
|
".Command_JoinRoom.ext" : CmdJoinRoom
|
|
});
|
|
|
|
WebClient.sendSessionCommand(sc, WebClient.processJoinRoom);
|
|
}
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if(raw[".Event_ServerIdentification.ext"]) {
|
|
if(this.options.serverIdentificationCallback)
|
|
this.options.serverIdentificationCallback(
|
|
raw[".Event_ServerIdentification.ext"]
|
|
);
|
|
|
|
if(raw[".Event_ServerIdentification.ext"].protocolVersion != WebClient.protocolVersion)
|
|
{
|
|
WebClient.socket.close();
|
|
WebClient.setStatus(StatusEnum.DISCONNECTED, 'Protocol version mismatch: ' + raw[".Event_ServerIdentification.ext"].protocolVersion);
|
|
return;
|
|
}
|
|
|
|
WebClient.setStatus(StatusEnum.CONNECTED, 'Logging in...');
|
|
WebClient.resetConnectionvars();
|
|
WebClient.doLogin();
|
|
return;
|
|
}
|
|
|
|
if(raw[".Event_ServerShutdown.ext"]) {
|
|
if(this.options.serverShutdownCallback)
|
|
this.options.serverShutdownCallback(raw[".Event_ServerShutdown.ext"]);
|
|
return;
|
|
}
|
|
|
|
if(raw[".Event_NotifyUser.ext"]) {
|
|
if(this.options.notifyUserCallback)
|
|
this.options.notifyUserCallback(raw[".Event_NotifyUser.ext"]);
|
|
return;
|
|
}
|
|
},
|
|
|
|
processRoomEvent : function (raw)
|
|
{
|
|
if(raw[".Event_ListGames.ext"]) {
|
|
if(this.options.roomListGamesCallback)
|
|
this.options.roomListGamesCallback(raw["roomId"], raw[".Event_ListGames.ext"]);
|
|
return;
|
|
}
|
|
|
|
if(raw[".Event_JoinRoom.ext"]) {
|
|
if(this.options.roomJoinCallback)
|
|
this.options.roomJoinCallback(raw["roomId"], raw[".Event_JoinRoom.ext"]);
|
|
return;
|
|
}
|
|
|
|
if(raw[".Event_LeaveRoom.ext"]) {
|
|
if(this.options.roomLeaveCallback)
|
|
this.options.roomLeaveCallback(raw["roomId"], raw[".Event_LeaveRoom.ext"]);
|
|
return;
|
|
}
|
|
|
|
if(raw[".Event_RoomSay.ext"]) {
|
|
if(this.options.roomMessageCallback)
|
|
this.options.roomMessageCallback(raw["roomId"], raw[".Event_RoomSay.ext"]);
|
|
return;
|
|
}
|
|
},
|
|
|
|
processJoinRoom : function(raw)
|
|
{
|
|
switch(raw["responseCode"])
|
|
{
|
|
case WebClient.pb.Response.ResponseCode.RespOk:
|
|
var roomInfo = raw[".Response_JoinRoom.ext"]["roomInfo"];
|
|
if(WebClient.options.joinRoomCallback)
|
|
WebClient.options.joinRoomCallback(roomInfo);
|
|
break;
|
|
case WebClient.pb.Response.ResponseCode.RespNameNotFound:
|
|
if(WebClient.options.errorCallback) WebClient.options.errorCallback(raw["responseCode"], "Failed to join the room: it doesn't exists on the server.");
|
|
return;
|
|
case WebClient.pb.Response.ResponseCode.RespContextError:
|
|
if(WebClient.options.errorCallback) WebClient.options.errorCallback(raw["responseCode"], "The server thinks you are in the room but Cockatrice is unable to display it. Try restarting Cockatrice.");
|
|
return;
|
|
case WebClient.pb.Response.ResponseCode.RespUserLevelTooLow:
|
|
if(WebClient.options.errorCallback) WebClient.options.errorCallback(raw["responseCode"], "You do not have the required permission to join this room.");
|
|
return;
|
|
default:
|
|
if(WebClient.options.errorCallback) WebClient.options.errorCallback(raw["responseCode"], "Failed to join the room due to an unknown error: " + raw["responseCode"]);
|
|
return;
|
|
}
|
|
},
|
|
|
|
connect : function(options) {
|
|
jQuery.extend(this.options, options || {});
|
|
|
|
if(!this.initialized)
|
|
this.initialize();
|
|
|
|
this.socket = new WebSocket('ws://' + this.options.host + ':' + this.options.port);
|
|
this.socket.binaryType = "arraybuffer"; // We are talking binary
|
|
this.setStatus(StatusEnum.CONNECTING, 'Connecting...');
|
|
|
|
this.socket.onclose = function() {
|
|
WebClient.setStatus(StatusEnum.DISCONNECTED, 'Connection closed');
|
|
}
|
|
|
|
this.socket.onerror = function() {
|
|
WebClient.setStatus(StatusEnum.DISCONNECTED, 'Connection failed');
|
|
}
|
|
|
|
this.socket.onopen = function(){
|
|
WebClient.setStatus(StatusEnum.CONNECTED, 'Connected');
|
|
}
|
|
|
|
this.socket.onmessage = function(event) {
|
|
//console.log("Received " + event.data.byteLength + " bytes");
|
|
|
|
var uint8msg = new Uint8Array(event.data);
|
|
try {
|
|
var msg = WebClient.pb.ServerMessage.decode(uint8msg);
|
|
if(WebClient.options.debug)
|
|
console.log(msg);
|
|
} catch (err) {
|
|
console.log("Processing failed:", err);
|
|
if(WebClient.options.debug)
|
|
{
|
|
var str = "";
|
|
for(var i = 0; i < uint8msg.length; i++)
|
|
{
|
|
str += String.fromCharCode(uint8msg[i]);
|
|
}
|
|
console.log(str);
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (msg.messageType) {
|
|
case WebClient.pb.ServerMessage.MessageType.RESPONSE:
|
|
var response = msg.response;
|
|
var cmdId = response.cmdId;
|
|
|
|
if(!WebClient.pendingCommands.hasOwnProperty(cmdId))
|
|
return;
|
|
WebClient.pendingCommands[cmdId](response);
|
|
delete WebClient.pendingCommands[cmdId];
|
|
break;
|
|
case WebClient.pb.ServerMessage.MessageType.SESSION_EVENT:
|
|
WebClient.processSessionEvent(msg.sessionEvent);
|
|
break;
|
|
case WebClient.pb.ServerMessage.MessageType.GAME_EVENT_CONTAINER:
|
|
// TODO
|
|
break;
|
|
case WebClient.pb.ServerMessage.MessageType.ROOM_EVENT:
|
|
WebClient.processRoomEvent(msg.roomEvent);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
disconnect : function() {
|
|
this.socket.close();
|
|
},
|
|
|
|
roomSay : function(roomId, msg) {
|
|
var CmdRoomSay = WebClient.pb.Command_RoomSay.create({
|
|
"message" : msg
|
|
});
|
|
|
|
var sc = WebClient.pb.RoomCommand.create({
|
|
".Command_RoomSay.ext" : CmdRoomSay
|
|
});
|
|
|
|
WebClient.sendRoomCommand(roomId, sc, function(raw) {
|
|
switch(raw["responseCode"])
|
|
{
|
|
case WebClient.pb.Response.ResponseCode.RespChatFlood:
|
|
console.log("room flood " + roomId);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
}
|