Structure change (#4220)

* Structure change

* Remove duplicate folders from previous structure

* Cleanup websocket protocol

* Updating from based off PR

* Fixup - remove wrong files during conflict and get the websocket working

* renaming tsx to ts

Co-authored-by: Jeremy Letto <jeremy.letto@datasite.com>
This commit is contained in:
Joseph Chamish 2021-01-20 18:50:18 -05:00 committed by GitHub
parent a0deb73df6
commit 1ddc9cc929
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
123 changed files with 424 additions and 228 deletions

View file

@ -0,0 +1,157 @@
const ProtoFiles = [
"admin_commands.proto",
"card_attributes.proto",
"color.proto",
"command_attach_card.proto",
"command_change_zone_properties.proto",
"command_concede.proto",
"command_create_arrow.proto",
"command_create_counter.proto",
"command_create_token.proto",
"command_deck_del.proto",
"command_deck_del_dir.proto",
"command_deck_download.proto",
"command_deck_list.proto",
"command_deck_new_dir.proto",
"command_deck_select.proto",
"command_deck_upload.proto",
"command_del_counter.proto",
"command_delete_arrow.proto",
"command_draw_cards.proto",
"command_dump_zone.proto",
"command_flip_card.proto",
"command_game_say.proto",
"command_inc_card_counter.proto",
"command_inc_counter.proto",
"command_kick_from_game.proto",
"command_leave_game.proto",
"command_move_card.proto",
"command_mulligan.proto",
"command_next_turn.proto",
"command_ready_start.proto",
"command_replay_delete_match.proto",
"command_replay_download.proto",
"command_replay_list.proto",
"command_replay_modify_match.proto",
"command_reveal_cards.proto",
"command_roll_die.proto",
"command_set_active_phase.proto",
"command_set_card_attr.proto",
"command_set_card_counter.proto",
"command_set_counter.proto",
"command_set_sideboard_lock.proto",
"command_set_sideboard_plan.proto",
"command_shuffle.proto",
"command_stop_dump_zone.proto",
"command_undo_draw.proto",
"commands.proto",
"context_concede.proto",
"context_connection_state_changed.proto",
"context_deck_select.proto",
"context_move_card.proto",
"context_mulligan.proto",
"context_ping_changed.proto",
"context_ready_start.proto",
"context_set_sideboard_lock.proto",
"context_undo_draw.proto",
"event_add_to_list.proto",
"event_attach_card.proto",
"event_change_zone_properties.proto",
"event_connection_closed.proto",
"event_create_arrow.proto",
"event_create_counter.proto",
"event_create_token.proto",
"event_del_counter.proto",
"event_delete_arrow.proto",
"event_destroy_card.proto",
"event_draw_cards.proto",
"event_dump_zone.proto",
"event_flip_card.proto",
"event_game_closed.proto",
"event_game_host_changed.proto",
"event_game_joined.proto",
"event_game_say.proto",
"event_game_state_changed.proto",
"event_join.proto",
"event_join_room.proto",
"event_kicked.proto",
"event_leave.proto",
"event_leave_room.proto",
"event_list_games.proto",
"event_list_rooms.proto",
"event_move_card.proto",
"event_notify_user.proto",
"event_player_properties_changed.proto",
"event_remove_from_list.proto",
"event_replay_added.proto",
"event_reveal_cards.proto",
"event_roll_die.proto",
"event_room_say.proto",
"event_server_complete_list.proto",
"event_server_identification.proto",
"event_server_message.proto",
"event_server_shutdown.proto",
"event_set_active_phase.proto",
"event_set_active_player.proto",
"event_set_card_attr.proto",
"event_set_card_counter.proto",
"event_set_counter.proto",
"event_shuffle.proto",
"event_stop_dump_zone.proto",
"event_user_joined.proto",
"event_user_left.proto",
"event_user_message.proto",
"game_commands.proto",
"game_event.proto",
"game_event_container.proto",
"game_event_context.proto",
"game_replay.proto",
"isl_message.proto",
"moderator_commands.proto",
"move_card_to_zone.proto",
"response.proto",
"response_activate.proto",
"response_adjust_mod.proto",
"response_ban_history.proto",
"response_deck_download.proto",
"response_deck_list.proto",
"response_deck_upload.proto",
"response_dump_zone.proto",
"response_forgotpasswordrequest.proto",
"response_get_games_of_user.proto",
"response_get_user_info.proto",
"response_join_room.proto",
"response_list_users.proto",
"response_login.proto",
"response_register.proto",
"response_replay_download.proto",
"response_replay_list.proto",
"response_viewlog_history.proto",
"response_warn_history.proto",
"response_warn_list.proto",
"room_commands.proto",
"room_event.proto",
"server_message.proto",
"serverinfo_arrow.proto",
"serverinfo_ban.proto",
"serverinfo_card.proto",
"serverinfo_cardcounter.proto",
"serverinfo_chat_message.proto",
"serverinfo_counter.proto",
"serverinfo_deckstorage.proto",
"serverinfo_game.proto",
"serverinfo_gametype.proto",
"serverinfo_player.proto",
"serverinfo_playerping.proto",
"serverinfo_playerproperties.proto",
"serverinfo_replay.proto",
"serverinfo_replay_match.proto",
"serverinfo_room.proto",
"serverinfo_user.proto",
"serverinfo_warning.proto",
"serverinfo_zone.proto",
"session_commands.proto",
"session_event.proto",
];
export default ProtoFiles;

View file

@ -0,0 +1,329 @@
import protobuf from "protobufjs";
import { StatusEnum } from "types";
import * as roomEvents from "./events/RoomEvents";
import * as sessionEvents from "./events/SessionEvents";
import { RoomService, SessionService } from "./services";
import { RoomCommand, SessionCommands } from "./commands";
import ProtoFiles from "./ProtoFiles";
const roomEventKeys = Object.keys(roomEvents);
const sessionEventKeys = Object.keys(sessionEvents);
interface ApplicationCommands {
room: RoomCommand;
session: SessionCommands;
}
interface ApplicationServices {
room: RoomService;
session: SessionService;
}
export class WebClient {
private socket: WebSocket;
private status: StatusEnum = StatusEnum.DISCONNECTED;
private keepalivecb;
private lastPingPending = false;
private cmdId = 0;
private pendingCommands = {};
public commands: ApplicationCommands;
public services: ApplicationServices;
public protocolVersion = 14;
public pb;
public clientConfig = {
"clientver" : "webclient-1.0 (2019-10-31)",
"clientfeatures" : [
"client_id",
"client_ver",
"feature_set",
"room_chat_history",
"client_warnings",
/* unimplemented features */
"forgot_password",
"idle_client",
"mod_log_lookup",
"user_ban_history",
// satisfy server reqs for POC
"websocket",
"2.6.1_min_version",
"2.7.0_min_version",
]
};
public options: any = {
host: "",
port: "",
user: "",
pass: "",
debug: false,
autojoinrooms: true,
keepalive: 5000
};
constructor() {
const files = ProtoFiles.map(file => `${WebClient.PB_FILE_DIR}/${file}`);
this.pb = new protobuf.Root();
this.pb.load(files, { keepCase: false }, (err, root) => {
if (err) {
throw err;
}
});
// This sucks. I can"t seem to get out of this
// circular dependency trap, so this is my current best.
this.commands = {
room: new RoomCommand(this),
session: new SessionCommands(this),
};
this.services = {
room: new RoomService(this),
session: new SessionService(this),
};
console.log(this);
}
private clearStores() {
this.services.room.clearStore();
this.services.session.clearStore();
}
public updateStatus(status, description) {
console.log(`Status: [${status}]: ${description}`);
this.status = status;
this.services.session.updateStatus(status, description);
if (status === StatusEnum.DISCONNECTED) {
this.clearStores();
this.endPingLoop();
this.resetConnectionvars();
}
}
public resetConnectionvars() {
this.cmdId = 0;
this.pendingCommands = {};
this.lastPingPending = false;
}
public sendCommand(cmd, callback) {
this.cmdId++;
cmd["cmdId"] = this.cmdId;
this.pendingCommands[this.cmdId] = callback;
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(this.pb.CommandContainer.encode(cmd).finish());
this.debug(() => console.log("Sent: " + cmd.toString()));
} else {
this.debug(() => console.log("Send: Not connected"));
}
}
public sendRoomCommand(roomId, roomCmd, callback?) {
const cmd = this.pb.CommandContainer.create({
"roomId" : roomId,
"roomCommand" : [ roomCmd ]
});
this.sendCommand(cmd, raw => {
this.debug(() => console.log(raw));
if (callback) {
callback(raw);
}
});
}
public sendSessionCommand(sesCmd, callback?) {
const cmd = this.pb.CommandContainer.create({
"sessionCommand" : [ sesCmd ]
});
this.sendCommand(cmd, (raw) => {
this.debug(() => console.log(raw));
if (callback) {
callback(raw);
}
});
}
public sendModeratorCommand(modCmd, callback?) {
const cmd = this.pb.CommandContainer.create({
"moderatorCommand" : [ modCmd ]
});
this.sendCommand(cmd, (raw) => {
this.debug(() => console.log(raw));
if (callback) {
callback(raw);
}
});
}
public startPingLoop() {
this.keepalivecb = setInterval(() => {
// check if the previous ping got no reply
if (this.lastPingPending) {
this.disconnect();
this.updateStatus(StatusEnum.DISCONNECTED, "Connection timeout");
}
// stop the ping loop if we"re disconnected
if (this.status !== StatusEnum.LOGGEDIN) {
this.endPingLoop();
return;
}
// send a ping
this.lastPingPending = true;
const ping = this.pb.Command_Ping.create();
const command = this.pb.SessionCommand.create({
".Command_Ping.ext" : ping
});
this.sendSessionCommand(command, () => this.lastPingPending = false);
}, this.options.keepalive);
}
private endPingLoop() {
clearInterval(this.keepalivecb);
this.keepalivecb = null;
}
public connect(options) {
this.options = { ...this.options, ...options };
const { host, port } = this.options;
this.socket = new WebSocket("ws://" + host + ":" + port);
this.socket.binaryType = "arraybuffer"; // We are talking binary
this.socket.onopen = () => {
this.updateStatus(StatusEnum.CONNECTED, "Connected");
};
this.socket.onclose = () => {
// dont overwrite failure messages
if (this.status !== StatusEnum.DISCONNECTED) {
this.updateStatus(StatusEnum.DISCONNECTED, "Connection Closed");
}
};
this.socket.onerror = () => {
this.updateStatus(StatusEnum.DISCONNECTED, "Connection Failed");
};
this.socket.onmessage = (event) => {
const msg = this.decodeServerMessage(event);
if (msg) {
switch (msg.messageType) {
case this.pb.ServerMessage.MessageType.RESPONSE:
this.processServerResponse(msg.response);
break;
case this.pb.ServerMessage.MessageType.ROOM_EVENT:
this.processRoomEvent(msg.roomEvent, msg);
break;
case this.pb.ServerMessage.MessageType.SESSION_EVENT:
this.processSessionEvent(msg.sessionEvent, msg);
break;
case this.pb.ServerMessage.MessageType.GAME_EVENT_CONTAINER:
// @TODO
break;
}
}
}
}
public disconnect() {
if (this.socket) {
this.socket.close();
}
}
public debug(debug) {
if (this.options.debug) {
debug();
}
}
private decodeServerMessage(event) {
const uint8msg = new Uint8Array(event.data);
let msg;
try {
msg = this.pb.ServerMessage.decode(uint8msg);
this.debug(() => console.log(msg));
return msg;
} catch (err) {
console.error("Processing failed:", err);
this.debug(() => {
let str = "";
for (let i = 0; i < uint8msg.length; i++) {
str += String.fromCharCode(uint8msg[i]);
}
console.log(str);
});
return;
}
}
private processServerResponse(response) {
const cmdId = response.cmdId;
if (!this.pendingCommands.hasOwnProperty(cmdId)) {
return;
}
this.pendingCommands[cmdId](response);
delete this.pendingCommands[cmdId];
}
private processRoomEvent(response, raw) {
this.processEvent(response, roomEvents, roomEventKeys, raw);
}
private processSessionEvent(response, raw) {
this.processEvent(response, sessionEvents, sessionEventKeys, raw);
}
private processEvent(response, events, keys, raw) {
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const event = events[key];
const payload = response[event.id];
if (payload) {
events[key].action(payload, this, raw);
return;
}
}
}
static PB_FILE_DIR = `${process.env.PUBLIC_URL}/pb`;
}
const webClient = new WebClient();
export default webClient;

