diff --git a/README.md b/README.md
index c641fa0..0de18c1 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,6 @@
Discord: https://discord.gg/4q9jpCa
-Teamspeak: teamlixo.net (that's an SRV record; ts.teamlixo.net is the A record)
-
TS3J is an open-source implementation of the reverse-engineered Teamspeak3 full server/client protocol, as an adaptation of Splamy's C# TS3Client source code. You can find that here: https://github.com/Splamy/TS3AudioBot/.
# Maven
@@ -112,4 +110,6 @@ public byte[] provide() {
```
# Receiving audio
-This is definitely possible, but it's a work in progress for now. Once I have a need for this in my own bot (I can forsee there being an opportunity), I will be adding full support to TS3J.
+Refer to the `setVoiceHandler` and `setWhisperHandler` methods to supply a Consumer object to receive Voice and Whisper packets. You will need to decode the packets yourself, and insert packet-loss-correction as needed.
+
+Note that the first 5 packets starting a voice session are marked with the COMPRESSED flag. The final voice packet, intended to singal to close your decoder and flush samples, is always empty (0-length byte array).
diff --git a/pom.xml b/pom.xml
index 9e6206b..faabc8b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -45,5 +45,11 @@
commons-lang
2.6
+
+
+ dnsjava
+ dnsjava
+ 2.1.8
+
\ No newline at end of file
diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/TS3DNS.java b/src/main/java/com/github/manevolent/ts3j/protocol/TS3DNS.java
new file mode 100644
index 0000000..8b6fab9
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/protocol/TS3DNS.java
@@ -0,0 +1,43 @@
+package com.github.manevolent.ts3j.protocol;
+
+import org.xbill.DNS.*;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class TS3DNS {
+ private static final int TS3_VOICE_DEFAULT_PORT = 9987;
+ private static final int TS3_QUERY_DEFAULT_PORT = 10011;
+ private static final int TS3_FILETRANSFER_DEFAULT_PORT = 30033;
+ private static final int TSDNS_DEFAULT_PORT = 41144;
+
+ private static final String DNS_PREFIX_TCP = "_tsdns._tcp.";
+ private static final String DNS_PREFIX_UDP = "_ts3._udp.";
+
+ private static List lookupSRV(String domain) throws IOException {
+ org.xbill.DNS.Record[] records = new Lookup(domain, Type.SRV).run();
+
+ if (records == null || records.length <= 0) throw new UnknownHostException("Host not found");
+
+ return Arrays.stream(records)
+ .filter(Objects::nonNull)
+ .map(x -> (SRVRecord) x)
+ .filter(x -> x.getTarget() != null)
+ .sorted((a, b) -> {
+ // Loose sorting
+ int prioComp = Integer.compare(a.getPriority(), b.getPriority());
+ if (prioComp != 0) return prioComp;
+ else return Integer.compare(a.getWeight(), b.getWeight());
+ })
+ .map(x -> new InetSocketAddress(x.getTarget().toString(), x.getPort()))
+ .collect(Collectors.toList());
+ }
+
+ public static List lookup(String domain) throws IOException {
+ return lookupSRV(DNS_PREFIX_UDP + domain);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/fragment/PacketReassembly.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/fragment/PacketReassembly.java
index e313725..36ad7c6 100644
--- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/fragment/PacketReassembly.java
+++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/fragment/PacketReassembly.java
@@ -40,7 +40,7 @@ public class PacketReassembly {
temporaryCounter.setPacketId(currentPacketId.getKey());
temporaryCounter.setGeneration(currentPacketId.getValue());
- while (true) {
+ while (queue.size() > 0) {
Pair thisPacketId = temporaryCounter.current();
Packet packet = queue.get(thisPacketId.getKey());
diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java
index 09f4b88..78a2bbb 100644
--- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java
+++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java
@@ -1,9 +1,11 @@
package com.github.manevolent.ts3j.protocol.packet.handler.client;
+import com.github.manevolent.ts3j.command.Command;
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.TS3DNS;
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command;
import com.github.manevolent.ts3j.protocol.packet.PacketBody8Init1;
@@ -188,49 +190,48 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
alphaBytes = null;
- getClient().writePacket(new PacketBody2Command(
+ Command clientinit = new SingleCommand(
+ "clientinit",
ProtocolRole.CLIENT,
- new SingleCommand(
- "clientinit",
- ProtocolRole.CLIENT,
- new CommandSingleParameter("client_nickname", getClient().getNickname()),
- new CommandSingleParameter("client_version", "3.?.? [Build: 5680278000]"),
- new CommandSingleParameter("client_platform", "Windows"),
- new CommandSingleParameter(
- "client_version_sign",
- "DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg=="
- ),
- new CommandSingleParameter("client_input_hardware", "1"),
- new CommandSingleParameter("client_output_hardware", "1"),
- new CommandSingleParameter("client_default_channel",
- getClient().getOption("client.default_channel", String.class)
- ),
- new CommandSingleParameter("client_default_channel_password",
- getClient().getOption("client.default_channel_password", String.class)
- ),
- new CommandSingleParameter(
- "client_server_password",
- getClient().getOption("client.server_password", String.class)
- ),
- new CommandSingleParameter("client_nickname_phonetic",
- getClient().getOption("client.nickname_phonetic", String.class)
- ),
- new CommandSingleParameter("client_meta_data", ""),
- new CommandSingleParameter("client_default_token",
- getClient().getOption("client.default_token", String.class)
- ),
- new CommandSingleParameter(
- "client_key_offset",
- Long.toString(getClient().getIdentity().getKeyOffset())
- ),
- new CommandSingleParameter(
- "hwid",
- getClient().getOption("client.hwid", String.class) != null ?
- getClient().getOption("client.hwid", String.class) :
- "+LyYqbDqOvEEpN5pdAbF8/v5kZ0="
- )
+ new CommandSingleParameter("client_nickname", getClient().getNickname()),
+ new CommandSingleParameter("client_version", "3.?.? [Build: 5680278000]"),
+ new CommandSingleParameter("client_platform", "Windows"),
+ new CommandSingleParameter(
+ "client_version_sign",
+ "DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg=="
+ ),
+ new CommandSingleParameter("client_input_hardware", "1"),
+ new CommandSingleParameter("client_output_hardware", "1"),
+ new CommandSingleParameter("client_default_channel",
+ getClient().getOption("client.default_channel", String.class)
+ ),
+ new CommandSingleParameter("client_default_channel_password",
+ getClient().getOption("client.default_channel_password", String.class)
+ ),
+ new CommandSingleParameter("client_server_password",
+ getClient().getOption("client.server_password", String.class)
+ ),
+ new CommandSingleParameter("client_nickname_phonetic",
+ getClient().getOption("client.nickname_phonetic", String.class)
+ ),
+ new CommandSingleParameter("client_meta_data", ""),
+ new CommandSingleParameter("client_default_token",
+ getClient().getOption("client.default_token", String.class)
+ ),
+ new CommandSingleParameter("client_key_offset",
+ Long.toString(getClient().getIdentity().getKeyOffset())
+ ),
+ new CommandSingleParameter(
+ "hwid",
+ getClient().getOption("client.hwid", String.class) != null ?
+ getClient().getOption("client.hwid", String.class) :
+ "+LyYqbDqOvEEpN5pdAbF8/v5kZ0="
)
- ));
+ );
+
+ Ts3Debugging.debug(clientinit.build());
+
+ getClient().writePacket(new PacketBody2Command(ProtocolRole.CLIENT, clientinit));
getClient().setCommandProcessor(getClient());
getClient().setState(ClientConnectionState.RETRIEVING_DATA);
diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerRetrievingData.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerRetrievingData.java
index 3804d90..77bbf49 100644
--- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerRetrievingData.java
+++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerRetrievingData.java
@@ -1,9 +1,31 @@
package com.github.manevolent.ts3j.protocol.packet.handler.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.LocalTeamspeakClientSocket;
+
public class LocalClientHandlerRetrievingData extends LocalClientHandlerFull {
public LocalClientHandlerRetrievingData(LocalTeamspeakClientSocket client) {
super(client);
}
+
+ @Override
+ protected void handleCommand(MultiCommand command) throws CommandProcessException {
+ if (command.getName().equalsIgnoreCase("error")) {
+ try {
+ getClient().setState(ClientConnectionState.DISCONNECTED);
+ } catch (Exception e) {
+ getClient().getExceptionHandler().accept(e);
+ }
+
+ throw new CommandProcessException(command.simplifyOne().get("msg").getValue());
+ }
+
+ CommandProcessor processor = getClient().getCommandProcessor();
+ if (processor != null) processor.process(getClient(), command);
+ }
+
}
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 a17f60c..ed99b09 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
@@ -107,8 +107,8 @@ public abstract class AbstractTeamspeakClientSocket
private boolean reading = false;
- protected AbstractTeamspeakClientSocket() {
- super(SocketRole.CLIENT);
+ protected AbstractTeamspeakClientSocket(SocketRole role) {
+ super(role);
// Initialize reassembly queue
for (PacketBodyType bodyType : PacketBodyType.values())
@@ -205,8 +205,6 @@ public abstract class AbstractTeamspeakClientSocket
}
protected void onDisconnect() {
- clientOptions.clear();
-
clientId = -1;
readQueue.clear();
diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java
index d9ec749..e7e6d31 100644
--- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java
+++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/LocalTeamspeakClientSocket.java
@@ -12,9 +12,7 @@ import com.github.manevolent.ts3j.command.response.CommandResponse;
import com.github.manevolent.ts3j.event.*;
import com.github.manevolent.ts3j.identity.LocalIdentity;
import com.github.manevolent.ts3j.identity.Uid;
-import com.github.manevolent.ts3j.protocol.NetworkPacket;
-import com.github.manevolent.ts3j.protocol.PacketKind;
-import com.github.manevolent.ts3j.protocol.ProtocolRole;
+import com.github.manevolent.ts3j.protocol.*;
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
import com.github.manevolent.ts3j.protocol.packet.PacketBody0Voice;
@@ -60,7 +58,7 @@ public class LocalTeamspeakClientSocket
private Map awaitingCommands = new ConcurrentHashMap<>();
public LocalTeamspeakClientSocket() {
- super();
+ super(SocketRole.CLIENT);
namedProcessors.put("initserver", new InitServerHandler());
@@ -254,12 +252,34 @@ public class LocalTeamspeakClientSocket
remote = null;
}
+
+ /**
+ * Initiates a connection to a server
+ * @param hostname Hostname to contact
+ * @param password server password (may be null)
+ * @param timeout timeout, in milliseconds, to complete a connection.
+ */
+ public void connect(String hostname, String password, long timeout) throws
+ IOException,
+ TimeoutException {
+
+ InetSocketAddress address;
+
+ try {
+ // Attempt TS3DNS
+ address = TS3DNS.lookup(hostname).stream().findFirst().orElseThrow(() -> new UnknownHostException(hostname));
+ } catch (UnknownHostException e) {
+ address = new InetSocketAddress(hostname, 9987);
+ }
+
+ connect(address, password, timeout);
+ }
+
/**
* Initiates a connection to a server
* @param remote remote sever to contact
- * @param password server password
+ * @param password server password (may be null)
* @param timeout timeout, in milliseconds, to complete a connection.
- * @throws IOException
*/
public void connect(InetSocketAddress remote, String password, long timeout)
throws IOException, TimeoutException {
@@ -286,7 +306,7 @@ public class LocalTeamspeakClientSocket
setOption("client.hostname", remote.getHostString());
- if (password != null) setOption("client.password", password);
+ if (password != null && password.length() > 0) setOption("client.password", password);
setState(ClientConnectionState.CONNECTING);
@@ -297,6 +317,7 @@ public class LocalTeamspeakClientSocket
0.01F,
new MicrophoneTask()
);
+
microphoneThread.setDaemon(true);
microphoneThread.start();
} catch (TimeoutException e) {
@@ -629,6 +650,14 @@ public class LocalTeamspeakClientSocket
super.setNickname(nickname);
}
+ public void setHWID(String hwid) {
+ setOption("client.hwid", hwid);
+ }
+
+ public String getHWID() {
+ return getOption("client.hwid", String.class);
+ }
+
/**
* Commands
*/
@@ -878,6 +907,26 @@ public class LocalTeamspeakClientSocket
return executeCommand(command, x -> new Client(x.toMap())).get();
}
+ public void serverGroupAddClient(int groupId, int clientDatabaseId)
+ throws IOException, TimeoutException, ExecutionException, InterruptedException {
+ Command command = new SingleCommand("servergroupaddclient", ProtocolRole.CLIENT);
+
+ command.add(new CommandSingleParameter("sgid", Integer.toString(groupId)));
+ command.add(new CommandSingleParameter("cldbid", Integer.toString(clientDatabaseId)));
+
+ executeCommand(command).complete();
+ }
+
+ public void serverGroupRemoveClient(int groupId, int clientDatabaseId)
+ throws IOException, TimeoutException, ExecutionException, InterruptedException {
+ Command command = new SingleCommand("servergroupdelclient", ProtocolRole.CLIENT);
+
+ command.add(new CommandSingleParameter("sgid", Integer.toString(groupId)));
+ command.add(new CommandSingleParameter("cldbid", Integer.toString(clientDatabaseId)));
+
+ executeCommand(command).complete();
+ }
+
/**
* Moves one or more clients specified with clid to the channel with ID cid. If
the target channel has a password, it needs to be specified with cpw. If the
diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java
index c332d85..97c710d 100644
--- a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java
+++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java
@@ -31,135 +31,25 @@ public class ServerConnectionTest {
identity.improveSecurity(10);
client.setIdentity(identity);
-
client.setNickname("Hello from Java");
+ client.setHWID("JAVAJAVAJAVA");
- client.setOption("client.hwid", "JAVAJAVAJAVA");
- client.connect(
- new InetSocketAddress(
- "ts.teamlixo.net",
- 9987
- ),
- null,
- 10000L
- );
+ while (true) {
+ try {
- client.addListener(new TS3Listener() {
- @Override
- public void onTextMessage(TextMessageEvent e) {
+ client.connect(
+ "teamlixo.net",
+ null,
+ 10000L
+ );
+ assertEquals(client.getState(), ClientConnectionState.CONNECTED);
+ } catch (Exception ex) {
+ ex.printStackTrace();
}
- @Override
- public void onClientJoin(ClientJoinEvent e) {
- System.err.println(e.get("cid"));
- }
-
- @Override
- public void onClientLeave(ClientLeaveEvent e) {
-
- }
-
- @Override
- public void onServerEdit(ServerEditedEvent e) {
-
- }
-
- @Override
- public void onChannelEdit(ChannelEditedEvent e) {
-
- }
-
- @Override
- public void onChannelDescriptionChanged(ChannelDescriptionEditedEvent e) {
-
- }
-
- @Override
- public void onClientMoved(ClientMovedEvent e) {
-
- }
-
- @Override
- public void onChannelCreate(ChannelCreateEvent e) {
-
- }
-
- @Override
- public void onChannelDeleted(ChannelDeletedEvent e) {
-
- }
-
- @Override
- public void onChannelMoved(ChannelMovedEvent e) {
-
- }
-
- @Override
- public void onChannelPasswordChanged(ChannelPasswordChangedEvent e) {
-
- }
-
- @Override
- public void onChannelList(ChannelListEvent e) {
-
- }
-
- @Override
- public void onPrivilegeKeyUsed(PrivilegeKeyUsedEvent e) {
-
- }
-
- @Override
- public void onChannelGroupList(ChannelGroupListEvent e) {
-
- }
-
- @Override
- public void onServerGroupList(ServerGroupListEvent e) {
-
- }
-
- @Override
- public void onClientNeededPermissions(ClientNeededPermissionsEvent e) {
-
- }
-
- @Override
- public void onClientChannelGroupChanged(ClientChannelGroupChangedEvent e) {
-
- }
-
- @Override
- public void onClientChanged(ClientUpdatedEvent e) {
-
- }
-
- @Override
- public void onDisconnected(DisconnectedEvent e) {
-
- }
-
- @Override
- public void onChannelSubscribed(ChannelSubscribedEvent e) {
-
- }
- });
-
- assertEquals(client.getState(), ClientConnectionState.CONNECTED);
-
- client.subscribeAll();
-
- try {
- client.setVoiceHandler(packetBody0Voice -> {
- System.err.println("clientId=" + packetBody0Voice.getClientId());
- System.err.println("packetId=" + packetBody0Voice.getPacketId());
- System.err.println("type=" + packetBody0Voice.getCodecType());
- System.err.println("len=" + packetBody0Voice.getCodecData().length);
- });
- } catch (Exception ex) {
- ex.printStackTrace();
+ client.disconnect();
}
}
}
diff --git a/src/test/java/com/github/manevolent/ts3j/TS3DNSTest.java b/src/test/java/com/github/manevolent/ts3j/TS3DNSTest.java
new file mode 100644
index 0000000..a34fbb6
--- /dev/null
+++ b/src/test/java/com/github/manevolent/ts3j/TS3DNSTest.java
@@ -0,0 +1,27 @@
+package com.github.manevolent.ts3j;
+
+import com.github.manevolent.ts3j.protocol.TS3DNS;
+import com.github.manevolent.ts3j.util.Ts3Debugging;
+import junit.framework.TestCase;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.List;
+
+public class TS3DNSTest extends TestCase {
+
+ public static void main(String[] args) throws Exception {
+ new TS3DNSTest().testParser();
+ }
+
+ public void testParser() throws Exception {
+ Ts3Debugging.setEnabled(true);
+
+ List soaddr = TS3DNS.lookup("voice.teamspeak.com");
+
+ assertTrue(soaddr.size() > 0);
+
+ for (SocketAddress socketAddress : soaddr)
+ Ts3Debugging.debug(socketAddress);
+ }
+}