diff --git a/src/main/java/com/github/manevolent/ts3j/command/Command.java b/src/main/java/com/github/manevolent/ts3j/command/Command.java index 033d50b..5347f73 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/Command.java +++ b/src/main/java/com/github/manevolent/ts3j/command/Command.java @@ -1,6 +1,6 @@ package com.github.manevolent.ts3j.command; -import com.github.manevolent.ts3j.command.part.CommandParameter; +import com.github.manevolent.ts3j.command.parameter.CommandParameter; import com.github.manevolent.ts3j.protocol.ProtocolRole; public interface Command { diff --git a/src/main/java/com/github/manevolent/ts3j/command/CommandHandler.java b/src/main/java/com/github/manevolent/ts3j/command/CommandHandler.java index b4ff205..dddeddb 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/CommandHandler.java +++ b/src/main/java/com/github/manevolent/ts3j/command/CommandHandler.java @@ -1,6 +1,9 @@ package com.github.manevolent.ts3j.command; +import com.github.manevolent.ts3j.command.client.ClientChannelListFinishedProcessor; +import com.github.manevolent.ts3j.command.client.ClientChannelListProcessor; import com.github.manevolent.ts3j.command.client.ClientInitServerProcessor; +import com.github.manevolent.ts3j.command.client.ClientNotifyClientEnterViewProcessor; import com.github.manevolent.ts3j.protocol.socket.client.AbstractTeamspeakClientSocket; import com.github.manevolent.ts3j.util.Ts3Debugging; @@ -15,19 +18,28 @@ public final class CommandHandler implements CommandProcessor { } @Override - public void process(AbstractTeamspeakClientSocket socket, ComplexCommand complexCommand) + public void process(AbstractTeamspeakClientSocket socket, MultiCommand multiCommand) throws CommandProcessException { - Ts3Debugging.debug("[COMMAND] " + complexCommand.build()); - - CommandProcessor processor = commandProcessors.get(complexCommand.getName().toLowerCase()); + CommandProcessor processor = commandProcessors.get(multiCommand.getName().toLowerCase()); if (processor != null) - processor.process(socket, complexCommand); + processor.process(socket, multiCommand); } - public static CommandHandler createLocalClient() { + @Override + public void process(AbstractTeamspeakClientSocket socket, SingleCommand command) + throws CommandProcessException { + CommandProcessor processor = commandProcessors.get(command.getName().toLowerCase()); + if (processor != null) + processor.process(socket, command); + } + + public static CommandHandler createLocalClientHandler() { CommandHandler handler = new CommandHandler(); handler.commandProcessors.put("initserver", new ClientInitServerProcessor()); + handler.commandProcessors.put("channellist", new ClientChannelListProcessor()); + handler.commandProcessors.put("notifycliententerview", new ClientNotifyClientEnterViewProcessor()); + handler.commandProcessors.put("channellistfinished", new ClientChannelListFinishedProcessor()); return handler; } diff --git a/src/main/java/com/github/manevolent/ts3j/command/CommandProcessor.java b/src/main/java/com/github/manevolent/ts3j/command/CommandProcessor.java index cf931d3..21ca334 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/CommandProcessor.java +++ b/src/main/java/com/github/manevolent/ts3j/command/CommandProcessor.java @@ -3,6 +3,14 @@ package com.github.manevolent.ts3j.command; import com.github.manevolent.ts3j.protocol.socket.client.AbstractTeamspeakClientSocket; public interface CommandProcessor { - void process(AbstractTeamspeakClientSocket socket, ComplexCommand complexCommand) - throws CommandProcessException; + default void process(AbstractTeamspeakClientSocket client, + MultiCommand multiCommand) + throws CommandProcessException { + for (SingleCommand singleCommand : multiCommand.simplify()) process(client, singleCommand); + } + + + default void process(AbstractTeamspeakClientSocket client, + SingleCommand singleCommand) + throws CommandProcessException { } } diff --git a/src/main/java/com/github/manevolent/ts3j/command/ComplexCommand.java b/src/main/java/com/github/manevolent/ts3j/command/ComplexCommand.java deleted file mode 100644 index 0087dac..0000000 --- a/src/main/java/com/github/manevolent/ts3j/command/ComplexCommand.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.github.manevolent.ts3j.command; - -import com.github.manevolent.ts3j.command.part.CommandOption; -import com.github.manevolent.ts3j.command.part.CommandParameter; -import com.github.manevolent.ts3j.command.part.CommandSingleParameter; -import com.github.manevolent.ts3j.protocol.ProtocolRole; -import com.github.manevolent.ts3j.util.Ts3String; - -import java.util.*; - -public class ComplexCommand implements Command { - private final String name; - private final ProtocolRole role; - private final List params; - - public ComplexCommand(String name, ProtocolRole role, CommandParameter... params) { - this(name, role, Arrays.asList(params)); - } - - public ComplexCommand(String name, ProtocolRole role, List params) { - this.name = name; - this.role = role; - this.params = new LinkedList<>(); - - for (CommandParameter parameter : params) add(parameter); - } - - @Override - public String getName() { - return name; - } - - @Override - public ProtocolRole getRole() { - return role; - } - - @Override - public Iterable getParameters() { - return Collections.unmodifiableCollection(params); - } - - public boolean has(String key) { - return params.stream().anyMatch(x -> x.getName().equalsIgnoreCase(key)); - } - - public void add(CommandParameter parameter) { - if (parameter.getName().trim().length() <= 0) - throw new IllegalArgumentException("invalid key (empty)"); - - this.params.add(parameter); - } - - public SimpleCommand simplify() { - return new SimpleCommand( - name, - role, - params - ); - } - - public static ComplexCommand parse(ProtocolRole role, String commandText) { - String[] commandParts = commandText.split(" "); - - String commandLabel = commandParts[0].toLowerCase(); - - List parameterList = new LinkedList<>(); - - for (int i = 1; i < commandParts.length; i ++) { - String commandParamText = commandParts[i].trim(); - if (commandParamText.length() <= 0) continue; - - if (commandParamText.startsWith("-")) { // Option - parameterList.add(new CommandOption(commandParamText.substring(1))); - } else { - String[] commandParamParts = commandParamText.split("=", 2); - - parameterList.add(new CommandSingleParameter( - commandParamParts[0].toLowerCase(), - commandParamParts.length > 1 ? - Ts3String.unescape(commandParamParts[1]) : - null - ) - ); - } - } - - return new ComplexCommand(commandLabel, role, parameterList); - } -} diff --git a/src/main/java/com/github/manevolent/ts3j/command/MultiCommand.java b/src/main/java/com/github/manevolent/ts3j/command/MultiCommand.java new file mode 100644 index 0000000..3e325d6 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/command/MultiCommand.java @@ -0,0 +1,117 @@ +package com.github.manevolent.ts3j.command; + +import com.github.manevolent.ts3j.command.parameter.CommandOption; +import com.github.manevolent.ts3j.command.parameter.CommandParameter; +import com.github.manevolent.ts3j.command.parameter.CommandSingleParameter; +import com.github.manevolent.ts3j.protocol.ProtocolRole; +import com.github.manevolent.ts3j.util.Ts3String; + +import java.util.*; + +public class MultiCommand implements Command { + private final String name; + private final ProtocolRole role; + private final List commands; + + public MultiCommand(String name, ProtocolRole role, SingleCommand... params) { + this(name, role, Arrays.asList(params)); + } + + public MultiCommand(String name, ProtocolRole role, List params) { + this.name = name; + this.role = role; + this.commands = Collections.unmodifiableList(params); + } + + @Override + public String getName() { + return name; + } + + @Override + public ProtocolRole getRole() { + return role; + } + + @Override + public Iterable getParameters() { + throw new UnsupportedOperationException(); + } + + @Override + public void add(CommandParameter parameter) { + throw new UnsupportedOperationException(); + } + + public List simplify() { + return Collections.unmodifiableList(commands); + } + + public SingleCommand simplifyFirst() { + return commands.stream().findFirst().orElse(null); + } + + public SingleCommand simplifyOne() { + if (commands.size() > 1) throw new IllegalArgumentException("ambiguous; more than one command"); + return commands.stream().findFirst().orElse(null); + } + + @Override + public String build() { + StringBuilder builder = new StringBuilder(); + + builder.append(getName().toLowerCase()); + + List singleCommands = simplify(); + + for (int i = 0; i < singleCommands.size(); i ++) { + boolean first = i == 0; + boolean last = i == singleCommands.size() - 1; + + if (!first) builder.append("\\p"); + + for (CommandParameter parameter : singleCommands.get(i).getParameters()) { + builder.append(" " + (parameter.toString()).trim()); + } + } + + return builder.toString(); + } + + public static MultiCommand parse(ProtocolRole role, String text) { + String[] labelAndText = text.split(" ", 2); + String label = labelAndText[0].toLowerCase(); + String[] commands = labelAndText.length > 1 ? labelAndText[1].split("\\|") : new String[0]; + + List singleCommands = new LinkedList<>(); + + for (String commandText : commands) { + String[] commandParts = commandText.split(" "); + + List parameterList = new LinkedList<>(); + + for (int i = 0; i < commandParts.length; i++) { + String commandParamText = commandParts[i].trim(); + if (commandParamText.length() <= 0) continue; + + if (commandParamText.startsWith("-")) { // Option + parameterList.add(new CommandOption(commandParamText.substring(1))); + } else { + String[] commandParamParts = commandParamText.split("=", 2); + + parameterList.add(new CommandSingleParameter( + commandParamParts[0].toLowerCase(), + commandParamParts.length > 1 ? + Ts3String.unescape(commandParamParts[1]) : + null + ) + ); + } + } + + singleCommands.add(new SingleCommand(label, role, parameterList)); + } + + return new MultiCommand(label, role, singleCommands); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/command/SimpleCommand.java b/src/main/java/com/github/manevolent/ts3j/command/SingleCommand.java similarity index 71% rename from src/main/java/com/github/manevolent/ts3j/command/SimpleCommand.java rename to src/main/java/com/github/manevolent/ts3j/command/SingleCommand.java index 3d95660..249bc58 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/SimpleCommand.java +++ b/src/main/java/com/github/manevolent/ts3j/command/SingleCommand.java @@ -1,23 +1,20 @@ package com.github.manevolent.ts3j.command; -import com.github.manevolent.ts3j.command.part.CommandOption; -import com.github.manevolent.ts3j.command.part.CommandParameter; -import com.github.manevolent.ts3j.command.part.CommandSingleParameter; +import com.github.manevolent.ts3j.command.parameter.CommandParameter; import com.github.manevolent.ts3j.protocol.ProtocolRole; -import com.github.manevolent.ts3j.util.Ts3String; import java.util.*; -public class SimpleCommand implements Command { +public class SingleCommand implements Command { private final String name; private final ProtocolRole role; private final Map params; - public SimpleCommand(String name, ProtocolRole role, CommandParameter... params) { + public SingleCommand(String name, ProtocolRole role, CommandParameter... params) { this(name, role, Arrays.asList(params)); } - public SimpleCommand(String name, ProtocolRole role, List params) { + public SingleCommand(String name, ProtocolRole role, List params) { this.name = name; this.role = role; @@ -47,7 +44,7 @@ public class SimpleCommand implements Command { public void add(CommandParameter parameter) { CommandParameter existing = get(parameter.getName()); - if (existing != null) throw new IllegalArgumentException(parameter.getName() + " already set"); + if (existing != null) throw new IllegalArgumentException(getName() + ": " + parameter.getName() + " already set"); if (parameter.getName().trim().length() <= 0) throw new IllegalArgumentException("invalid key (empty)"); @@ -63,7 +60,7 @@ public class SimpleCommand implements Command { return this.params.remove(key); } - public static SimpleCommand parse(ProtocolRole role, String commandText) { - return ComplexCommand.parse(role, commandText).simplify(); + public static SingleCommand parse(ProtocolRole role, String commandText) { + return MultiCommand.parse(role, commandText).simplifyFirst(); } } diff --git a/src/main/java/com/github/manevolent/ts3j/command/client/ClientChannelListFinishedProcessor.java b/src/main/java/com/github/manevolent/ts3j/command/client/ClientChannelListFinishedProcessor.java new file mode 100644 index 0000000..c840367 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/command/client/ClientChannelListFinishedProcessor.java @@ -0,0 +1,20 @@ +package com.github.manevolent.ts3j.command.client; + +import com.github.manevolent.ts3j.command.CommandProcessException; +import com.github.manevolent.ts3j.command.CommandProcessor; +import com.github.manevolent.ts3j.command.MultiCommand; +import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; +import com.github.manevolent.ts3j.protocol.socket.client.AbstractTeamspeakClientSocket; + +public class ClientChannelListFinishedProcessor implements CommandProcessor { + @Override + public void process(AbstractTeamspeakClientSocket client, + MultiCommand command) throws CommandProcessException { + if (client.getState() == ClientConnectionState.RETRIEVING_DATA) + try { + client.setState(ClientConnectionState.CONNECTED); + } catch (Exception e) { + throw new CommandProcessException(e); + } + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/command/client/ClientChannelListProcessor.java b/src/main/java/com/github/manevolent/ts3j/command/client/ClientChannelListProcessor.java new file mode 100644 index 0000000..4f77589 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/command/client/ClientChannelListProcessor.java @@ -0,0 +1,45 @@ +package com.github.manevolent.ts3j.command.client; + +import com.github.manevolent.ts3j.command.CommandProcessException; +import com.github.manevolent.ts3j.command.CommandProcessor; +import com.github.manevolent.ts3j.command.SingleCommand; +import com.github.manevolent.ts3j.enums.CodecType; +import com.github.manevolent.ts3j.model.Channel; +import com.github.manevolent.ts3j.protocol.socket.client.AbstractTeamspeakClientSocket; + +public class ClientChannelListProcessor implements CommandProcessor { + @Override + public void process(AbstractTeamspeakClientSocket client, + SingleCommand command) throws CommandProcessException { + int cid = Integer.parseInt(command.get("cid").getValue()); + + Channel channel = client.getVirtualServer().getChannelById(cid); + if (channel == null) + channel = new Channel( + client.getVirtualServer(), Integer.parseInt(command.get("cid").getValue()) + ); + + channel.setName(command.get("channel_name").getValue()); + channel.setCodec(CodecType.fromId(Integer.parseInt(command.get("channel_codec").getValue()))); + channel.setCodecQuality(Integer.parseInt(command.get("channel_codec_quality").getValue())); + channel.setMaxClients(Integer.parseInt(command.get("channel_maxclients").getValue())); + channel.setMaxFamilyClients(Integer.parseInt(command.get("channel_maxfamilyclients").getValue())); + channel.setOrder(Integer.parseInt(command.get("channel_order").getValue())); + channel.setPermanent(command.get("channel_flag_permanent").getValue().equals("1")); + channel.setSemiPermanent(command.get("channel_flag_semi_permanent").getValue().equals("1")); + channel.setServerDefault(command.get("channel_flag_default").getValue().equals("1")); + channel.setHasPassword(command.get("channel_flag_password").getValue().equals("1")); + channel.setCodecLatencyFactor(command.get("channel_codec_latency_factor").getValue().equals("1")); + channel.setUnencrypted(command.get("channel_codec_is_unencrypted").getValue().equals("1")); + channel.setDeleteDelay(Integer.parseInt(command.get("channel_delete_delay").getValue())); + channel.setUnlimited(command.get("channel_flag_maxclients_unlimited").getValue().equals("1")); + channel.setFamilyUnlimited(command.get("channel_flag_maxfamilyclients_unlimited").getValue().equals("1")); + channel.setInheritingMaxfamilyUnlimited(command.get("channel_flag_maxfamilyclients_inherited").getValue().equals("1")); + channel.setNeededTalkPower(Integer.parseInt(command.get("channel_needed_talk_power").getValue())); + channel.setForcingSilence(command.get("channel_forced_silence").getValue().equals("1")); + channel.setIconId(Integer.parseInt(command.get("channel_icon_id").getValue())); + channel.setPrivated(command.get("channel_flag_private").getValue().equals("1")); + + client.getVirtualServer().updateChannel(channel); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/command/client/ClientInitServerProcessor.java b/src/main/java/com/github/manevolent/ts3j/command/client/ClientInitServerProcessor.java index 567d859..8e47a02 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/client/ClientInitServerProcessor.java +++ b/src/main/java/com/github/manevolent/ts3j/command/client/ClientInitServerProcessor.java @@ -2,24 +2,46 @@ package com.github.manevolent.ts3j.command.client; import com.github.manevolent.ts3j.command.CommandProcessException; import com.github.manevolent.ts3j.command.CommandProcessor; -import com.github.manevolent.ts3j.command.ComplexCommand; -import com.github.manevolent.ts3j.command.SimpleCommand; +import com.github.manevolent.ts3j.command.MultiCommand; +import com.github.manevolent.ts3j.command.SingleCommand; +import com.github.manevolent.ts3j.enums.HostBannerMode; +import com.github.manevolent.ts3j.enums.HostMessageMode; +import com.github.manevolent.ts3j.enums.VoiceEncryptionMode; +import com.github.manevolent.ts3j.model.VirtualServer; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.socket.client.AbstractTeamspeakClientSocket; -import com.github.manevolent.ts3j.util.Ts3Debugging; public class ClientInitServerProcessor implements CommandProcessor { @Override - public void process(AbstractTeamspeakClientSocket socket, ComplexCommand complexCommand) throws CommandProcessException { - SimpleCommand command = complexCommand.simplify(); + public void process(AbstractTeamspeakClientSocket client, + MultiCommand multiCommand) throws CommandProcessException { + SingleCommand command = multiCommand.simplifyOne(); - Ts3Debugging.debug(command.get("virtualserver_welcomemessage").getValue()); + VirtualServer server = new VirtualServer(); - if (socket.getState() == ClientConnectionState.RETRIEVING_DATA) - try { - socket.setState(ClientConnectionState.CONNECTED); - } catch (Exception e) { - throw new CommandProcessException(e); - } + server.setName(command.get("virtualserver_name").getValue()); + server.setWelcomeMessage(command.get("virtualserver_welcomemessage").getValue()); + server.setPlatform(command.get("virtualserver_platform").getValue()); + server.setVersion(command.get("virtualserver_version").getValue()); + server.setMaxClients(Integer.parseInt(command.get("virtualserver_maxclients").getValue())); + server.setCreated(Long.parseLong(command.get("virtualserver_created").getValue())); + server.setEncryptionMode(VoiceEncryptionMode.fromId(Integer.parseInt(command.get("virtualserver_codec_encryption_mode").getValue()))); + server.setHostMessageMode(HostMessageMode.fromId(Integer.parseInt(command.get("virtualserver_hostmessage_mode").getValue()))); + server.setDefaultGroupId(Integer.parseInt(command.get("virtualserver_default_server_group").getValue())); + server.setDefaultChannelGroupId(Integer.parseInt(command.get("virtualserver_default_channel_group").getValue())); + server.setHostbannerUrl(command.get("virtualserver_hostbanner_gfx_url").getValue()); + server.setPrioritySpeakerDimmModifier(Double.parseDouble(command.get("virtualserver_priority_speaker_dimm_modificator").getValue())); + server.setId(Integer.parseInt(command.get("virtualserver_id").getValue())); + server.setNamePhonetic(command.get("virtualserver_name_phonetic").getValue()); + server.setIconId(Long.parseLong(command.get("virtualserver_icon_id").getValue())); + server.setServerIp(command.get("virtualserver_ip").getValue()); + server.setAskForPriviledgeKey(Integer.parseInt(command.get("virtualserver_ask_for_privilegekey").getValue()) == 1); + server.setHostBannerMode(HostBannerMode.fromId(Integer.parseInt(command.get("virtualserver_hostbanner_mode").getValue()))); + server.setDefaultTempChannelDeleteDelay(Integer.parseInt(command.get("virtualserver_channel_temp_delete_delay_default").getValue())); + server.setNickname(command.get("virtualserver_nickname").getValue()); + + client.setVirtualServer(server); + + client.setClientId(Integer.parseInt(command.get("aclid").getValue())); } } diff --git a/src/main/java/com/github/manevolent/ts3j/command/client/ClientNotifyClientEnterViewProcessor.java b/src/main/java/com/github/manevolent/ts3j/command/client/ClientNotifyClientEnterViewProcessor.java new file mode 100644 index 0000000..9fe4d9e --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/command/client/ClientNotifyClientEnterViewProcessor.java @@ -0,0 +1,71 @@ +package com.github.manevolent.ts3j.command.client; + +import com.github.manevolent.ts3j.command.CommandProcessException; +import com.github.manevolent.ts3j.command.CommandProcessor; +import com.github.manevolent.ts3j.command.SingleCommand; +import com.github.manevolent.ts3j.enums.ClientType; +import com.github.manevolent.ts3j.identity.Uid; +import com.github.manevolent.ts3j.model.Client; +import com.github.manevolent.ts3j.protocol.socket.client.AbstractTeamspeakClientSocket; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public class ClientNotifyClientEnterViewProcessor implements CommandProcessor { + @Override + public void process(AbstractTeamspeakClientSocket client, + SingleCommand command) throws CommandProcessException { + int clientId = Integer.parseInt(command.get("clid").getValue()); + + Client newClient = client.getVirtualServer().getClientById(clientId); + if (newClient == null) + newClient = new Client( + Integer.parseInt(command.get("clid").getValue()), + Integer.parseInt(command.get("client_database_id").getValue()), + client.getVirtualServer(), + new Uid(command.get("client_unique_identifier").getValue()), + ClientType.fromId(Integer.parseInt(command.get("client_type").getValue()))); + + newClient.setNickname(command.get("client_nickname").getValue()); + + newClient.setInputMuted(command.get("client_input_muted").getValue().equals("1")); + newClient.setOutputMuted(command.get("client_output_muted").getValue().equals("1")); + + newClient.setInputHardware(command.get("client_input_hardware").getValue().equals("1")); + newClient.setOutputHardware(command.get("client_output_hardware").getValue().equals("1")); + + newClient.setRecording(command.get("client_is_recording").getValue().equals("1")); + + newClient.setChannelGroupId(Integer.parseInt(command.get("client_channel_group_id").getValue())); + + newClient.setServerGroupIds( + Arrays.stream(command.get("client_servergroups").getValue().split(",")).map( + Integer::parseInt + ).collect(Collectors.toList()) + ); + + newClient.setAway(command.get("client_away").getValue().equals("1")); + + newClient.setTalkPower(Integer.parseInt(command.get("client_talk_power").getValue())); + + newClient.setTalkRequested(command.get("client_talk_request").getValue().equals("1")); + + newClient.setTalker(command.get("client_is_talker").getValue().equals("1")); + + newClient.setPrioritySpeaker(command.get("client_is_priority_speaker").getValue().equals("1")); + + newClient.setUnreadMessages(Integer.parseInt(command.get("client_unread_messages").getValue())); + + newClient.setNeededServerQueryViewPower(Integer.parseInt(command.get("client_needed_serverquery_view_power").getValue())); + + newClient.setIconId(Integer.parseInt(command.get("client_icon_id").getValue())); + + newClient.setChannelCommander(command.get("client_is_channel_commander").getValue().equals("1")); + + newClient.setCountry(command.get("client_country").getValue()); + + newClient.setInheritedChannelGroupChannelId(Integer.parseInt(command.get("client_channel_group_inherited_channel_id").getValue())); + + client.getVirtualServer().updateConnectedClient(newClient); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java b/src/main/java/com/github/manevolent/ts3j/command/parameter/CommandOption.java similarity index 96% rename from src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java rename to src/main/java/com/github/manevolent/ts3j/command/parameter/CommandOption.java index c832b2f..e298886 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java +++ b/src/main/java/com/github/manevolent/ts3j/command/parameter/CommandOption.java @@ -1,4 +1,4 @@ -package com.github.manevolent.ts3j.command.part; +package com.github.manevolent.ts3j.command.parameter; public class CommandOption implements CommandParameter { private final String key; diff --git a/src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java b/src/main/java/com/github/manevolent/ts3j/command/parameter/CommandParameter.java similarity index 91% rename from src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java rename to src/main/java/com/github/manevolent/ts3j/command/parameter/CommandParameter.java index a6b9040..ee4f24c 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java +++ b/src/main/java/com/github/manevolent/ts3j/command/parameter/CommandParameter.java @@ -1,4 +1,4 @@ -package com.github.manevolent.ts3j.command.part; +package com.github.manevolent.ts3j.command.parameter; import java.util.Base64; diff --git a/src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java b/src/main/java/com/github/manevolent/ts3j/command/parameter/CommandSingleParameter.java similarity index 96% rename from src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java rename to src/main/java/com/github/manevolent/ts3j/command/parameter/CommandSingleParameter.java index 8fbb19f..234e81b 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java +++ b/src/main/java/com/github/manevolent/ts3j/command/parameter/CommandSingleParameter.java @@ -1,4 +1,4 @@ -package com.github.manevolent.ts3j.command.part; +package com.github.manevolent.ts3j.command.parameter; import com.github.manevolent.ts3j.util.Ts3String; diff --git a/src/main/java/com/github/manevolent/ts3j/command/response/CommandResponse.java b/src/main/java/com/github/manevolent/ts3j/command/response/CommandResponse.java index 60bfced..68e7666 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/response/CommandResponse.java +++ b/src/main/java/com/github/manevolent/ts3j/command/response/CommandResponse.java @@ -1,7 +1,7 @@ package com.github.manevolent.ts3j.command.response; import com.github.manevolent.ts3j.command.Command; -import com.github.manevolent.ts3j.command.ComplexCommand; +import com.github.manevolent.ts3j.command.MultiCommand; import java.util.concurrent.TimeoutException; @@ -11,7 +11,7 @@ public class CommandResponse { private final int returnCode; private long dispatchedTime; - private ComplexCommand returnedCommand; + private MultiCommand returnedCommand; public CommandResponse(Command command, int returnCode) { this.command = command; @@ -26,12 +26,12 @@ public class CommandResponse { return returnCode; } - public ComplexCommand getResponse() + public MultiCommand getResponse() throws InterruptedException, TimeoutException { return getResponse(Integer.MAX_VALUE); } - public ComplexCommand getResponse(int millis) + public MultiCommand getResponse(int millis) throws InterruptedException, TimeoutException { if (returnCode < 0) return null; @@ -59,11 +59,11 @@ public class CommandResponse { this.dispatchedTime = dispatchedTime; } - public ComplexCommand getReturnedCommand() { + public MultiCommand getReturnedCommand() { return returnedCommand; } - public void setReturnedCommand(ComplexCommand returnedCommand) { + public void setReturnedCommand(MultiCommand returnedCommand) { synchronized (returnLock) { if (this.returnCode < 0 ) throw new IllegalArgumentException("not expecting return command"); if (this.returnedCommand != null) throw new IllegalArgumentException("already set return command"); diff --git a/src/main/java/com/github/manevolent/ts3j/identity/Identity.java b/src/main/java/com/github/manevolent/ts3j/identity/Identity.java index 0d4ab9a..eca8218 100644 --- a/src/main/java/com/github/manevolent/ts3j/identity/Identity.java +++ b/src/main/java/com/github/manevolent/ts3j/identity/Identity.java @@ -21,7 +21,7 @@ public class Identity { } private final ECPoint publicKey; - private final String fingerprint; + private final Uid uid; private final String publicKeyString; private long keyOffset = 0L, lastCheckedKeyOffset = 0L; @@ -29,7 +29,7 @@ public class Identity { public Identity(ECPoint publicKey) { this.publicKey = publicKey; this.publicKeyString = Base64.getEncoder().encodeToString(Ts3Crypt.encodePublicKey(publicKey)); - this.fingerprint = generateFingerprint(); + this.uid = generateUid(); } public ECPoint getPublicKey() { @@ -40,8 +40,8 @@ public class Identity { return publicKeyString; } - public String getFingerprint() { - return fingerprint; + public Uid getUid() { + return uid; } public long getLastCheckedKeyOffset() { @@ -129,11 +129,9 @@ public class Identity { return curr; } - private String generateFingerprint() { + private Uid generateUid() { synchronized (sha1) { - return Base64.getEncoder().encodeToString( - sha1.digest(getPublicKeyString().getBytes(Charset.forName("ASCII"))) - ); + return new Uid(sha1.digest(getPublicKeyString().getBytes(Charset.forName("ASCII")))); } } } diff --git a/src/main/java/com/github/manevolent/ts3j/identity/Uid.java b/src/main/java/com/github/manevolent/ts3j/identity/Uid.java new file mode 100644 index 0000000..d3bb893 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/identity/Uid.java @@ -0,0 +1,42 @@ +package com.github.manevolent.ts3j.identity; + +import java.util.Arrays; +import java.util.Base64; + +public final class Uid { + private final byte[] uidBytes; + private final String base64; + + public Uid(String base64) { + this.base64 = base64; + this.uidBytes = Base64.getDecoder().decode(base64); + } + + public Uid(byte[] bytes) { + this.uidBytes = bytes; + this.base64 = Base64.getEncoder().encodeToString(bytes); + } + + public String toBase64() { + return base64; + } + + public byte[] toByteArray() { + return uidBytes; + } + + @Override + public int hashCode() { + return Arrays.hashCode(toByteArray()); + } + + @Override + public boolean equals(Object b) { + if (b == null || !(b instanceof Uid)) return false; + return equals((Uid) b); + } + + public boolean equals(Uid b) { + return b != null && Arrays.equals(b.toByteArray(), toByteArray()); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/model/Channel.java b/src/main/java/com/github/manevolent/ts3j/model/Channel.java new file mode 100644 index 0000000..0925d6e --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/model/Channel.java @@ -0,0 +1,201 @@ +package com.github.manevolent.ts3j.model; + +import com.github.manevolent.ts3j.enums.CodecType; + +public class Channel { + private final VirtualServer server; + private final int channelId; + private String name; + private CodecType codec; + private int codecQuality; + private int maxClients; + private int maxFamilyClients; + private int order; + private boolean permanent; + private boolean semiPermanent; + private boolean serverDefault; + private boolean hasPassword; + private boolean codecLatencyFactor; + private boolean unencrypted; + private int deleteDelay; + private boolean unlimited; + private boolean familyUnlimited; + private boolean inheritingMaxfamilyUnlimited; + private int neededTalkPower; + private boolean forcingSilence; + private int iconId; + private boolean privated; + + public Channel(VirtualServer server, int channelId) { + this.server = server; + this.channelId = channelId; + } + + public int getChannelId() { + return channelId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public CodecType getCodec() { + return codec; + } + + public void setCodec(CodecType codec) { + this.codec = codec; + } + + public int getCodecQuality() { + return codecQuality; + } + + public void setCodecQuality(int codecQuality) { + this.codecQuality = codecQuality; + } + + public int getMaxClients() { + return maxClients; + } + + public void setMaxClients(int maxClients) { + this.maxClients = maxClients; + } + + public int getMaxFamilyClients() { + return maxFamilyClients; + } + + public void setMaxFamilyClients(int maxFamilyClients) { + this.maxFamilyClients = maxFamilyClients; + } + + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + + public boolean isPermanent() { + return permanent; + } + + public void setPermanent(boolean permanent) { + this.permanent = permanent; + } + + public boolean isSemiPermanent() { + return semiPermanent; + } + + public void setSemiPermanent(boolean semiPermanent) { + this.semiPermanent = semiPermanent; + } + + public boolean isServerDefault() { + return serverDefault; + } + + public void setServerDefault(boolean serverDefault) { + this.serverDefault = serverDefault; + } + + public boolean hasPassword() { + return hasPassword; + } + + public void setHasPassword(boolean hasPassword) { + this.hasPassword = hasPassword; + } + + public boolean isCodecLatencyFactor() { + return codecLatencyFactor; + } + + public void setCodecLatencyFactor(boolean codecLatencyFactor) { + this.codecLatencyFactor = codecLatencyFactor; + } + + public boolean isUnencrypted() { + return unencrypted; + } + + public void setUnencrypted(boolean unencrypted) { + this.unencrypted = unencrypted; + } + + public int getDeleteDelay() { + return deleteDelay; + } + + public void setDeleteDelay(int deleteDelay) { + this.deleteDelay = deleteDelay; + } + + public boolean isUnlimited() { + return unlimited; + } + + public void setUnlimited(boolean unlimited) { + this.unlimited = unlimited; + } + + public boolean isFamilyUnlimited() { + return familyUnlimited; + } + + public void setFamilyUnlimited(boolean familyUnlimited) { + this.familyUnlimited = familyUnlimited; + } + + public boolean isInheritingMaxfamilyUnlimited() { + return inheritingMaxfamilyUnlimited; + } + + public void setInheritingMaxfamilyUnlimited(boolean inheritingMaxfamilyUnlimited) { + this.inheritingMaxfamilyUnlimited = inheritingMaxfamilyUnlimited; + } + + public int getNeededTalkPower() { + return neededTalkPower; + } + + public void setNeededTalkPower(int neededTalkPower) { + this.neededTalkPower = neededTalkPower; + } + + public boolean isForcingSilence() { + return forcingSilence; + } + + public void setForcingSilence(boolean forcingSilence) { + this.forcingSilence = forcingSilence; + } + + public int getIconId() { + return iconId; + } + + public void setIconId(int iconId) { + this.iconId = iconId; + } + + public boolean isPrivated() { + return privated; + } + + public void setPrivated(boolean privated) { + this.privated = privated; + } + + public VirtualServer getServer() { + return server; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/model/Client.java b/src/main/java/com/github/manevolent/ts3j/model/Client.java new file mode 100644 index 0000000..632cb87 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/model/Client.java @@ -0,0 +1,261 @@ +package com.github.manevolent.ts3j.model; + +import com.github.manevolent.ts3j.enums.ClientType; +import com.github.manevolent.ts3j.identity.Uid; + +import java.util.*; + +public class Client { + private final int id; + private final int databaseId; + private final VirtualServer server; + private final Uid uid; + private final ClientType clientType; + + private String nickname; + + private int channelId; + private int channelGroupId; + + private boolean speaking; + private boolean outputMuted, inputMuted; + private boolean recording; + private boolean inputHardware, outputHardware; + + private boolean away; + private int talkPower; + private boolean talkRequested; + private boolean talker; + private boolean prioritySpeaker; + private int unreadMessages; + private int neededServerQueryViewPower; + private int iconId; + private boolean channelCommander; + private String country; + private int inheritedChannelGroupChannelId; + + private List serverGroupIds = new ArrayList<>(); + + public Client(int id, int databaseId, VirtualServer server, Uid uid, ClientType clientType) { + this.id = id; + this.databaseId = databaseId; + this.server = server; + this.uid = uid; + this.clientType = clientType; + } + + public VirtualServer getServer() { + return server; + } + + public Uid getUid() { + return uid; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + /** + * Gets the client's current client connection ID. + * @return client ID. + */ + public int getId() { + return id; + } + + /** + * Gets the current connected-to channel. + * @return channel currently connected to by the client. + */ + public Channel getChannel() { + return server.getChannelById(getChannelId()); + } + + /** + * Gets the channel ID currently connected to. + * @return channel currently connected to by the client. + */ + public int getChannelId() { + return channelId; + } + + public void setChannelId(int channelId) { + this.channelId = channelId; + } + + public boolean isSpeaking() { + return speaking; + } + + public void setSpeaking(boolean speaking) { + this.speaking = speaking; + } + + public boolean isInputMuted() { + return inputMuted; + } + + public void setInputMuted(boolean inputMuted) { + this.inputMuted = inputMuted; + } + + public boolean isOutputMuted() { + return outputMuted; + } + + public void setOutputMuted(boolean outputMuted) { + this.outputMuted = outputMuted; + } + + public boolean hasOutputHardware() { + return outputHardware; + } + + public void setOutputHardware(boolean outputHardware) { + this.outputHardware = outputHardware; + } + + public boolean hasInputHardware() { + return inputHardware; + } + + public void setInputHardware(boolean inputHardware) { + this.inputHardware = inputHardware; + } + + public int getDatabaseId() { + return databaseId; + } + + public boolean isRecording() { + return recording; + } + + public void setRecording(boolean recording) { + this.recording = recording; + } + + public boolean isInServerGroup(int serverGroupId) { + return serverGroupIds.contains(serverGroupId); + } + + public void setServerGroupIds(Collection serverGroupIds) { + synchronized (serverGroupIds) { + this.serverGroupIds.clear(); + this.serverGroupIds.addAll(serverGroupIds); + } + } + + public void setServerGroupIds(Integer... serverGroupIds) { + setServerGroupIds(Arrays.asList(serverGroupIds)); + } + + public Iterable getServerGroupIds() { + return Collections.unmodifiableList(serverGroupIds); + } + + public int getChannelGroupId() { + return channelGroupId; + } + + public void setChannelGroupId(int channelGroupId) { + this.channelGroupId = channelGroupId; + } + + public boolean isAway() { + return away; + } + + public void setAway(boolean away) { + this.away = away; + } + + public int getTalkPower() { + return talkPower; + } + + public void setTalkPower(int talkPower) { + this.talkPower = talkPower; + } + + public boolean isTalkRequested() { + return talkRequested; + } + + public void setTalkRequested(boolean talkRequested) { + this.talkRequested = talkRequested; + } + + public boolean isTalker() { + return talker; + } + + public void setTalker(boolean talker) { + this.talker = talker; + } + + public boolean isPrioritySpeaker() { + return prioritySpeaker; + } + + public void setPrioritySpeaker(boolean prioritySpeaker) { + this.prioritySpeaker = prioritySpeaker; + } + + public int getUnreadMessages() { + return unreadMessages; + } + + public void setUnreadMessages(int unreadMessages) { + this.unreadMessages = unreadMessages; + } + + public int getNeededServerQueryViewPower() { + return neededServerQueryViewPower; + } + + public void setNeededServerQueryViewPower(int neededServerQueryViewPower) { + this.neededServerQueryViewPower = neededServerQueryViewPower; + } + + public int getIconId() { + return iconId; + } + + public void setIconId(int iconId) { + this.iconId = iconId; + } + + public boolean isChannelCommander() { + return channelCommander; + } + + public void setChannelCommander(boolean channelCommander) { + this.channelCommander = channelCommander; + } + + public int getInheritedChannelGroupChannelId() { + return inheritedChannelGroupChannelId; + } + + public void setInheritedChannelGroupChannelId(int inheritedChannelGroupChannelId) { + this.inheritedChannelGroupChannelId = inheritedChannelGroupChannelId; + } + + public ClientType getClientType() { + return clientType; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/model/VirtualServer.java b/src/main/java/com/github/manevolent/ts3j/model/VirtualServer.java new file mode 100644 index 0000000..46257f7 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/model/VirtualServer.java @@ -0,0 +1,322 @@ +package com.github.manevolent.ts3j.model; + +import com.github.manevolent.ts3j.enums.HostBannerMode; +import com.github.manevolent.ts3j.enums.HostMessageMode; +import com.github.manevolent.ts3j.enums.VoiceEncryptionMode; +import com.github.manevolent.ts3j.identity.Uid; + +import java.util.*; +import java.util.stream.Collectors; + +public class VirtualServer { + private int id; + private String name; + private String nickname; + private String welcomeMessage; + private String platform; + private String version; + private int maxClients; + private long created; + private VoiceEncryptionMode encryptionMode; + private HostMessageMode hostMessageMode; + private int defaultGroupId; + private int defaultChannelGroupId; + private String hostbannerUrl; + private int hostBannerInterval; + private double prioritySpeakerDimmModifier; + private String namePhonetic; + private long iconId; + private String serverIp; + private boolean askForPriviledgeKey; + private HostBannerMode hostBannerMode; + private int defaultTempChannelDeleteDelay; + + /** + * Connected clients + */ + private final Map clients = new HashMap<>(); + + /** + * Server channels + */ + private final Map channels = new HashMap<>(); + + public VirtualServer() { + + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public String getWelcomeMessage() { + return welcomeMessage; + } + + public void setWelcomeMessage(String welcomeMessage) { + this.welcomeMessage = welcomeMessage; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public int getMaxClients() { + return maxClients; + } + + public void setMaxClients(int maxClients) { + this.maxClients = maxClients; + } + + public long getCreated() { + return created; + } + + public void setCreated(long created) { + this.created = created; + } + + public VoiceEncryptionMode getEncryptionMode() { + return encryptionMode; + } + + public void setEncryptionMode(VoiceEncryptionMode encryptionMode) { + this.encryptionMode = encryptionMode; + } + + public HostMessageMode getHostMessageMode() { + return hostMessageMode; + } + + public void setHostMessageMode(HostMessageMode hostMessageMode) { + this.hostMessageMode = hostMessageMode; + } + + /** + * Gets the default server group ID users are assigned to when connecting to the server for the first time. + * @return default server group ID. + */ + public int getDefaultGroupId() { + return defaultGroupId; + } + + public void setDefaultGroupId(int defaultGroupId) { + this.defaultGroupId = defaultGroupId; + } + + public int getDefaultChannelGroupId() { + return defaultChannelGroupId; + } + + public void setDefaultChannelGroupId(int defaultChannelGroupId) { + this.defaultChannelGroupId = defaultChannelGroupId; + } + + public String getHostbannerUrl() { + return hostbannerUrl; + } + + public void setHostbannerUrl(String hostbannerUrl) { + this.hostbannerUrl = hostbannerUrl; + } + + public int getHostBannerInterval() { + return hostBannerInterval; + } + + public void setHostBannerInterval(int hostBannerInterval) { + this.hostBannerInterval = hostBannerInterval; + } + + public double getPrioritySpeakerDimmModifier() { + return prioritySpeakerDimmModifier; + } + + public void setPrioritySpeakerDimmModifier(double prioritySpeakerDimmModifier) { + this.prioritySpeakerDimmModifier = prioritySpeakerDimmModifier; + } + + public String getNamePhonetic() { + return namePhonetic; + } + + public void setNamePhonetic(String namePhonetic) { + this.namePhonetic = namePhonetic; + } + + public long getIconId() { + return iconId; + } + + public void setIconId(long iconId) { + this.iconId = iconId; + } + + public String getServerIp() { + return serverIp; + } + + public void setServerIp(String serverIp) { + this.serverIp = serverIp; + } + + public boolean isAskForPriviledgeKey() { + return askForPriviledgeKey; + } + + public void setAskForPriviledgeKey(boolean askForPriviledgeKey) { + this.askForPriviledgeKey = askForPriviledgeKey; + } + + public HostBannerMode getHostBannerMode() { + return hostBannerMode; + } + + public void setHostBannerMode(HostBannerMode hostBannerMode) { + this.hostBannerMode = hostBannerMode; + } + + public int getDefaultTempChannelDeleteDelay() { + return defaultTempChannelDeleteDelay; + } + + public void setDefaultTempChannelDeleteDelay(int defaultTempChannelDeleteDelay) { + this.defaultTempChannelDeleteDelay = defaultTempChannelDeleteDelay; + } + + /** + * Gets a channel by its numeric ID. + * @param id channel ID. + * @return the corresponding channel, if such a channel exists, null otherwise. + */ + public Channel getChannelById(int id) { + return channels.get(id); + } + + /** + * Tries to add a new channel to the server. If the channel ID is already in use, IllegalStateException is thrown. + * @param channel channel to add. + */ + public void updateChannel(Channel channel) { + synchronized (channels) { + this.channels.put(channel.getChannelId(), channel); + } + } + + /** + * Gets all channels by a specific name. + * @param name Name to search for. + * @return List of channels with the specified name. + */ + public List getChannelsByName(String name) { + return this.channels.values() + .stream() + .filter(x -> x.getName().equalsIgnoreCase(name)) + .collect(Collectors.toList()); + } + + /** + * Gets the first channel by a specific name. + * @param name Name to search for. + * @return List of channels with the specified name. + */ + public Channel getChannelByName(String name) throws IllegalArgumentException { + return getChannelsOrderd() + .stream() + .filter(x -> x.getName().equalsIgnoreCase(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("channel not found")); + } + + /** + * Gets a real channel list, potentially unordered. + * @return Unordered channel list. + */ + public Collection getChannels() { + return Collections.unmodifiableCollection(channels.values()); + } + + /** + * Gets the channel list as ordered by the server. + * @return Iterable list of ordered channels. + */ + public Collection getChannelsOrderd() { + return Collections.unmodifiableCollection(channels.values() + .stream() + .sorted(Comparator.comparingInt(Channel::getOrder)) + .collect(Collectors.toList()) + ); + } + + /** + * Gets a list of known clients to the server. + * @return clients + */ + public Collection getClients() { + throw new UnsupportedOperationException(); // TODO + } + + /** + * Gets a list of connected clients. + * @return connected clients. + */ + public Collection getConnectedClients() { + return Collections.unmodifiableCollection(clients.values()); + } + + public List getClientsByUid(Uid uid) { + return getConnectedClients().stream().filter(x -> x.getUid().equals(uid)).collect(Collectors.toList()); + } + + public void updateConnectedClient(Client client) { + synchronized (clients) { + this.clients.put(client.getId(), client); + } + } + + public void removeConnectedClient(Client client) { + synchronized (clients) { + if (this.clients.containsKey(client.getId())) + this.clients.remove(client.getId()); + else + throw new IllegalStateException("client ID doesn't exist"); + } + } + + public Client getClientById(int clientId) { + return this.clients.get(clientId); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody2Command.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody2Command.java index 142de7e..6596a3c 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody2Command.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody2Command.java @@ -1,8 +1,7 @@ package com.github.manevolent.ts3j.protocol.packet; import com.github.manevolent.ts3j.command.Command; -import com.github.manevolent.ts3j.command.ComplexCommand; -import com.github.manevolent.ts3j.command.SimpleCommand; +import com.github.manevolent.ts3j.command.MultiCommand; import com.github.manevolent.ts3j.protocol.ProtocolRole; import java.nio.ByteBuffer; @@ -29,8 +28,8 @@ public class PacketBody2Command extends PacketBody { return text; } - public ComplexCommand parse() { - return ComplexCommand.parse(getRole(), getText()); + public MultiCommand parse() { + return MultiCommand.parse(getRole(), getText()); } public void setText(String text) { diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody3CommandLow.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody3CommandLow.java index 24cc04c..810a359 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody3CommandLow.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBody3CommandLow.java @@ -1,7 +1,7 @@ package com.github.manevolent.ts3j.protocol.packet; import com.github.manevolent.ts3j.command.Command; -import com.github.manevolent.ts3j.command.ComplexCommand; +import com.github.manevolent.ts3j.command.MultiCommand; import com.github.manevolent.ts3j.protocol.ProtocolRole; import java.nio.ByteBuffer; @@ -28,8 +28,8 @@ public class PacketBody3CommandLow extends PacketBody { return text; } - public ComplexCommand parse() { - return ComplexCommand.parse(getRole(), getText()); + public MultiCommand parse() { + return MultiCommand.parse(getRole(), getText()); } public void setText(String text) { diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/local/LocalClientHandlerConnecting.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/local/LocalClientHandlerConnecting.java index 2d49b41..191e306 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/local/LocalClientHandlerConnecting.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/local/LocalClientHandlerConnecting.java @@ -1,8 +1,8 @@ package com.github.manevolent.ts3j.protocol.packet.handler.local; import com.github.manevolent.ts3j.command.CommandHandler; -import com.github.manevolent.ts3j.command.SimpleCommand; -import com.github.manevolent.ts3j.command.part.CommandSingleParameter; +import com.github.manevolent.ts3j.command.SingleCommand; +import com.github.manevolent.ts3j.command.parameter.CommandSingleParameter; import com.github.manevolent.ts3j.protocol.Packet; import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; @@ -116,7 +116,7 @@ public class LocalClientHandlerConnecting extends LocalClientHandler { alphaBytes = new byte[10]; new Random().nextBytes(alphaBytes); - SimpleCommand initiv = new SimpleCommand("clientinitiv", ProtocolRole.CLIENT); + SingleCommand initiv = new SingleCommand("clientinitiv", ProtocolRole.CLIENT); initiv.add(new CommandSingleParameter("alpha", Base64.getEncoder().encodeToString(alphaBytes))); @@ -142,7 +142,7 @@ public class LocalClientHandlerConnecting extends LocalClientHandler { } } else if (packet.getBody() instanceof PacketBody2Command) { - SimpleCommand command = ((PacketBody2Command) packet.getBody()).parse().simplify(); + SingleCommand command = ((PacketBody2Command) packet.getBody()).parse().simplifyOne(); if (command.getName().equalsIgnoreCase("initivexpand2")) { // 3.2.2 initivexpand2 (Client <- Server) @@ -177,7 +177,7 @@ public class LocalClientHandlerConnecting extends LocalClientHandler { ); getClient().sendCommand( - new SimpleCommand( + new SingleCommand( "clientek", ProtocolRole.CLIENT, new CommandSingleParameter("ek", Base64.getEncoder().encodeToString(keyPair.getKey())), new CommandSingleParameter("proof", Base64.getEncoder().encodeToString(signature)) @@ -186,7 +186,7 @@ public class LocalClientHandlerConnecting extends LocalClientHandler { getClient().setSecureParameters(Ts3Crypt.cryptoInit2(license, alphaBytes, beta, keyPair.getValue())); - getClient().sendCommand(new SimpleCommand( + getClient().sendCommand(new SingleCommand( "clientinit", ProtocolRole.CLIENT, new CommandSingleParameter("client_nickname", getClient().getNickname()), @@ -229,7 +229,7 @@ public class LocalClientHandlerConnecting extends LocalClientHandler { ) ); - getClient().setCommandProcessor(CommandHandler.createLocalClient()); + getClient().setCommandProcessor(CommandHandler.createLocalClientHandler()); getClient().setState(ClientConnectionState.RETRIEVING_DATA); } else if (command.getName().equals("error")) { getClient().setState(ClientConnectionState.DISCONNECTED); diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/local/LocalClientHandlerFull.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/local/LocalClientHandlerFull.java index 7565330..0359a78 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/local/LocalClientHandlerFull.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/local/LocalClientHandlerFull.java @@ -2,11 +2,12 @@ package com.github.manevolent.ts3j.protocol.packet.handler.local; import com.github.manevolent.ts3j.command.CommandProcessException; import com.github.manevolent.ts3j.command.CommandProcessor; -import com.github.manevolent.ts3j.command.ComplexCommand; +import com.github.manevolent.ts3j.command.MultiCommand; import com.github.manevolent.ts3j.protocol.Packet; import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command; import com.github.manevolent.ts3j.protocol.packet.PacketBody3CommandLow; import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket; +import com.github.manevolent.ts3j.util.Ts3Debugging; import java.io.IOException; import java.util.concurrent.TimeoutException; @@ -16,7 +17,7 @@ public abstract class LocalClientHandlerFull extends LocalClientHandler { super(client); } - protected void handleCommand(ComplexCommand command) throws CommandProcessException { + protected void handleCommand(MultiCommand command) throws CommandProcessException { CommandProcessor processor = getClient().getCommandProcessor(); if (processor != null) processor.process(getClient(), command); } @@ -25,8 +26,9 @@ public abstract class LocalClientHandlerFull extends LocalClientHandler { public void handlePacket(Packet packet) throws IOException, TimeoutException { switch (packet.getBody().getType()) { case COMMAND: + Ts3Debugging.debug(((PacketBody2Command) packet.getBody()).getText()); try { - handleCommand(ComplexCommand.parse( + handleCommand(MultiCommand.parse( packet.getRole(), ((PacketBody2Command) packet.getBody()).getText() )); @@ -36,7 +38,7 @@ public abstract class LocalClientHandlerFull extends LocalClientHandler { break; case COMMAND_LOW: try { - handleCommand(ComplexCommand.parse( + handleCommand(MultiCommand.parse( packet.getRole(), ((PacketBody3CommandLow) packet.getBody()).getText() )); diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java index 7ac4b32..5f997bd 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java @@ -2,9 +2,11 @@ package com.github.manevolent.ts3j.protocol.socket.client; import com.github.manevolent.ts3j.command.Command; import com.github.manevolent.ts3j.command.CommandProcessor; -import com.github.manevolent.ts3j.command.part.CommandSingleParameter; +import com.github.manevolent.ts3j.command.parameter.CommandSingleParameter; import com.github.manevolent.ts3j.command.response.CommandResponse; import com.github.manevolent.ts3j.identity.Identity; +import com.github.manevolent.ts3j.model.Client; +import com.github.manevolent.ts3j.model.VirtualServer; import com.github.manevolent.ts3j.protocol.NetworkPacket; import com.github.manevolent.ts3j.protocol.Packet; import com.github.manevolent.ts3j.protocol.ProtocolRole; @@ -43,10 +45,13 @@ public abstract class AbstractTeamspeakClientSocket private final Map reassemblyQueue = new ConcurrentHashMap<>(); private final Map clientOptions = new ConcurrentHashMap<>(); private final Map sendQueue = new ConcurrentHashMap<>(); - private final Map sendCounter = new ConcurrentHashMap<>(); + private final Map localSendCounter = new ConcurrentHashMap<>(); + private final Map remoteSendCounter = new ConcurrentHashMap<>(); private CommandProcessor commandProcessor; + private VirtualServer virtualServer; + private Identity identity = null; private ClientConnectionState connectionState = null; private PacketTransformation transformation = new DefaultPacketTransformation(); @@ -80,10 +85,13 @@ public abstract class AbstractTeamspeakClientSocket // Initialize counters for (PacketBodyType bodyType : PacketBodyType.values()) - if (bodyType == PacketBodyType.INIT1) - sendCounter.put(PacketBodyType.INIT1, new PacketCounterZero()); - else - sendCounter.put(bodyType, new PacketCounterFull()); + if (bodyType == PacketBodyType.INIT1) { + localSendCounter.put(PacketBodyType.INIT1, new PacketCounterZero()); + remoteSendCounter.put(PacketBodyType.INIT1, new PacketCounterZero()); + } else { + localSendCounter.put(bodyType, new PacketCounterFull()); + remoteSendCounter.put(bodyType, new PacketCounterFull(-1)); + } } protected void start() { @@ -228,7 +236,7 @@ public abstract class AbstractTeamspeakClientSocket // Set header values packet.getBody().setHeaderValues(packet.getHeader()); - Pair counterResult = sendCounter.get(packet.getHeader().getType()).next(); + Pair counterResult = localSendCounter.get(packet.getHeader().getType()).next(); if (packet.getHeader() instanceof ClientPacketHeader) ((ClientPacketHeader) packet.getHeader()).setClientId(clientId); @@ -460,11 +468,22 @@ public abstract class AbstractTeamspeakClientSocket } // Find if the packet must be reassembled - if (packet.getBody() instanceof PacketBodyFragment) { + if (packet.getHeader().getType().isSplittable()) { packet = reassemblyQueue.get(packet.getHeader().getType()).put(packet); if (packet == null) continue; } + if (packet.getHeader().getType().canResend() && packet.getHeader().getType() != PacketBodyType.INIT1) + // Find if we already acknowledged this packet + if (!remoteSendCounter + .get(packet.getHeader().getType()) + .setPacketId(packet.getHeader().getPacketId())) { + Ts3Debugging.debug("[PROTOCOL] Ignoring handling resent packet: " + + packet.getHeader().getType().name() + " id=" + + packet.getHeader().getPacketId()); + continue; // Drop the packet + } + // Find if the packet is itself an acknowledgement boolean handle; PacketResponse response; @@ -580,6 +599,18 @@ public abstract class AbstractTeamspeakClientSocket this.commandProcessor = commandProcessor; } + public VirtualServer getVirtualServer() { + return virtualServer; + } + + public void setVirtualServer(VirtualServer virtualServer) { + this.virtualServer = virtualServer; + } + + public Client getSelf() { + return virtualServer.getClientById(clientId); + } + private class NetworkHandler implements Runnable { @Override public void run() { @@ -692,6 +723,8 @@ public abstract class AbstractTeamspeakClientSocket private interface PacketCounter { Pair next(); + int getPacketId(); + boolean setPacketId(int packetId); int getGenerationId(); } @@ -701,6 +734,16 @@ public abstract class AbstractTeamspeakClientSocket return new Pair<>(0,0); } + @Override + public int getPacketId() { + return 0; + } + + @Override + public boolean setPacketId(int packetId) { + throw new UnsupportedOperationException(); + } + @Override public int getGenerationId() { return 0; @@ -712,10 +755,34 @@ public abstract class AbstractTeamspeakClientSocket private int packetId = 0; private int generationId = 0; + public PacketCounterFull() { + + } + + public PacketCounterFull(int start) { + setPacketId(start); + } + public int getPacketId() { return packetId; } + @Override + public boolean setPacketId(int packetId) { + synchronized (sendLock) { + if (this.packetId == packetId) return false; + + if (packetId >= 65536) { + packetId = 0; + generationId++; + } else { + this.packetId = packetId; + } + + return true; + } + } + public int getGenerationId() { return generationId; } @@ -723,13 +790,7 @@ public abstract class AbstractTeamspeakClientSocket @Override public Pair next() { synchronized (sendLock) { - packetId++; - - if (packetId >= 65536) { - packetId = 0; - generationId++; - } - + setPacketId(packetId + 1); return new Pair<>(packetId, generationId); } } @@ -781,6 +842,8 @@ public abstract class AbstractTeamspeakClientSocket } public Packet put(Packet packet) { + Packet reassembled; + if (packet.getHeader().getPacketFlag(HeaderFlag.FRAGMENTED)) { boolean oldState = state; @@ -791,19 +854,22 @@ public abstract class AbstractTeamspeakClientSocket queue.put(packet.getHeader().getPacketId(), packet); - if (oldState) - return reassemble(packet); - - return null; + if (oldState) { + reassembled = reassemble(packet); + } else { + reassembled = null; + } } else { if (state) { queue.put(packet.getHeader().getPacketId(), packet); - return null; + reassembled = null; } else { - return packet; + reassembled = packet; } } + + return reassembled; } public boolean isReassembling() { diff --git a/src/test/java/com/github/manevolent/ts3j/ClientUidGenerationTest.java b/src/test/java/com/github/manevolent/ts3j/ClientUidGenerationTest.java index 9b5ee59..7ce9b33 100644 --- a/src/test/java/com/github/manevolent/ts3j/ClientUidGenerationTest.java +++ b/src/test/java/com/github/manevolent/ts3j/ClientUidGenerationTest.java @@ -1,7 +1,6 @@ package com.github.manevolent.ts3j; import com.github.manevolent.ts3j.identity.LocalIdentity; -import com.github.manevolent.ts3j.util.Ts3Crypt; import junit.framework.TestCase; import java.math.BigInteger; @@ -26,7 +25,7 @@ public class ClientUidGenerationTest extends TestCase { assertEquals("MEsDAgcAAgEgAiBpPRbTliVt9KxtIz8saYdwcnNgcwaKLb" + "KYSpDNO87u9gIgSWWPKcSJ9P6VZKJfRdpWwcfMdJv+NA9/hXUtz1uwRVI=", keyPair.getPublicKeyString()); - assertEquals("27QHSPRzQe+enxh56eUokkLmsFg=", keyPair.getFingerprint()); + assertEquals("27QHSPRzQe+enxh56eUokkLmsFg=", keyPair.getUid().toBase64()); assertEquals( "MEsDAgcAAgEgAiBpPRbTliVt9KxtIz8saYdwcnNgcwaKLbKY" + diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java index 28931cb..5a8f599 100644 --- a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java +++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java @@ -1,27 +1,24 @@ package com.github.manevolent.ts3j; import com.github.manevolent.ts3j.identity.LocalIdentity; +import com.github.manevolent.ts3j.model.Channel; import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket; import com.github.manevolent.ts3j.util.Ts3Crypt; +import com.github.manevolent.ts3j.util.Ts3Debugging; import junit.framework.TestCase; import java.math.BigInteger; import java.net.InetSocketAddress; import java.util.Base64; +import java.util.Collections; +import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; public class ServerConnectionTest { public static void main(String[] args) throws Exception { try { - /**LocalIdentity localIdentity = LocalIdentity.load( - Ts3Crypt.decodePublicKey(Base64.getDecoder() - .decode("MEsDAgcAAgEgAiBpPRbTliVt9KxtIz8saYdwcnNgcwaKLbKYSpDNO87u9gIgSWWPKcSJ9P6VZKJfRdpWwcfMdJv+NA9/hXUtz1uwRVI=")), - new BigInteger(Base64.getDecoder().decode("Tj6YXM3qyRv8n25L2pH+OEJnRUl4auQf8+znjYrOmWU=")), - 2294, - 2295 - );**/ LocalIdentity localIdentity = LocalIdentity.generateNew(10); LocalTeamspeakClientSocket client = new LocalTeamspeakClientSocket(); @@ -40,6 +37,13 @@ public class ServerConnectionTest { ); assertEquals(client.getState(), ClientConnectionState.CONNECTED); + + Ts3Debugging.info("Connected to " + client.getVirtualServer().getName() + "."); + Ts3Debugging.info(" Client name: " + client.getSelf().getNickname()); + Ts3Debugging.info(" Channels: " + + client.getVirtualServer().getChannelsOrderd() + .stream().map(Channel::getName).collect(Collectors.toList()) + ); } catch (Throwable ex) { ex.printStackTrace(); }