View file

@ -0,0 +1,48 @@
import { RoomsDispatch, RoomsSelectors, store } from "store";
import { WebClient } from "../../WebClient";
import { NormalizeService } from "websocket";
export default class RoomService {
webClient: WebClient;
constructor(webClient) {
this.webClient = webClient;
}
clearStore() {
RoomsDispatch.clearStore();
}
joinRoom(roomInfo) {
NormalizeService.normalizeRoomInfo(roomInfo);
RoomsDispatch.joinRoom(roomInfo);
}
updateRooms(rooms) {
RoomsDispatch.updateRooms(rooms);
}
updateGames(roomId, gameList) {
const game = gameList[0];
if (!game.gameType) {
const { gametypeMap } = RoomsSelectors.getRoom(store.getState(), roomId);
NormalizeService.normalizeGameObject(game, gametypeMap);
}
RoomsDispatch.updateGames(roomId, gameList);
}
addMessage(roomId, message) {
NormalizeService.normalizeUserMessage(message);
RoomsDispatch.addMessage(roomId, message);
}
userJoined(roomId, user) {
RoomsDispatch.userJoined(roomId, user);
}
userLeft(roomId, name) {
RoomsDispatch.userLeft(roomId, name);
}
}

View file

@ -0,0 +1,93 @@
import { ServerDispatch, ServerConnectParams } from "store";
import { StatusEnum } from "types";
import { sanitizeHtml } from "websocket/utils";
import { WebClient } from "../../WebClient";
import { NormalizeService } from "websocket";
export default class SessionService {
webClient: WebClient;
constructor(webClient) {
this.webClient = webClient;
}
clearStore() {
ServerDispatch.clearStore();
}
connectServer(options: ServerConnectParams) {
ServerDispatch.connectServer();
this.webClient.updateStatus(StatusEnum.CONNECTING, "Connecting...");
this.webClient.connect(options);
}
disconnectServer() {
this.webClient.updateStatus(StatusEnum.DISCONNECTING, "Disconnecting...");
this.webClient.disconnect();
}
connectionClosed(reason) {
ServerDispatch.connectionClosed(reason);
}
updateBuddyList(buddyList) {
ServerDispatch.updateBuddyList(buddyList);
}
addToBuddyList(user) {
ServerDispatch.addToBuddyList(user);
}
removeFromBuddyList(userName) {
ServerDispatch.removeFromBuddyList(userName);
}
updateIgnoreList(ignoreList) {
ServerDispatch.updateIgnoreList(ignoreList);
}
addToIgnoreList(user) {
ServerDispatch.addToIgnoreList(user);
}
removeFromIgnoreList(userName) {
ServerDispatch.removeFromIgnoreList(userName);
}
updateInfo(name, version) {
ServerDispatch.updateInfo(name, version);
}
updateStatus(state, description) {
ServerDispatch.updateStatus(state, description);
if (state === StatusEnum.DISCONNECTED) {
this.connectionClosed({ reason: description });
}
}
updateUser(user) {
ServerDispatch.updateUser(user);
}
updateUsers(users) {
ServerDispatch.updateUsers(users);
}
userJoined(user) {
ServerDispatch.userJoined(user);
}
userLeft(userId) {
ServerDispatch.userLeft(userId);
}
viewLogs(logs) {
ServerDispatch.viewLogs(NormalizeService.normalizeLogs(logs));
}
serverMessage(message) {
ServerDispatch.serverMessage(sanitizeHtml(message));
}
}

