TeaWeb/shared/js/ui/modal/connect/Controller.ts

383 lines
14 KiB
TypeScript
Raw Normal View History

2021-01-09 14:25:11 +01:00
import {Registry} from "tc-shared/events";
import {ConnectProperties, ConnectUiEvents, PropertyValidState} from "tc-shared/ui/modal/connect/Definitions";
2021-01-09 14:25:11 +01:00
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
import {ConnectModal} from "tc-shared/ui/modal/connect/Renderer";
import {LogCategory, logError, logWarn} from "tc-shared/log";
import {
availableConnectProfiles,
ConnectionProfile,
defaultConnectProfile,
findConnectProfile
} from "tc-shared/profiles/ConnectionProfile";
import {Settings, settings} from "tc-shared/settings";
import {connectionHistory, ConnectionHistoryEntry} from "tc-shared/connectionlog/History";
import {global_client_actions} from "tc-shared/events/GlobalEvents";
import {createErrorModal} from "tc-shared/ui/elements/Modal";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {server_connections} from "tc-shared/ConnectionManager";
import _ = require("lodash");
import {parseServerAddress} from "tc-shared/tree/Server";
import * as ipRegex from "ip-regex";
const kRegexDomain = /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))$/i;
export type ConnectParameters = {
targetAddress: string,
targetPassword?: string,
targetPasswordHashed?: boolean,
nickname: string,
nicknameSpecified: boolean,
profile: ConnectionProfile,
token?: string,
defaultChannel?: string | number,
defaultChannelPassword?: string,
}
type ValidityStates = {[T in keyof PropertyValidState]: boolean};
const kDefaultValidityStates: ValidityStates = {
address: false,
nickname: false,
password: false,
profile: false
}
2021-01-09 14:25:11 +01:00
class ConnectController {
readonly uiEvents: Registry<ConnectUiEvents>;
private readonly defaultAddress: string;
private readonly propertyProvider: {[K in keyof ConnectProperties]?: () => Promise<ConnectProperties[K]>} = {};
private historyShown: boolean;
private currentAddress: string;
private currentNickname: string;
private currentPassword: string;
private currentPasswordHashed: boolean;
private currentProfile: ConnectionProfile | undefined;
private addressChanged: boolean;
private nicknameChanged: boolean;
private selectedHistoryId: number;
private history: ConnectionHistoryEntry[];
private validStates: {[T in keyof PropertyValidState]: boolean} = {
address: false,
nickname: false,
password: false,
profile: false
};
private validateStates: {[T in keyof PropertyValidState]: boolean} = {
profile: false,
password: false,
nickname: false,
address: false
};
2021-01-09 14:25:11 +01:00
constructor() {
this.uiEvents = new Registry<ConnectUiEvents>();
this.uiEvents.enableDebug("modal-connect");
this.defaultAddress = "ts.teaspeak.de";
this.historyShown = settings.static_global(Settings.KEY_CONNECT_SHOW_HISTORY);
this.currentAddress = settings.static_global(Settings.KEY_CONNECT_ADDRESS);
this.currentProfile = findConnectProfile(settings.static_global(Settings.KEY_CONNECT_PROFILE)) || defaultConnectProfile();
this.currentNickname = settings.static_global(Settings.KEY_CONNECT_USERNAME);
this.addressChanged = false;
this.nicknameChanged = false;
this.propertyProvider["nickname"] = async () => {
return {
defaultNickname: this.currentProfile?.connectUsername(),
currentNickname: this.currentNickname,
};
};
this.propertyProvider["address"] = async () => {
return {
currentAddress: this.currentAddress,
defaultAddress: this.defaultAddress,
};
};
this.propertyProvider["password"] = async () => this.currentPassword ? ({
hashed: this.currentPasswordHashed,
password: this.currentPassword
}) : undefined;
this.propertyProvider["profiles"] = async () => ({
selected: this.currentProfile?.id,
profiles: availableConnectProfiles().map(profile => ({
id: profile.id,
valid: profile.valid(),
name: profile.profileName
}))
});
this.propertyProvider["historyShown"] = async () => this.historyShown;
this.propertyProvider["history"] = async () => {
if(!this.history) {
this.history = await connectionHistory.lastConnectedServers(10);
}
return {
selected: this.selectedHistoryId,
history: this.history.map(entry => ({
id: entry.id,
targetAddress: entry.targetAddress,
uniqueServerId: entry.serverUniqueId
}))
};
};
this.uiEvents.on("query_property", event => this.sendProperty(event.property));
this.uiEvents.on("query_property_valid", event => this.uiEvents.fire_react("notify_property_valid", { property: event.property, value: this.validStates[event.property] }));
this.uiEvents.on("query_history_connections", event => {
connectionHistory.countConnectCount(event.target, event.targetType).catch(async error => {
logError(LogCategory.GENERAL, tr("Failed to query the connect count for %s (%s): %o"), event.target, event.targetType, error);
return -1;
}).then(count => {
this.uiEvents.fire_react("notify_history_connections", {
target: event.target,
targetType: event.targetType,
value: count
});
});
});
this.uiEvents.on("query_history_entry", event => {
connectionHistory.queryServerInfo(event.serverUniqueId).then(info => {
this.uiEvents.fire_react("notify_history_entry", {
serverUniqueId: event.serverUniqueId,
info: {
icon: {
iconId: info.iconId,
serverUniqueId: event.serverUniqueId,
handlerId: undefined
},
name: info.name,
password: info.passwordProtected,
country: info.country,
clients: info.clientsOnline,
maxClients: info.clientsMax
}
});
}).catch(async error => {
logError(LogCategory.GENERAL, tr("Failed to query the history server info for %s: %o"), event.serverUniqueId, error);
});
});
this.uiEvents.on("action_toggle_history", event => {
if(this.historyShown === event.enabled) {
return;
}
this.historyShown = event.enabled;
this.sendProperty("historyShown").then(undefined);
settings.changeGlobal(Settings.KEY_CONNECT_SHOW_HISTORY, event.enabled);
});
this.uiEvents.on("action_manage_profiles", () => {
/* FIXME: Reload profiles if their status have changed... */
global_client_actions.fire("action_open_window_settings", { defaultCategory: "identity-profiles" });
});
this.uiEvents.on("action_select_profile", event => {
const profile = findConnectProfile(event.id);
if(!profile) {
createErrorModal(tr("Invalid profile"), tr("Target connect profile is missing.")).open();
return;
}
this.currentProfile = profile;
this.sendProperty("profiles").then(undefined);
settings.changeGlobal(Settings.KEY_CONNECT_PROFILE, profile.id);
/* Clear out the nickname on profile switch and use the default nickname */
this.uiEvents.fire("action_set_nickname", { nickname: undefined, validate: true });
this.validateStates["profile"] = true;
this.updateValidityStates();
});
this.uiEvents.on("action_set_address", event => {
if(this.currentAddress !== event.address) {
this.currentAddress = event.address;
this.sendProperty("address").then(undefined);
settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, event.address);
this.setSelectedHistoryId(-1);
}
this.validateStates["address"] = event.validate;
this.updateValidityStates();
});
this.uiEvents.on("action_set_nickname", event => {
if(this.currentNickname !== event.nickname) {
this.currentNickname = event.nickname;
this.sendProperty("nickname").then(undefined);
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, event.nickname);
}
this.validateStates["nickname"] = event.validate;
this.updateValidityStates();
});
this.uiEvents.on("action_set_password", event => {
if(this.currentPassword === event.password) {
return;
}
this.currentPassword = event.password;
this.currentPasswordHashed = event.hashed;
this.sendProperty("password").then(undefined);
this.validateStates["password"] = true;
this.updateValidityStates();
});
this.uiEvents.on("action_select_history", event => this.setSelectedHistoryId(event.id));
this.uiEvents.on("action_connect", () => {
Object.keys(this.validateStates).forEach(key => this.validateStates[key] = true);
this.updateValidityStates();
});
this.updateValidityStates();
2021-01-09 14:25:11 +01:00
}
destroy() {
Object.keys(this.propertyProvider).forEach(key => delete this.propertyProvider[key]);
this.uiEvents.destroy();
}
generateConnectParameters() : ConnectParameters | undefined {
if(Object.keys(this.validStates).findIndex(key => this.validStates[key] === false) !== -1) {
return undefined;
}
return {
nickname: this.currentNickname || this.currentProfile?.connectUsername(),
nicknameSpecified: !!this.currentNickname,
targetAddress: this.currentAddress || this.defaultAddress,
profile: this.currentProfile,
2021-01-09 14:25:11 +01:00
targetPassword: this.currentPassword,
targetPasswordHashed: this.currentPasswordHashed
};
2021-01-09 14:25:11 +01:00
}
setSelectedHistoryId(id: number | -1) {
if(this.selectedHistoryId === id) {
return;
}
this.selectedHistoryId = id;
this.sendProperty("history").then(undefined);
const historyEntry = this.history?.find(entry => entry.id === id);
if(!historyEntry) { return; }
this.currentAddress = historyEntry.targetAddress;
this.currentNickname = historyEntry.nickname;
this.currentPassword = historyEntry.hashedPassword;
this.currentPasswordHashed = true;
this.sendProperty("address").then(undefined);
this.sendProperty("password").then(undefined);
this.sendProperty("nickname").then(undefined);
}
private updateValidityStates() {
const newStates = Object.assign({}, kDefaultValidityStates);
if(this.validateStates["nickname"]) {
const nickname = this.currentNickname || this.currentProfile?.connectUsername() || "";
newStates["nickname"] = nickname.length >= 3 && nickname.length <= 30;
} else {
newStates["nickname"] = true;
}
if(this.validateStates["address"]) {
const address = this.currentAddress || this.defaultAddress || "";
const parsedAddress = parseServerAddress(address);
2021-01-09 14:25:11 +01:00
if(parsedAddress) {
kRegexDomain.lastIndex = 0;
newStates["address"] = kRegexDomain.test(parsedAddress.host) || ipRegex({ exact: true }).test(parsedAddress.host);
} else {
newStates["address"] = false;
}
} else {
newStates["address"] = true;
}
newStates["profile"] = !!this.currentProfile?.valid();
newStates["password"] = true;
for(const key of Object.keys(newStates)) {
if(_.isEqual(this.validStates[key], newStates[key])) {
continue;
}
this.validStates[key] = newStates[key];
this.uiEvents.fire_react("notify_property_valid", { property: key as any, value: this.validStates[key] });
2021-01-09 14:25:11 +01:00
}
}
private async sendProperty(property: keyof ConnectProperties) {
if(!this.propertyProvider[property]) {
logWarn(LogCategory.GENERAL, tr("Tried to send a property where we don't have a provider for"));
return;
}
this.uiEvents.fire_react("notify_property", {
property: property,
value: await this.propertyProvider[property]()
});
}
2021-01-09 14:25:11 +01:00
}
export type ConnectModalOptions = {
connectInANewTab?: boolean,
defaultAddress?: string,
defaultProfile?: string
}
export function spawnConnectModalNew(options: ConnectModalOptions) {
const controller = new ConnectController();
const modal = spawnReactModal(ConnectModal, controller.uiEvents, options.connectInANewTab || false);
modal.show();
modal.events.one("destroy", () => {
controller.destroy();
});
controller.uiEvents.on("action_connect", event => {
const parameters = controller.generateConnectParameters();
if(!parameters) {
/* invalid parameters detected */
return;
}
modal.destroy();
let connection: ConnectionHandler;
if(event.newTab) {
connection = server_connections.spawn_server_connection();
} else {
connection = server_connections.active_connection();
}
if(!connection) {
return;
}
connection.startConnectionNew(parameters, false).then(undefined);
});
2021-01-09 14:25:11 +01:00
}
(window as any).spawnConnectModalNew = spawnConnectModalNew;