diff --git a/shared/js/clientservice/index.ts b/shared/js/clientservice/index.ts index 220b33db..eddb03fa 100644 --- a/shared/js/clientservice/index.ts +++ b/shared/js/clientservice/index.ts @@ -2,7 +2,14 @@ import * as loader from "tc-loader"; import {Stage} from "tc-loader"; import {config} from "tc-shared/i18n/localize"; import {getBackend} from "tc-shared/backend"; -import {ClientServiceConfig, ClientServiceInvite, ClientServices, ClientSessionType, LocalAgent} from "tc-services"; +import { + ClientServiceConfig, + ClientServiceInvite, + ClientServices, + ClientSessionType, + initializeClientServices, + LocalAgent +} from "tc-services"; import translation_config = config.translation_config; @@ -12,6 +19,7 @@ export let clientServiceInvite: ClientServiceInvite; loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { priority: 30, function: async () => { + initializeClientServices(); clientServices = new ClientServices(new class implements ClientServiceConfig { getServiceHost(): string { //return "localhost:1244"; diff --git a/shared/js/connection/ErrorCode.ts b/shared/js/connection/ErrorCode.ts index cb574dfc..96800443 100644 --- a/shared/js/connection/ErrorCode.ts +++ b/shared/js/connection/ErrorCode.ts @@ -142,6 +142,8 @@ export enum ErrorCode { SERVER_CONNECT_BANNED = 0xD01, BAN_FLOODING = 0xD03, TOKEN_INVALID_ID = 0xF00, + TOKEN_EXPIRED = 0xf10, + TOKEN_USE_LIMIT_EXCEEDED = 0xf11, WEB_HANDSHAKE_INVALID = 0x1000, WEB_HANDSHAKE_UNSUPPORTED = 0x1001, WEB_HANDSHAKE_IDENTITY_UNSUPPORTED = 0x1002, diff --git a/shared/js/main.tsx b/shared/js/main.tsx index 28f7c129..e536c90e 100644 --- a/shared/js/main.tsx +++ b/shared/js/main.tsx @@ -51,6 +51,8 @@ import "./clientservice"; import "./text/bbcode/InviteController"; import {clientServiceInvite} from "tc-shared/clientservice"; import {ActionResult} from "tc-services"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import {ErrorCode} from "tc-shared/connection/ErrorCode"; assertMainApplication(); @@ -218,6 +220,8 @@ async function doHandleConnectRequest(serverAddress: string, serverUniqueId: str const channel = parameters.getValue(AppParameters.KEY_CONNECT_CHANNEL, undefined); const channelPassword = parameters.getValue(AppParameters.KEY_CONNECT_CHANNEL_PASSWORD, undefined); + const connectToken = parameters.getValue(AppParameters.KEY_CONNECT_TOKEN, undefined); + if(!targetServerConnection) { targetServerConnection = server_connections.getActiveConnectionHandler(); if(targetServerConnection.connected) { @@ -229,12 +233,29 @@ async function doHandleConnectRequest(serverAddress: string, serverUniqueId: str if(targetServerConnection.getCurrentServerUniqueId() === serverUniqueId) { /* Just join the new channel and may use the token (before) */ - /* TODO: Use the token! */ - let containsToken = false; + if(connectToken) { + try { + await targetServerConnection.serverConnection.send_command("tokenuse", { token: connectToken }, { process_result: false }); + } catch (error) { + if(error instanceof CommandResult) { + if(error.id === ErrorCode.TOKEN_INVALID_ID) { + targetServerConnection.log.log("error.custom", { message: tr("Try to use invite key token but the token is invalid.")}); + } else if(error.id == ErrorCode.TOKEN_EXPIRED) { + targetServerConnection.log.log("error.custom", { message: tr("Try to use invite key token but the token is expired.")}); + } else if(error.id === ErrorCode.TOKEN_USE_LIMIT_EXCEEDED) { + targetServerConnection.log.log("error.custom", { message: tr("Try to use invite key token but the token has been used too many times.")}); + } else { + targetServerConnection.log.log("error.custom", { message: tra("Try to use invite key token but an error occurred: {}", error.formattedMessage())}); + } + } else { + logError(LogCategory.GENERAL, tr("Failed to use token: {}"), error); + } + } + } if(!channel) { /* No need to join any channel */ - if(!containsToken) { + if(!connectToken) { createInfoModal(tr("Already connected"), tr("You're already connected to the target server.")).open(); } else { /* Don't show a message since a token has been used */ @@ -263,7 +284,8 @@ async function doHandleConnectRequest(serverAddress: string, serverUniqueId: str } targetChannel.setCachedHashedPassword(channelPassword); - if(await targetChannel.joinChannel()) { + /* Force join the channel. Either we have the password, can ignore the password or we don't want to join. */ + if(await targetChannel.joinChannel(true)) { return { status: "success" }; } else { /* TODO: More detail? */ @@ -277,7 +299,7 @@ async function doHandleConnectRequest(serverAddress: string, serverUniqueId: str nicknameSpecified: false, profile: profile, - token: undefined, + token: connectToken, serverPassword: serverPassword, serverPasswordHashed: passwordsHashed, @@ -467,7 +489,9 @@ const task_connect_handler: loader.Task = { preventWelcomeUI = true; loader.register_task(loader.Stage.LOADED, { priority: 0, - function: async () => handleConnectRequest(address, undefined, AppParameters.Instance), + function: async () => { + handleConnectRequest(address, undefined, AppParameters.Instance).then(undefined); + }, name: tr("default url connect") }); loader.register_task(loader.Stage.LOADED, task_teaweb_starter); diff --git a/shared/js/text/markdown.ts b/shared/js/text/markdown.ts index d666e8e1..1a18d50d 100644 --- a/shared/js/text/markdown.ts +++ b/shared/js/text/markdown.ts @@ -1,4 +1,3 @@ -import * as log from "../log"; import {LogCategory, logDebug, logTrace, logWarn} from "../log"; import { CodeToken, diff --git a/shared/js/tree/Channel.ts b/shared/js/tree/Channel.ts index 67d3d41e..1b699b7d 100644 --- a/shared/js/tree/Channel.ts +++ b/shared/js/tree/Channel.ts @@ -699,7 +699,9 @@ export class ChannelEntry extends ChannelTreeEntry { } + let passwordPrompted = false; if(this.properties.channel_flag_password === true && !this.cachedPasswordHash && !ignorePasswordFlag) { + passwordPrompted = true; const password = await this.requestChannelPassword(PermissionType.B_CHANNEL_JOIN_IGNORE_PASSWORD); if(typeof password === "undefined") { /* aborted */ @@ -717,8 +719,12 @@ export class ChannelEntry extends ChannelTreeEntry { return true; } catch (error) { if(error instanceof CommandResult) { - if(error.id == ErrorCode.CHANNEL_INVALID_PASSWORD) { //Invalid password + if(error.id == ErrorCode.CHANNEL_INVALID_PASSWORD) { this.invalidateCachedPassword(); + if(!passwordPrompted) { + /* It seems like our cached password isn't valid any more */ + return await this.joinChannel(false); + } } } return false; diff --git a/shared/js/tree/ChannelTree.tsx b/shared/js/tree/ChannelTree.tsx index f1ca0bb1..96cf50ed 100644 --- a/shared/js/tree/ChannelTree.tsx +++ b/shared/js/tree/ChannelTree.tsx @@ -902,6 +902,7 @@ export class ChannelTree { spawnCreateChannel(parent?: ChannelEntry) { spawnChannelEditNew(this.client, undefined, parent, (properties, permissions) => { properties["cpid"] = parent ? parent.channelId : 0; + logDebug(LogCategory.CHANNEL, tr("Creating a new channel.\nProperties: %o\nPermissions: %o"), properties); this.client.serverConnection.send_command("channelcreate", properties).then(() => { let channel = this.find_channel_by_name(properties.channel_name, parent, true); @@ -910,6 +911,7 @@ export class ChannelTree { return; } + channel.setCachedHashedPassword(properties.channel_password); if(permissions && permissions.length > 0) { let perms = []; for(let perm of permissions) { diff --git a/shared/js/tree/Server.ts b/shared/js/tree/Server.ts index 25bd29c8..dd092070 100644 --- a/shared/js/tree/Server.ts +++ b/shared/js/tree/Server.ts @@ -5,7 +5,6 @@ import * as log from "../log"; import {LogCategory, logInfo, LogType} from "../log"; import {Sound} from "../sound/Sounds"; import * as bookmarks from "../bookmarks"; -import {spawnInviteEditor} from "../ui/modal/ModalInvite"; import {openServerInfo} from "../ui/modal/ModalServerInfo"; import {createServerModal} from "../ui/modal/ModalServerEdit"; import {spawnIconSelect} from "../ui/modal/ModalIconSelect"; diff --git a/shared/js/ui/modal/ModalInvite.ts b/shared/js/ui/modal/ModalInvite.ts deleted file mode 100644 index 027c7e3b..00000000 --- a/shared/js/ui/modal/ModalInvite.ts +++ /dev/null @@ -1,217 +0,0 @@ -import {settings, Settings} from "../../settings"; -import {createModal, Modal} from "../../ui/elements/Modal"; -import {ConnectionHandler} from "../../ConnectionHandler"; -import {ServerAddress} from "../../tree/Server"; -import {tr} from "tc-shared/i18n/localize"; - -type URLGeneratorSettings = { - flag_direct: boolean, - flag_resolved: boolean -} - -const DefaultGeneratorSettings: URLGeneratorSettings = { - flag_direct: true, - flag_resolved: false -}; - -type URLGenerator = { - generate: (properties: { - address: ServerAddress, - resolved_address: ServerAddress - } & URLGeneratorSettings) => string; - - setting_available: (key: keyof URLGeneratorSettings) => boolean; -}; - -const build_url = (base, params) => { - if (Object.keys(params).length == 0) - return base; - - return base + "?" + Object.keys(params) - .map(e => e + "=" + encodeURIComponent(params[e])) - .join("&"); -}; - -//TODO: Server password -const url_generators: { [key: string]: URLGenerator } = { - "tea-web": { - generate: properties => { - const address = properties.resolved_address ? properties.resolved_address : properties.address; - const address_str = address.host + (address.port === 9987 ? "" : ":" + address.port); - const parameter = "connect_default=" + (properties.flag_direct ? 1 : 0) + "&connect_address=" + encodeURIComponent(address_str); - - let pathbase = ""; - if (document.location.protocol !== 'https:') { - /* - * Seems to be a test environment or the TeaClient for localhost where we dont have to use https. - */ - pathbase = "https://web.teaspeak.de/"; - } else if (document.location.hostname === "localhost" || document.location.host.startsWith("127.")) { - pathbase = "https://web.teaspeak.de/"; - } else { - pathbase = document.location.origin + document.location.pathname; - } - return pathbase + "?" + parameter; - }, - setting_available: setting => { - return { - flag_direct: true, - flag_resolved: true - }[setting] || false; - } - }, - "tea-client": { - generate: properties => { - const address = properties.resolved_address ? properties.resolved_address : properties.address; - - - let parameters = { - connect_default: properties.flag_direct ? 1 : 0 - }; - - if (address.port != 9987) - parameters["port"] = address.port; - - return build_url("teaclient://" + address.host + "/", parameters); - }, - setting_available: setting => { - return { - flag_direct: true, - flag_resolved: true - }[setting] || false; - } - }, - "teamspeak": { - generate: properties => { - const address = properties.resolved_address ? properties.resolved_address : properties.address; - - let parameters = {}; - if (address.port != 9987) - parameters["port"] = address.port; - - /* - ts3server://? - port=9987 - nickname=UserNickname - password=serverPassword - channel=MyDefaultChannel - cid=channelID - channelpassword=defaultChannelPassword - token=TokenKey - addbookmark=MyBookMarkLabel - */ - return build_url("ts3server://" + address.host + "/", parameters); - }, - setting_available: setting => { - return { - flag_direct: false, - flag_resolved: true - }[setting] || false; - } - } -}; - -export function spawnInviteEditor(connection: ConnectionHandler) { - let modal: Modal; - modal = createModal({ - header: tr("Invite URL creator"), - body: () => { - let template = $("#tmpl_invite").renderTag(); - - template.find(".button-close").on('click', event => modal.close()); - return template; - }, - footer: undefined, - min_width: "20em", - width: "50em" - }); - - modal.htmlTag.find(".modal-body").addClass("modal-invite"); - - const button_copy = modal.htmlTag.find(".button-copy"); - const input_type = modal.htmlTag.find(".property-type select"); - const label_output = modal.htmlTag.find(".text-output"); - - const invite_settings = [ - { - key: "flag_direct", - node: modal.htmlTag.find(".flag-direct-connect input"), - value: node => node.prop('checked'), - set_value: (node, value) => node.prop('checked', value == "1"), - disable: (node, flag) => node.prop('disabled', flag) - .firstParent('.checkbox').toggleClass('disabled', flag) - }, - - { - key: "flag_resolved", - node: modal.htmlTag.find(".flag-resolved-address input"), - value: node => node.prop('checked'), - set_value: (node, value) => node.prop('checked', value == "1"), - disable: (node, flag) => node.prop('disabled', flag) - .firstParent('.checkbox').toggleClass('disabled', flag) - } - ]; - - const update_buttons = () => { - const generator = url_generators[input_type.val() as string]; - if (!generator) { - for (const s of invite_settings) - s.disable(s.node, true); - return; - } - - for (const s of invite_settings) - s.disable(s.node, !generator.setting_available(s.key as any)); - }; - - const update_link = () => { - const generator = url_generators[input_type.val() as string]; - if (!generator) { - button_copy.prop('disabled', true); - label_output.text(tr("Missing link generator")); - return; - } - button_copy.prop('disabled', false); - - const properties = { - address: connection.channelTree.server.remote_address, - resolved_address: connection.channelTree.client.serverConnection.remote_address() - }; - for (const s of invite_settings) - properties[s.key] = s.value(s.node); - - label_output.text(generator.generate(properties as any)); - }; - - - for (const s of invite_settings) { - s.node.on('change keyup', () => { - settings.setValue(Settings.FN_INVITE_LINK_SETTING(s.key), s.value(s.node)); - update_link() - }); - - s.set_value(s.node, settings.getValue(Settings.FN_INVITE_LINK_SETTING(s.key), DefaultGeneratorSettings[s.key])); - } - - input_type.on('change', () => { - settings.setValue(Settings.KEY_LAST_INVITE_LINK_TYPE, input_type.val() as string); - update_buttons(); - update_link(); - }).val(settings.getValue(Settings.KEY_LAST_INVITE_LINK_TYPE)); - - button_copy.on('click', event => { - label_output.select(); - document.execCommand('copy'); - }); - - update_buttons(); - update_link(); - modal.open(); -} - -/* - - - - -*/ \ No newline at end of file