View file

@ -0,0 +1,27 @@
import * as _ from 'lodash';
import { WebClient } from "../WebClient";
export default class RoomCommands {
private webClient: WebClient;
constructor(webClient) {
this.webClient = webClient;
}
roomSay(roomId, message) {
const trimmed = _.trim(message);
if (!trimmed) return;
var CmdRoomSay = this.webClient.pb.Command_RoomSay.create({
"message" : trimmed
});
var rc = this.webClient.pb.RoomCommand.create({
".Command_RoomSay.ext" : CmdRoomSay
});
this.webClient.sendRoomCommand(roomId, rc);
}
}

View file

@ -0,0 +1,234 @@
import { StatusEnum } from "types";
import { WebClient } from "../WebClient";
import { guid } from "../utils";
export default class SessionCommands {
private webClient: WebClient;
constructor(webClient) {
this.webClient = webClient;
}
login() {
const loginConfig = {
...this.webClient.clientConfig,
"userName" : this.webClient.options.user,
"password" : this.webClient.options.pass,
"clientid" : guid()
};
const CmdLogin = this.webClient.pb.Command_Login.create(loginConfig);
const command = this.webClient.pb.SessionCommand.create({
".Command_Login.ext" : CmdLogin
});
this.webClient.sendSessionCommand(command, raw => {
const resp = raw[".Response_Login.ext"];
this.webClient.debug(() => console.log(".Response_Login.ext", resp));
switch(raw.responseCode) {
case this.webClient.pb.Response.ResponseCode.RespOk:
const { buddyList, ignoreList, userInfo } = resp;
this.webClient.services.session.updateBuddyList(buddyList);
this.webClient.services.session.updateIgnoreList(ignoreList);
this.webClient.services.session.updateUser(userInfo);
this.webClient.commands.session.listUsers();
this.webClient.commands.session.listRooms();
this.webClient.updateStatus(StatusEnum.LOGGEDIN, "Logged in.");
this.webClient.startPingLoop();
break;
case this.webClient.pb.Response.ResponseCode.RespClientUpdateRequired:
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: missing features");
break;
case this.webClient.pb.Response.ResponseCode.RespWrongPassword:
case this.webClient.pb.Response.ResponseCode.RespUsernameInvalid:
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: incorrect username or password");
break;
case this.webClient.pb.Response.ResponseCode.RespWouldOverwriteOldSession:
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: duplicated user session");
break;
case this.webClient.pb.Response.ResponseCode.RespUserIsBanned:
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: banned user");
break;
case this.webClient.pb.Response.ResponseCode.RespRegistrationRequired:
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: registration required");
break;
case this.webClient.pb.Response.ResponseCode.RespClientIdRequired:
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: missing client ID");
break;
case this.webClient.pb.Response.ResponseCode.RespContextError:
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: server error");
break;
case this.webClient.pb.Response.ResponseCode.RespAccountNotActivated:
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: account not activated");
break;
default:
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: unknown error " + raw.responseCode);
}
});
}
listUsers() {
const CmdListUsers = this.webClient.pb.Command_ListUsers.create();
const sc = this.webClient.pb.SessionCommand.create({
".Command_ListUsers.ext" : CmdListUsers
});
this.webClient.sendSessionCommand(sc, raw => {
const { responseCode } = raw;
const response = raw[".Response_ListUsers.ext"];
if (response) {
switch (responseCode) {
case this.webClient.pb.Response.ResponseCode.RespOk:
this.webClient.services.session.updateUsers(response.userList);
break;
default:
console.log(`Failed to fetch Server Rooms [${responseCode}] : `, raw);
}
}
});
}
listRooms() {
const CmdListRooms = this.webClient.pb.Command_ListRooms.create();
const sc = this.webClient.pb.SessionCommand.create({
".Command_ListRooms.ext" : CmdListRooms
});
this.webClient.sendSessionCommand(sc);
}
joinRoom(roomId: string) {
const CmdJoinRoom = this.webClient.pb.Command_JoinRoom.create({
"roomId" : roomId
});
const sc = this.webClient.pb.SessionCommand.create({
".Command_JoinRoom.ext" : CmdJoinRoom
});
this.webClient.sendSessionCommand(sc, (raw) => {
const { responseCode } = raw;
let error;
switch(responseCode) {
case this.webClient.pb.Response.ResponseCode.RespOk:
const { roomInfo } = raw[".Response_JoinRoom.ext"];
this.webClient.services.room.joinRoom(roomInfo);
this.webClient.debug(() => console.log("Join Room: ", roomInfo.name));
return;
case this.webClient.pb.Response.ResponseCode.RespNameNotFound:
error = "Failed to join the room: it doesn\"t exist on the server.";
break;
case this.webClient.pb.Response.ResponseCode.RespContextError:
error = "The server thinks you are in the room but Cockatrice is unable to display it. Try restarting Cockatrice.";
break;
case this.webClient.pb.Response.ResponseCode.RespUserLevelTooLow:
error = "You do not have the required permission to join this room.";
break;
default:
error = "Failed to join the room due to an unknown error.";
break;
}
if (error) {
console.error(responseCode, error);
}
});
}
addToBuddyList(userName) {
this.addToList('buddy', userName);
}
removeFromBuddyList(userName) {
this.removeFromList('buddy', userName);
}
addToIgnoreList(userName) {
this.addToList('ignore', userName);
}
removeFromIgnoreList(userName) {
this.removeFromList('ignore', userName);
}
addToList(list: string, userName: string) {
const CmdAddToList = this.webClient.pb.Command_AddToList.create({ list, userName });
const sc = this.webClient.pb.SessionCommand.create({
".Command_AddToList.ext" : CmdAddToList
});
this.webClient.sendSessionCommand(sc, ({ responseCode }) => {
// @TODO: filter responseCode, pop snackbar for error
this.webClient.debug(() => console.log('Added to List Response: ', responseCode));
});
}
removeFromList(list: string, userName: string) {
const CmdRemoveFromList = this.webClient.pb.Command_RemoveFromList.create({ list, userName });
const sc = this.webClient.pb.SessionCommand.create({
".Command_RemoveFromList.ext" : CmdRemoveFromList
});
this.webClient.sendSessionCommand(sc, ({ responseCode }) => {
// @TODO: filter responseCode, pop snackbar for error
this.webClient.debug(() => console.log('Removed from List Response: ', responseCode));
});
}
viewLogHistory(filters) {
const CmdViewLogHistory = this.webClient.pb.Command_ViewLogHistory.create(filters);
const sc = this.webClient.pb.ModeratorCommand.create({
".Command_ViewLogHistory.ext" : CmdViewLogHistory
});
this.webClient.sendModeratorCommand(sc, (raw) => {
const { responseCode } = raw;
let error;
switch(responseCode) {
case this.webClient.pb.Response.ResponseCode.RespOk:
const { logMessage } = raw[".Response_ViewLogHistory.ext"];
console.log("Response_ViewLogHistory: ", logMessage)
this.webClient.services.session.viewLogs(logMessage)
this.webClient.debug(() => console.log("View Log History: ", logMessage));
return;
default:
error = "Failed to retrieve log history.";
break;
}
if (error) {
console.error(responseCode, error);
}
});
}
}

