Add the Create Channel dialog
The official client creates and edits channels with one dialog, so ChannelEditDialog becomes ChannelDialog with two entry points. Creating starts from caller-chosen defaults instead of a channelinfo and sends a channelcreate carrying only what deviates from a plain new channel — TeamSpeak checks a create permission per property that is present. The server row now has a context menu with "Create Channel" and "Create Spacer" (pre-filled with the lowest free [spacerN], since channel names must be unique), and a channel's menu offers "Create Channel" and "Create Sub-Channel", both starting out temporary. Verified against the test server: a temporary channel is created, and a permanent one a guest may not create reports the server's refusal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,31 @@ final class ChannelAdmin {
|
||||
return ChannelSettings.from(channelId, info.getMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a channel below {@code parentId} (0 being the top level).
|
||||
*
|
||||
* @param properties the new channel's properties, as {@link ChannelSettings#creationParameters}
|
||||
* @return the new channel's id
|
||||
*/
|
||||
int create(int parentId, Map<String, String> properties) throws Exception {
|
||||
SingleCommand cmd = new SingleCommand("channelcreate", ProtocolRole.CLIENT);
|
||||
cmd.add(new CommandSingleParameter("cpid", Integer.toString(parentId)));
|
||||
for (Map.Entry<String, String> property : properties.entrySet()) {
|
||||
cmd.add(new CommandSingleParameter(property.getKey(), property.getValue()));
|
||||
}
|
||||
for (SingleCommand answer : conn.socket().executeCommand(cmd).get()) {
|
||||
int cid = parseInt(answer.toMap().get("cid"), -1);
|
||||
if (cid > 0) return cid;
|
||||
}
|
||||
// Some servers acknowledge the command without naming the channel; the
|
||||
// notifychannelcreated they also send does name it, so read it off the model
|
||||
// once that event has been handled.
|
||||
conn.awaitEventsProcessed();
|
||||
ChannelNode created = conn.getModel().findChannelByName(parentId, properties.get("channel_name"));
|
||||
if (created == null) throw new IllegalStateException("The server did not report the new channel");
|
||||
return created.id;
|
||||
}
|
||||
|
||||
/** Applies {@code changes} (property name to value) with a single {@code channeledit}. */
|
||||
void edit(int channelId, Map<String, String> changes) throws Exception {
|
||||
if (changes.isEmpty()) return;
|
||||
|
||||
@@ -143,6 +143,32 @@ public final class ChannelSettings {
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@code channelcreate} parameters for a channel that does not exist yet.
|
||||
*
|
||||
* <p>Everything a fresh channel would get anyway is left out, because TeamSpeak checks
|
||||
* a create permission per property that is present — a client allowed to create plain
|
||||
* channels but not, say, ones with a topic must not send an empty topic. What the
|
||||
* server cannot infer is always sent: the name, the channel type (whose absent flags
|
||||
* would silently mean temporary), the codec, and the client limits when they are not
|
||||
* unlimited.
|
||||
*/
|
||||
public Map<String, String> creationParameters() {
|
||||
Map<String, String> params = changesFrom(new ChannelSettings());
|
||||
params.put("channel_name", name);
|
||||
params.put("channel_flag_permanent", flag(type == Type.PERMANENT));
|
||||
params.put("channel_flag_semi_permanent", flag(type == Type.SEMI_PERMANENT));
|
||||
params.put("channel_flag_temporary", flag(type == Type.TEMPORARY));
|
||||
params.put("channel_codec", Integer.toString(codec));
|
||||
params.put("channel_codec_quality", Integer.toString(codecQuality));
|
||||
if (!maxClientsUnlimited) params.put("channel_maxclients", Integer.toString(maxClients));
|
||||
if (!familyInherited && !familyUnlimited) {
|
||||
params.put("channel_maxfamilyclients", Integer.toString(maxFamilyClients));
|
||||
}
|
||||
if (password == null || password.isEmpty()) params.remove("channel_password");
|
||||
return params;
|
||||
}
|
||||
|
||||
private static void putIfChanged(Map<String, String> into, String key, String value, String was) {
|
||||
if (!value.equals(was)) into.put(key, value);
|
||||
}
|
||||
|
||||
@@ -218,17 +218,34 @@ public final class ServerModel {
|
||||
public synchronized List<ChannelNode> siblingsOf(int channelId) {
|
||||
ChannelNode channel = channels.get(channelId);
|
||||
if (channel == null) return new ArrayList<>();
|
||||
List<ChannelNode> siblings = new ArrayList<>();
|
||||
for (ChannelNode c : channels.values()) {
|
||||
if (c.parentId == channel.parentId) siblings.add(c);
|
||||
}
|
||||
// Ordered with the channel still in place: the order links form a chain, and
|
||||
// pulling a link out of it first would strand everything below.
|
||||
sortSiblings(siblings);
|
||||
List<ChannelNode> siblings = childrenOf(channel.parentId);
|
||||
siblings.remove(channel);
|
||||
return siblings;
|
||||
}
|
||||
|
||||
/**
|
||||
* The channels directly below {@code parentId} (0 being the top level), in the order
|
||||
* they appear in the tree — what a new channel can be sorted after.
|
||||
*/
|
||||
public synchronized List<ChannelNode> childrenOf(int parentId) {
|
||||
List<ChannelNode> children = new ArrayList<>();
|
||||
for (ChannelNode c : channels.values()) {
|
||||
if (c.parentId == parentId) children.add(c);
|
||||
}
|
||||
sortSiblings(children);
|
||||
return children;
|
||||
}
|
||||
|
||||
/** 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()) {
|
||||
if (c.parentId == parentId && c.name.equals(name)) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The "/"-separated path of a channel from the root, e.g. {@code "Lobby/Games"}. */
|
||||
public synchronized String channelPath(int id) {
|
||||
ChannelNode c = channels.get(id);
|
||||
|
||||
@@ -923,6 +923,29 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a channel below {@code parentId} (0 being the top level) and gives it the
|
||||
* channel permissions it was created with.
|
||||
*
|
||||
* @param callback given the new channel's id, or the failure message. A channel whose
|
||||
* permissions could not be set still exists, and is reported as an error
|
||||
* saying so.
|
||||
*/
|
||||
public void createChannel(int parentId, Map<String, String> properties,
|
||||
Map<String, Integer> permissions,
|
||||
BiConsumer<Integer, String> callback) {
|
||||
run("ts3j-channel-create", callback, () -> {
|
||||
int channelId = channels.create(parentId, properties);
|
||||
try {
|
||||
channels.writePermissions(channelId, permissions, List.of());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("The channel was created, but its permissions"
|
||||
+ " could not be set: " + rootMessage(e));
|
||||
}
|
||||
return channelId;
|
||||
});
|
||||
}
|
||||
|
||||
/** The ids of the icons uploaded to this virtual server. */
|
||||
public void requestServerIcons(BiConsumer<List<Long>, String> callback) {
|
||||
run("ts3j-icon-list", callback, channels::listIcons);
|
||||
|
||||
Reference in New Issue
Block a user