TeamSpeak: Eltern-Channel per Name, Security-Level 15, fehlende Channels erneut anlegen
This commit is contained in:
1 parent
c0c290d3dc
commit
5787c0cc68
4 files changed
+76
-16
No files matched your search
@@ -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) {
|
||||||
socket.executeCommand(new SingleCommand("channelcreate", ProtocolRole.CLIENT,
|
try {
|
||||||
new CommandSingleParameter("channel_name", room.name()),
|
socket.executeCommand(new SingleCommand("channelcreate", ProtocolRole.CLIENT,
|
||||||
new CommandSingleParameter("cpid", Integer.toString(config.parentChannel())),
|
new CommandSingleParameter("channel_name", room.name()),
|
||||||
new CommandSingleParameter("channel_flag_permanent", "1"),
|
new CommandSingleParameter("cpid", Integer.toString(parentChannel)),
|
||||||
new CommandSingleParameter("channel_codec", "4"))).get();
|
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.
|
// 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());
|
||||||
|
|||||||
Reference in new issue
Block a user