View file

@ -0,0 +1,2 @@
export { default as RoomCommand } from "./RoomCommands";
export { default as SessionCommands } from "./SessionCommands";

View file

@ -0,0 +1,7 @@
export const JoinRoom = {
id: ".Event_JoinRoom.ext",
action: ({ userInfo }, webClient, { roomEvent }) => {
const { roomId } = roomEvent;
webClient.services.room.userJoined(roomId, userInfo);
}
};

View file

@ -0,0 +1,7 @@
export const LeaveRoom = {
id: ".Event_LeaveRoom.ext",
action: ({ name }, webClient, { roomEvent }) => {
const { roomId } = roomEvent;
webClient.services.room.userLeft(roomId, name);
}
};

View file

@ -0,0 +1,7 @@
export const ListGames = {
id: ".Event_ListGames.ext",
action: ({ gameList }, webClient, { roomEvent }) => {
const { roomId } = roomEvent;
webClient.services.room.updateGames(roomId, gameList);
}
};

View file

@ -0,0 +1,7 @@
export const RoomSay = {
id: ".Event_RoomSay.ext",
action: (message, webClient, { roomEvent }) => {
const { roomId } = roomEvent;
webClient.services.room.addMessage(roomId, message);
}
};

View file

@ -0,0 +1,4 @@
export { JoinRoom } from "./JoinRoom";
export { LeaveRoom } from "./LeaveRoom";
export { ListGames } from "./ListGames";
export { RoomSay } from "./RoomSay";

