Add a Permissions menu with privilege keys: list, create, remove and use
Permissions > Use Privilege Key redeems a key (tokenuse). Permissions > Privilege Keys opens a per-tab window listing the server's keys with their group, channel, creation date and description; keys can be created, removed, copied and reloaded. The create dialog takes a server group, or a channel group in a chosen channel, plus a description, offers only groups we have the power to add members to, and shows each new key for copying. tokenlist and tokenadd answer with notifytokenlist / notifytokenadd ahead of the command's completion, so core collects them the way it does the ban list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -662,6 +662,8 @@ final class ConnectionEventHandler implements TS3Listener {
|
||||
if ("notifyconnectioninfo".equals(e.getCommand())) conn.stats.onReport(e);
|
||||
else if ("notifychannelpermlist".equals(e.getCommand())) conn.channels.onPermListEntry(e);
|
||||
else if ("notifybanlist".equals(e.getCommand())) conn.bans.onListEntry(e);
|
||||
else if ("notifytokenlist".equals(e.getCommand())) conn.privilegeKeys.onListEntry(e);
|
||||
else if ("notifytokenadd".equals(e.getCommand())) conn.privilegeKeys.onAdded(e);
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ts3client.net;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A privilege key (token): a one-time code that puts whoever redeems it into a server
|
||||
* group, or into a channel group in one channel.
|
||||
*
|
||||
* @param groupId the server or channel group it grants
|
||||
* @param channelId the channel a channel group key applies in; 0 for a server group key
|
||||
*/
|
||||
public record PrivilegeKey(String token, boolean channelGroup, int groupId, int channelId,
|
||||
long createdEpochSeconds, String description) {
|
||||
|
||||
/** Reads a key off a {@code notifytokenlist} event's fields. */
|
||||
static PrivilegeKey from(Map<String, String> fields) {
|
||||
return new PrivilegeKey(orEmpty(fields.get("token")),
|
||||
parseLong(fields.get("token_type")) == 1,
|
||||
(int) parseLong(fields.get("token_id1")),
|
||||
(int) parseLong(fields.get("token_id2")),
|
||||
parseLong(fields.get("token_created")),
|
||||
orEmpty(fields.get("token_description")));
|
||||
}
|
||||
|
||||
private static String orEmpty(String s) {
|
||||
return s == null || s.equals("null") ? "" : s;
|
||||
}
|
||||
|
||||
private static long parseLong(String s) {
|
||||
try {
|
||||
return s == null ? 0 : Long.parseLong(s.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.ts3client.net;
|
||||
|
||||
import com.github.manevolent.ts3j.command.CommandException;
|
||||
import com.github.manevolent.ts3j.command.SingleCommand;
|
||||
import com.github.manevolent.ts3j.command.parameter.CommandSingleParameter;
|
||||
import com.github.manevolent.ts3j.event.UnknownTeamspeakEvent;
|
||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The server's privilege keys: listing, creating, deleting and redeeming them.
|
||||
*
|
||||
* <p>Split out of {@link TeamspeakConnection} like {@link BanAdmin}, and answered the same
|
||||
* way: {@code tokenlist} and {@code tokenadd} reply with notifications ahead of the
|
||||
* command's own completion. Every call here blocks and must run off the UI thread; the
|
||||
* connection's public wrappers do that.
|
||||
*/
|
||||
final class PrivilegeKeyAdmin {
|
||||
|
||||
/** What {@code tokenlist} answers when there are no keys at all. */
|
||||
private static final int ERROR_DATABASE_EMPTY_RESULT = 0x0501;
|
||||
|
||||
private final TeamspeakConnection conn;
|
||||
|
||||
/** The {@code tokenlist} reply in progress; one request may be in flight at a time. */
|
||||
private volatile List<PrivilegeKey> listing;
|
||||
/** The key the {@code tokenadd} in flight was answered with. */
|
||||
private volatile String created;
|
||||
|
||||
PrivilegeKeyAdmin(TeamspeakConnection conn) {
|
||||
this.conn = conn;
|
||||
}
|
||||
|
||||
List<PrivilegeKey> list() throws Exception {
|
||||
List<PrivilegeKey> keys = Collections.synchronizedList(new ArrayList<>());
|
||||
listing = keys;
|
||||
try {
|
||||
conn.socket().executeCommand(new SingleCommand("tokenlist", ProtocolRole.CLIENT)).complete();
|
||||
conn.awaitEventsProcessed();
|
||||
} catch (CommandException e) {
|
||||
if (e.getErrorId() != ERROR_DATABASE_EMPTY_RESULT) throw e;
|
||||
} finally {
|
||||
listing = null;
|
||||
}
|
||||
return new ArrayList<>(keys);
|
||||
}
|
||||
|
||||
void onListEntry(UnknownTeamspeakEvent e) {
|
||||
List<PrivilegeKey> keys = listing;
|
||||
if (keys != null) keys.add(PrivilegeKey.from(e.getMap()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a key.
|
||||
*
|
||||
* @param channelId the channel a channel group key applies in; ignored for a server group
|
||||
* @return the new key
|
||||
*/
|
||||
synchronized String add(boolean channelGroup, int groupId, int channelId, String description) throws Exception {
|
||||
SingleCommand cmd = new SingleCommand("tokenadd", ProtocolRole.CLIENT);
|
||||
cmd.add(new CommandSingleParameter("tokentype", channelGroup ? "1" : "0"));
|
||||
cmd.add(new CommandSingleParameter("tokenid1", Integer.toString(groupId)));
|
||||
cmd.add(new CommandSingleParameter("tokenid2", channelGroup ? Integer.toString(channelId) : "0"));
|
||||
cmd.add(new CommandSingleParameter("tokendescription", description));
|
||||
created = null;
|
||||
conn.socket().executeCommand(cmd).complete();
|
||||
conn.awaitEventsProcessed();
|
||||
String token = created;
|
||||
if (token == null || token.isEmpty()) throw new IllegalStateException("The server did not return the key");
|
||||
return token;
|
||||
}
|
||||
|
||||
void onAdded(UnknownTeamspeakEvent e) {
|
||||
created = e.get("token");
|
||||
}
|
||||
|
||||
void delete(String token) throws Exception {
|
||||
conn.socket().executeCommand(new SingleCommand("tokendelete", ProtocolRole.CLIENT,
|
||||
new CommandSingleParameter("token", token))).complete();
|
||||
}
|
||||
|
||||
/** Redeems a key; the server announces the resulting group change itself. */
|
||||
void use(String token) throws Exception {
|
||||
conn.socket().executeCommand(new SingleCommand("tokenuse", ProtocolRole.CLIENT,
|
||||
new CommandSingleParameter("token", token))).complete();
|
||||
}
|
||||
}
|
||||
@@ -243,6 +243,15 @@ public final class ServerModel {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The regular server or channel groups we have the power to put someone in, so may make a privilege key for. */
|
||||
public synchronized List<Group> privilegeKeyGroups(boolean channelGroups) {
|
||||
List<Group> out = new ArrayList<>();
|
||||
for (Group g : channelGroups ? allChannelGroups() : allServerGroups()) {
|
||||
if (canToggleGroup(g, false)) out.add(g);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static boolean hasPower(int needed, int own) {
|
||||
if (own == -1) return true;
|
||||
if (needed == -1) return false;
|
||||
@@ -397,6 +406,20 @@ public final class ServerModel {
|
||||
return children;
|
||||
}
|
||||
|
||||
/** Every channel, depth first in the order the tree shows them. */
|
||||
public synchronized List<ChannelNode> channelsInTreeOrder() {
|
||||
List<ChannelNode> out = new ArrayList<>();
|
||||
addInTreeOrder(0, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
private void addInTreeOrder(int parentId, List<ChannelNode> out) {
|
||||
for (ChannelNode c : childrenOf(parentId)) {
|
||||
out.add(c);
|
||||
addInTreeOrder(c.id, out);
|
||||
}
|
||||
}
|
||||
|
||||
/** The channel of that exact name directly below {@code parentId}, or {@code null}. */
|
||||
public synchronized ChannelNode findChannelByName(int parentId, String name) {
|
||||
for (ChannelNode c : channels.values()) {
|
||||
|
||||
@@ -95,8 +95,9 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
final ConnectionStatsCollector stats = new ConnectionStatsCollector(this);
|
||||
/** Channel editing and the server's icon store; package-private for {@link ConnectionEventHandler}. */
|
||||
final ChannelAdmin channels = new ChannelAdmin(this);
|
||||
/** The server's ban list; also package-private for {@link ConnectionEventHandler}. */
|
||||
/** The server's ban list and privilege keys; also package-private for {@link ConnectionEventHandler}. */
|
||||
final BanAdmin bans = new BanAdmin(this);
|
||||
final PrivilegeKeyAdmin privilegeKeys = new PrivilegeKeyAdmin(this);
|
||||
private final AvatarAdmin avatarAdmin = new AvatarAdmin(this);
|
||||
|
||||
/*
|
||||
@@ -954,6 +955,43 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
});
|
||||
}
|
||||
|
||||
// ---- privilege keys ----
|
||||
|
||||
/** Reads the server's privilege keys, delivering them (or a failure message) off the UI thread. */
|
||||
public void requestPrivilegeKeys(BiConsumer<List<PrivilegeKey>, String> callback) {
|
||||
run(callback, privilegeKeys::list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a privilege key for a server group, or for a channel group in {@code channelId}.
|
||||
*
|
||||
* @param callback given the new key, or the failure message
|
||||
*/
|
||||
public void addPrivilegeKey(boolean channelGroup, int groupId, int channelId, String description,
|
||||
BiConsumer<String, String> callback) {
|
||||
run(callback, () -> privilegeKeys.add(channelGroup, groupId, channelId, description));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes privilege keys one by one, stopping at the first the server refuses.
|
||||
*
|
||||
* @param callback given {@code null} on success, or the failure message
|
||||
*/
|
||||
public void deletePrivilegeKeys(Collection<String> tokens, Consumer<String> callback) {
|
||||
run((ignored, error) -> callback.accept(error), () -> {
|
||||
for (String token : tokens) privilegeKeys.delete(token);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param callback given {@code null} on success, or the failure message */
|
||||
public void usePrivilegeKey(String token, Consumer<String> callback) {
|
||||
run((ignored, error) -> callback.accept(error), () -> {
|
||||
privilegeKeys.use(token);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
// ---- bans ----
|
||||
|
||||
/** Reads the server's ban list, delivering it (or a failure message) off the UI thread. */
|
||||
|
||||
@@ -387,6 +387,11 @@ public final class ServerSession {
|
||||
return permits("b_client_ban_list");
|
||||
}
|
||||
|
||||
/** Whether the server grants us {@code b_virtualserver_token_list}; offered while unknown. */
|
||||
public boolean canListPrivilegeKeys() {
|
||||
return permits("b_virtualserver_token_list");
|
||||
}
|
||||
|
||||
/** Whether the server lets us upload an avatar ({@code i_client_max_avatar_filesize}); offered while unknown. */
|
||||
public boolean canSetAvatar() {
|
||||
return permits("i_client_max_avatar_filesize");
|
||||
|
||||
Reference in New Issue
Block a user