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:
2026-10-01 16:27:41 +00:00
parent b1890cbf79
commit bda114d2c0
9 changed files with 80 additions and 1 deletions

View File

@@ -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_<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 {
SingleCommand cmd = new SingleCommand("clientupdate", ProtocolRole.CLIENT,
new CommandSingleParameter("client_flag_avatar", flag));

View File

@@ -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). */
private byte[] fetchAvatar(String uniqueId) throws Exception {
FileTransferManager ft = fileTransfers;

View File

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