View file

@ -0,0 +1,18 @@
export const AddToList = {
id: ".Event_AddToList.ext",
action: ({ listName, userInfo}, webClient) => {
switch (listName) {
case 'buddy': {
webClient.services.session.addToBuddyList(userInfo);
break;
}
case 'ignore': {
webClient.services.session.addToIgnoreList(userInfo);
break;
}
default: {
webClient.debug(() => console.log('Attempted to add to unknown list: ', listName));
}
}
}
};

View file

@ -0,0 +1,39 @@
import { StatusEnum } from "types";
export const ConnectionClosed = {
id: ".Event_ConnectionClosed.ext",
action: ({ reason }, webClient) => {
let message = "";
// @TODO (5)
switch(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.DEMOTED:
message = "You were demoted";
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;
}
webClient.updateStatus(StatusEnum.DISCONNECTED, message);
}
};

View file

@ -0,0 +1,16 @@
import * as _ from "lodash";
export const ListRooms = {
id: ".Event_ListRooms.ext",
action: ({ roomList }, webClient) => {
webClient.services.room.updateRooms(roomList);
if (webClient.options.autojoinrooms) {
_.each(roomList, ({ autoJoin, roomId }) => {
if (autoJoin) {
webClient.commands.session.joinRoom(roomId);
}
});
}
}
};

