From ba22c9c79f63e6b7d33c4473f7988d0554ee0108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 18 Oct 2026 13:20:06 +0200 Subject: [PATCH] =?UTF-8?q?TeamSpeak:=20Backoff=20beim=20Verbinden=20der?= =?UTF-8?q?=20Raum-Clients,=20k=C3=BCrzere=20Nicknames,=20alle=20Channels?= =?UTF-8?q?=20abonnieren,=20Anwesenheit=20loggen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bridge/config.example.yml | 6 ++--- .../eu/mhsl/voicebridge/bridge/Bridge.java | 6 +++++ .../mhsl/voicebridge/bridge/BridgeConfig.java | 4 +-- .../bridge/discord/DiscordRoomBot.java | 1 + .../bridge/teamspeak/TeamspeakRoomClient.java | 4 ++- .../bridge/teamspeak/TeamspeakSide.java | 25 +++++++++++++++++-- 6 files changed, 38 insertions(+), 8 deletions(-) diff --git a/bridge/config.example.yml b/bridge/config.example.yml index 9ea4247..3b8de2c 100644 --- a/bridge/config.example.yml +++ b/bridge/config.example.yml @@ -22,8 +22,8 @@ teamspeak: parentChannel: "Crosstalk" # Name des Beobachter-Clients, der die Channels im Blick behält. nickname: "Crosstalk" - # Name der Raum-Clients; %room% wird durch den Raumnamen ersetzt. - roomNickname: "%room% (Minecraft/Discord)" + # Name der Raum-Clients; %room% wird durch den Raumnamen ersetzt (TeamSpeak erlaubt höchstens 30 Zeichen). + roomNickname: "%room% (MC/DC)" # Der TS6-Server auf mhsl.eu verlangt Level 15 (Fehler 519 "could not validate client identity"). securityLevel: 15 # Optional, falls der Server eine bestimmte Client-Version verlangt: @@ -37,7 +37,7 @@ discord: # In dieser Kategorie legt die Bridge fehlende Raum-Channels an (leer = ohne Kategorie). category: "000000000000000000" # Server-Nickname der Bots; %room% wird durch den Raumnamen ersetzt. - nickname: "%room% (Minecraft/TS)" + nickname: "%room% (MC/TS)" rooms: - key: crosstalk-1 diff --git a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/Bridge.java b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/Bridge.java index 1ce8192..a057e86 100644 --- a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/Bridge.java +++ b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/Bridge.java @@ -38,6 +38,7 @@ public final class Bridge { /** Zeitpunkte, bis zu denen Raum bzw. Bot noch gebraucht werden (für das Nachlaufen). */ private static final class Presence { long roomWanted, teamspeakWanted, discordWanted; + String lastCounts = ""; boolean teamspeakConnected, discordConnected; } @@ -84,6 +85,11 @@ public final class Bridge { int mc = plugin.minecraftMembers(room.key()); int ts = teamspeak != null ? teamspeak.humansIn(room) : 0; int dc = bot != null ? bot.humans() : 0; + String counts = "Minecraft " + mc + ", TeamSpeak " + ts + ", Discord " + dc; + if (!counts.equals(p.lastCounts)) { + LOG.info("{}: {}", room.name(), counts); + p.lastCounts = counts; + } int sides = (mc > 0 ? 1 : 0) + (ts > 0 ? 1 : 0) + (dc > 0 ? 1 : 0); if (sides >= 2) p.roomWanted = now; diff --git a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/BridgeConfig.java b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/BridgeConfig.java index f90f415..51b765e 100644 --- a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/BridgeConfig.java +++ b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/BridgeConfig.java @@ -73,9 +73,9 @@ public record BridgeConfig( integer(root, "lingerSeconds", 15) * 1000L, new Teamspeak(string(ts, "host", ""), integer(ts, "port", 9987), string(ts, "password", ""), string(ts, "parentChannel", "0"), string(ts, "nickname", "Crosstalk"), - string(ts, "roomNickname", "%room% (Minecraft/Discord)"), integer(ts, "securityLevel", 15), version), + string(ts, "roomNickname", "%room% (MC/DC)"), integer(ts, "securityLevel", 15), version), new Discord(string(dc, "guild", ""), string(dc, "category", ""), - string(dc, "nickname", "%room% (Minecraft/TS)")), + string(dc, "nickname", "%room% (MC/TS)")), List.copyOf(rooms)); } diff --git a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/discord/DiscordRoomBot.java b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/discord/DiscordRoomBot.java index 53fffa9..7b4facf 100644 --- a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/discord/DiscordRoomBot.java +++ b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/discord/DiscordRoomBot.java @@ -83,6 +83,7 @@ public final class DiscordRoomBot implements AudioSendHandler, AudioReceiveHandl LOG.info("{}: Discord-Bot {} bereit, Channel #{}", room.name(), jda.getSelfUser().getName(), channel.getName()); String nickname = config.nickname().replace("%room%", room.name()); + if (nickname.length() > 32) nickname = nickname.substring(0, 32); if (!nickname.equals(guild.getSelfMember().getNickname())) { guild.getSelfMember().modifyNickname(nickname).queue(null, e -> LOG.debug("{}: Nickname nicht gesetzt: {}", room.name(), e.getMessage())); diff --git a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/teamspeak/TeamspeakRoomClient.java b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/teamspeak/TeamspeakRoomClient.java index 83a4173..f27f0e3 100644 --- a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/teamspeak/TeamspeakRoomClient.java +++ b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/teamspeak/TeamspeakRoomClient.java @@ -21,6 +21,7 @@ import java.util.function.Consumer; /** Ein TS-Client im Raum-Channel: spielt die Mischung aus Minecraft und Discord ab und hört zu. */ final class TeamspeakRoomClient implements Microphone, TS3Listener { private static final Logger LOG = LoggerFactory.getLogger(TeamspeakRoomClient.class); + private static final int MAX_NICKNAME = 30; private final BridgeConfig.Teamspeak config; private final Room room; @@ -46,7 +47,8 @@ final class TeamspeakRoomClient implements Microphone, TS3Listener { static LocalTeamspeakClientSocket newSocket(BridgeConfig.Teamspeak config, LocalIdentity identity, String nickname) { LocalTeamspeakClientSocket socket = new LocalTeamspeakClientSocket(); socket.setIdentity(identity); - socket.setNickname(nickname); + // Längere Nicknames lehnt der Server mit "invalid parameter size" ab + socket.setNickname(nickname.length() > MAX_NICKNAME ? nickname.substring(0, MAX_NICKNAME) : nickname); BridgeConfig.ClientVersion version = config.clientVersion(); if (version != null) socket.setClientVersion(version.platform(), version.version(), version.sign()); socket.setExceptionHandler(e -> LOG.debug("ts3j: {}", e.toString())); diff --git a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/teamspeak/TeamspeakSide.java b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/teamspeak/TeamspeakSide.java index 887575a..7b64e9e 100644 --- a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/teamspeak/TeamspeakSide.java +++ b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/teamspeak/TeamspeakSide.java @@ -44,6 +44,8 @@ public final class TeamspeakSide implements TS3Listener { private static final long RECONNECT_SECONDS = 30; /** client_type 0 = normaler Client, 1 = ServerQuery. */ private static final int CLIENT_TYPE_VOICE = 0; + private static final long RETRY_MIN_MILLIS = 30_000; + private static final long RETRY_MAX_MILLIS = 300_000; private record TsClient(int channel, int type) {} private record TsChannel(int parent, String name) {} @@ -58,6 +60,9 @@ public final class TeamspeakSide implements TS3Listener { private final Map channels = new ConcurrentHashMap<>(); private final Map roomChannels = new ConcurrentHashMap<>(); private final Map roomClients = new ConcurrentHashMap<>(); + private final Set pendingConnect = ConcurrentHashMap.newKeySet(); + private final Map backoff = new ConcurrentHashMap<>(); + private final Map nextAttempt = new ConcurrentHashMap<>(); private volatile LocalTeamspeakClientSocket observer; private volatile boolean ready; @@ -106,9 +111,18 @@ public final class TeamspeakSide implements TS3Listener { return count; } - /** Raum-Client verbinden oder trennen. Läuft asynchron, mehrfaches Aufrufen schadet nicht. */ + /** + * Raum-Client verbinden oder trennen. Läuft asynchron, mehrfaches Aufrufen schadet nicht. + * Nach einem Fehlschlag wird mit wachsendem Abstand neu versucht, sonst greift der Flood-Schutz + * des Servers und bannt die IP. + */ public void setRoomConnected(Room room, boolean connected) { + if (connected) { + if (roomClients.containsKey(room.key()) || System.currentTimeMillis() < nextAttempt.getOrDefault(room.key(), 0L)) return; + if (!pendingConnect.add(room.key())) return; + } executor.execute(() -> { + pendingConnect.remove(room.key()); TeamspeakRoomClient client = roomClients.get(room.key()); if (connected && client == null && ready) { Integer channel = roomChannels.get(room.key()); @@ -118,9 +132,14 @@ public final class TeamspeakSide implements TS3Listener { loadIdentity("ts-" + room.key() + ".ini"), this::onRoomClientLost); roomClients.put(room.key(), client); client.connect(); + backoff.remove(room.key()); LOG.info("{}: TeamSpeak-Client im Channel {}", room.name(), channel); } catch (Exception e) { - LOG.warn("{}: TeamSpeak-Client konnte nicht verbinden: {}", room.name(), e.toString()); + long wait = Math.min(backoff.getOrDefault(room.key(), RETRY_MIN_MILLIS / 2) * 2, RETRY_MAX_MILLIS); + backoff.put(room.key(), wait); + nextAttempt.put(room.key(), System.currentTimeMillis() + wait); + LOG.warn("{}: TeamSpeak-Client konnte nicht verbinden ({}), neuer Versuch in {} s", + room.name(), e.toString(), wait / 1000); roomClients.remove(room.key()); if (client != null) client.disconnect(); } @@ -151,6 +170,8 @@ public final class TeamspeakSide implements TS3Listener { socket.addListener(this); observer = socket; socket.connect(new InetSocketAddress(config.host(), config.port()), emptyToNull(config.password()), 15000L); + // Clients in nicht abonnierten Channels meldet der Server nicht + socket.subscribeAll(); awaitInitialState(); LOG.info("TeamSpeak verbunden mit {}:{} ({} Channels, {} Clients sichtbar)", config.host(), config.port(), channels.size(), clients.size());