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);