TeamSpeak: Eltern-Channel per Name, Security-Level 15, fehlende Channels erneut anlegen

This commit is contained in:
MineTec committed 2026-10-18 13:12:17 +02:00
1 parent c0c290d3dc
commit 5787c0cc68
4 files changed
+72 -12

No files matched your search

+5 -3
View File
@@ -17,13 +17,15 @@ teamspeak:
host: ts.example.org host: ts.example.org
port: 9987 port: 9987
password: "" password: ""
# Unter diesem Channel legt die Bridge fehlende Raum-Channels an (Channel-ID, 0 = oberste Ebene). # Unter diesem Channel legt die Bridge fehlende Raum-Channels an:
parentChannel: 0 # 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. # Name des Beobachter-Clients, der die Channels im Blick behält.
nickname: "Crosstalk" nickname: "Crosstalk"
# Name der Raum-Clients; %room% wird durch den Raumnamen ersetzt. # Name der Raum-Clients; %room% wird durch den Raumnamen ersetzt.
roomNickname: "%room% (Minecraft/Discord)" 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: # Optional, falls der Server eine bestimmte Client-Version verlangt:
# clientVersion: # clientVersion:
# platform: "Linux" # platform: "Linux"
@@ -21,7 +21,7 @@ public record BridgeConfig(
Discord discord, Discord discord,
List<RoomConfig> rooms List<RoomConfig> 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) { String roomNickname, int securityLevel, ClientVersion clientVersion) {
public boolean enabled() { return !host.isBlank(); } public boolean enabled() { return !host.isBlank(); }
} }
@@ -72,8 +72,8 @@ public record BridgeConfig(
Path.of(string(root, "dataDir", "data")), Path.of(string(root, "dataDir", "data")),
integer(root, "lingerSeconds", 15) * 1000L, integer(root, "lingerSeconds", 15) * 1000L,
new Teamspeak(string(ts, "host", ""), integer(ts, "port", 9987), string(ts, "password", ""), new Teamspeak(string(ts, "host", ""), integer(ts, "port", 9987), string(ts, "password", ""),
integer(ts, "parentChannel", 0), string(ts, "nickname", "Crosstalk"), string(ts, "parentChannel", "0"), string(ts, "nickname", "Crosstalk"),
string(ts, "roomNickname", "%room% (Minecraft/Discord)"), integer(ts, "securityLevel", 8), version), string(ts, "roomNickname", "%room% (Minecraft/Discord)"), integer(ts, "securityLevel", 15), version),
new Discord(string(dc, "guild", ""), string(dc, "category", ""), new Discord(string(dc, "guild", ""), string(dc, "category", ""),
string(dc, "nickname", "%room% (Minecraft/TS)")), string(dc, "nickname", "%room% (Minecraft/TS)")),
List.copyOf(rooms)); List.copyOf(rooms));
@@ -1,5 +1,6 @@
package eu.mhsl.voicebridge.bridge; package eu.mhsl.voicebridge.bridge;
import com.github.manevolent.ts3j.util.Ts3Debugging;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -18,6 +19,9 @@ public final class Main {
System.exit(1); 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)); Bridge bridge = new Bridge(BridgeConfig.load(configFile));
Runtime.getRuntime().addShutdownHook(new Thread(bridge::stop, "shutdown")); Runtime.getRuntime().addShutdownHook(new Thread(bridge::stop, "shutdown"));
bridge.start(); bridge.start();
@@ -61,6 +61,8 @@ public final class TeamspeakSide implements TS3Listener {
private volatile LocalTeamspeakClientSocket observer; private volatile LocalTeamspeakClientSocket observer;
private volatile boolean ready; 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<Room> rooms) { public TeamspeakSide(BridgeConfig.Teamspeak config, Path dataDir, List<Room> rooms) {
this.config = config; this.config = config;
@@ -70,6 +72,21 @@ public final class TeamspeakSide implements TS3Listener {
public void start() { public void start() {
executor.execute(this::connectObserver); 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() { 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); socket.connect(new InetSocketAddress(config.host(), config.port()), emptyToNull(config.password()), 15000L);
LOG.info("TeamSpeak verbunden mit {}:{} ({} Channels, {} Clients sichtbar)", LOG.info("TeamSpeak verbunden mit {}:{} ({} Channels, {} Clients sichtbar)",
config.host(), config.port(), channels.size(), clients.size()); config.host(), config.port(), channels.size(), clients.size());
parentChannel = resolveParent();
resolveChannels(socket); resolveChannels(socket);
if (config.parentChannel() > 0) socket.joinChannel(config.parentChannel(), null); if (parentChannel > 0) socket.joinChannel(parentChannel, null);
ready = true; ready = true;
} catch (Exception e) { } catch (Exception e) {
LOG.warn("TeamSpeak-Verbindung fehlgeschlagen ({}), neuer Versuch in {} s", e.toString(), RECONNECT_SECONDS); 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) { for (Room room : rooms) {
if (roomChannels.containsKey(room.key())) continue;
int configured = room.config().teamspeakChannel(); int configured = room.config().teamspeakChannel();
if (configured > 0) { if (configured > 0) {
roomChannels.put(room.key(), configured); roomChannels.put(room.key(), configured);
@@ -155,26 +174,53 @@ public final class TeamspeakSide implements TS3Listener {
} }
Integer found = findChannel(room.name()); Integer found = findChannel(room.name());
if (found == null) { if (found == null) {
try {
socket.executeCommand(new SingleCommand("channelcreate", ProtocolRole.CLIENT, socket.executeCommand(new SingleCommand("channelcreate", ProtocolRole.CLIENT,
new CommandSingleParameter("channel_name", room.name()), new CommandSingleParameter("channel_name", room.name()),
new CommandSingleParameter("cpid", Integer.toString(config.parentChannel())), new CommandSingleParameter("cpid", Integer.toString(parentChannel)),
new CommandSingleParameter("channel_flag_permanent", "1"), new CommandSingleParameter("channel_flag_permanent", "1"),
new CommandSingleParameter("channel_codec", "4"))).get(); 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. // Die Antwort enthält die cid nicht, sie kommt per notifychannelcreated.
for (int i = 0; i < 50 && found == null; i++) { for (int i = 0; i < 50 && found == null; i++) {
Thread.sleep(100); Thread.sleep(100);
found = findChannel(room.name()); 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); LOG.info("TeamSpeak-Channel angelegt: {} ({})", room.name(), found);
} }
roomChannels.put(room.key(), 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) { private Integer findChannel(String name) {
return findChannel(parentChannel, name);
}
private Integer findChannel(int parent, String name) {
for (Map.Entry<Integer, TsChannel> e : channels.entrySet()) { for (Map.Entry<Integer, TsChannel> 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; return null;
} }
@@ -198,7 +244,15 @@ public final class TeamspeakSide implements TS3Listener {
LocalIdentity loadIdentity(String file) throws IOException, GeneralSecurityException { LocalIdentity loadIdentity(String file) throws IOException, GeneralSecurityException {
Path path = dataDir.resolve(file); 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); Files.createDirectories(dataDir);
LOG.info("Erzeuge TeamSpeak-Identität {} (Security-Level {})", file, config.securityLevel()); LOG.info("Erzeuge TeamSpeak-Identität {} (Security-Level {})", file, config.securityLevel());
LocalIdentity identity = LocalIdentity.generateNew(config.securityLevel()); LocalIdentity identity = LocalIdentity.generateNew(config.securityLevel());