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_<unique id> (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 <noreply@anthropic.com>
This commit is contained in:
@@ -77,12 +77,29 @@ final class AvatarAdmin {
|
|||||||
void remove() throws Exception {
|
void remove() throws Exception {
|
||||||
setFlag("");
|
setFlag("");
|
||||||
try {
|
try {
|
||||||
fileTransfers().delete(0, "", "/" + ownFileName());
|
deleteFile(conn.getSelfUniqueId());
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
// The flag is what other clients go by; a leftover file only wastes space.
|
// 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_<uid>}, 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 {
|
private void setFlag(String flag) throws Exception {
|
||||||
SingleCommand cmd = new SingleCommand("clientupdate", ProtocolRole.CLIENT,
|
SingleCommand cmd = new SingleCommand("clientupdate", ProtocolRole.CLIENT,
|
||||||
new CommandSingleParameter("client_flag_avatar", flag));
|
new CommandSingleParameter("client_flag_avatar", flag));
|
||||||
|
|||||||
@@ -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<String> 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). */
|
/** Reads a client's avatar out of the virtual server's file repository (channel 0). */
|
||||||
private byte[] fetchAvatar(String uniqueId) throws Exception {
|
private byte[] fetchAvatar(String uniqueId) throws Exception {
|
||||||
FileTransferManager ft = fileTransfers;
|
FileTransferManager ft = fileTransfers;
|
||||||
|
|||||||
@@ -390,6 +390,13 @@ public final class ServerSession {
|
|||||||
return !model.hasPermissionNames() || model.selfPermissionValue("i_client_max_avatar_filesize") > 0;
|
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. */
|
/** Whether we publish an avatar on this server. */
|
||||||
public boolean hasAvatar() {
|
public boolean hasAvatar() {
|
||||||
if (!conn.isConnected()) return false;
|
if (!conn.isConnected()) return false;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.ts3client.ui;
|
package com.ts3client.ui;
|
||||||
|
|
||||||
|
import com.ts3client.net.ClientEntry;
|
||||||
import com.ts3client.net.TeamspeakConnection;
|
import com.ts3client.net.TeamspeakConnection;
|
||||||
import com.ts3client.net.avatar.Avatar;
|
import com.ts3client.net.avatar.Avatar;
|
||||||
|
|
||||||
@@ -60,6 +61,12 @@ final class AvatarActions {
|
|||||||
conn.removeAvatar(error -> fail("Error deleting avatar: ", error));
|
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. */
|
/** Shows {@code error}, if there is one; callable from any thread. */
|
||||||
private void fail(String what, String error) {
|
private void fail(String what, String error) {
|
||||||
if (error == null) return;
|
if (error == null) return;
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ final class ClientMenu {
|
|||||||
JMenuItem description = new JMenuItem("Change Description", Icons.of("EDIT"));
|
JMenuItem description = new JMenuItem("Change Description", Icons.of("EDIT"));
|
||||||
description.addActionListener(a -> actions.changeClientDescription(client));
|
description.addActionListener(a -> actions.changeClientDescription(client));
|
||||||
menu.add(description);
|
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"));
|
JMenuItem info = new JMenuItem("Connection Info", Icons.of("INFO"));
|
||||||
info.addActionListener(a -> actions.showConnectionInfo(client));
|
info.addActionListener(a -> actions.showConnectionInfo(client));
|
||||||
menu.add(info);
|
menu.add(info);
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ public final class InfoPanel extends JPanel {
|
|||||||
/** Whether we may upload an avatar. */
|
/** Whether we may upload an avatar. */
|
||||||
boolean canSet();
|
boolean canSet();
|
||||||
|
|
||||||
|
/** Whether we may delete other clients' avatars. */
|
||||||
|
boolean canDeleteOthers();
|
||||||
|
|
||||||
|
void deleteOf(ClientEntry client);
|
||||||
|
|
||||||
/** Asks for an image to upload as our avatar. */
|
/** Asks for an image to upload as our avatar. */
|
||||||
void set();
|
void set();
|
||||||
|
|
||||||
@@ -314,6 +319,10 @@ public final class InfoPanel extends JPanel {
|
|||||||
menu.add(item("Save…", null, () -> saveAvatar(avatar, name)));
|
menu.add(item("Save…", null, () -> saveAvatar(avatar, name)));
|
||||||
if (avatarHandler != null && !shownClient.self) {
|
if (avatarHandler != null && !shownClient.self) {
|
||||||
if (avatarHandler.canSet()) menu.add(item("Steal for avatar", null, () -> avatarHandler.steal(avatar)));
|
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) {
|
} else if (avatarHandler != null) {
|
||||||
if (avatarHandler.canSet()) menu.add(item("Set avatar…", "UPLOAD_AVATAR", avatarHandler::set));
|
if (avatarHandler.canSet()) menu.add(item("Set avatar…", "UPLOAD_AVATAR", avatarHandler::set));
|
||||||
menu.add(item("Delete avatar", "DELETE_AVATAR", avatarHandler::delete));
|
menu.add(item("Delete avatar", "DELETE_AVATAR", avatarHandler::delete));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.ts3client.config.IdentityStore;
|
|||||||
import com.ts3client.config.Settings;
|
import com.ts3client.config.Settings;
|
||||||
import com.ts3client.contacts.ContactStore;
|
import com.ts3client.contacts.ContactStore;
|
||||||
import com.ts3client.net.ChannelNode;
|
import com.ts3client.net.ChannelNode;
|
||||||
|
import com.ts3client.net.ClientEntry;
|
||||||
import com.ts3client.net.ConnectionListener.ChatScope;
|
import com.ts3client.net.ConnectionListener.ChatScope;
|
||||||
import com.ts3client.net.PictureDownloads;
|
import com.ts3client.net.PictureDownloads;
|
||||||
import com.ts3client.net.TeamspeakConnection;
|
import com.ts3client.net.TeamspeakConnection;
|
||||||
@@ -115,6 +116,16 @@ final class ServerTab {
|
|||||||
return session.canSetAvatar();
|
return session.canSetAvatar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canDeleteOthers() {
|
||||||
|
return session.canDeleteOtherAvatars();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteOf(ClientEntry client) {
|
||||||
|
avatar.deleteOf(client);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void set() {
|
public void set() {
|
||||||
avatar.choose();
|
avatar.choose();
|
||||||
|
|||||||
@@ -215,6 +215,16 @@ final class ServerTabTreeActions implements ServerTreePanel.Actions {
|
|||||||
* Reads the description from the server before prompting, since the model only
|
* Reads the description from the server before prompting, since the model only
|
||||||
* carries one for clients whose info has been looked at.
|
* 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
|
@Override
|
||||||
public void changeClientDescription(ClientEntry client) {
|
public void changeClientDescription(ClientEntry client) {
|
||||||
if (!conn.isConnected()) return;
|
if (!conn.isConnected()) return;
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ public final class ServerTreePanel extends JPanel {
|
|||||||
/** Edits a client's description — our own included. */
|
/** Edits a client's description — our own included. */
|
||||||
void changeClientDescription(ClientEntry client);
|
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. */
|
/** A new nickname for ourselves, typed into the tree row. */
|
||||||
void renameSelf(String nickname);
|
void renameSelf(String nickname);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user