Run a connection's requests on its own pool
Each action started a thread that read the socket field once it ran; a disconnect in between cleared it, and the user saw "Could not join channel: NullPointerException". Actions now capture the socket when they are issued and run on a pool that lives as long as the connection. What fails because the connection went away is no longer reported as an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
@@ -102,6 +103,8 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
* has already sent has been handled. Closed together with the socket.
|
* has already sent has been handled. Closed together with the socket.
|
||||||
*/
|
*/
|
||||||
private ExecutorService eventExecutor;
|
private ExecutorService eventExecutor;
|
||||||
|
/** Runs this connection's requests to the server; created on connect, shut down with the socket. */
|
||||||
|
private volatile ExecutorService actions;
|
||||||
|
|
||||||
private volatile boolean connected;
|
private volatile boolean connected;
|
||||||
/**
|
/**
|
||||||
@@ -211,7 +214,6 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
private void pushInputHardware() {
|
private void pushInputHardware() {
|
||||||
boolean hardware = microphoneActive;
|
boolean hardware = microphoneActive;
|
||||||
updateSelf(self -> self.inputHardware = hardware);
|
updateSelf(self -> self.inputHardware = hardware);
|
||||||
if (client == null || !connected) return;
|
|
||||||
selfUpdate(cmd ->
|
selfUpdate(cmd ->
|
||||||
cmd.add(new CommandSingleParameter("client_input_hardware", hardware ? "1" : "0")),
|
cmd.add(new CommandSingleParameter("client_input_hardware", hardware ? "1" : "0")),
|
||||||
"Microphone hardware status update failed");
|
"Microphone hardware status update failed");
|
||||||
@@ -283,6 +285,11 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
return t;
|
return t;
|
||||||
});
|
});
|
||||||
client.setCommandExecutorService(eventExecutor);
|
client.setCommandExecutorService(eventExecutor);
|
||||||
|
actions = Executors.newCachedThreadPool(r -> {
|
||||||
|
Thread t = new Thread(r, "ts3j-action");
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
});
|
||||||
client.setIdentity(identity);
|
client.setIdentity(identity);
|
||||||
client.setNickname(nickname);
|
client.setNickname(nickname);
|
||||||
client.setHWID("ts3jswing-" + Integer.toHexString(nickname.hashCode()));
|
client.setHWID("ts3jswing-" + Integer.toHexString(nickname.hashCode()));
|
||||||
@@ -520,7 +527,9 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
LocalTeamspeakClientSocket sock = client;
|
LocalTeamspeakClientSocket sock = client;
|
||||||
FileTransferManager ft = fileTransfers;
|
FileTransferManager ft = fileTransfers;
|
||||||
ExecutorService events = eventExecutor;
|
ExecutorService events = eventExecutor;
|
||||||
|
ExecutorService pool = actions;
|
||||||
eventExecutor = null;
|
eventExecutor = null;
|
||||||
|
actions = null;
|
||||||
microphone = null;
|
microphone = null;
|
||||||
playback = null;
|
playback = null;
|
||||||
client = null;
|
client = null;
|
||||||
@@ -555,6 +564,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (events != null) events.shutdownNow();
|
if (events != null) events.shutdownNow();
|
||||||
|
if (pool != null) pool.shutdownNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- self actions ----
|
// ---- self actions ----
|
||||||
@@ -594,7 +604,6 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
// Publish the input/output muted flags to the server so others see them. These
|
// Publish the input/output muted flags to the server so others see them. These
|
||||||
// are runtime status, not editable client-database properties, so they go out
|
// are runtime status, not editable client-database properties, so they go out
|
||||||
// through clientupdate (like nickname/away), not clientedit.
|
// through clientupdate (like nickname/away), not clientedit.
|
||||||
if (client == null || !connected) return;
|
|
||||||
selfUpdate(cmd -> {
|
selfUpdate(cmd -> {
|
||||||
cmd.add(new CommandSingleParameter("client_input_muted", micMuted ? "1" : "0"));
|
cmd.add(new CommandSingleParameter("client_input_muted", micMuted ? "1" : "0"));
|
||||||
cmd.add(new CommandSingleParameter("client_output_muted", deaf ? "1" : "0"));
|
cmd.add(new CommandSingleParameter("client_output_muted", deaf ? "1" : "0"));
|
||||||
@@ -602,25 +611,14 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void joinChannel(int channelId, String password) {
|
public void joinChannel(int channelId, String password) {
|
||||||
new Thread(() -> {
|
act("Could not join channel: ", sock ->
|
||||||
try {
|
sock.joinChannel(channelId, (password == null || password.isEmpty()) ? null : password));
|
||||||
client.joinChannel(channelId, (password == null || password.isEmpty()) ? null : password);
|
|
||||||
} catch (Exception e) {
|
|
||||||
error("Could not join channel: " + rootMessage(e));
|
|
||||||
}
|
|
||||||
}, "ts3j-join").start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Moves another client (or ourselves) into a channel. */
|
/** Moves another client (or ourselves) into a channel. */
|
||||||
public void moveClient(int clientId, int channelId, String password) {
|
public void moveClient(int clientId, int channelId, String password) {
|
||||||
new Thread(() -> {
|
act("Could not move client: ", sock -> sock.clientMove(clientId, channelId,
|
||||||
try {
|
(password == null || password.isEmpty()) ? null : password));
|
||||||
client.clientMove(clientId, channelId,
|
|
||||||
(password == null || password.isEmpty()) ? null : password);
|
|
||||||
} catch (Exception e) {
|
|
||||||
error("Could not move client: " + rootMessage(e));
|
|
||||||
}
|
|
||||||
}, "ts3j-move-client").start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Kicks a client out of its channel, back into the server's default one. */
|
/** Kicks a client out of its channel, back into the server's default one. */
|
||||||
@@ -634,19 +632,15 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void kick(int clientId, int reasonId, String reason, String errorPrefix) {
|
private void kick(int clientId, int reasonId, String reason, String errorPrefix) {
|
||||||
new Thread(() -> {
|
act(errorPrefix, sock -> {
|
||||||
try {
|
SingleCommand cmd = new SingleCommand("clientkick", ProtocolRole.CLIENT);
|
||||||
SingleCommand cmd = new SingleCommand("clientkick", ProtocolRole.CLIENT);
|
cmd.add(new CommandSingleParameter("clid", Integer.toString(clientId)));
|
||||||
cmd.add(new CommandSingleParameter("clid", Integer.toString(clientId)));
|
cmd.add(new CommandSingleParameter("reasonid", Integer.toString(reasonId)));
|
||||||
cmd.add(new CommandSingleParameter("reasonid", Integer.toString(reasonId)));
|
if (reason != null && !reason.isEmpty()) {
|
||||||
if (reason != null && !reason.isEmpty()) {
|
cmd.add(new CommandSingleParameter("reasonmsg", reason));
|
||||||
cmd.add(new CommandSingleParameter("reasonmsg", reason));
|
|
||||||
}
|
|
||||||
client.executeCommand(cmd).complete();
|
|
||||||
} catch (Exception e) {
|
|
||||||
error(errorPrefix + rootMessage(e));
|
|
||||||
}
|
}
|
||||||
}, "ts3j-kick").start();
|
sock.executeCommand(cmd).complete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -655,14 +649,9 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
* @param seconds how long the ban lasts, or 0 for a permanent one
|
* @param seconds how long the ban lasts, or 0 for a permanent one
|
||||||
*/
|
*/
|
||||||
public void banClient(int clientId, long seconds, String reason) {
|
public void banClient(int clientId, long seconds, String reason) {
|
||||||
new Thread(() -> {
|
act("Could not ban client: ", sock -> sock.banClient(clientId,
|
||||||
try {
|
seconds <= 0 ? null : (int) Math.min(seconds, Integer.MAX_VALUE),
|
||||||
client.banClient(clientId, seconds <= 0 ? null : (int) Math.min(seconds, Integer.MAX_VALUE),
|
reason == null || reason.isEmpty() ? null : reason));
|
||||||
reason == null || reason.isEmpty() ? null : reason);
|
|
||||||
} catch (Exception e) {
|
|
||||||
error("Could not ban client: " + rootMessage(e));
|
|
||||||
}
|
|
||||||
}, "ts3j-ban").start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -676,24 +665,20 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
// The server refuses channelmove within the same parent ("already member of
|
// The server refuses channelmove within the same parent ("already member of
|
||||||
// channel"); sorting among the same siblings is an edit of channel_order.
|
// channel"); sorting among the same siblings is an edit of channel_order.
|
||||||
boolean reorder = channel != null && channel.parentId == newParentId;
|
boolean reorder = channel != null && channel.parentId == newParentId;
|
||||||
new Thread(() -> {
|
act("Could not move channel: ", sock -> {
|
||||||
try {
|
SingleCommand cmd;
|
||||||
SingleCommand cmd;
|
if (reorder) {
|
||||||
if (reorder) {
|
cmd = new SingleCommand("channeledit", ProtocolRole.CLIENT);
|
||||||
cmd = new SingleCommand("channeledit", ProtocolRole.CLIENT);
|
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
||||||
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
cmd.add(new CommandSingleParameter("channel_order", Integer.toString(orderPredecessorId)));
|
||||||
cmd.add(new CommandSingleParameter("channel_order", Integer.toString(orderPredecessorId)));
|
} else {
|
||||||
} else {
|
cmd = new SingleCommand("channelmove", ProtocolRole.CLIENT);
|
||||||
cmd = new SingleCommand("channelmove", ProtocolRole.CLIENT);
|
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
||||||
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
cmd.add(new CommandSingleParameter("cpid", Integer.toString(newParentId)));
|
||||||
cmd.add(new CommandSingleParameter("cpid", Integer.toString(newParentId)));
|
cmd.add(new CommandSingleParameter("order", Integer.toString(orderPredecessorId)));
|
||||||
cmd.add(new CommandSingleParameter("order", Integer.toString(orderPredecessorId)));
|
|
||||||
}
|
|
||||||
client.executeCommand(cmd).complete();
|
|
||||||
} catch (Exception e) {
|
|
||||||
error("Could not move channel: " + rootMessage(e));
|
|
||||||
}
|
}
|
||||||
}, "ts3j-move-channel").start();
|
sock.executeCommand(cmd).complete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -705,40 +690,27 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
* won't be known yet.
|
* won't be known yet.
|
||||||
*/
|
*/
|
||||||
private void requestPermissionNames() {
|
private void requestPermissionNames() {
|
||||||
new Thread(() -> {
|
// Best-effort: the menus fall back to treating unresolved permissions as 0.
|
||||||
try {
|
attempt(sock -> sock.executeCommand(new SingleCommand("permissionlist", ProtocolRole.CLIENT)).complete());
|
||||||
client.executeCommand(new SingleCommand("permissionlist", ProtocolRole.CLIENT)).complete();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// Best-effort: the menus fall back to treating unresolved permissions as 0.
|
|
||||||
}
|
|
||||||
}, "ts3j-permission-list").start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Assigns or removes a server group for a client (by database id, as {@code servergroupaddclient} needs). */
|
/** Assigns or removes a server group for a client (by database id, as {@code servergroupaddclient} needs). */
|
||||||
public void setClientServerGroup(int clientDatabaseId, int groupId, boolean assign) {
|
public void setClientServerGroup(int clientDatabaseId, int groupId, boolean assign) {
|
||||||
new Thread(() -> {
|
act("Could not " + (assign ? "assign" : "remove") + " server group: ", sock -> {
|
||||||
try {
|
if (assign) sock.serverGroupAddClient(groupId, clientDatabaseId);
|
||||||
if (assign) client.serverGroupAddClient(groupId, clientDatabaseId);
|
else sock.serverGroupRemoveClient(groupId, clientDatabaseId);
|
||||||
else client.serverGroupRemoveClient(groupId, clientDatabaseId);
|
});
|
||||||
} catch (Exception e) {
|
|
||||||
error("Could not " + (assign ? "assign" : "remove") + " server group: " + rootMessage(e));
|
|
||||||
}
|
|
||||||
}, "ts3j-server-group").start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Assigns a channel group for a client in the channel it currently sits in. */
|
/** Assigns a channel group for a client in the channel it currently sits in. */
|
||||||
public void setClientChannelGroup(int clientDatabaseId, int channelId, int groupId) {
|
public void setClientChannelGroup(int clientDatabaseId, int channelId, int groupId) {
|
||||||
new Thread(() -> {
|
act("Could not set channel group: ", sock -> {
|
||||||
try {
|
SingleCommand cmd = new SingleCommand("setclientchannelgroup", ProtocolRole.CLIENT);
|
||||||
SingleCommand cmd = new SingleCommand("setclientchannelgroup", ProtocolRole.CLIENT);
|
cmd.add(new CommandSingleParameter("cgid", Integer.toString(groupId)));
|
||||||
cmd.add(new CommandSingleParameter("cgid", Integer.toString(groupId)));
|
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
||||||
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
cmd.add(new CommandSingleParameter("cldbid", Integer.toString(clientDatabaseId)));
|
||||||
cmd.add(new CommandSingleParameter("cldbid", Integer.toString(clientDatabaseId)));
|
sock.executeCommand(cmd).complete();
|
||||||
client.executeCommand(cmd).complete();
|
});
|
||||||
} catch (Exception e) {
|
|
||||||
error("Could not set channel group: " + rootMessage(e));
|
|
||||||
}
|
|
||||||
}, "ts3j-channel-group").start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -749,76 +721,56 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
if (channelIds.isEmpty()) return;
|
if (channelIds.isEmpty()) return;
|
||||||
List<Integer> ids = List.copyOf(channelIds);
|
List<Integer> ids = List.copyOf(channelIds);
|
||||||
String name = subscribed ? "channelsubscribe" : "channelunsubscribe";
|
String name = subscribed ? "channelsubscribe" : "channelunsubscribe";
|
||||||
new Thread(() -> {
|
act(subscribed ? "Could not subscribe: " : "Could not unsubscribe: ", sock -> {
|
||||||
try {
|
List<SingleCommand> parts = new ArrayList<>(ids.size());
|
||||||
List<SingleCommand> parts = new ArrayList<>(ids.size());
|
for (int id : ids) {
|
||||||
for (int id : ids) {
|
parts.add(new SingleCommand(name, ProtocolRole.CLIENT,
|
||||||
parts.add(new SingleCommand(name, ProtocolRole.CLIENT,
|
new CommandSingleParameter("cid", Integer.toString(id))));
|
||||||
new CommandSingleParameter("cid", Integer.toString(id))));
|
|
||||||
}
|
|
||||||
client.executeCommand(new MultiCommand(name, ProtocolRole.CLIENT, parts)).complete();
|
|
||||||
} catch (Exception e) {
|
|
||||||
error((subscribed ? "Could not subscribe: " : "Could not unsubscribe: ") + rootMessage(e));
|
|
||||||
}
|
}
|
||||||
}, "ts3j-channel-subscribe").start();
|
sock.executeCommand(new MultiCommand(name, ProtocolRole.CLIENT, parts)).complete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendChannelMessage(String text) {
|
public void sendChannelMessage(String text) {
|
||||||
new Thread(() -> {
|
act("Message failed: ", sock -> {
|
||||||
try {
|
ClientEntry self = model.getClient(selfClientId);
|
||||||
ClientEntry self = model.getClient(selfClientId);
|
int cid = self != null ? self.channelId : 0;
|
||||||
int cid = self != null ? self.channelId : 0;
|
sock.sendChannelMessage(cid, text);
|
||||||
client.sendChannelMessage(cid, text);
|
persistOutgoingChat(ConnectionListener.ChatScope.CHANNEL, 0, "", text);
|
||||||
persistOutgoingChat(ConnectionListener.ChatScope.CHANNEL, 0, "", text);
|
sound(SoundEvent.CHAT_SENT_MESSAGE_CHANNEL, channelVars(cid, null));
|
||||||
sound(SoundEvent.CHAT_SENT_MESSAGE_CHANNEL, channelVars(cid, null));
|
});
|
||||||
} catch (Exception e) {
|
|
||||||
error("Message failed: " + rootMessage(e));
|
|
||||||
}
|
|
||||||
}, "ts3j-chan-msg").start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendServerMessage(String text) {
|
public void sendServerMessage(String text) {
|
||||||
new Thread(() -> {
|
act("Message failed: ", sock -> {
|
||||||
try {
|
sock.sendServerMessage(text);
|
||||||
client.sendServerMessage(text);
|
persistOutgoingChat(ConnectionListener.ChatScope.SERVER, 0, "", text);
|
||||||
persistOutgoingChat(ConnectionListener.ChatScope.SERVER, 0, "", text);
|
sound(SoundEvent.CHAT_SENT_MESSAGE_SERVER, serverVars());
|
||||||
sound(SoundEvent.CHAT_SENT_MESSAGE_SERVER, serverVars());
|
});
|
||||||
} catch (Exception e) {
|
|
||||||
error("Message failed: " + rootMessage(e));
|
|
||||||
}
|
|
||||||
}, "ts3j-srv-msg").start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendPrivateMessage(int clientId, String text) {
|
public void sendPrivateMessage(int clientId, String text) {
|
||||||
new Thread(() -> {
|
act("Message failed: ", sock -> {
|
||||||
try {
|
sock.sendPrivateMessage(clientId, text);
|
||||||
client.sendPrivateMessage(clientId, text);
|
ClientEntry peer = model.getClient(clientId);
|
||||||
ClientEntry peer = model.getClient(clientId);
|
String peerUid = peer != null ? peer.uniqueId : "";
|
||||||
String peerUid = peer != null ? peer.uniqueId : "";
|
if (peerUid == null || peerUid.isBlank()) {
|
||||||
if (peerUid == null || peerUid.isBlank()) {
|
try {
|
||||||
try {
|
Client info = sock.getClientInfo(clientId);
|
||||||
Client info = client.getClientInfo(clientId);
|
if (info != null) peerUid = info.getUniqueIdentifier();
|
||||||
if (info != null) peerUid = info.getUniqueIdentifier();
|
} catch (Exception ignored) {
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
persistOutgoingChat(ConnectionListener.ChatScope.PRIVATE, clientId, peerUid, text);
|
|
||||||
sound(SoundEvent.CHAT_SENT_MESSAGE_CLIENT, clientVars(clientId, null));
|
|
||||||
} catch (Exception e) {
|
|
||||||
error("Message failed: " + rootMessage(e));
|
|
||||||
}
|
}
|
||||||
}, "ts3j-pm").start();
|
persistOutgoingChat(ConnectionListener.ChatScope.PRIVATE, clientId, peerUid, text);
|
||||||
|
sound(SoundEvent.CHAT_SENT_MESSAGE_CLIENT, clientVars(clientId, null));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void poke(int clientId, String message) {
|
public void poke(int clientId, String message) {
|
||||||
new Thread(() -> {
|
act("Poke failed: ", sock -> {
|
||||||
try {
|
sock.clientPoke(clientId, message);
|
||||||
client.clientPoke(clientId, message);
|
sound(SoundEvent.OTHER_SENT_POKE, clientVars(clientId, null));
|
||||||
sound(SoundEvent.OTHER_SENT_POKE, clientVars(clientId, null));
|
});
|
||||||
} catch (Exception e) {
|
|
||||||
error("Poke failed: " + rootMessage(e));
|
|
||||||
}
|
|
||||||
}, "ts3j-poke").start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAway(boolean away, String message) {
|
public void setAway(boolean away, String message) {
|
||||||
@@ -852,17 +804,13 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
ui.onModelChanged();
|
ui.onModelChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sends a {@code clientupdate} for the local client on a background thread. */
|
/** Sends a {@code clientupdate} for the local client in the background. */
|
||||||
private void selfUpdate(java.util.function.Consumer<SingleCommand> fill, String errorLabel) {
|
private void selfUpdate(java.util.function.Consumer<SingleCommand> fill, String errorLabel) {
|
||||||
new Thread(() -> {
|
act(errorLabel + ": ", sock -> {
|
||||||
try {
|
SingleCommand cmd = new SingleCommand("clientupdate", ProtocolRole.CLIENT);
|
||||||
SingleCommand cmd = new SingleCommand("clientupdate", ProtocolRole.CLIENT);
|
fill.accept(cmd);
|
||||||
fill.accept(cmd);
|
sock.executeCommand(cmd).complete();
|
||||||
client.executeCommand(cmd).complete();
|
});
|
||||||
} catch (Exception e) {
|
|
||||||
error(errorLabel + ": " + rootMessage(e));
|
|
||||||
}
|
|
||||||
}, "ts3j-selfupdate").start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public double getPingMillis() {
|
public double getPingMillis() {
|
||||||
@@ -874,13 +822,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setNickname(String nickname) {
|
public void setNickname(String nickname) {
|
||||||
new Thread(() -> {
|
act("Rename failed: ", sock -> sock.setNickname(nickname));
|
||||||
try {
|
|
||||||
client.setNickname(nickname);
|
|
||||||
} catch (Exception e) {
|
|
||||||
error("Rename failed: " + rootMessage(e));
|
|
||||||
}
|
|
||||||
}, "ts3j-rename").start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -894,7 +836,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
* {@link ConnectionEventHandler#onChannelEdit}.
|
* {@link ConnectionEventHandler#onChannelEdit}.
|
||||||
*/
|
*/
|
||||||
public void requestChannelInfo(int channelId) {
|
public void requestChannelInfo(int channelId) {
|
||||||
requestInfo("channelgetdescription", "cid", channelId, "ts3j-channelinfo");
|
requestInfo("channelgetdescription", "cid", channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -902,18 +844,13 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
* lacks. The answer arrives as a client update, see {@link ConnectionEventHandler#onClientChanged}.
|
* lacks. The answer arrives as a client update, see {@link ConnectionEventHandler#onClientChanged}.
|
||||||
*/
|
*/
|
||||||
public void requestClientInfo(int clientId) {
|
public void requestClientInfo(int clientId) {
|
||||||
requestInfo("clientgetvariables", "clid", clientId, "ts3j-clientinfo");
|
requestInfo("clientgetvariables", "clid", clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestInfo(String command, String idParameter, int id, String threadName) {
|
/** May be refused for lack of permission; the details then stay as they are. */
|
||||||
new Thread(() -> {
|
private void requestInfo(String command, String idParameter, int id) {
|
||||||
try {
|
attempt(sock -> sock.executeCommand(new SingleCommand(command, ProtocolRole.CLIENT,
|
||||||
client.executeCommand(new SingleCommand(command, ProtocolRole.CLIENT,
|
new CommandSingleParameter(idParameter, Integer.toString(id)))).complete());
|
||||||
new CommandSingleParameter(idParameter, Integer.toString(id)))).complete();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// may be permission-restricted; leave as-is
|
|
||||||
}
|
|
||||||
}, threadName).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -921,8 +858,8 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
* live text instead of whatever an earlier info request cached.
|
* live text instead of whatever an earlier info request cached.
|
||||||
*/
|
*/
|
||||||
public void requestClientDescription(int clientId, BiConsumer<String, String> callback) {
|
public void requestClientDescription(int clientId, BiConsumer<String, String> callback) {
|
||||||
run("ts3j-client-description", callback, () -> {
|
run(callback, () -> {
|
||||||
Client c = client.getClientInfo(clientId);
|
Client c = socket().getClientInfo(clientId);
|
||||||
String description = c == null ? "" : orEmpty(c.get("client_description"));
|
String description = c == null ? "" : orEmpty(c.get("client_description"));
|
||||||
ClientEntry e = model.getClient(clientId);
|
ClientEntry e = model.getClient(clientId);
|
||||||
if (e != null) e.description = description;
|
if (e != null) e.description = description;
|
||||||
@@ -939,11 +876,11 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
*/
|
*/
|
||||||
public void setClientDescription(int clientId, String description, Consumer<String> callback) {
|
public void setClientDescription(int clientId, String description, Consumer<String> callback) {
|
||||||
boolean self = clientId == selfClientId;
|
boolean self = clientId == selfClientId;
|
||||||
run("ts3j-client-description-set", (ignored, error) -> callback.accept(error), () -> {
|
run((ignored, error) -> callback.accept(error), () -> {
|
||||||
SingleCommand cmd = new SingleCommand("clientedit", ProtocolRole.CLIENT);
|
SingleCommand cmd = new SingleCommand("clientedit", ProtocolRole.CLIENT);
|
||||||
cmd.add(new CommandSingleParameter("clid", Integer.toString(clientId)));
|
cmd.add(new CommandSingleParameter("clid", Integer.toString(clientId)));
|
||||||
cmd.add(new CommandSingleParameter("client_description", description));
|
cmd.add(new CommandSingleParameter("client_description", description));
|
||||||
client.executeCommand(cmd).complete();
|
socket().executeCommand(cmd).complete();
|
||||||
// Only the other clients are told about our own update, so the local entry
|
// Only the other clients are told about our own update, so the local entry
|
||||||
// (and the info panel showing it) has to be caught up here.
|
// (and the info panel showing it) has to be caught up here.
|
||||||
if (self) {
|
if (self) {
|
||||||
@@ -959,12 +896,12 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
|
|
||||||
/** Reads the server's ban list, delivering it (or a failure message) off the UI thread. */
|
/** Reads the server's ban list, delivering it (or a failure message) off the UI thread. */
|
||||||
public void requestBanList(BiConsumer<List<BanEntry>, String> callback) {
|
public void requestBanList(BiConsumer<List<BanEntry>, String> callback) {
|
||||||
run("ts3j-ban-list", callback, bans::list);
|
run(callback, bans::list);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param callback given {@code null} on success, or the failure message */
|
/** @param callback given {@code null} on success, or the failure message */
|
||||||
public void addBan(BanRule rule, Consumer<String> callback) {
|
public void addBan(BanRule rule, Consumer<String> callback) {
|
||||||
run("ts3j-ban-add", (ignored, error) -> callback.accept(error), () -> {
|
run((ignored, error) -> callback.accept(error), () -> {
|
||||||
bans.add(rule);
|
bans.add(rule);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
@@ -976,7 +913,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
* @param callback given {@code null} on success, or the failure message
|
* @param callback given {@code null} on success, or the failure message
|
||||||
*/
|
*/
|
||||||
public void replaceBan(int banId, BanRule rule, Consumer<String> callback) {
|
public void replaceBan(int banId, BanRule rule, Consumer<String> callback) {
|
||||||
run("ts3j-ban-edit", (ignored, error) -> callback.accept(error), () -> {
|
run((ignored, error) -> callback.accept(error), () -> {
|
||||||
bans.replace(banId, rule);
|
bans.replace(banId, rule);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
@@ -988,7 +925,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
* @param callback given {@code null} on success, or the failure message
|
* @param callback given {@code null} on success, or the failure message
|
||||||
*/
|
*/
|
||||||
public void deleteBans(Collection<Integer> banIds, Consumer<String> callback) {
|
public void deleteBans(Collection<Integer> banIds, Consumer<String> callback) {
|
||||||
run("ts3j-ban-delete", (ignored, error) -> callback.accept(error), () -> {
|
run((ignored, error) -> callback.accept(error), () -> {
|
||||||
for (int id : banIds) bans.delete(id);
|
for (int id : banIds) bans.delete(id);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
@@ -1082,7 +1019,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
* {@code callback} off the UI thread.
|
* {@code callback} off the UI thread.
|
||||||
*/
|
*/
|
||||||
public void requestChannelSettings(int channelId, BiConsumer<ChannelSettings, String> callback) {
|
public void requestChannelSettings(int channelId, BiConsumer<ChannelSettings, String> callback) {
|
||||||
run("ts3j-channel-read", callback, () -> channels.readSettings(channelId));
|
run(callback, () -> channels.readSettings(channelId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1091,7 +1028,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
*/
|
*/
|
||||||
public void requestChannelPermissions(int channelId,
|
public void requestChannelPermissions(int channelId,
|
||||||
BiConsumer<Map<String, Integer>, String> callback) {
|
BiConsumer<Map<String, Integer>, String> callback) {
|
||||||
run("ts3j-channel-perms", callback, () -> channels.readPermissions(channelId));
|
run(callback, () -> channels.readPermissions(channelId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1104,7 +1041,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
Map<String, Integer> setPermissions,
|
Map<String, Integer> setPermissions,
|
||||||
Collection<String> removePermissions,
|
Collection<String> removePermissions,
|
||||||
Consumer<String> callback) {
|
Consumer<String> callback) {
|
||||||
run("ts3j-channel-edit", (ignored, error) -> callback.accept(error), () -> {
|
run((ignored, error) -> callback.accept(error), () -> {
|
||||||
channels.edit(channelId, changes);
|
channels.edit(channelId, changes);
|
||||||
channels.writePermissions(channelId, setPermissions, removePermissions);
|
channels.writePermissions(channelId, setPermissions, removePermissions);
|
||||||
if (changes.containsKey("channel_description")) {
|
if (changes.containsKey("channel_description")) {
|
||||||
@@ -1130,7 +1067,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
public void createChannel(int parentId, Map<String, String> properties,
|
public void createChannel(int parentId, Map<String, String> properties,
|
||||||
Map<String, Integer> permissions,
|
Map<String, Integer> permissions,
|
||||||
BiConsumer<Integer, String> callback) {
|
BiConsumer<Integer, String> callback) {
|
||||||
run("ts3j-channel-create", callback, () -> {
|
run(callback, () -> {
|
||||||
int channelId = channels.create(parentId, properties);
|
int channelId = channels.create(parentId, properties);
|
||||||
try {
|
try {
|
||||||
channels.writePermissions(channelId, permissions, List.of());
|
channels.writePermissions(channelId, permissions, List.of());
|
||||||
@@ -1144,43 +1081,82 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
|
|
||||||
/** The ids of the icons uploaded to this virtual server. */
|
/** The ids of the icons uploaded to this virtual server. */
|
||||||
public void requestServerIcons(BiConsumer<List<Long>, String> callback) {
|
public void requestServerIcons(BiConsumer<List<Long>, String> callback) {
|
||||||
run("ts3j-icon-list", callback, channels::listIcons);
|
run(callback, channels::listIcons);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Uploads an image as a server icon and reports the new icon's id. */
|
/** Uploads an image as a server icon and reports the new icon's id. */
|
||||||
public void uploadIcon(File source, BiConsumer<Long, String> callback) {
|
public void uploadIcon(File source, BiConsumer<Long, String> callback) {
|
||||||
run("ts3j-icon-upload", callback, () -> channels.uploadIcon(source));
|
run(callback, () -> channels.uploadIcon(source));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Deletes a server icon; {@code callback} is given {@code null} on success. */
|
/** Deletes a server icon; {@code callback} is given {@code null} on success. */
|
||||||
public void deleteIcon(long iconId, Consumer<String> callback) {
|
public void deleteIcon(long iconId, Consumer<String> callback) {
|
||||||
run("ts3j-icon-delete", (ignored, error) -> callback.accept(error), () -> {
|
run((ignored, error) -> callback.accept(error), () -> {
|
||||||
channels.deleteIcon(iconId);
|
channels.deleteIcon(iconId);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Work that produces a value or an error message, run on its own thread. */
|
/** A request made on the socket of the connection it was issued on. */
|
||||||
|
private interface Action {
|
||||||
|
void run(LocalTeamspeakClientSocket sock) throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Work that produces a value or an error message. */
|
||||||
private interface Job<T> {
|
private interface Job<T> {
|
||||||
T call() throws Exception;
|
T call() throws Exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sends a request in the background, reporting a failure as {@code errorPrefix} and the reason. */
|
||||||
|
private void act(String errorPrefix, Action action) {
|
||||||
|
submit(action, e -> error(errorPrefix + rootMessage(e)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sends a request in the background whose failure nobody needs to hear about. */
|
||||||
|
private void attempt(Action action) {
|
||||||
|
submit(action, e -> {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void submit(Action action, Consumer<Exception> onFailure) {
|
||||||
|
LocalTeamspeakClientSocket sock = client;
|
||||||
|
ExecutorService pool = actions;
|
||||||
|
if (sock == null || pool == null || !connected) return;
|
||||||
|
try {
|
||||||
|
pool.execute(() -> {
|
||||||
|
try {
|
||||||
|
action.run(sock);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Leaving fails whatever was still in flight; that is no error to report.
|
||||||
|
if (client == sock && connected) onFailure.accept(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (RejectedExecutionException ignored) {
|
||||||
|
// disconnected in the meantime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs a blocking server request in the background and hands the result — or the
|
* Runs a blocking server request in the background and hands the result — or the
|
||||||
* failure message — to {@code callback}, which therefore never runs on the UI thread.
|
* failure message — to {@code callback}, which therefore never runs on the UI thread.
|
||||||
*/
|
*/
|
||||||
private <T> void run(String threadName, BiConsumer<T, String> callback, Job<T> job) {
|
private <T> void run(BiConsumer<T, String> callback, Job<T> job) {
|
||||||
new Thread(() -> {
|
ExecutorService pool = actions;
|
||||||
if (!connected || client == null) {
|
if (pool == null || client == null || !connected) {
|
||||||
callback.accept(null, "Not connected");
|
callback.accept(null, "Not connected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
callback.accept(job.call(), null);
|
pool.execute(() -> {
|
||||||
} catch (Exception e) {
|
try {
|
||||||
callback.accept(null, rootMessage(e));
|
callback.accept(job.call(), null);
|
||||||
}
|
} catch (Exception e) {
|
||||||
}, threadName).start();
|
callback.accept(null, connected ? rootMessage(e) : "Not connected");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (RejectedExecutionException e) {
|
||||||
|
callback.accept(null, "Not connected");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The live socket. Package-private for {@link ChannelAdmin}. */
|
/** The live socket. Package-private for {@link ChannelAdmin}. */
|
||||||
@@ -1248,7 +1224,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
* @param callback given {@code null} on success, or the failure message
|
* @param callback given {@code null} on success, or the failure message
|
||||||
*/
|
*/
|
||||||
public void setAvatar(File source, Consumer<String> callback) {
|
public void setAvatar(File source, Consumer<String> callback) {
|
||||||
run("ts3j-avatar-upload", (flag, error) -> {
|
run((flag, error) -> {
|
||||||
if (error == null) updateSelf(self -> self.avatarFlag = flag);
|
if (error == null) updateSelf(self -> self.avatarFlag = flag);
|
||||||
callback.accept(error);
|
callback.accept(error);
|
||||||
}, () -> avatarAdmin.upload(source));
|
}, () -> avatarAdmin.upload(source));
|
||||||
@@ -1256,7 +1232,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
|
|
||||||
/** Withdraws our avatar; {@code callback} is given {@code null} on success. */
|
/** Withdraws our avatar; {@code callback} is given {@code null} on success. */
|
||||||
public void removeAvatar(Consumer<String> callback) {
|
public void removeAvatar(Consumer<String> callback) {
|
||||||
run("ts3j-avatar-remove", (ignored, error) -> {
|
run((ignored, error) -> {
|
||||||
if (error == null) updateSelf(self -> self.avatarFlag = "");
|
if (error == null) updateSelf(self -> self.avatarFlag = "");
|
||||||
callback.accept(error);
|
callback.accept(error);
|
||||||
}, () -> {
|
}, () -> {
|
||||||
|
|||||||
Reference in New Issue
Block a user