From bda114d2c0e0ed74fa1fa7036aac14fa271a5408 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Thu, 1 Oct 2026 16:27:41 +0000 Subject: [PATCH] Let admins delete other clients' avatars With b_client_avatar_delete_other, the client's context menu and the menu on their avatar offer "Delete Avatar", as in TeamSpeak 3. The server takes the deletion as ftdeletefile on /avatar_ (not the stored file's name, which it refuses with "convert error") and clears the client's avatar flag for everyone. Our own avatar's file is now deleted the same way, which only such clients are allowed. Co-Authored-By: Claude Opus 5.5 --- .../java/com/ts3client/net/AvatarAdmin.java | 19 ++++++++++++++++++- .../ts3client/net/TeamspeakConnection.java | 8 ++++++++ .../com/ts3client/session/ServerSession.java | 7 +++++++ .../java/com/ts3client/ui/AvatarActions.java | 7 +++++++ .../java/com/ts3client/ui/ClientMenu.java | 5 +++++ .../main/java/com/ts3client/ui/InfoPanel.java | 9 +++++++++ .../main/java/com/ts3client/ui/ServerTab.java | 11 +++++++++++ .../ts3client/ui/ServerTabTreeActions.java | 10 ++++++++++ .../com/ts3client/ui/ServerTreePanel.java | 5 +++++ 9 files changed, 80 insertions(+), 1 deletion(-) diff --git a/ts3-client/core/src/main/java/com/ts3client/net/AvatarAdmin.java b/ts3-client/core/src/main/java/com/ts3client/net/AvatarAdmin.java index d060e6b..88aa9a1 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/AvatarAdmin.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/AvatarAdmin.java @@ -77,12 +77,29 @@ final class AvatarAdmin { void remove() throws Exception { setFlag(""); try { - fileTransfers().delete(0, "", "/" + ownFileName()); + deleteFile(conn.getSelfUniqueId()); } catch (Exception ignored) { // The flag is what other clients go by; a leftover file only wastes space. + // Servers refuse the deletion unless we may delete others' avatars too. } } + /** + * Deletes another client's avatar, which needs {@code b_client_avatar_delete_other}; + * the server then clears their flag for everyone. + */ + void removeOther(String uniqueId) throws Exception { + deleteFile(uniqueId); + } + + /** + * Deleting names the avatar by its owner's unique id, {@code /avatar_}, which the + * server maps to the stored file itself; the stored name is refused ("convert error"). + */ + private void deleteFile(String uniqueId) throws Exception { + fileTransfers().delete(0, "", "/avatar_" + uniqueId); + } + private void setFlag(String flag) throws Exception { SingleCommand cmd = new SingleCommand("clientupdate", ProtocolRole.CLIENT, new CommandSingleParameter("client_flag_avatar", flag)); 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 57320fe..5155ec0 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 @@ -1302,6 +1302,14 @@ public final class TeamspeakConnection implements TS3Listener { }); } + /** Deletes another client's avatar; {@code callback} is given {@code null} on success. */ + public void removeAvatarOf(ClientEntry client, Consumer callback) { + run((ignored, error) -> callback.accept(error), () -> { + avatarAdmin.removeOther(client.uniqueId); + return null; + }); + } + /** Reads a client's avatar out of the virtual server's file repository (channel 0). */ private byte[] fetchAvatar(String uniqueId) throws Exception { FileTransferManager ft = fileTransfers; diff --git a/ts3-client/core/src/main/java/com/ts3client/session/ServerSession.java b/ts3-client/core/src/main/java/com/ts3client/session/ServerSession.java index 0338709..0175d42 100644 --- a/ts3-client/core/src/main/java/com/ts3client/session/ServerSession.java +++ b/ts3-client/core/src/main/java/com/ts3client/session/ServerSession.java @@ -390,6 +390,13 @@ public final class ServerSession { return !model.hasPermissionNames() || model.selfPermissionValue("i_client_max_avatar_filesize") > 0; } + /** Whether the server lets us delete other clients' avatars; offered while unknown. */ + public boolean canDeleteOtherAvatars() { + if (!conn.isConnected()) return false; + ServerModel model = conn.getModel(); + return !model.hasPermissionNames() || model.selfPermissionValue("b_client_avatar_delete_other") > 0; + } + /** Whether we publish an avatar on this server. */ public boolean hasAvatar() { if (!conn.isConnected()) return false; diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarActions.java b/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarActions.java index e54935d..b615108 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarActions.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/AvatarActions.java @@ -1,5 +1,6 @@ package com.ts3client.ui; +import com.ts3client.net.ClientEntry; import com.ts3client.net.TeamspeakConnection; import com.ts3client.net.avatar.Avatar; @@ -60,6 +61,12 @@ final class AvatarActions { conn.removeAvatar(error -> fail("Error deleting avatar: ", error)); } + /** Deletes another client's avatar for everyone. */ + void deleteOf(ClientEntry client) { + if (!conn.isConnected()) return; + conn.removeAvatarOf(client, error -> fail("Error deleting avatar: ", error)); + } + /** Shows {@code error}, if there is one; callable from any thread. */ private void fail(String what, String error) { if (error == null) return; diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ClientMenu.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ClientMenu.java index cbe0ce6..93b71d6 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ClientMenu.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ClientMenu.java @@ -74,6 +74,11 @@ final class ClientMenu { JMenuItem description = new JMenuItem("Change Description", Icons.of("EDIT")); description.addActionListener(a -> actions.changeClientDescription(client)); menu.add(description); + if (!self && !client.avatarFlag.isEmpty() && actions.canDeleteOtherAvatars()) { + JMenuItem avatar = new JMenuItem("Delete Avatar", Icons.of("DELETE_AVATAR_OTHER")); + avatar.addActionListener(a -> actions.deleteClientAvatar(client)); + menu.add(avatar); + } JMenuItem info = new JMenuItem("Connection Info", Icons.of("INFO")); info.addActionListener(a -> actions.showConnectionInfo(client)); menu.add(info); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java index a14f38f..417a66e 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java @@ -71,6 +71,11 @@ public final class InfoPanel extends JPanel { /** Whether we may upload an avatar. */ boolean canSet(); + /** Whether we may delete other clients' avatars. */ + boolean canDeleteOthers(); + + void deleteOf(ClientEntry client); + /** Asks for an image to upload as our avatar. */ void set(); @@ -314,6 +319,10 @@ public final class InfoPanel extends JPanel { menu.add(item("Save…", null, () -> saveAvatar(avatar, name))); if (avatarHandler != null && !shownClient.self) { if (avatarHandler.canSet()) menu.add(item("Steal for avatar", null, () -> avatarHandler.steal(avatar))); + ClientEntry client = shownClient; + if (avatarHandler.canDeleteOthers()) { + menu.add(item("Delete avatar", "DELETE_AVATAR_OTHER", () -> avatarHandler.deleteOf(client))); + } } else if (avatarHandler != null) { if (avatarHandler.canSet()) menu.add(item("Set avatar…", "UPLOAD_AVATAR", avatarHandler::set)); menu.add(item("Delete avatar", "DELETE_AVATAR", avatarHandler::delete)); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java index ca57c46..036bc42 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java @@ -6,6 +6,7 @@ import com.ts3client.config.IdentityStore; import com.ts3client.config.Settings; import com.ts3client.contacts.ContactStore; import com.ts3client.net.ChannelNode; +import com.ts3client.net.ClientEntry; import com.ts3client.net.ConnectionListener.ChatScope; import com.ts3client.net.PictureDownloads; import com.ts3client.net.TeamspeakConnection; @@ -115,6 +116,16 @@ final class ServerTab { return session.canSetAvatar(); } + @Override + public boolean canDeleteOthers() { + return session.canDeleteOtherAvatars(); + } + + @Override + public void deleteOf(ClientEntry client) { + avatar.deleteOf(client); + } + @Override public void set() { avatar.choose(); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java index ed7612e..d408dc4 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java @@ -215,6 +215,16 @@ final class ServerTabTreeActions implements ServerTreePanel.Actions { * Reads the description from the server before prompting, since the model only * carries one for clients whose info has been looked at. */ + @Override + public boolean canDeleteOtherAvatars() { + return tab.session().canDeleteOtherAvatars(); + } + + @Override + public void deleteClientAvatar(ClientEntry client) { + tab.avatar().deleteOf(client); + } + @Override public void changeClientDescription(ClientEntry client) { if (!conn.isConnected()) return; diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java index e681c29..325c254 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java @@ -118,6 +118,11 @@ public final class ServerTreePanel extends JPanel { /** Edits a client's description — our own included. */ void changeClientDescription(ClientEntry client); + /** Whether we may delete other clients' avatars. */ + boolean canDeleteOtherAvatars(); + + void deleteClientAvatar(ClientEntry client); + /** A new nickname for ourselves, typed into the tree row. */ void renameSelf(String nickname);