From ee0f15fb3c3eb53a97d28a814c28f5568d5b872f Mon Sep 17 00:00:00 2001 From: ericek111 Date: Thu, 24 Sep 2026 18:32:05 +0000 Subject: [PATCH] Stop tripping the server's flood protection with query commands channelinfo, clientinfo, channellist and clientlist are query commands that count heavily against anti-flood; clicking through the tree got every command refused within seconds. Use what the official client does: - Channel descriptions via channelgetdescription, whose answer arrives as an invoker-less channel edit. - Client version and platform via clientgetvariables, answered as a client update. - Connection info takes version and platform from the model and idle time from notifyconnectioninfo instead of polling clientinfo. - Drop the channellist/clientlist re-read after connecting: the events during connect already carry everything, and more (groups, unique ids, away state). Co-Authored-By: Claude Opus 5.5 --- .../ts3client/net/ConnectionEventHandler.java | 14 +++ .../net/ConnectionStatsCollector.java | 36 +++--- .../ts3client/net/TeamspeakConnection.java | 104 +++++------------- 3 files changed, 56 insertions(+), 98 deletions(-) diff --git a/ts3-client/core/src/main/java/com/ts3client/net/ConnectionEventHandler.java b/ts3-client/core/src/main/java/com/ts3client/net/ConnectionEventHandler.java index f863e30..f573fd9 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/ConnectionEventHandler.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/ConnectionEventHandler.java @@ -322,6 +322,12 @@ final class ConnectionEventHandler implements TS3Listener { c.description = e.get("client_description"); conn.ui.onInfoUpdated(); } + // The answer to clientgetvariables. + if (has(e, "client_version") || has(e, "client_platform")) { + if (has(e, "client_version")) c.version = e.get("client_version"); + if (has(e, "client_platform")) c.platform = e.get("client_platform"); + conn.ui.onInfoUpdated(); + } if (has(e, "client_is_channel_commander")) c.channelCommander = e.getBoolean("client_is_channel_commander"); if (has(e, "client_flag_avatar")) c.avatarFlag = TeamspeakConnection.orEmpty(e.get("client_flag_avatar")); @@ -391,6 +397,14 @@ final class ConnectionEventHandler implements TS3Listener { public void onChannelEdit(ChannelEditedEvent e) { ChannelNode ch = conn.getModel().getChannel(safeInt(e, "cid")); if (ch == null) return; + // The answer to channelgetdescription; an empty description comes as a bare key. + if (e.getMap().containsKey("channel_description")) { + ch.description = TeamspeakConnection.orEmpty(e.get("channel_description")); + ch.descriptionLoaded = true; + conn.ui.onInfoUpdated(); + } + // That answer has no invoker: nothing was edited. + if (!e.getMap().containsKey("invokerid")) return; applyChannelProperties(ch, e); // Sorting among the same siblings arrives here too, and TS3 announces it as an edit. if (has(e, "channel_order")) conn.getModel().relinkChannel(ch.id, ch.parentId, e.getInt("channel_order")); diff --git a/ts3-client/core/src/main/java/com/ts3client/net/ConnectionStatsCollector.java b/ts3-client/core/src/main/java/com/ts3client/net/ConnectionStatsCollector.java index cf6265c..27aad8f 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/ConnectionStatsCollector.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/ConnectionStatsCollector.java @@ -1,6 +1,5 @@ package com.ts3client.net; -import com.github.manevolent.ts3j.api.Client; import com.github.manevolent.ts3j.command.SingleCommand; import com.github.manevolent.ts3j.command.parameter.CommandSingleParameter; import com.github.manevolent.ts3j.event.UnknownTeamspeakEvent; @@ -124,33 +123,24 @@ final class ConnectionStatsCollector { } /** - * Requests a remote client's connection info. First loads {@code clientinfo} - * for the stable fields, then issues {@code getconnectioninfo} whose - * {@code notifyconnectioninfo} report arrives asynchronously via - * {@link #onReport}. If the report does not arrive shortly (e.g. the - * server withholds it), the clientinfo-only snapshot is delivered instead. + * Requests a remote client's connection info: {@code getconnectioninfo}, whose + * {@code notifyconnectioninfo} report arrives asynchronously via {@link #onReport}. + * Version and platform come from the model; {@code clientinfo} would have them too, + * but it counts heavily against the server's flood protection, and this is polled. + * If the report does not arrive shortly (e.g. the server withholds it), the + * model-only snapshot is delivered instead. */ private void requestRemote(int clientId, Consumer callback) { ConnectionStats s = new ConnectionStats(); s.clientId = clientId; ClientEntry entry = conn.getModel().getClient(clientId); - if (entry != null) s.nickname = entry.nickname; - - LocalTeamspeakClientSocket client = conn.client; - try { - Client c = client.getClientInfo(clientId); - if (c != null) { - s.version = TeamspeakConnection.orEmpty(c.getVersion()); - s.platform = TeamspeakConnection.orEmpty(c.getPlatform()); - s.ip = TeamspeakConnection.orEmpty(c.getIp()); - s.idleTimeMs = c.getIdleTime(); - if (entry == null) s.nickname = TeamspeakConnection.orEmpty(c.getNickname()); - applyConnectionFields(s, c.getMap()); - } - } catch (Exception ignored) { - // clientinfo may be permission-restricted; continue with what we have + if (entry != null) { + s.nickname = entry.nickname; + s.version = entry.version; + s.platform = entry.platform; } + LocalTeamspeakClientSocket client = conn.client; PendingConnInfo p = new PendingConnInfo(callback, s); pending.put(clientId, p); @@ -161,7 +151,7 @@ final class ConnectionStatsCollector { client.executeCommand(cmd).complete(); sent = true; } catch (Exception ignored) { - // command failed; fall back to the clientinfo snapshot below + // command failed; fall back to the model snapshot below } if (sent) { @@ -189,6 +179,8 @@ final class ConnectionStatsCollector { long connected = parseLong(m.get("connection_connected_time")); if (connected >= 0) s.connectedTimeMs = connected; + long idle = parseLong(m.get("connection_idle_time")); + if (idle >= 0) s.idleTimeMs = idle; String ip = m.get("connection_client_ip"); if (ip != null && !ip.isEmpty()) s.ip = ip; 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 dfd191b..57bb9ba 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 @@ -1,6 +1,5 @@ package com.ts3client.net; -import com.github.manevolent.ts3j.api.Channel; import com.github.manevolent.ts3j.api.Client; import com.github.manevolent.ts3j.command.MultiCommand; import com.github.manevolent.ts3j.command.SingleCommand; @@ -449,10 +448,11 @@ public final class TeamspeakConnection implements TS3Listener { } /** - * Populates the model from the server. Each step is best-effort: a restricted - * guest group may deny {@code channelsubscribeall} or the list commands without - * that being a fatal connection error, and channel data already arrives via - * events during {@code connect()}. + * Completes the model once connected. Channels and clients already arrived as events + * during {@code connect()}, complete, as the official client gets them. The + * {@code channellist}/{@code clientlist} query commands must not be used to re-read + * them: their rows lack groups, unique ids and away state, and they count heavily + * against the server's flood protection. */ private void syncAll() { try { @@ -464,45 +464,6 @@ public final class TeamspeakConnection implements TS3Listener { model.setServerName(client.getServerName()); model.setDefaultChannelGroupId( (int) safeLong(client.getServerProperties().get("virtualserver_default_channel_group"))); - try { - for (Channel ch : client.listChannels()) { - model.putChannel(ch.getId(), ch.getName(), ch.getParentChannelId(), ch.getOrder()); - ChannelNode node = model.getChannel(ch.getId()); - if (node != null) { - node.hasPassword = ch.hasPassword(); - node.permanent = ch.isPermanent(); - node.topic = ch.getTopic() == null ? "" : ch.getTopic(); - node.maxClients = ch.getMaxClients(); - // The channellist command answers with a narrower field set than the - // events do, so a missing (or blank) icon here means "not reported", - // not "no icon" — never let it clear what the events delivered. - long icon = safeLong(ch.get("channel_icon_id")); - if (icon != 0) node.iconId = icon; - } - } - } catch (Exception e) { - ui.onStatus("Channel list unavailable: " + rootMessage(e)); - } - try { - for (Client cl : client.listClients()) { - ClientEntry e = model.putClient(cl.getId(), cl.getNickname(), cl.getChannelId()); - e.type = cl.getType(); - e.talkPower = cl.getTalkPower(); - e.inputMuted = cl.isInputMuted(); - e.outputMuted = cl.isOutputMuted(); - e.inputHardware = cl.isInputHardware(); - e.outputHardware = cl.isOutputHardware(); - e.away = cl.isAway(); - e.awayMessage = orEmpty(cl.get("client_away_message")); - e.uniqueId = cl.getUniqueIdentifier(); - e.avatarFlag = orEmpty(cl.get("client_flag_avatar")); - e.serverGroupIds = cl.getServerGroups(); - e.channelGroupId = cl.getChannelGroupId(); - e.self = (cl.getId() == selfClientId); - } - } catch (Exception e) { - ui.onStatus("Client list unavailable: " + rootMessage(e)); - } } public void disconnect() { @@ -919,46 +880,37 @@ public final class TeamspeakConnection implements TS3Listener { }, "ts3j-rename").start(); } - /** Fetches a channel's description (and topic) on demand, then notifies the UI. */ + /* + * Info lookups use the commands the official client does. channelinfo/clientinfo would + * answer directly, but they are query commands that count heavily against the server's + * flood protection: clicking through the tree gets every command refused within seconds. + */ + + /** + * Asks for a channel's description. The answer arrives as a channel edit, see + * {@link ConnectionEventHandler#onChannelEdit}. + */ public void requestChannelInfo(int channelId) { - new Thread(() -> { - try { - Channel ch = client.getChannelInfo(channelId); - ChannelNode node = model.getChannel(channelId); - if (ch != null && node != null) { - node.description = orEmpty(ch.get("channel_description")); - node.topic = orEmpty(ch.getTopic()); - node.descriptionLoaded = true; - } - } catch (Exception ignored) { - // description may be permission-restricted; leave as-is - } finally { - ui.onInfoUpdated(); - } - }, "ts3j-channelinfo").start(); + requestInfo("channelgetdescription", "cid", channelId, "ts3j-channelinfo"); } - /** Fetches a client's extended info (groups, platform, version) on demand. */ + /** + * Asks for a client's version and platform, the only details the enter-view event + * lacks. The answer arrives as a client update, see {@link ConnectionEventHandler#onClientChanged}. + */ public void requestClientInfo(int clientId) { + requestInfo("clientgetvariables", "clid", clientId, "ts3j-clientinfo"); + } + + private void requestInfo(String command, String idParameter, int id, String threadName) { new Thread(() -> { try { - Client c = client.getClientInfo(clientId); - ClientEntry e = model.getClient(clientId); - if (c != null && e != null) { - e.platform = orEmpty(c.getPlatform()); - e.version = orEmpty(c.getVersion()); - e.idleTimeMs = c.getIdleTime(); - e.description = orEmpty(c.get("client_description")); - e.avatarFlag = orEmpty(c.get("client_flag_avatar")); - int[] groups = c.getServerGroups(); - if (groups != null && groups.length > 0) e.serverGroupIds = groups; - e.channelGroupId = c.getChannelGroupId(); - } + client.executeCommand(new SingleCommand(command, ProtocolRole.CLIENT, + new CommandSingleParameter(idParameter, Integer.toString(id)))).complete(); } catch (Exception ignored) { - } finally { - ui.onInfoUpdated(); + // may be permission-restricted; leave as-is } - }, "ts3j-clientinfo").start(); + }, threadName).start(); } /**