diff --git a/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java b/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java index 9561538..eec84bc 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java @@ -40,6 +40,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; 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. */ 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; /** @@ -211,7 +214,6 @@ public final class TeamspeakConnection implements TS3Listener { private void pushInputHardware() { boolean hardware = microphoneActive; updateSelf(self -> self.inputHardware = hardware); - if (client == null || !connected) return; selfUpdate(cmd -> cmd.add(new CommandSingleParameter("client_input_hardware", hardware ? "1" : "0")), "Microphone hardware status update failed"); @@ -283,6 +285,11 @@ public final class TeamspeakConnection implements TS3Listener { return t; }); client.setCommandExecutorService(eventExecutor); + actions = Executors.newCachedThreadPool(r -> { + Thread t = new Thread(r, "ts3j-action"); + t.setDaemon(true); + return t; + }); client.setIdentity(identity); client.setNickname(nickname); client.setHWID("ts3jswing-" + Integer.toHexString(nickname.hashCode())); @@ -520,7 +527,9 @@ public final class TeamspeakConnection implements TS3Listener { LocalTeamspeakClientSocket sock = client; FileTransferManager ft = fileTransfers; ExecutorService events = eventExecutor; + ExecutorService pool = actions; eventExecutor = null; + actions = null; microphone = null; playback = null; client = null; @@ -555,6 +564,7 @@ public final class TeamspeakConnection implements TS3Listener { } } if (events != null) events.shutdownNow(); + if (pool != null) pool.shutdownNow(); } // ---- 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 // are runtime status, not editable client-database properties, so they go out // through clientupdate (like nickname/away), not clientedit. - if (client == null || !connected) return; selfUpdate(cmd -> { cmd.add(new CommandSingleParameter("client_input_muted", micMuted ? "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) { - new Thread(() -> { - try { - client.joinChannel(channelId, (password == null || password.isEmpty()) ? null : password); - } catch (Exception e) { - error("Could not join channel: " + rootMessage(e)); - } - }, "ts3j-join").start(); + act("Could not join channel: ", sock -> + sock.joinChannel(channelId, (password == null || password.isEmpty()) ? null : password)); } /** Moves another client (or ourselves) into a channel. */ public void moveClient(int clientId, int channelId, String password) { - new Thread(() -> { - try { - client.clientMove(clientId, channelId, - (password == null || password.isEmpty()) ? null : password); - } catch (Exception e) { - error("Could not move client: " + rootMessage(e)); - } - }, "ts3j-move-client").start(); + act("Could not move client: ", sock -> sock.clientMove(clientId, channelId, + (password == null || password.isEmpty()) ? null : password)); } /** 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) { - new Thread(() -> { - try { - SingleCommand cmd = new SingleCommand("clientkick", ProtocolRole.CLIENT); - cmd.add(new CommandSingleParameter("clid", Integer.toString(clientId))); - cmd.add(new CommandSingleParameter("reasonid", Integer.toString(reasonId))); - if (reason != null && !reason.isEmpty()) { - cmd.add(new CommandSingleParameter("reasonmsg", reason)); - } - client.executeCommand(cmd).complete(); - } catch (Exception e) { - error(errorPrefix + rootMessage(e)); + act(errorPrefix, sock -> { + SingleCommand cmd = new SingleCommand("clientkick", ProtocolRole.CLIENT); + cmd.add(new CommandSingleParameter("clid", Integer.toString(clientId))); + cmd.add(new CommandSingleParameter("reasonid", Integer.toString(reasonId))); + if (reason != null && !reason.isEmpty()) { + cmd.add(new CommandSingleParameter("reasonmsg", reason)); } - }, "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 */ public void banClient(int clientId, long seconds, String reason) { - new Thread(() -> { - try { - client.banClient(clientId, seconds <= 0 ? null : (int) Math.min(seconds, Integer.MAX_VALUE), - reason == null || reason.isEmpty() ? null : reason); - } catch (Exception e) { - error("Could not ban client: " + rootMessage(e)); - } - }, "ts3j-ban").start(); + act("Could not ban client: ", sock -> sock.banClient(clientId, + seconds <= 0 ? null : (int) Math.min(seconds, Integer.MAX_VALUE), + reason == null || reason.isEmpty() ? null : reason)); } /** @@ -676,24 +665,20 @@ public final class TeamspeakConnection implements TS3Listener { // The server refuses channelmove within the same parent ("already member of // channel"); sorting among the same siblings is an edit of channel_order. boolean reorder = channel != null && channel.parentId == newParentId; - new Thread(() -> { - try { - SingleCommand cmd; - if (reorder) { - cmd = new SingleCommand("channeledit", ProtocolRole.CLIENT); - cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId))); - cmd.add(new CommandSingleParameter("channel_order", Integer.toString(orderPredecessorId))); - } else { - cmd = new SingleCommand("channelmove", ProtocolRole.CLIENT); - cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId))); - cmd.add(new CommandSingleParameter("cpid", Integer.toString(newParentId))); - cmd.add(new CommandSingleParameter("order", Integer.toString(orderPredecessorId))); - } - client.executeCommand(cmd).complete(); - } catch (Exception e) { - error("Could not move channel: " + rootMessage(e)); + act("Could not move channel: ", sock -> { + SingleCommand cmd; + if (reorder) { + cmd = new SingleCommand("channeledit", ProtocolRole.CLIENT); + cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId))); + cmd.add(new CommandSingleParameter("channel_order", Integer.toString(orderPredecessorId))); + } else { + cmd = new SingleCommand("channelmove", ProtocolRole.CLIENT); + cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId))); + cmd.add(new CommandSingleParameter("cpid", Integer.toString(newParentId))); + cmd.add(new CommandSingleParameter("order", Integer.toString(orderPredecessorId))); } - }, "ts3j-move-channel").start(); + sock.executeCommand(cmd).complete(); + }); } /** @@ -705,40 +690,27 @@ public final class TeamspeakConnection implements TS3Listener { * won't be known yet. */ private void requestPermissionNames() { - new Thread(() -> { - try { - 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(); + // Best-effort: the menus fall back to treating unresolved permissions as 0. + attempt(sock -> sock.executeCommand(new SingleCommand("permissionlist", ProtocolRole.CLIENT)).complete()); } /** 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) { - new Thread(() -> { - try { - if (assign) client.serverGroupAddClient(groupId, clientDatabaseId); - else client.serverGroupRemoveClient(groupId, clientDatabaseId); - } catch (Exception e) { - error("Could not " + (assign ? "assign" : "remove") + " server group: " + rootMessage(e)); - } - }, "ts3j-server-group").start(); + act("Could not " + (assign ? "assign" : "remove") + " server group: ", sock -> { + if (assign) sock.serverGroupAddClient(groupId, clientDatabaseId); + else sock.serverGroupRemoveClient(groupId, clientDatabaseId); + }); } /** Assigns a channel group for a client in the channel it currently sits in. */ public void setClientChannelGroup(int clientDatabaseId, int channelId, int groupId) { - new Thread(() -> { - try { - SingleCommand cmd = new SingleCommand("setclientchannelgroup", ProtocolRole.CLIENT); - cmd.add(new CommandSingleParameter("cgid", Integer.toString(groupId))); - cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId))); - cmd.add(new CommandSingleParameter("cldbid", Integer.toString(clientDatabaseId))); - client.executeCommand(cmd).complete(); - } catch (Exception e) { - error("Could not set channel group: " + rootMessage(e)); - } - }, "ts3j-channel-group").start(); + act("Could not set channel group: ", sock -> { + SingleCommand cmd = new SingleCommand("setclientchannelgroup", ProtocolRole.CLIENT); + cmd.add(new CommandSingleParameter("cgid", Integer.toString(groupId))); + cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId))); + cmd.add(new CommandSingleParameter("cldbid", Integer.toString(clientDatabaseId))); + sock.executeCommand(cmd).complete(); + }); } /** @@ -749,76 +721,56 @@ public final class TeamspeakConnection implements TS3Listener { if (channelIds.isEmpty()) return; List ids = List.copyOf(channelIds); String name = subscribed ? "channelsubscribe" : "channelunsubscribe"; - new Thread(() -> { - try { - List parts = new ArrayList<>(ids.size()); - for (int id : ids) { - parts.add(new SingleCommand(name, ProtocolRole.CLIENT, - 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)); + act(subscribed ? "Could not subscribe: " : "Could not unsubscribe: ", sock -> { + List parts = new ArrayList<>(ids.size()); + for (int id : ids) { + parts.add(new SingleCommand(name, ProtocolRole.CLIENT, + new CommandSingleParameter("cid", Integer.toString(id)))); } - }, "ts3j-channel-subscribe").start(); + sock.executeCommand(new MultiCommand(name, ProtocolRole.CLIENT, parts)).complete(); + }); } public void sendChannelMessage(String text) { - new Thread(() -> { - try { - ClientEntry self = model.getClient(selfClientId); - int cid = self != null ? self.channelId : 0; - client.sendChannelMessage(cid, text); - persistOutgoingChat(ConnectionListener.ChatScope.CHANNEL, 0, "", text); - sound(SoundEvent.CHAT_SENT_MESSAGE_CHANNEL, channelVars(cid, null)); - } catch (Exception e) { - error("Message failed: " + rootMessage(e)); - } - }, "ts3j-chan-msg").start(); + act("Message failed: ", sock -> { + ClientEntry self = model.getClient(selfClientId); + int cid = self != null ? self.channelId : 0; + sock.sendChannelMessage(cid, text); + persistOutgoingChat(ConnectionListener.ChatScope.CHANNEL, 0, "", text); + sound(SoundEvent.CHAT_SENT_MESSAGE_CHANNEL, channelVars(cid, null)); + }); } public void sendServerMessage(String text) { - new Thread(() -> { - try { - client.sendServerMessage(text); - persistOutgoingChat(ConnectionListener.ChatScope.SERVER, 0, "", text); - sound(SoundEvent.CHAT_SENT_MESSAGE_SERVER, serverVars()); - } catch (Exception e) { - error("Message failed: " + rootMessage(e)); - } - }, "ts3j-srv-msg").start(); + act("Message failed: ", sock -> { + sock.sendServerMessage(text); + persistOutgoingChat(ConnectionListener.ChatScope.SERVER, 0, "", text); + sound(SoundEvent.CHAT_SENT_MESSAGE_SERVER, serverVars()); + }); } public void sendPrivateMessage(int clientId, String text) { - new Thread(() -> { - try { - client.sendPrivateMessage(clientId, text); - ClientEntry peer = model.getClient(clientId); - String peerUid = peer != null ? peer.uniqueId : ""; - if (peerUid == null || peerUid.isBlank()) { - try { - Client info = client.getClientInfo(clientId); - if (info != null) peerUid = info.getUniqueIdentifier(); - } catch (Exception ignored) { - } + act("Message failed: ", sock -> { + sock.sendPrivateMessage(clientId, text); + ClientEntry peer = model.getClient(clientId); + String peerUid = peer != null ? peer.uniqueId : ""; + if (peerUid == null || peerUid.isBlank()) { + try { + Client info = sock.getClientInfo(clientId); + if (info != null) peerUid = info.getUniqueIdentifier(); + } 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) { - new Thread(() -> { - try { - client.clientPoke(clientId, message); - sound(SoundEvent.OTHER_SENT_POKE, clientVars(clientId, null)); - } catch (Exception e) { - error("Poke failed: " + rootMessage(e)); - } - }, "ts3j-poke").start(); + act("Poke failed: ", sock -> { + sock.clientPoke(clientId, message); + sound(SoundEvent.OTHER_SENT_POKE, clientVars(clientId, null)); + }); } public void setAway(boolean away, String message) { @@ -852,17 +804,13 @@ public final class TeamspeakConnection implements TS3Listener { 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 fill, String errorLabel) { - new Thread(() -> { - try { - SingleCommand cmd = new SingleCommand("clientupdate", ProtocolRole.CLIENT); - fill.accept(cmd); - client.executeCommand(cmd).complete(); - } catch (Exception e) { - error(errorLabel + ": " + rootMessage(e)); - } - }, "ts3j-selfupdate").start(); + act(errorLabel + ": ", sock -> { + SingleCommand cmd = new SingleCommand("clientupdate", ProtocolRole.CLIENT); + fill.accept(cmd); + sock.executeCommand(cmd).complete(); + }); } public double getPingMillis() { @@ -874,13 +822,7 @@ public final class TeamspeakConnection implements TS3Listener { } public void setNickname(String nickname) { - new Thread(() -> { - try { - client.setNickname(nickname); - } catch (Exception e) { - error("Rename failed: " + rootMessage(e)); - } - }, "ts3j-rename").start(); + act("Rename failed: ", sock -> sock.setNickname(nickname)); } /* @@ -894,7 +836,7 @@ public final class TeamspeakConnection implements TS3Listener { * {@link ConnectionEventHandler#onChannelEdit}. */ 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}. */ 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) { - new Thread(() -> { - try { - client.executeCommand(new SingleCommand(command, ProtocolRole.CLIENT, - new CommandSingleParameter(idParameter, Integer.toString(id)))).complete(); - } catch (Exception ignored) { - // may be permission-restricted; leave as-is - } - }, threadName).start(); + /** May be refused for lack of permission; the details then stay as they are. */ + private void requestInfo(String command, String idParameter, int id) { + attempt(sock -> sock.executeCommand(new SingleCommand(command, ProtocolRole.CLIENT, + new CommandSingleParameter(idParameter, Integer.toString(id)))).complete()); } /** @@ -921,8 +858,8 @@ public final class TeamspeakConnection implements TS3Listener { * live text instead of whatever an earlier info request cached. */ public void requestClientDescription(int clientId, BiConsumer callback) { - run("ts3j-client-description", callback, () -> { - Client c = client.getClientInfo(clientId); + run(callback, () -> { + Client c = socket().getClientInfo(clientId); String description = c == null ? "" : orEmpty(c.get("client_description")); ClientEntry e = model.getClient(clientId); if (e != null) e.description = description; @@ -939,11 +876,11 @@ public final class TeamspeakConnection implements TS3Listener { */ public void setClientDescription(int clientId, String description, Consumer callback) { 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); cmd.add(new CommandSingleParameter("clid", Integer.toString(clientId))); 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 // (and the info panel showing it) has to be caught up here. 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. */ public void requestBanList(BiConsumer, String> callback) { - run("ts3j-ban-list", callback, bans::list); + run(callback, bans::list); } /** @param callback given {@code null} on success, or the failure message */ public void addBan(BanRule rule, Consumer callback) { - run("ts3j-ban-add", (ignored, error) -> callback.accept(error), () -> { + run((ignored, error) -> callback.accept(error), () -> { bans.add(rule); return null; }); @@ -976,7 +913,7 @@ public final class TeamspeakConnection implements TS3Listener { * @param callback given {@code null} on success, or the failure message */ public void replaceBan(int banId, BanRule rule, Consumer callback) { - run("ts3j-ban-edit", (ignored, error) -> callback.accept(error), () -> { + run((ignored, error) -> callback.accept(error), () -> { bans.replace(banId, rule); return null; }); @@ -988,7 +925,7 @@ public final class TeamspeakConnection implements TS3Listener { * @param callback given {@code null} on success, or the failure message */ public void deleteBans(Collection banIds, Consumer callback) { - run("ts3j-ban-delete", (ignored, error) -> callback.accept(error), () -> { + run((ignored, error) -> callback.accept(error), () -> { for (int id : banIds) bans.delete(id); return null; }); @@ -1082,7 +1019,7 @@ public final class TeamspeakConnection implements TS3Listener { * {@code callback} off the UI thread. */ public void requestChannelSettings(int channelId, BiConsumer 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, BiConsumer, 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 setPermissions, Collection removePermissions, Consumer callback) { - run("ts3j-channel-edit", (ignored, error) -> callback.accept(error), () -> { + run((ignored, error) -> callback.accept(error), () -> { channels.edit(channelId, changes); channels.writePermissions(channelId, setPermissions, removePermissions); if (changes.containsKey("channel_description")) { @@ -1130,7 +1067,7 @@ public final class TeamspeakConnection implements TS3Listener { public void createChannel(int parentId, Map properties, Map permissions, BiConsumer callback) { - run("ts3j-channel-create", callback, () -> { + run(callback, () -> { int channelId = channels.create(parentId, properties); try { 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. */ public void requestServerIcons(BiConsumer, 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. */ public void uploadIcon(File source, BiConsumer 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. */ public void deleteIcon(long iconId, Consumer callback) { - run("ts3j-icon-delete", (ignored, error) -> callback.accept(error), () -> { + run((ignored, error) -> callback.accept(error), () -> { channels.deleteIcon(iconId); 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 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 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 * failure message — to {@code callback}, which therefore never runs on the UI thread. */ - private void run(String threadName, BiConsumer callback, Job job) { - new Thread(() -> { - if (!connected || client == null) { - callback.accept(null, "Not connected"); - return; - } - try { - callback.accept(job.call(), null); - } catch (Exception e) { - callback.accept(null, rootMessage(e)); - } - }, threadName).start(); + private void run(BiConsumer callback, Job job) { + ExecutorService pool = actions; + if (pool == null || client == null || !connected) { + callback.accept(null, "Not connected"); + return; + } + try { + pool.execute(() -> { + try { + callback.accept(job.call(), null); + } catch (Exception e) { + callback.accept(null, connected ? rootMessage(e) : "Not connected"); + } + }); + } catch (RejectedExecutionException e) { + callback.accept(null, "Not connected"); + } } /** 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 */ public void setAvatar(File source, Consumer callback) { - run("ts3j-avatar-upload", (flag, error) -> { + run((flag, error) -> { if (error == null) updateSelf(self -> self.avatarFlag = flag); callback.accept(error); }, () -> avatarAdmin.upload(source)); @@ -1256,7 +1232,7 @@ public final class TeamspeakConnection implements TS3Listener { /** Withdraws our avatar; {@code callback} is given {@code null} on success. */ public void removeAvatar(Consumer callback) { - run("ts3j-avatar-remove", (ignored, error) -> { + run((ignored, error) -> { if (error == null) updateSelf(self -> self.avatarFlag = ""); callback.accept(error); }, () -> {