View file

@ -0,0 +1,6 @@
export const NotifyUser = {
id: ".Event_NotifyUser.ext",
action: (payload) => {
// console.info("Event_NotifyUser", payload);
}
};

View file

@ -0,0 +1,6 @@
export const PlayerPropertiesChanges = {
id: ".Event_PlayerPropertiesChanges.ext",
action: (payload) => {
// console.info("Event_PlayerPropertiesChanges", payload);
}
};

View file

@ -0,0 +1,18 @@
export const RemoveFromList = {
id: ".Event_RemoveFromList.ext",
action: ({ listName, userName }, webClient) => {
switch (listName) {
case 'buddy': {
webClient.services.session.removeFromBuddyList(userName);
break;
}
case 'ignore': {
webClient.services.session.removeFromIgnoreList(userName);
break;
}
default: {
webClient.debug(() => console.log('Attempted to remove from unknown list: ', listName));
}
}
}
};

View file

@ -0,0 +1,19 @@
import { StatusEnum } from "types";
export const ServerIdentification = {
id: ".Event_ServerIdentification.ext",
action: (info, webClient, _raw) => {
const { serverName, serverVersion, protocolVersion } = info;
if (protocolVersion !== webClient.protocolVersion) {
webClient.disconnect();
webClient.updateStatus(StatusEnum.DISCONNECTED, "Protocol version mismatch: " + protocolVersion);
return;
}
webClient.resetConnectionvars();
webClient.updateStatus(StatusEnum.LOGGINGIN, "Logging in...");
webClient.services.session.updateInfo(serverName, serverVersion);
webClient.commands.session.login();
}
};

View file

@ -0,0 +1,6 @@
export const ServerMessage = {
id: ".Event_ServerMessage.ext",
action: ({ message }, webClient) => {
webClient.services.session.serverMessage(message);
}
};

View file

@ -0,0 +1,6 @@
export const ServerShutdown = {
id: ".Event_ServerShutdown.ext",
action: (payload, webClient) => {
// console.info("Event_ServerShutdown", payload);
}
};

View file

@ -0,0 +1,6 @@
export const UserJoined = {
id: ".Event_UserJoined.ext",
action: ({ userInfo }, webClient) => {
webClient.services.session.userJoined(userInfo);
}
};

View file

@ -0,0 +1,6 @@
export const UserLeft = {
id: ".Event_UserLeft.ext",
action: ({ name }, webClient) => {
webClient.services.session.userLeft(name);
}
};

