TeamSpeak: Backoff beim Verbinden der Raum-Clients, kürzere Nicknames, alle Channels abonnieren, Anwesenheit loggen

This commit is contained in:
MineTec committed 2026-10-18 13:20:06 +02:00
1 parent 3c3a9dd836
commit ba22c9c79f
6 files changed
+38 -8

No files matched your search

+3 -3
View File
@@ -22,8 +22,8 @@ teamspeak:
parentChannel: "Crosstalk" 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 (TeamSpeak erlaubt höchstens 30 Zeichen).
roomNickname: "%room% (Minecraft/Discord)" roomNickname: "%room% (MC/DC)"
# Der TS6-Server auf mhsl.eu verlangt Level 15 (Fehler 519 "could not validate client identity"). # Der TS6-Server auf mhsl.eu verlangt Level 15 (Fehler 519 "could not validate client identity").
securityLevel: 15 securityLevel: 15
# Optional, falls der Server eine bestimmte Client-Version verlangt: # 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). # In dieser Kategorie legt die Bridge fehlende Raum-Channels an (leer = ohne Kategorie).
category: "000000000000000000" category: "000000000000000000"
# Server-Nickname der Bots; %room% wird durch den Raumnamen ersetzt. # Server-Nickname der Bots; %room% wird durch den Raumnamen ersetzt.
nickname: "%room% (Minecraft/TS)" nickname: "%room% (MC/TS)"
rooms: rooms:
- key: crosstalk-1 - key: crosstalk-1
@@ -38,6 +38,7 @@ public final class Bridge {
/** Zeitpunkte, bis zu denen Raum bzw. Bot noch gebraucht werden (für das Nachlaufen). */ /** Zeitpunkte, bis zu denen Raum bzw. Bot noch gebraucht werden (für das Nachlaufen). */
private static final class Presence { private static final class Presence {
long roomWanted, teamspeakWanted, discordWanted; long roomWanted, teamspeakWanted, discordWanted;
String lastCounts = "";
boolean teamspeakConnected, discordConnected; boolean teamspeakConnected, discordConnected;
} }
@@ -84,6 +85,11 @@ public final class Bridge {
int mc = plugin.minecraftMembers(room.key()); int mc = plugin.minecraftMembers(room.key());
int ts = teamspeak != null ? teamspeak.humansIn(room) : 0; int ts = teamspeak != null ? teamspeak.humansIn(room) : 0;
int dc = bot != null ? bot.humans() : 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); int sides = (mc > 0 ? 1 : 0) + (ts > 0 ? 1 : 0) + (dc > 0 ? 1 : 0);
if (sides >= 2) p.roomWanted = now; if (sides >= 2) p.roomWanted = now;
@@ -73,9 +73,9 @@ public record BridgeConfig(
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", ""),
string(ts, "parentChannel", "0"), string(ts, "nickname", "Crosstalk"), 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", ""), new Discord(string(dc, "guild", ""), string(dc, "category", ""),
string(dc, "nickname", "%room% (Minecraft/TS)")), string(dc, "nickname", "%room% (MC/TS)")),
List.copyOf(rooms)); List.copyOf(rooms));
} }
@@ -83,6 +83,7 @@ public final class DiscordRoomBot implements AudioSendHandler, AudioReceiveHandl
LOG.info("{}: Discord-Bot {} bereit, Channel #{}", room.name(), jda.getSelfUser().getName(), channel.getName()); LOG.info("{}: Discord-Bot {} bereit, Channel #{}", room.name(), jda.getSelfUser().getName(), channel.getName());
String nickname = config.nickname().replace("%room%", room.name()); String nickname = config.nickname().replace("%room%", room.name());
if (nickname.length() > 32) nickname = nickname.substring(0, 32);
if (!nickname.equals(guild.getSelfMember().getNickname())) { if (!nickname.equals(guild.getSelfMember().getNickname())) {
guild.getSelfMember().modifyNickname(nickname).queue(null, guild.getSelfMember().modifyNickname(nickname).queue(null,
e -> LOG.debug("{}: Nickname nicht gesetzt: {}", room.name(), e.getMessage())); e -> LOG.debug("{}: Nickname nicht gesetzt: {}", room.name(), e.getMessage()));
@@ -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. */ /** Ein TS-Client im Raum-Channel: spielt die Mischung aus Minecraft und Discord ab und hört zu. */
final class TeamspeakRoomClient implements Microphone, TS3Listener { final class TeamspeakRoomClient implements Microphone, TS3Listener {
private static final Logger LOG = LoggerFactory.getLogger(TeamspeakRoomClient.class); private static final Logger LOG = LoggerFactory.getLogger(TeamspeakRoomClient.class);
private static final int MAX_NICKNAME = 30;
private final BridgeConfig.Teamspeak config; private final BridgeConfig.Teamspeak config;
private final Room room; private final Room room;
@@ -46,7 +47,8 @@ final class TeamspeakRoomClient implements Microphone, TS3Listener {
static LocalTeamspeakClientSocket newSocket(BridgeConfig.Teamspeak config, LocalIdentity identity, String nickname) { static LocalTeamspeakClientSocket newSocket(BridgeConfig.Teamspeak config, LocalIdentity identity, String nickname) {
LocalTeamspeakClientSocket socket = new LocalTeamspeakClientSocket(); LocalTeamspeakClientSocket socket = new LocalTeamspeakClientSocket();
socket.setIdentity(identity); 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(); BridgeConfig.ClientVersion version = config.clientVersion();
if (version != null) socket.setClientVersion(version.platform(), version.version(), version.sign()); if (version != null) socket.setClientVersion(version.platform(), version.version(), version.sign());
socket.setExceptionHandler(e -> LOG.debug("ts3j: {}", e.toString())); socket.setExceptionHandler(e -> LOG.debug("ts3j: {}", e.toString()));
@@ -44,6 +44,8 @@ public final class TeamspeakSide implements TS3Listener {
private static final long RECONNECT_SECONDS = 30; private static final long RECONNECT_SECONDS = 30;
/** client_type 0 = normaler Client, 1 = ServerQuery. */ /** client_type 0 = normaler Client, 1 = ServerQuery. */
private static final int CLIENT_TYPE_VOICE = 0; 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 TsClient(int channel, int type) {}
private record TsChannel(int parent, String name) {} private record TsChannel(int parent, String name) {}
@@ -58,6 +60,9 @@ public final class TeamspeakSide implements TS3Listener {
private final Map<Integer, TsChannel> channels = new ConcurrentHashMap<>(); private final Map<Integer, TsChannel> channels = new ConcurrentHashMap<>();
private final Map<String, Integer> roomChannels = new ConcurrentHashMap<>(); private final Map<String, Integer> roomChannels = new ConcurrentHashMap<>();
private final Map<String, TeamspeakRoomClient> roomClients = new ConcurrentHashMap<>(); private final Map<String, TeamspeakRoomClient> roomClients = new ConcurrentHashMap<>();
private final Set<String> pendingConnect = ConcurrentHashMap.newKeySet();
private final Map<String, Long> backoff = new ConcurrentHashMap<>();
private final Map<String, Long> nextAttempt = new ConcurrentHashMap<>();
private volatile LocalTeamspeakClientSocket observer; private volatile LocalTeamspeakClientSocket observer;
private volatile boolean ready; private volatile boolean ready;
@@ -106,9 +111,18 @@ public final class TeamspeakSide implements TS3Listener {
return count; 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) { 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(() -> { executor.execute(() -> {
pendingConnect.remove(room.key());
TeamspeakRoomClient client = roomClients.get(room.key()); TeamspeakRoomClient client = roomClients.get(room.key());
if (connected && client == null && ready) { if (connected && client == null && ready) {
Integer channel = roomChannels.get(room.key()); Integer channel = roomChannels.get(room.key());
@@ -118,9 +132,14 @@ public final class TeamspeakSide implements TS3Listener {
loadIdentity("ts-" + room.key() + ".ini"), this::onRoomClientLost); loadIdentity("ts-" + room.key() + ".ini"), this::onRoomClientLost);
roomClients.put(room.key(), client); roomClients.put(room.key(), client);
client.connect(); client.connect();
backoff.remove(room.key());
LOG.info("{}: TeamSpeak-Client im Channel {}", room.name(), channel); LOG.info("{}: TeamSpeak-Client im Channel {}", room.name(), channel);
} catch (Exception e) { } 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()); roomClients.remove(room.key());
if (client != null) client.disconnect(); if (client != null) client.disconnect();
} }
@@ -151,6 +170,8 @@ public final class TeamspeakSide implements TS3Listener {
socket.addListener(this); socket.addListener(this);
observer = socket; observer = socket;
socket.connect(new InetSocketAddress(config.host(), config.port()), emptyToNull(config.password()), 15000L); socket.connect(new InetSocketAddress(config.host(), config.port()), emptyToNull(config.password()), 15000L);
// Clients in nicht abonnierten Channels meldet der Server nicht
socket.subscribeAll();
awaitInitialState(); awaitInitialState();
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());