From 5787c0cc684b126b07833fd492aedc2e212ca695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 18 Oct 2026 13:12:17 +0200 Subject: [PATCH] TeamSpeak: Eltern-Channel per Name, Security-Level 15, fehlende Channels erneut anlegen --- bridge/config.example.yml | 8 +- .../mhsl/voicebridge/bridge/BridgeConfig.java | 6 +- .../java/eu/mhsl/voicebridge/bridge/Main.java | 4 + .../bridge/teamspeak/TeamspeakSide.java | 74 ++++++++++++++++--- 4 files changed, 76 insertions(+), 16 deletions(-) diff --git a/bridge/config.example.yml b/bridge/config.example.yml index 8c4354c..9ea4247 100644 --- a/bridge/config.example.yml +++ b/bridge/config.example.yml @@ -17,13 +17,15 @@ teamspeak: host: ts.example.org port: 9987 password: "" - # Unter diesem Channel legt die Bridge fehlende Raum-Channels an (Channel-ID, 0 = oberste Ebene). - parentChannel: 0 + # Unter diesem Channel legt die Bridge fehlende Raum-Channels an: + # Channel-ID, Name eines Channels auf oberster Ebene oder 0 für die oberste Ebene selbst. + 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)" - securityLevel: 8 + # 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: # clientVersion: # platform: "Linux" 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 79bd649..f90f415 100644 --- a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/BridgeConfig.java +++ b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/BridgeConfig.java @@ -21,7 +21,7 @@ public record BridgeConfig( Discord discord, List rooms ) { - public record Teamspeak(String host, int port, String password, int parentChannel, String nickname, + public record Teamspeak(String host, int port, String password, String parentChannel, String nickname, String roomNickname, int securityLevel, ClientVersion clientVersion) { public boolean enabled() { return !host.isBlank(); } } @@ -72,8 +72,8 @@ public record BridgeConfig( Path.of(string(root, "dataDir", "data")), integer(root, "lingerSeconds", 15) * 1000L, new Teamspeak(string(ts, "host", ""), integer(ts, "port", 9987), string(ts, "password", ""), - integer(ts, "parentChannel", 0), string(ts, "nickname", "Crosstalk"), - string(ts, "roomNickname", "%room% (Minecraft/Discord)"), integer(ts, "securityLevel", 8), version), + string(ts, "parentChannel", "0"), string(ts, "nickname", "Crosstalk"), + string(ts, "roomNickname", "%room% (Minecraft/Discord)"), integer(ts, "securityLevel", 15), version), new Discord(string(dc, "guild", ""), string(dc, "category", ""), string(dc, "nickname", "%room% (Minecraft/TS)")), List.copyOf(rooms)); diff --git a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/Main.java b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/Main.java index 1b3316a..e4a2413 100644 --- a/bridge/src/main/java/eu/mhsl/voicebridge/bridge/Main.java +++ b/bridge/src/main/java/eu/mhsl/voicebridge/bridge/Main.java @@ -1,5 +1,6 @@ package eu.mhsl.voicebridge.bridge; +import com.github.manevolent.ts3j.util.Ts3Debugging; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,6 +19,9 @@ public final class Main { System.exit(1); } + // -Dvoicebridge.ts3debug=true schaltet das Protokoll-Log von ts3j ein + if (Boolean.getBoolean("voicebridge.ts3debug")) Ts3Debugging.setEnabled(true); + Bridge bridge = new Bridge(BridgeConfig.load(configFile)); Runtime.getRuntime().addShutdownHook(new Thread(bridge::stop, "shutdown")); bridge.start(); 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 d8c9126..f3cbe64 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 @@ -61,6 +61,8 @@ public final class TeamspeakSide implements TS3Listener { private volatile LocalTeamspeakClientSocket observer; private volatile boolean ready; + /** Channel, unter dem die Raum-Channels liegen (0 = oberste Ebene). */ + private volatile int parentChannel; public TeamspeakSide(BridgeConfig.Teamspeak config, Path dataDir, List rooms) { this.config = config; @@ -70,6 +72,21 @@ public final class TeamspeakSide implements TS3Listener { public void start() { executor.execute(this::connectObserver); + executor.scheduleWithFixedDelay(this::retryMissingChannels, RECONNECT_SECONDS, RECONNECT_SECONDS, TimeUnit.SECONDS); + } + + /** Fehlende Raum-Channels erneut anlegen, z. B. nachdem jemand dem Beobachter die Rechte gegeben hat. */ + private void retryMissingChannels() { + LocalTeamspeakClientSocket socket = observer; + if (!ready || socket == null || rooms.stream().allMatch(room -> roomChannels.containsKey(room.key()))) return; + try { + resolveChannels(socket); + if (parentChannel > 0) socket.joinChannel(parentChannel, null); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + LOG.warn("Fehlende TeamSpeak-Channels: {}", e.toString()); + } } public boolean ready() { @@ -136,8 +153,9 @@ public final class TeamspeakSide implements TS3Listener { socket.connect(new InetSocketAddress(config.host(), config.port()), emptyToNull(config.password()), 15000L); LOG.info("TeamSpeak verbunden mit {}:{} ({} Channels, {} Clients sichtbar)", config.host(), config.port(), channels.size(), clients.size()); + parentChannel = resolveParent(); resolveChannels(socket); - if (config.parentChannel() > 0) socket.joinChannel(config.parentChannel(), null); + if (parentChannel > 0) socket.joinChannel(parentChannel, null); ready = true; } catch (Exception e) { LOG.warn("TeamSpeak-Verbindung fehlgeschlagen ({}), neuer Versuch in {} s", e.toString(), RECONNECT_SECONDS); @@ -146,8 +164,9 @@ public final class TeamspeakSide implements TS3Listener { } } - private void resolveChannels(LocalTeamspeakClientSocket socket) throws Exception { + private void resolveChannels(LocalTeamspeakClientSocket socket) throws InterruptedException { for (Room room : rooms) { + if (roomChannels.containsKey(room.key())) continue; int configured = room.config().teamspeakChannel(); if (configured > 0) { roomChannels.put(room.key(), configured); @@ -155,26 +174,53 @@ public final class TeamspeakSide implements TS3Listener { } Integer found = findChannel(room.name()); if (found == null) { - socket.executeCommand(new SingleCommand("channelcreate", ProtocolRole.CLIENT, - new CommandSingleParameter("channel_name", room.name()), - new CommandSingleParameter("cpid", Integer.toString(config.parentChannel())), - new CommandSingleParameter("channel_flag_permanent", "1"), - new CommandSingleParameter("channel_codec", "4"))).get(); + try { + socket.executeCommand(new SingleCommand("channelcreate", ProtocolRole.CLIENT, + new CommandSingleParameter("channel_name", room.name()), + new CommandSingleParameter("cpid", Integer.toString(parentChannel)), + new CommandSingleParameter("channel_flag_permanent", "1"), + new CommandSingleParameter("channel_codec", "4"))).get(); + } catch (InterruptedException e) { + throw e; + } catch (Exception e) { + LOG.error("{}: TeamSpeak-Channel konnte nicht angelegt werden ({}). Fehlen dem Client " + + "\"{}\" die Rechte zum Anlegen? Bis dahin ohne TeamSpeak.", room.name(), e, config.nickname()); + continue; + } // Die Antwort enthält die cid nicht, sie kommt per notifychannelcreated. for (int i = 0; i < 50 && found == null; i++) { Thread.sleep(100); found = findChannel(room.name()); } - if (found == null) throw new IOException("Channel " + room.name() + " wurde nicht angelegt"); + if (found == null) { + LOG.error("{}: TeamSpeak-Channel wurde nicht angelegt", room.name()); + continue; + } LOG.info("TeamSpeak-Channel angelegt: {} ({})", room.name(), found); } roomChannels.put(room.key(), found); } } + /** {@code parentChannel} ist eine Channel-ID oder der Name eines Channels auf oberster Ebene. */ + private int resolveParent() throws IOException { + String configured = config.parentChannel(); + if (configured.isBlank()) return 0; + int id = parse(configured, -1); + if (id >= 0) return id; + Integer found = findChannel(0, configured); + if (found == null) throw new IOException("TeamSpeak-Channel \"" + configured + "\" gibt es nicht"); + LOG.info("TeamSpeak-Eltern-Channel {} hat die ID {}", configured, found); + return found; + } + private Integer findChannel(String name) { + return findChannel(parentChannel, name); + } + + private Integer findChannel(int parent, String name) { for (Map.Entry e : channels.entrySet()) { - if (e.getValue().parent() == config.parentChannel() && e.getValue().name().equals(name)) return e.getKey(); + if (e.getValue().parent() == parent && e.getValue().name().equals(name)) return e.getKey(); } return null; } @@ -198,7 +244,15 @@ public final class TeamspeakSide implements TS3Listener { LocalIdentity loadIdentity(String file) throws IOException, GeneralSecurityException { Path path = dataDir.resolve(file); - if (Files.exists(path)) return LocalIdentity.read(path.toFile()); + if (Files.exists(path)) { + LocalIdentity identity = LocalIdentity.read(path.toFile()); + if (identity.getSecurityLevel() < config.securityLevel()) { + LOG.info("Rechne TeamSpeak-Identität {} auf Security-Level {} hoch", file, config.securityLevel()); + identity.improveSecurity(config.securityLevel()); + identity.save(path.toFile()); + } + return identity; + } Files.createDirectories(dataDir); LOG.info("Erzeuge TeamSpeak-Identität {} (Security-Level {})", file, config.securityLevel()); LocalIdentity identity = LocalIdentity.generateNew(config.securityLevel());