View file

@ -0,0 +1,6 @@
export const UserMessage = {
id: ".Event_UserMessage.ext",
action: (payload) => {
// console.info("Event_UserMessage", payload);
}
};

View file

@ -0,0 +1,12 @@
export * from "./ConnectionClosed";
export * from "./ListRooms";
export * from "./AddToList";
export * from "./RemoveFromList";
export * from "./NotifyUser"; // @TODO
export * from "./PlayerPropertiesChanges"; // @TODO
export * from "./ServerIdentification";
export * from "./ServerMessage";
export * from "./ServerShutdown"; // @TODO
export * from "./UserJoined";
export * from "./UserLeft";
export * from "./UserMessage"; // @TODO

View file

@ -0,0 +1,11 @@
export { default as webClient } from './WebClient';
export { default as ProtoFiles } from './ProtoFiles';
// Export common used services
export { NormalizeService, RoomService} from "./services";
// Note: this has to come after webClient
export { AuthenticationService, ModeratorService, RoomsService, SessionService } from "./instanceService";

View file

@ -0,0 +1,23 @@
import { StatusEnum } from "types";
import { webClient } from "websocket";
export default class AuthenticationService {
static connect(options) {
webClient.services.session.connectServer(options);
}
static disconnect() {
webClient.services.session.disconnectServer();
}
static isConnected(state) {
return state === StatusEnum.LOGGEDIN;
}
static isModerator(user) {
return user.userLevel >= webClient.pb.ServerInfo_User.UserLevelFlag.IsModerator;
}
static isAdmin() {
}
}

View file

@ -0,0 +1,7 @@
import { webClient } from "..";
export default class ModeratorService {
static viewLogHistory(filters) {
webClient.commands.session.viewLogHistory(filters);
}
}

View file

@ -0,0 +1,11 @@
import { webClient } from "..";
export default class RoomsService {
static joinRoom(roomId) {
webClient.commands.session.joinRoom(roomId);
}
static roomSay(roomId, message) {
webClient.commands.room.roomSay(roomId, message);
}
}

View file

@ -0,0 +1,7 @@
import { RouteEnum } from "../../types";
export class RouterService {
resolveUrl(path, params) {
}
}

View file

@ -0,0 +1,19 @@
import { webClient } from "..";
export default class SessionService {
static addToBuddyList(userName) {
webClient.commands.session.addToBuddyList(userName);
}
static removeFromBuddyList(userName) {
webClient.commands.session.removeFromBuddyList(userName);
}
static addToIgnoreList(userName) {
webClient.commands.session.addToIgnoreList(userName);
}
static removeFromIgnoreList(userName) {
webClient.commands.session.removeFromIgnoreList(userName);
}
}

View file

@ -0,0 +1,4 @@
export { default as AuthenticationService } from "./AuthenticationService";
export { default as ModeratorService } from "./ModeratorService";
export { default as RoomsService } from "./RoomsService";
export { default as SessionService } from "./SessionService";

View file

@ -0,0 +1,44 @@
export default class NormalizeService {
// Flatten room gameTypes into map object
static normalizeRoomInfo(roomInfo) {
roomInfo.gametypeMap = {};
const { gametypeList, gametypeMap, gameList } = roomInfo;
gametypeList.reduce((map, type) => {
map[type.gameTypeId] = type.description;
return map;
}, gametypeMap);
gameList.forEach((game) => NormalizeService.normalizeGameObject(game, gametypeMap));
}
// Flatten gameTypes[] into gameType field
// Default sortable values ("" || 0 || -1)
static normalizeGameObject(game, gametypeMap) {
const { gameTypes, description } = game;
const hasType = gameTypes && gameTypes.length;
game.gameType = hasType ? gametypeMap[gameTypes[0]] : "";
game.description = description || "";
}
// Flatten logs[] into object mapped by targetType (room, game, chat)
static normalizeLogs(logs) {
return logs.reduce((obj, log) => {
const { targetType } = log;
obj[targetType] = obj[targetType] || [];
obj[targetType].push(log);
return obj;
}, {});
}
// messages sent by current user dont have their username prepended
static normalizeUserMessage(message) {
const { name } = message;
if (name) {
message.message = `${name}: ${message.message}`;
}
}
}

View file

