mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-10 08:14:47 -07:00
Webatrice P.O.C. (#3854)
* port webclient POC into react shell * Abstract websocket messaging behind redux store * refactor architecture * add rooms store * introduce application service layer and login form * display room messages * implement roomSay * improve Room view styling * display room games * improve gameList update logic * hide protected games * improve game update logic * move mapping to earlier lifecycle hook * add autoscroll to bottom * tabs to spaces, refresh guard * implement server joins/leaves * show users in room * add material-ui to build * refactor, add room joins/leaves to store and render * begin using Material UI components * fix spectatorsCount * remove unused package * improve Server and Room styling * fix scroll context * route on room join * refactor room path * add auth guard * refactor authGuard export * add missing files * clear store on disconnect, add logout button to Account view * fix disconnect handling * Safari fixes * organize current todos * improve login page and server status tracking * improve login page * introduce sorting arch, refine reducers, begin viewLogHistory * audit fix for handlebars * implement moderator log view * comply with code style rules * remove original POC from codebase * add missing semi * minor improvements, begin registration functionality * retry as ws when wss fails additionally, dont mutate the default options when connecting * retain user/pass in WebClient.options for login * take protocol off of options, make it a connect param that defaults to wss * cleanup server page styling * match wss logic with desktop client * add virtual scroll component, add context menu to UserDisplay * revert VirtualTable on messages * improve styling for Room view * add routing to Player view * increase tooltip delay * begin implementing Account view * disable app level contextMenu * implement buddy/ignore list management * fix gitignore Co-authored-by: Jay Letto <jeremy.letto@merrillcorp.com> Co-authored-by: skwerlman <skwerlman@users.noreply.github.com> Co-authored-by: Jeremy Letto <jeremy.letto@datasite.com>
This commit is contained in:
parent
d5b36e8b8a
commit
0457e65751
152 changed files with 19573 additions and 1071 deletions
157
webclient/src/WebClient/ProtoFiles.tsx
Normal file
157
webclient/src/WebClient/ProtoFiles.tsx
Normal 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;
|
||||
329
webclient/src/WebClient/WebClient.tsx
Normal file
329
webclient/src/WebClient/WebClient.tsx
Normal 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 { RoomCommands, SessionCommands } from "./commands";
|
||||
|
||||
import ProtoFiles from "./ProtoFiles";
|
||||
|
||||
const roomEventKeys = Object.keys(roomEvents);
|
||||
const sessionEventKeys = Object.keys(sessionEvents);
|
||||
|
||||
interface ApplicationCommands {
|
||||
room: RoomCommands;
|
||||
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 RoomCommands(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;
|
||||
const protocol = port === '443' ? 'wss' : 'ws';
|
||||
|
||||
this.socket = new WebSocket(protocol + "://" + 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;
|
||||
27
webclient/src/WebClient/commands/RoomCommands.tsx
Normal file
27
webclient/src/WebClient/commands/RoomCommands.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import * as _ from 'lodash';
|
||||
|
||||
import { WebClient } from "../WebClient";
|
||||
|
||||
export 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);
|
||||
}
|
||||
}
|
||||
234
webclient/src/WebClient/commands/SessionCommands.tsx
Normal file
234
webclient/src/WebClient/commands/SessionCommands.tsx
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import { StatusEnum } from "types";
|
||||
|
||||
import { WebClient } from "../WebClient";
|
||||
import { guid } from "../util";
|
||||
|
||||
export 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
2
webclient/src/WebClient/commands/index.tsx
Normal file
2
webclient/src/WebClient/commands/index.tsx
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./RoomCommands";
|
||||
export * from "./SessionCommands";
|
||||
7
webclient/src/WebClient/events/RoomEvents/JoinRoom.tsx
Normal file
7
webclient/src/WebClient/events/RoomEvents/JoinRoom.tsx
Normal 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);
|
||||
}
|
||||
};
|
||||
7
webclient/src/WebClient/events/RoomEvents/LeaveRoom.tsx
Normal file
7
webclient/src/WebClient/events/RoomEvents/LeaveRoom.tsx
Normal 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);
|
||||
}
|
||||
};
|
||||
7
webclient/src/WebClient/events/RoomEvents/ListGames.tsx
Normal file
7
webclient/src/WebClient/events/RoomEvents/ListGames.tsx
Normal 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);
|
||||
}
|
||||
};
|
||||
7
webclient/src/WebClient/events/RoomEvents/RoomSay.tsx
Normal file
7
webclient/src/WebClient/events/RoomEvents/RoomSay.tsx
Normal 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);
|
||||
}
|
||||
};
|
||||
4
webclient/src/WebClient/events/RoomEvents/index.tsx
Normal file
4
webclient/src/WebClient/events/RoomEvents/index.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./JoinRoom";
|
||||
export * from "./LeaveRoom";
|
||||
export * from "./ListGames";
|
||||
export * from "./RoomSay";
|
||||
18
webclient/src/WebClient/events/SessionEvents/AddToList.tsx
Normal file
18
webclient/src/WebClient/events/SessionEvents/AddToList.tsx
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
16
webclient/src/WebClient/events/SessionEvents/ListRooms.tsx
Normal file
16
webclient/src/WebClient/events/SessionEvents/ListRooms.tsx
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const NotifyUser = {
|
||||
id: ".Event_NotifyUser.ext",
|
||||
action: (payload) => {
|
||||
// console.info("Event_NotifyUser", payload);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const PlayerPropertiesChanges = {
|
||||
id: ".Event_PlayerPropertiesChanges.ext",
|
||||
action: (payload) => {
|
||||
// console.info("Event_PlayerPropertiesChanges", payload);
|
||||
}
|
||||
};
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const ServerMessage = {
|
||||
id: ".Event_ServerMessage.ext",
|
||||
action: ({ message }, webClient) => {
|
||||
webClient.services.session.serverMessage(message);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const ServerShutdown = {
|
||||
id: ".Event_ServerShutdown.ext",
|
||||
action: (payload, webClient) => {
|
||||
// console.info("Event_ServerShutdown", payload);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const UserJoined = {
|
||||
id: ".Event_UserJoined.ext",
|
||||
action: ({ userInfo }, webClient) => {
|
||||
webClient.services.session.userJoined(userInfo);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const UserLeft = {
|
||||
id: ".Event_UserLeft.ext",
|
||||
action: ({ name }, webClient) => {
|
||||
webClient.services.session.userLeft(name);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const UserMessage = {
|
||||
id: ".Event_UserMessage.ext",
|
||||
action: (payload) => {
|
||||
// console.info("Event_UserMessage", payload);
|
||||
}
|
||||
};
|
||||
12
webclient/src/WebClient/events/SessionEvents/index.ts
Normal file
12
webclient/src/WebClient/events/SessionEvents/index.ts
Normal 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
|
||||
44
webclient/src/WebClient/services/NormalizeService.tsx
Normal file
44
webclient/src/WebClient/services/NormalizeService.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
export 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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
webclient/src/WebClient/services/RoomService.tsx
Normal file
53
webclient/src/WebClient/services/RoomService.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { Dispatch, Selectors } from "store/rooms";
|
||||
import { store } from "store";
|
||||
|
||||
|
||||
import { WebClient } from "../WebClient";
|
||||
|
||||
import { NormalizeService } from "./NormalizeService";
|
||||
|
||||
export class RoomService {
|
||||
webClient: WebClient;
|
||||
|
||||
constructor(webClient) {
|
||||
this.webClient = webClient;
|
||||
}
|
||||
|
||||
clearStore() {
|
||||
Dispatch.clearStore();
|
||||
}
|
||||
|
||||
joinRoom(roomInfo) {
|
||||
NormalizeService.normalizeRoomInfo(roomInfo);
|
||||
Dispatch.joinRoom(roomInfo);
|
||||
}
|
||||
|
||||
updateRooms(rooms) {
|
||||
Dispatch.updateRooms(rooms);
|
||||
}
|
||||
|
||||
updateGames(roomId, gameList) {
|
||||
const game = gameList[0];
|
||||
|
||||
if (!game.gameType) {
|
||||
const { gametypeMap } = Selectors.getRoom(store.getState(), roomId);
|
||||
NormalizeService.normalizeGameObject(game, gametypeMap);
|
||||
}
|
||||
|
||||
Dispatch.updateGames(roomId, gameList);
|
||||
}
|
||||
|
||||
addMessage(roomId, message) {
|
||||
NormalizeService.normalizeUserMessage(message);
|
||||
|
||||
Dispatch.addMessage(roomId, message);
|
||||
}
|
||||
|
||||
userJoined(roomId, user) {
|
||||
Dispatch.userJoined(roomId, user);
|
||||
}
|
||||
|
||||
userLeft(roomId, name) {
|
||||
Dispatch.userLeft(roomId, name);
|
||||
}
|
||||
}
|
||||
94
webclient/src/WebClient/services/SessionService.tsx
Normal file
94
webclient/src/WebClient/services/SessionService.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { Dispatch, ServerConnectParams } from "store/server";
|
||||
import { StatusEnum } from "types";
|
||||
|
||||
import { sanitizeHtml } from "../util";
|
||||
import { WebClient } from "../WebClient";
|
||||
|
||||
import { NormalizeService } from "./NormalizeService";
|
||||
|
||||
export class SessionService {
|
||||
webClient: WebClient;
|
||||
|
||||
constructor(webClient) {
|
||||
this.webClient = webClient;
|
||||
}
|
||||
|
||||
clearStore() {
|
||||
Dispatch.clearStore();
|
||||
}
|
||||
|
||||
connectServer(options: ServerConnectParams) {
|
||||
Dispatch.connectServer();
|
||||
this.webClient.updateStatus(StatusEnum.CONNECTING, "Connecting...");
|
||||
this.webClient.connect(options);
|
||||
}
|
||||
|
||||
disconnectServer() {
|
||||
this.webClient.updateStatus(StatusEnum.DISCONNECTING, "Disconnecting...");
|
||||
this.webClient.disconnect();
|
||||
}
|
||||
|
||||
connectionClosed(reason) {
|
||||
Dispatch.connectionClosed(reason);
|
||||
}
|
||||
|
||||
updateBuddyList(buddyList) {
|
||||
Dispatch.updateBuddyList(buddyList);
|
||||
}
|
||||
|
||||
addToBuddyList(user) {
|
||||
Dispatch.addToBuddyList(user);
|
||||
}
|
||||
|
||||
removeFromBuddyList(userName) {
|
||||
Dispatch.removeFromBuddyList(userName);
|
||||
}
|
||||
|
||||
updateIgnoreList(ignoreList) {
|
||||
Dispatch.updateIgnoreList(ignoreList);
|
||||
}
|
||||
|
||||
addToIgnoreList(user) {
|
||||
Dispatch.addToIgnoreList(user);
|
||||
}
|
||||
|
||||
removeFromIgnoreList(userName) {
|
||||
Dispatch.removeFromIgnoreList(userName);
|
||||
}
|
||||
|
||||
updateInfo(name, version) {
|
||||
Dispatch.updateInfo(name, version);
|
||||
}
|
||||
|
||||
updateStatus(state, description) {
|
||||
Dispatch.updateStatus(state, description);
|
||||
|
||||
if (state === StatusEnum.DISCONNECTED) {
|
||||
this.connectionClosed({ reason: description });
|
||||
}
|
||||
}
|
||||
|
||||
updateUser(user) {
|
||||
Dispatch.updateUser(user);
|
||||
}
|
||||
|
||||
updateUsers(users) {
|
||||
Dispatch.updateUsers(users);
|
||||
}
|
||||
|
||||
userJoined(user) {
|
||||
Dispatch.userJoined(user);
|
||||
}
|
||||
|
||||
userLeft(userId) {
|
||||
Dispatch.userLeft(userId);
|
||||
}
|
||||
|
||||
viewLogs(logs) {
|
||||
Dispatch.viewLogs(NormalizeService.normalizeLogs(logs));
|
||||
}
|
||||
|
||||
serverMessage(message) {
|
||||
Dispatch.serverMessage(sanitizeHtml(message));
|
||||
}
|
||||
}
|
||||
3
webclient/src/WebClient/services/index.ts
Normal file
3
webclient/src/WebClient/services/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./NormalizeService";
|
||||
export * from "./RoomService";
|
||||
export * from "./SessionService";
|
||||
8
webclient/src/WebClient/util/guid.util.tsx
Normal file
8
webclient/src/WebClient/util/guid.util.tsx
Normal 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();
|
||||
}
|
||||
2
webclient/src/WebClient/util/index.tsx
Normal file
2
webclient/src/WebClient/util/index.tsx
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./guid.util";
|
||||
export * from "./sanitizeHtml.util";
|
||||
51
webclient/src/WebClient/util/sanitizeHtml.util.tsx
Normal file
51
webclient/src/WebClient/util/sanitizeHtml.util.tsx
Normal 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");
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue