diff --git a/.gitignore b/.gitignore index 5546f84..c245070 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,15 @@ out/ *.eml *.iml .userlibraries + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar \ No newline at end of file diff --git a/pom.xml b/pom.xml index 90df29d..c00d129 100644 --- a/pom.xml +++ b/pom.xml @@ -8,5 +8,26 @@ ts3j 1.0-SNAPSHOT + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + 1.8 + 1.8 + + + + + + + junit + junit + RELEASE + test + + \ No newline at end of file diff --git a/src/main/java/com/github/manevolent/ts3j/TeamspeakClient.java b/src/main/java/com/github/manevolent/ts3j/TeamspeakClient.java new file mode 100644 index 0000000..f59237f --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/TeamspeakClient.java @@ -0,0 +1,300 @@ +package com.github.manevolent.ts3j; + +import com.github.manevolent.ts3j.command.Command; +import com.github.manevolent.ts3j.command.part.CommandSingleParameter; +import com.github.manevolent.ts3j.command.response.CommandResponse; +import com.github.manevolent.ts3j.enums.ConnectionState; +import com.github.manevolent.ts3j.enums.PacketFlags; +import com.github.manevolent.ts3j.enums.PacketType; +import com.github.manevolent.ts3j.handler.TeamspeakClientHandler; +import com.github.manevolent.ts3j.util.Ts3Logging; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.util.Date; +import java.util.concurrent.TimeoutException; + +public class TeamspeakClient { + private final PacketReceiver packetReceiver = new PacketReceiver(); + + private final DatagramSocket datagramSocket; + private final DatagramPacket datagramPacket = new DatagramPacket(new byte[1500], 1500); + + private final Object connectionStateLock = new Object(); + + private TeamspeakClientHandler handler; + + private InetSocketAddress remote = null; + private ConnectionState connectionState = null; + private int returnCodeIndex = 0; + + public TeamspeakClient(DatagramSocket datagramSocket) { + this.datagramSocket = datagramSocket; + } + + public TeamspeakClient() throws SocketException { + this(new DatagramSocket()); + + setConnectionState(ConnectionState.DISCONNECTED); + } + + protected void setConnectionState(ConnectionState state) { + synchronized (connectionStateLock) { + if (state != this.connectionState) { + boolean wasDisconnected = this.connectionState == ConnectionState.DISCONNECTED; + + this.connectionState = state; + + if (wasDisconnected) { + Ts3Logging.debug("Starting network receiver thread..."); + + Thread receiverThread = new Thread(new PacketReceiver()); + receiverThread.setDaemon(true); + receiverThread.start(); + } + + if (handler != null) + handler.handleConnectionStateChanging(state); + + if (handler == null || handler.getClass() != state.getHandlerClass()) { + Ts3Logging.debug("Assigning " + state.getHandlerClass().getName() + " handler..."); + + handler = state.createHandler(this); + + try { + handler.onAssigned(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + connectionStateLock.notifyAll(); + } + } + } + + public boolean isConnected() { + return connectionState == ConnectionState.CONNECTED; + } + + public ConnectionState getConnectionState() { + return connectionState; + } + + private void joinConnectionState(ConnectionState state, long wait) + throws InterruptedException, TimeoutException { + synchronized (connectionStateLock) { + if (connectionState != state) { + connectionStateLock.wait(wait); + + if (connectionState != state) + throw new TimeoutException("timeout waiting for " + state.name() + " state"); + } + } + } + + public CommandResponse sendCommand(Command command) throws IOException { + if (command.willExpectResponse() && !isConnected()) + throw new IOException("not connected"); + + CommandResponse response; + + if (command.willExpectResponse()) { + int returnCodeIndex = ++ this.returnCodeIndex; + command.appendParameter(new CommandSingleParameter("return_code", returnCodeIndex)); + response = new CommandResponse(command, returnCodeIndex); + } else { + response = new CommandResponse(command, -1); + } + + String commandString = command.toString(); + + response.setDispatchedTime(System.currentTimeMillis()); + + return response; + } + + public void connect(InetSocketAddress remote, String password, long timeout) throws IOException { + try { + if (connectionState != ConnectionState.DISCONNECTED) + throw new IllegalStateException(connectionState.name()); + + datagramSocket.connect(remote); + + setConnectionState(ConnectionState.CONNECTING); + joinConnectionState(ConnectionState.CONNECTED, timeout); + } catch (Throwable e) { + setConnectionState(ConnectionState.DISCONNECTED); + + throw new IOException(e); + } + } + + public void send(PacketType type, byte[] data) throws IOException { + C2SPacket packet = createOutgoingPacket(type, data); + } + + private C2SPacket createOutgoingPacket(PacketType type, byte[] data) throws IOException { + C2SPacket packet = new C2SPacket(); + + //TODO + packet.setPacketId(0); + + //TODO + packet.setGenerationId(0); + + //TODO + packet.setClientId(0); + + //TODO + packet.packetFlags |= 0; + + switch (type) { + case VOICE: + case VOICE_WHISPER: + + break; + case COMMAND: + case COMMAND_LOW: + + break; + case PING: + + break; + case PONG: + + break; + case ACK: + case ACK_LOW: + + break; + case INIT: + packet.setPacketFlags(packet.getPacketFlags() | PacketFlags.UNENCRYPTED.getIndex()); + break; + } + + return packet; + } + + /** + * Wire packet write + * @param packet Packet to write + * @throws IOException + */ + private void sendNetworkPacket(DatagramPacket packet) throws IOException { + Ts3Logging.debug( + "[NETWORK] WRITE Len=" + packet.getLength() + " to " + datagramSocket.getRemoteSocketAddress() + + "\n" + Ts3Logging.getHex(packet.getData()) + ); + + packet.setSocketAddress(datagramSocket.getRemoteSocketAddress()); + + datagramSocket.send(packet); + } + + private DatagramPacket receiveNetworkPacket() throws IOException { + datagramSocket.receive(datagramPacket); + return datagramPacket; + } + + private void handleNetworkPacket(DatagramPacket packet) { + Ts3Logging.debug( + "[NETWORK] READ Len=" + packet.getLength() + " from " + packet.getSocketAddress() + + "\n" + Ts3Logging.getHex(packet.getData()) + ); + + if (handler != null) handler.handleNetworkPacket(packet); + } + + public InetSocketAddress getRemote() { + return remote; + } + + private class PacketReceiver implements Runnable { + @Override + public void run() { + while (connectionState != ConnectionState.DISCONNECTED) { + try { + handleNetworkPacket(receiveNetworkPacket()); + } catch (IOException e) { + Ts3Logging.info("Problem handling packet: " + e.getMessage()); + } + } + + Ts3Logging.debug("Leaving packet receiver"); + } + } + + private class C2SPacket { + private int packetId; + private int generationId; + private int clientId; + private int packetFlags; + + private PacketType type; + private byte[] data; + private Date lastSendTime; + + public PacketType getType() { + return type; + } + + public void setType(PacketType type) { + this.type = type; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + + public Date getLastSendTime() { + return lastSendTime; + } + + public int getPacketId() { + return packetId; + } + + public void setPacketId(int packetId) { + this.packetId = packetId; + } + + public int getGenerationId() { + return generationId; + } + + public void setGenerationId(int generationId) { + this.generationId = generationId; + } + + public int getClientId() { + return clientId; + } + + public void setClientId(int clientId) { + this.clientId = clientId; + } + + public int getPacketFlags() { + return packetFlags; + } + + public void setPacketFlags(int packetFlags) { + this.packetFlags = packetFlags; + } + + /** + * Sends, or re-sends, a packet + */ + public void send() { + + } + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/command/Command.java b/src/main/java/com/github/manevolent/ts3j/command/Command.java new file mode 100644 index 0000000..37051df --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/command/Command.java @@ -0,0 +1,12 @@ +package com.github.manevolent.ts3j.command; + +import com.github.manevolent.ts3j.command.part.CommandParameter; + +public interface Command { + boolean willExpectResponse(); + + Iterable getParameters(); + + void appendParameter(CommandParameter parameter); + +} diff --git a/src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java b/src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java new file mode 100644 index 0000000..7dd10b5 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java @@ -0,0 +1,17 @@ +package com.github.manevolent.ts3j.command.part; + +import com.github.manevolent.ts3j.enums.CommandPartType; + +public class CommandOption implements CommandParameter { + private final String value; + + public CommandOption(String value) { + this.value = " -" + value; + } + + @Override + public CommandPartType getType() { + return CommandPartType.OPTION; + } + +} diff --git a/src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java b/src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java new file mode 100644 index 0000000..1af70fc --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java @@ -0,0 +1,9 @@ +package com.github.manevolent.ts3j.command.part; + +import com.github.manevolent.ts3j.enums.CommandPartType; + +public interface CommandParameter { + + CommandPartType getType(); + +} diff --git a/src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java b/src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java new file mode 100644 index 0000000..ef57d87 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java @@ -0,0 +1,81 @@ +package com.github.manevolent.ts3j.command.part; + +import com.github.manevolent.ts3j.enums.CommandPartType; + +public class CommandSingleParameter implements CommandParameter { + private final String key, value; + + public CommandSingleParameter(String key, String value) { + this.key = key; + this.value = value; + } + + public CommandSingleParameter(String key, boolean value) { + this.key = key; + this.value = serialize(value); + } + + public CommandSingleParameter(String key, byte value) { + this.key = key; + this.value = serialize(value); + } + + public CommandSingleParameter(String key, short value) { + this.key = key; + this.value = serialize(value); + } + + public CommandSingleParameter(String key, int value) { + this.key = key; + this.value = serialize(value); + } + + public CommandSingleParameter(String key, long value) { + this.key = key; + this.value = serialize(value); + } + + public CommandSingleParameter(String key, float value) { + this.key = key; + this.value = serialize(value); + } + + public CommandSingleParameter(String key, double value) { + this.key = key; + this.value = serialize(value); + } + + @Override + public CommandPartType getType() { + return CommandPartType.SINGLE_PARAMETER; + } + + public static String serialize(boolean value) { + return value ? "1": "0"; + } + + public static String serialize(byte value) { + return Byte.toString(value); + } + + public static String serialize(short value) { + return Short.toString(value); + } + + public static String serialize(int value) { + return Integer.toString(value); + } + + public static String serialize(long value) { + return Long.toString(value); + } + + public static String serialize(float value) { + return Float.toString(value); + } + + public static String serialize(double value) { + return Double.toString(value); + } + +} 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 new file mode 100644 index 0000000..3d21a87 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/command/response/CommandResponse.java @@ -0,0 +1,47 @@ +package com.github.manevolent.ts3j.command.response; + +import com.github.manevolent.ts3j.command.Command; + +public class CommandResponse { + private final Object returnLock = new Object(); + private final Command command; + private final int returnCode; + private long dispatchedTime; + + public CommandResponse(Command command, int returnCode) { + this.command = command; + this.returnCode = returnCode; + } + + public Command getCommand() { + return command; + } + + public int getReturnCode() { + return returnCode; + } + + public CommandResponse join() throws InterruptedException { + synchronized (returnLock) { + returnLock.wait(); + } + + return this; + } + + public CommandResponse join(int millis) throws InterruptedException { + synchronized (returnLock) { + returnLock.wait(millis); + } + + return this; + } + + public long getDispatchedTime() { + return dispatchedTime; + } + + public void setDispatchedTime(long dispatchedTime) { + this.dispatchedTime = dispatchedTime; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/ClientListOptions.java b/src/main/java/com/github/manevolent/ts3j/enums/ClientListOptions.java new file mode 100644 index 0000000..6d3f3de --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/ClientListOptions.java @@ -0,0 +1,29 @@ +package com.github.manevolent.ts3j.enums; + +public enum ClientListOptions { + UID(1 << 0), + AWAY(1 << 1), + VOICE(1 << 2), + TIMES(1 << 3), + GROUPS(1 << 4), + INFO(1 << 5), + ICON(1 << 6), + COUNTRY(1 << 7); + + private final int index; + + ClientListOptions(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static ClientListOptions fromId(int index) { + for (ClientListOptions value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/ClientType.java b/src/main/java/com/github/manevolent/ts3j/enums/ClientType.java new file mode 100644 index 0000000..69e44a7 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/ClientType.java @@ -0,0 +1,23 @@ +package com.github.manevolent.ts3j.enums; + +public enum ClientType { + FULL(0), + QUERY(1); + + private final int index; + + ClientType(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static ClientType fromId(int index) { + for (ClientType value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/CodecType.java b/src/main/java/com/github/manevolent/ts3j/enums/CodecType.java new file mode 100644 index 0000000..3f3d246 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/CodecType.java @@ -0,0 +1,31 @@ +package com.github.manevolent.ts3j.enums; + +public enum CodecType { + SPEEX_NARROWBAND(0), + SPEEX_WIDEBAND(1), + SPEEX_ULTRA_WIDEBAND(2), + CELT_MONO(3), + OPUS_VOICE(4), + OPUS_MUSIC(5), + + RAW(127) + + ; + + private final int index; + + CodecType(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static CodecType fromId(int index) { + for (CodecType value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/CommandPartType.java b/src/main/java/com/github/manevolent/ts3j/enums/CommandPartType.java new file mode 100644 index 0000000..f5044bc --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/CommandPartType.java @@ -0,0 +1,7 @@ +package com.github.manevolent.ts3j.enums; + +public enum CommandPartType { + SINGLE_PARAMETER, + MULTI_PAREMTER, + OPTION; +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/ConnectionState.java b/src/main/java/com/github/manevolent/ts3j/enums/ConnectionState.java new file mode 100644 index 0000000..05390ea --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/ConnectionState.java @@ -0,0 +1,30 @@ + +package com.github.manevolent.ts3j.enums; + +import com.github.manevolent.ts3j.TeamspeakClient; +import com.github.manevolent.ts3j.handler.*; + +public enum ConnectionState { + CONNECTING(TeamspeakClientHandlerConnecting.class), + NEGOTIATING(TeamspeakClientHandlerNegotiating.class), + CONNECTED(TeamspeakClientHandlerConnected.class), + DISCONNECTED(TeamspeakClientHandlerDisconnected.class); + + private final Class clazz; + + ConnectionState(Class clazz) { + this.clazz = clazz; + } + + public Class getHandlerClass() { + return clazz; + } + + public TeamspeakClientHandler createHandler(TeamspeakClient client) { + try { + return clazz.getConstructor(TeamspeakClient.class).newInstance(client); + } catch (ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/manevolent/ts3j/enums/DisconnectReason.java b/src/main/java/com/github/manevolent/ts3j/enums/DisconnectReason.java new file mode 100644 index 0000000..a621047 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/DisconnectReason.java @@ -0,0 +1,35 @@ +package com.github.manevolent.ts3j.enums; + +public enum DisconnectReason { + USER_ACTION(0), + USER_OR_CHANNEL_MOVED(1), + SUBSCRIPTION_CHANGED(2), + TIMEOUT(3), + KICKED_FROM_CHANNEL(4), + KICKED_FROM_SERVER(5), + BANNED(6), + SERVER_STOPPED(7), + LEFT_SERVER(8), + CHANNEL_UPDATED(9), + SERVER_OR_CHANNEL_EDITED(10), + SERVER_SHUTDOWN(11) + + ; + + private final int index; + + DisconnectReason(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static DisconnectReason fromId(int index) { + for (DisconnectReason value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/GroupNamingMode.java b/src/main/java/com/github/manevolent/ts3j/enums/GroupNamingMode.java new file mode 100644 index 0000000..3506c7c --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/GroupNamingMode.java @@ -0,0 +1,29 @@ +package com.github.manevolent.ts3j.enums; + +public enum GroupNamingMode { + UID(1 << 0), + AWAY(1 << 1), + VOICE(1 << 2), + TIMES(1 << 3), + GROUPS(1 << 4), + INFO(1 << 5), + ICON(1 << 6), + COUNTRY(1 << 7); + + private final int index; + + GroupNamingMode(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static GroupNamingMode fromId(int index) { + for (GroupNamingMode value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/GroupType.java b/src/main/java/com/github/manevolent/ts3j/enums/GroupType.java new file mode 100644 index 0000000..e950104 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/GroupType.java @@ -0,0 +1,26 @@ +package com.github.manevolent.ts3j.enums; + +public enum GroupType { + TEMPLATE(0), + REGULAR(1), + QUERY(2) + + ; + + private final int index; + + GroupType(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static GroupType fromId(int index) { + for (GroupType value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/GroupWhisperType.java b/src/main/java/com/github/manevolent/ts3j/enums/GroupWhisperType.java new file mode 100644 index 0000000..6b63ab7 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/GroupWhisperType.java @@ -0,0 +1,29 @@ +package com.github.manevolent.ts3j.enums; + +public enum GroupWhisperType { + ALL_CHANNELS(0), + CURRENT_CHANNEL(1), + ALL_PARENT_CHANNEL(2), + CHANNEL_FAMILY(3), + COMPLETE_CHANNEL_FAMILY(4), + SUBCHANNELS(5) + + ; + + private final int index; + + GroupWhisperType(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static GroupWhisperType fromId(int index) { + for (GroupWhisperType value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/HostBannerMode.java b/src/main/java/com/github/manevolent/ts3j/enums/HostBannerMode.java new file mode 100644 index 0000000..3e4f106 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/HostBannerMode.java @@ -0,0 +1,26 @@ +package com.github.manevolent.ts3j.enums; + +public enum HostBannerMode { + NO_ADJUST(0), + IGNORE_ASPECT_RATIO(1), + KEEP_ASPECT(2) + + ; + + private final int index; + + HostBannerMode(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static HostBannerMode fromId(int index) { + for (HostBannerMode value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/HostMessageMode.java b/src/main/java/com/github/manevolent/ts3j/enums/HostMessageMode.java new file mode 100644 index 0000000..df3a35c --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/HostMessageMode.java @@ -0,0 +1,27 @@ +package com.github.manevolent.ts3j.enums; + +public enum HostMessageMode { + NONE(0), + LOG(1), + MODAL(2), + MODAL_QUIT(3) + + ; + + private final int index; + + HostMessageMode(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static HostMessageMode fromId(int index) { + for (HostMessageMode value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/KickIdentifier.java b/src/main/java/com/github/manevolent/ts3j/enums/KickIdentifier.java new file mode 100644 index 0000000..7b1c878 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/KickIdentifier.java @@ -0,0 +1,25 @@ +package com.github.manevolent.ts3j.enums; + +public enum KickIdentifier { + CHANNEL(4), + SERVER(5) + + ; + + private final int index; + + KickIdentifier(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static KickIdentifier fromId(int index) { + for (KickIdentifier value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/LicenseType.java b/src/main/java/com/github/manevolent/ts3j/enums/LicenseType.java new file mode 100644 index 0000000..2c08887 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/LicenseType.java @@ -0,0 +1,28 @@ +package com.github.manevolent.ts3j.enums; + +public enum LicenseType { + NO_LICENSE(0), + ATHP(1), + LAN(2), + NPL(3), + UNKNOWN(4) + + ; + + private final int index; + + LicenseType(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static LicenseType fromId(int index) { + for (LicenseType value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/LogLevel.java b/src/main/java/com/github/manevolent/ts3j/enums/LogLevel.java new file mode 100644 index 0000000..9669f53 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/LogLevel.java @@ -0,0 +1,27 @@ +package com.github.manevolent.ts3j.enums; + +public enum LogLevel { + ERROR(1), + WARNING(2), + DEBUG(3), + INFO(4) + + ; + + private final int index; + + LogLevel(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static LogLevel fromId(int index) { + for (LogLevel value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/PacketFlags.java b/src/main/java/com/github/manevolent/ts3j/enums/PacketFlags.java new file mode 100644 index 0000000..92c3c90 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/PacketFlags.java @@ -0,0 +1,26 @@ +package com.github.manevolent.ts3j.enums; + +public enum PacketFlags { + NONE(0x0), + FRAGMENTED(0X10), + NEW_PROTOCOL(0x20), + COMPRESSED(0x40), + UNENCRYPTED(0x80); + + private final int index; + + PacketFlags(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static PacketFlags fromId(int index) { + for (PacketFlags value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/PacketType.java b/src/main/java/com/github/manevolent/ts3j/enums/PacketType.java new file mode 100644 index 0000000..23fe063 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/PacketType.java @@ -0,0 +1,31 @@ +package com.github.manevolent.ts3j.enums; + +public enum PacketType { + VOICE(0x0), + VOICE_WHISPER(0x1), + COMMAND(0x2), + COMMAND_LOW(0x3), + PING(0x4), + PONG(0x5), + ACK(0x6), + ACK_LOW(0x7), + INIT(0x8) + ; + + private final int index; + + PacketType(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static PacketType fromId(int index) { + for (PacketType value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/PermissionGroupType.java b/src/main/java/com/github/manevolent/ts3j/enums/PermissionGroupType.java new file mode 100644 index 0000000..96c73fc --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/PermissionGroupType.java @@ -0,0 +1,28 @@ +package com.github.manevolent.ts3j.enums; + +public enum PermissionGroupType { + SERVER_GROUP(0), + GLOBAL_CLIENT(1), + CHANNEL(2), + CHANNEL_GROUP(3), + CHANNEL_CLIENT(4) + + ; + + private final int index; + + PermissionGroupType(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static PermissionGroupType fromId(int index) { + for (PermissionGroupType value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/PluginTargetMode.java b/src/main/java/com/github/manevolent/ts3j/enums/PluginTargetMode.java new file mode 100644 index 0000000..5e8aabb --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/PluginTargetMode.java @@ -0,0 +1,27 @@ +package com.github.manevolent.ts3j.enums; + +public enum PluginTargetMode { + CURRENT_CHANNEL(0), + SERVER(1), + CLIENT(2), + CURRENT_CHANNEL_SUBSCRIBED_CLIENTS(3) + + ; + + private final int index; + + PluginTargetMode(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static PluginTargetMode fromId(int index) { + for (PluginTargetMode value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/PriviledgeTokenType.java b/src/main/java/com/github/manevolent/ts3j/enums/PriviledgeTokenType.java new file mode 100644 index 0000000..39789b7 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/PriviledgeTokenType.java @@ -0,0 +1,25 @@ +package com.github.manevolent.ts3j.enums; + +public enum PriviledgeTokenType { + SERVER_GROUP(0), + CHANNEL_GROUP(1) + + ; + + private final int index; + + PriviledgeTokenType(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static PriviledgeTokenType fromId(int index) { + for (PriviledgeTokenType value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/TextMessageTargetMode.java b/src/main/java/com/github/manevolent/ts3j/enums/TextMessageTargetMode.java new file mode 100644 index 0000000..6ff6386 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/TextMessageTargetMode.java @@ -0,0 +1,26 @@ +package com.github.manevolent.ts3j.enums; + +public enum TextMessageTargetMode { + PRIVATE(1), + CHANNEL(2), + SERVER(3) + + ; + + private final int index; + + TextMessageTargetMode(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static TextMessageTargetMode fromId(int index) { + for (TextMessageTargetMode value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/enums/VoiceEncryptionMode.java b/src/main/java/com/github/manevolent/ts3j/enums/VoiceEncryptionMode.java new file mode 100644 index 0000000..0b05eb7 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/enums/VoiceEncryptionMode.java @@ -0,0 +1,26 @@ +package com.github.manevolent.ts3j.enums; + +public enum VoiceEncryptionMode { + INDIVIDUAL(0), + DISABLED(1), + ENABLED(2) + + ; + + private final int index; + + VoiceEncryptionMode(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static VoiceEncryptionMode fromId(int index) { + for (VoiceEncryptionMode value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandler.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandler.java new file mode 100644 index 0000000..73697a7 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandler.java @@ -0,0 +1,31 @@ +package com.github.manevolent.ts3j.handler; + +import com.github.manevolent.ts3j.TeamspeakClient; +import com.github.manevolent.ts3j.enums.ConnectionState; + +import java.io.IOException; +import java.net.DatagramPacket; + +public abstract class TeamspeakClientHandler { + private final TeamspeakClient client; + + protected TeamspeakClientHandler(TeamspeakClient client) { + this.client = client; + } + + public void handleConnectionStateChanging(ConnectionState connectionState) { + + } + + public void handleNetworkPacket(DatagramPacket packet) { + + } + + public void onAssigned() throws IOException { + + } + + protected TeamspeakClient getClient() { + return client; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnected.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnected.java new file mode 100644 index 0000000..64ae81d --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnected.java @@ -0,0 +1,9 @@ +package com.github.manevolent.ts3j.handler; + +import com.github.manevolent.ts3j.TeamspeakClient; + +public class TeamspeakClientHandlerConnected extends TeamspeakClientHandler { + public TeamspeakClientHandlerConnected(TeamspeakClient client) { + super(client); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnecting.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnecting.java new file mode 100644 index 0000000..fd62e6a --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnecting.java @@ -0,0 +1,80 @@ +package com.github.manevolent.ts3j.handler; + +import com.github.manevolent.ts3j.TeamspeakClient; +import com.github.manevolent.ts3j.enums.PacketType; +import com.github.manevolent.ts3j.util.Ts3Logging; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Random; + +public class TeamspeakClientHandlerConnecting extends TeamspeakClientHandler { + + + public TeamspeakClientHandlerConnecting(TeamspeakClient client) { + super(client); + } + + @Override + public void onAssigned() throws IOException { + Ts3Logging.debug("Connecting: sending Init1..."); + + processInit1(); + } + + /** + * Processes the first client dispatch of the Init1 packet (C2S initial handshake packet) + */ + public void processInit1() throws IOException { + processInit1(null); + } + + public void processInit1(byte[] data) throws IOException { + int versionLen = 4; + int initTypeLen = 1; + Integer type = null; + + if (data != null) { + type = (int) data[0]; + + if (data.length < initTypeLen) + throw new IllegalArgumentException("invalid Init1 packet (too short)"); + } + + ByteBuffer buffer = null; + + if (type == null) { + buffer = ByteBuffer.allocate(versionLen + initTypeLen + 4 + 4 + 8); + + buffer.put(new byte[] { 0x09, (byte)0x83, (byte)0x8C, (byte)0xCF }); + buffer.put((byte) 0x00); // initType + buffer.order(ByteOrder.BIG_ENDIAN); + buffer.putInt((int) (System.currentTimeMillis() / 1000L)); + + Random random = new Random(); + for (int i = 0; i < 4; i++) + buffer.put((byte) random.nextInt(256)); // 4byte random + } else { + switch (type) { + case 1: + + break; + case 3: + + break; + case 0x7F: + // 0x7F: Some strange servers do this + // the normal client responds by starting again + break; + default: + throw new IllegalArgumentException("invalid Init1 packet id: " + type); + } + } + + if (buffer == null || buffer.position() <= 0) + throw new IllegalStateException("invalid send buffer"); + + getClient().send(PacketType.INIT, buffer.array()); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerDisconnected.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerDisconnected.java new file mode 100644 index 0000000..25549d2 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerDisconnected.java @@ -0,0 +1,9 @@ +package com.github.manevolent.ts3j.handler; + +import com.github.manevolent.ts3j.TeamspeakClient; + +public class TeamspeakClientHandlerDisconnected extends TeamspeakClientHandler { + public TeamspeakClientHandlerDisconnected(TeamspeakClient client) { + super(client); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerNegotiating.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerNegotiating.java new file mode 100644 index 0000000..0b9cb38 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerNegotiating.java @@ -0,0 +1,9 @@ +package com.github.manevolent.ts3j.handler; + +import com.github.manevolent.ts3j.TeamspeakClient; + +public class TeamspeakClientHandlerNegotiating extends TeamspeakClientHandler { + public TeamspeakClientHandlerNegotiating(TeamspeakClient client) { + super(client); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/util/Ts3Const.java b/src/main/java/com/github/manevolent/ts3j/util/Ts3Const.java new file mode 100644 index 0000000..603c5d7 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3Const.java @@ -0,0 +1,38 @@ +package com.github.manevolent.ts3j.util; + +public final class Ts3Const { + // Common Definitions + + //limited length, measured in characters + public static int MaxSizeChannelName = 40; + public static int MaxSizeVirtualserverName = 64; + public static int MaxSizeClientNicknameSdk = 64; + public static int MinSizeClientNicknameSdk = 3; + public static int MaxSizeReasonMessage = 80; + + //limited length, measured in bytes (utf8 encoded) + public static int MaxSizeTextMessage = 1024; + public static int MaxSizeChannelTopic = 255; + public static int MaxSizeChannelDescription = 8192; + public static int MaxSizeVirtualserverWelcomeMessage = 1024; + + // Rare Definitions + + //limited length, measured in characters + public static int MaxSizeClientNickname = 30; + public static int MinSizeClientNickname = 3; + public static int MaxSizeAwayMessage = 80; + public static int MaxSizeGroupName = 30; + public static int MaxSizeTalkRequestMessage = 50; + public static int MaxSizeComplainMessage = 200; + public static int MaxSizeClientDescription = 200; + public static int MaxSizeHostMessage = 200; + public static int MaxSizeHostbuttonTooltip = 50; + public static int MaxSizePokeMessage = 100; + public static int MaxSizeOfflineMessage = 4096; + public static int MaxSizeOfflineMessageSubject = 200; + + //limited length, measured in bytes (utf8 encoded) + public static int MaxSizePluginCommand = 1024 * 8; + public static int MaxSizeVirtualserverHostbannerGfxUrl = 2000; +} \ No newline at end of file diff --git a/src/main/java/com/github/manevolent/ts3j/util/Ts3Logging.java b/src/main/java/com/github/manevolent/ts3j/util/Ts3Logging.java new file mode 100644 index 0000000..09d05c7 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3Logging.java @@ -0,0 +1,20 @@ +package com.github.manevolent.ts3j.util; + +public final class Ts3Logging { + private static final String HEXES = "0123456789ABCDEF"; + public static String getHex(byte[] raw) { + final StringBuilder hex = new StringBuilder(2 * raw.length); + for (final byte b : raw) { + hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))); + } + return hex.toString(); + } + + public static void info(String message) { + System.err.println("[INFO] " + message); + } + + public static void debug(String message) { + System.err.println("[DEBUG] " + message); + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/util/Ts3String.java b/src/main/java/com/github/manevolent/ts3j/util/Ts3String.java new file mode 100644 index 0000000..aeafc1d --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3String.java @@ -0,0 +1,67 @@ +package com.github.manevolent.ts3j.util; + +public final class Ts3String { + public static String escape(String string) { + StringBuilder sb = new StringBuilder(string.length()); + + for (int i = 0; i < string.length(); i ++) { + switch (string.charAt(i)) { + case '\\': sb.append("\\\\"); break; + case '/': sb.append("\\/"); break; + case ' ': sb.append("\\s"); break; + case '|': sb.append("\\p"); break; + case '\f': sb.append("\\f"); break; + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + //case '\v': sb.append("\\v"); break; + default: + sb.append(string.charAt(i)); + } + } + + return sb.toString(); + } + + public static String unescape(String string) { + StringBuilder sb = new StringBuilder(string.length()); + + for (int i = 0; i < string.length(); i ++) { + char c = string.charAt(i); + if (c == '\\') { + if (i++ >= string.length()) throw new IllegalArgumentException("illegal escape at end of string"); + switch (string.charAt(i)) { + //case 'v': sb.append('\v'); break; + case 't': sb.append('\t'); break; + case 'r': sb.append('\r'); break; + case 'n': sb.append('\n'); break; + case 'f': sb.append('\f'); break; + case 'p': sb.append('|'); break; + case 's': sb.append(' '); break; + case '/': sb.append('/'); break; + case '\\': sb.append('\\'); break; + default: + throw new IllegalArgumentException("invalid escape code: " + c); + } + } + } + + return sb.toString(); + } + + public static int getTokenLength(String s) { + return (int) s.length() + (int) s.chars().filter(x -> isDoubleChar((char)x)).count(); + } + + public static boolean isDoubleChar(char c) { + return c == '\\' || + c == '/' || + c == ' ' || + c == '|' || + c == '\f' || + c == '\n' || + c == '\r' || + c == '\t';// || + //c == '\v'; + } +} diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java new file mode 100644 index 0000000..772b4f8 --- /dev/null +++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java @@ -0,0 +1,25 @@ +package com.github.manevolent.ts3j; + +import com.github.manevolent.ts3j.enums.ConnectionState; +import junit.framework.TestCase; + +import java.net.InetSocketAddress; + +public class ServerConnectionTest extends TestCase { + public ServerConnectionTest(String name) { + super(name); + } + + public void testParser() throws Exception { + TeamspeakClient client = new TeamspeakClient(); + + client.connect(new InetSocketAddress( + "voice.teamspeak.com", + 9987), + null, + 10000L + ); + + assertEquals(client.getConnectionState(), ConnectionState.CONNECTED); + } +}