@ -0,0 +1,50 @@
import { store, RoomsDispatch, RoomsSelectors } from "store";
import { WebClient } from "../WebClient";
import { NormalizeService } from "websocket";
export default class RoomService {
webClient: WebClient;
constructor(webClient) {
this.webClient = webClient;
}
clearStore() {
RoomsDispatch.clearStore();
}
joinRoom(roomInfo) {
NormalizeService.normalizeRoomInfo(roomInfo);
RoomsDispatch.joinRoom(roomInfo);
}
updateRooms(rooms) {
RoomsDispatch.updateRooms(rooms);
}
updateGames(roomId, gameList) {
const game = gameList[0];
if (!game.gameType) {
const { gametypeMap } = RoomsSelectors.getRoom(store.getState(), roomId);
NormalizeService.normalizeGameObject(game, gametypeMap);
}
RoomsDispatch.updateGames(roomId, gameList);
}
addMessage(roomId, message) {
NormalizeService.normalizeUserMessage(message);
RoomsDispatch.addMessage(roomId, message);
}
userJoined(roomId, user) {
RoomsDispatch.userJoined(roomId, user);
}
userLeft(roomId, name) {
RoomsDispatch.userLeft(roomId, name);
}
}

View file

@ -0,0 +1,94 @@
import { ServerDispatch, ServerConnectParams } from "store";
import { StatusEnum } from "types";
import { sanitizeHtml } from "websocket/utils";
import { WebClient } from "websocket/WebClient";
import { NormalizeService } from "websocket";
export default class SessionService {
webClient: WebClient;
constructor(webClient) {
this.webClient = webClient;
}
clearStore() {
ServerDispatch.clearStore();
}
connectServer(options: ServerConnectParams) {
ServerDispatch.connectServer();
this.webClient.updateStatus(StatusEnum.CONNECTING, "Connecting...");
this.webClient.connect(options);
}
disconnectServer() {
this.webClient.updateStatus(StatusEnum.DISCONNECTING, "Disconnecting...");
this.webClient.disconnect();
}
connectionClosed(reason) {
ServerDispatch.connectionClosed(reason);
}
updateBuddyList(buddyList) {
ServerDispatch.updateBuddyList(buddyList);
}
addToBuddyList(user) {
ServerDispatch.addToBuddyList(user);
}
removeFromBuddyList(userName) {
ServerDispatch.removeFromBuddyList(userName);
}
updateIgnoreList(ignoreList) {
ServerDispatch.updateIgnoreList(ignoreList);
}
addToIgnoreList(user) {
ServerDispatch.addToIgnoreList(user);
}
removeFromIgnoreList(userName) {
ServerDispatch.removeFromIgnoreList(userName);
}
updateInfo(name, version) {
ServerDispatch.updateInfo(name, version);
}
updateStatus(state, description) {
ServerDispatch.updateStatus(state, description);
if (state === StatusEnum.DISCONNECTED) {
this.connectionClosed({ reason: description });
}
}
updateUser(user) {
ServerDispatch.updateUser(user);
}
updateUsers(users) {
ServerDispatch.updateUsers(users);
}
userJoined(user) {
ServerDispatch.userJoined(user);
}
userLeft(userId) {
ServerDispatch.userLeft(userId);
}
viewLogs(logs) {
ServerDispatch.viewLogs(NormalizeService.normalizeLogs(logs));
}
serverMessage(message) {
ServerDispatch.serverMessage(sanitizeHtml(message));
}
}

View file

@ -0,0 +1,3 @@
export { default as NormalizeService } from "./NormalizeService";
export { default as RoomService } from "./RoomService";
export { default as SessionService } from "./SessionService";

View file

@ -0,0 +1,8 @@
function s4() {
const s4 = Math.floor((1 + Math.random()) * 0x10000);
return s4.toString(16).substring(1);
}
export function guid() {
return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4();
}

View file

@ -0,0 +1,2 @@
export * from "./guid.util";
export * from "./sanitizeHtml.util";

View file

@ -0,0 +1,51 @@
import $ from "jquery";
export function sanitizeHtml(msg) {
const $div = $("<div>").html(msg);
const whitelist = {
tags: "br,a,img,center,b,font",
attrs: ["href","color"],
href: ["http://","https://","ftp://","//"]
};
// remove all tags, attributes, and href protocols except some
enforceTagWhitelist($div, whitelist.tags);
enforceAttrWhitelist($div, whitelist.attrs);
enforceHrefWhitelist($div, whitelist.href);
return $div.html();
}
function enforceTagWhitelist($el, tags) {
$el.find("*").not(tags).each(function() {
$(this).replaceWith(this.innerHTML);
});
}
function enforceAttrWhitelist($el, attrs) {
$el.find("*").each(function() {
var attributes = this.attributes;
var i = attributes.length;
while( i-- ) {
var attr = attributes[i];
if( $.inArray(attr.name,attrs) === -1 )
this.removeAttributeNode(attr);
}
});
}
function enforceHrefWhitelist($el, hrefs) {
$el.find("[href]").each(function() {
const $_el = $(this);
const attributeValue = $_el.attr("href");
for (let protocol in hrefs) {
if (attributeValue.indexOf(hrefs[protocol]) === 0) {
$_el.attr("target", "_blank");
return;
}
}
$_el.removeAttr("href");
});
}