Channels per Name oder ID, Namen werden aufgelöst und als ID in die Config zurückgeschrieben
This commit is contained in:
1 parent
70e51abaf3
commit
3c0e3ed17a
9 files changed
+208
-40
No files matched your search
@@ -41,6 +41,10 @@ im Spiel kommt jeder externe Sprecher einzeln an (Lautstärke-Kategorie „Disco
|
|||||||
|
|
||||||
Vorlage: `bridge/config.example.yml`. Die echte `config.yml` enthält Bot-Tokens und gehört nicht ins Repo.
|
Vorlage: `bridge/config.example.yml`. Die echte `config.yml` enthält Bot-Tokens und gehört nicht ins Repo.
|
||||||
|
|
||||||
|
`teamspeakChannel` und `discordChannel` pro Raum dürfen ein Name oder eine ID sein. Einen Namen löst die
|
||||||
|
Bridge beim Start auf (oder legt den Channel an) und schreibt die ID in die `config.yml` zurück, nur diese
|
||||||
|
Zeile, mit dem alten Namen als Kommentar. Die Datei muss dafür für den Dienst beschreibbar sein.
|
||||||
|
|
||||||
## Stolpersteine
|
## Stolpersteine
|
||||||
|
|
||||||
- **glibc ≥ 2.38**: Die `libdave.so` aus JDave (und Discords eigene Builds) brauchen glibc 2.38 und
|
- **glibc ≥ 2.38**: Die `libdave.so` aus JDave (und Discords eigene Builds) brauchen glibc 2.38 und
|
||||||
|
|||||||
@@ -17,10 +17,8 @@ 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:
|
# Nur zum Anlegen fehlender Raum-Channels: darunter landen sie (Channel-ID oder Name, 0 = oberste Ebene).
|
||||||
# Channel-ID, Name eines Channels auf oberster Ebene oder 0 für die oberste Ebene selbst.
|
parentChannel: 0
|
||||||
# Besser die ID: Wird der Channel umbenannt, findet die Bridge ihn über den Namen nicht mehr.
|
|
||||||
parentChannel: 264
|
|
||||||
# ServerQuery über SSH: sieht alle Clients und Channels, belegt keinen Slot.
|
# ServerQuery über SSH: sieht alle Clients und Channels, belegt keinen Slot.
|
||||||
# Den Login legt man in TeamSpeak unter Extras → ServerQuery-Login an (für eine Identität in der Gruppe "Bridge").
|
# Den Login legt man in TeamSpeak unter Extras → ServerQuery-Login an (für eine Identität in der Gruppe "Bridge").
|
||||||
# Der Query-Flood-Schutz sperrt die IP schnell; am besten die IP der Bridge in query_ip_allowlist.txt eintragen.
|
# Der Query-Flood-Schutz sperrt die IP schnell; am besten die IP der Bridge in query_ip_allowlist.txt eintragen.
|
||||||
@@ -39,7 +37,7 @@ teamspeak:
|
|||||||
|
|
||||||
discord:
|
discord:
|
||||||
guild: "000000000000000000"
|
guild: "000000000000000000"
|
||||||
# In dieser Kategorie legt die Bridge fehlende Raum-Channels an (leer = ohne Kategorie).
|
# Nur zum Anlegen fehlender Raum-Channels: in dieser Kategorie (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% (MC/TS)"
|
nickname: "%room% (MC/TS)"
|
||||||
@@ -49,11 +47,11 @@ rooms:
|
|||||||
name: Crosstalk 1
|
name: Crosstalk 1
|
||||||
# Ein eigener Bot pro Raum (Discord erlaubt pro Bot nur einen Voice-Channel). Leer = ohne Discord.
|
# Ein eigener Bot pro Raum (Discord erlaubt pro Bot nur einen Voice-Channel). Leer = ohne Discord.
|
||||||
discordToken: ""
|
discordToken: ""
|
||||||
# Feste Channels per ID: dann dürfen sie beliebig umbenannt und verschoben werden, und
|
# Channel als Name oder ID (leer = Raumname). Einen Namen sucht die Bridge auf dem ganzen Server,
|
||||||
# teamspeak.parentChannel / discord.category werden nicht gebraucht. Ohne ID sucht die Bridge
|
# legt den Channel sonst an (unter teamspeak.parentChannel bzw. in discord.category) und schreibt
|
||||||
# den Channel über den Raumnamen unter parentChannel bzw. in category und legt ihn sonst an.
|
# danach die ID hier hinein. Ab dann dürfen die Channels frei umbenannt und verschoben werden.
|
||||||
# discordChannel: "000000000000000000"
|
teamspeakChannel: "Crosstalk 1"
|
||||||
# teamspeakChannel: 0
|
discordChannel: "Crosstalk 1"
|
||||||
- key: crosstalk-2
|
- key: crosstalk-2
|
||||||
name: Crosstalk 2
|
name: Crosstalk 2
|
||||||
discordToken: ""
|
discordToken: ""
|
||||||
|
|||||||
@@ -44,18 +44,18 @@ public final class Bridge {
|
|||||||
boolean teamspeakConnected, discordConnected;
|
boolean teamspeakConnected, discordConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bridge(BridgeConfig config) throws IOException {
|
public Bridge(BridgeConfig config, ConfigUpdater updater) throws IOException {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
for (BridgeConfig.RoomConfig roomConfig : config.rooms()) {
|
for (BridgeConfig.RoomConfig roomConfig : config.rooms()) {
|
||||||
Room room = new Room(roomConfig);
|
Room room = new Room(roomConfig);
|
||||||
rooms.put(room.key(), room);
|
rooms.put(room.key(), room);
|
||||||
presence.put(room.key(), new Presence());
|
presence.put(room.key(), new Presence());
|
||||||
if (roomConfig.discordEnabled()) discordBots.put(room.key(), new DiscordRoomBot(config.discord(), room));
|
if (roomConfig.discordEnabled()) discordBots.put(room.key(), new DiscordRoomBot(config.discord(), room, updater));
|
||||||
}
|
}
|
||||||
this.plugin = new PluginLink(config.pluginBind(), config.pluginPort(), rooms, () -> roomInfos);
|
this.plugin = new PluginLink(config.pluginBind(), config.pluginPort(), rooms, () -> roomInfos);
|
||||||
for (Room room : rooms.values()) room.setMinecraftSink(plugin::sendAudio);
|
for (Room room : rooms.values()) room.setMinecraftSink(plugin::sendAudio);
|
||||||
this.teamspeak = config.teamspeak().enabled()
|
this.teamspeak = config.teamspeak().enabled()
|
||||||
? new TeamspeakSide(config.teamspeak(), config.dataDir(), List.copyOf(rooms.values()))
|
? new TeamspeakSide(config.teamspeak(), config.dataDir(), List.copyOf(rooms.values()), updater)
|
||||||
: null;
|
: null;
|
||||||
roomInfos = buildInfos();
|
roomInfos = buildInfos();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,12 @@ public record BridgeConfig(
|
|||||||
|
|
||||||
public record Discord(String guild, String category, String nickname) {}
|
public record Discord(String guild, String category, String nickname) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code discordChannel} und {@code teamspeakChannel} sind eine Channel-ID oder ein Channel-Name
|
||||||
|
* (leer = Raumname). Namen löst die Bridge auf und schreibt die ID zurück in die Config.
|
||||||
|
*/
|
||||||
public record RoomConfig(String key, String name, String discordToken, String discordChannel,
|
public record RoomConfig(String key, String name, String discordToken, String discordChannel,
|
||||||
int teamspeakChannel) {
|
String teamspeakChannel) {
|
||||||
public boolean discordEnabled() { return !discordToken.isBlank(); }
|
public boolean discordEnabled() { return !discordToken.isBlank(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +67,7 @@ public record BridgeConfig(
|
|||||||
if (key.isBlank()) throw new IOException("Raum ohne key in " + file);
|
if (key.isBlank()) throw new IOException("Raum ohne key in " + file);
|
||||||
if (!keys.add(key)) throw new IOException("Raum-key doppelt: " + key);
|
if (!keys.add(key)) throw new IOException("Raum-key doppelt: " + key);
|
||||||
rooms.add(new RoomConfig(key, string(room, "name", key), string(room, "discordToken", ""),
|
rooms.add(new RoomConfig(key, string(room, "name", key), string(room, "discordToken", ""),
|
||||||
string(room, "discordChannel", ""), integer(room, "teamspeakChannel", 0)));
|
string(room, "discordChannel", ""), string(room, "teamspeakChannel", "")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package eu.mhsl.voicebridge.bridge;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schreibt aufgelöste Channel-IDs in die config.yml zurück. Geändert (oder ergänzt) wird nur die eine
|
||||||
|
* Zeile im Block des Raums, damit Kommentare und Formatierung erhalten bleiben.
|
||||||
|
*/
|
||||||
|
public final class ConfigUpdater {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ConfigUpdater.class);
|
||||||
|
private static final Pattern ROOM_START = Pattern.compile("^(\\s*)-\\s+key:\\s*\"?([^\"#\\s]+)\"?.*$");
|
||||||
|
|
||||||
|
private final Path file;
|
||||||
|
|
||||||
|
public ConfigUpdater(Path file) {
|
||||||
|
this.file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Setzt {@code field} im Raum {@code roomKey} auf {@code value}; {@code was} landet als Kommentar dahinter. */
|
||||||
|
public synchronized void setRoomValue(String roomKey, String field, String value, String was) {
|
||||||
|
try {
|
||||||
|
List<String> lines = new ArrayList<>(Files.readAllLines(file));
|
||||||
|
if (!apply(lines, roomKey, field, value, was)) {
|
||||||
|
LOG.warn("Raum {} nicht in {} gefunden, {} bitte von Hand eintragen: {}", roomKey, file, field, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Path tmp = file.resolveSibling(file.getFileName() + ".tmp");
|
||||||
|
Files.write(tmp, lines);
|
||||||
|
// Rechte übernehmen, die Datei enthält Tokens
|
||||||
|
try {
|
||||||
|
Files.setPosixFilePermissions(tmp, Files.getPosixFilePermissions(file));
|
||||||
|
} catch (UnsupportedOperationException ignored) {
|
||||||
|
// kein POSIX-Dateisystem
|
||||||
|
}
|
||||||
|
Files.move(tmp, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
LOG.info("{}: {} = {} in {} eingetragen", roomKey, field, value, file.getFileName());
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warn("Konnte {} nicht aktualisieren ({}), {} bitte von Hand eintragen: {}", file, e.getMessage(), field, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean apply(List<String> lines, String roomKey, String field, String value, String was) {
|
||||||
|
int start = -1;
|
||||||
|
String itemIndent = null;
|
||||||
|
for (int i = 0; i < lines.size(); i++) {
|
||||||
|
Matcher m = ROOM_START.matcher(lines.get(i));
|
||||||
|
if (m.matches() && m.group(2).equals(roomKey)) {
|
||||||
|
start = i;
|
||||||
|
itemIndent = m.group(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (start < 0) return false;
|
||||||
|
|
||||||
|
int end = lines.size();
|
||||||
|
for (int i = start + 1; i < lines.size(); i++) {
|
||||||
|
String line = lines.get(i);
|
||||||
|
if (line.isBlank() || line.stripLeading().startsWith("#")) continue;
|
||||||
|
int indent = line.length() - line.stripLeading().length();
|
||||||
|
if (indent <= itemIndent.length()) {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String fieldIndent = itemIndent + " ";
|
||||||
|
String comment = was == null || was.isBlank() ? "" : " # war: " + was;
|
||||||
|
String newLine = fieldIndent + field + ": " + value + comment;
|
||||||
|
Pattern fieldPattern = Pattern.compile("^\\s*" + Pattern.quote(field) + ":.*$");
|
||||||
|
for (int i = start + 1; i < end; i++) {
|
||||||
|
if (fieldPattern.matcher(lines.get(i)).matches()) {
|
||||||
|
lines.set(i, newLine);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.add(start + 1, newLine);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ public final class Main {
|
|||||||
// -Dvoicebridge.ts3debug=true schaltet das Protokoll-Log von ts3j ein
|
// -Dvoicebridge.ts3debug=true schaltet das Protokoll-Log von ts3j ein
|
||||||
if (Boolean.getBoolean("voicebridge.ts3debug")) Ts3Debugging.setEnabled(true);
|
if (Boolean.getBoolean("voicebridge.ts3debug")) Ts3Debugging.setEnabled(true);
|
||||||
|
|
||||||
Bridge bridge = new Bridge(BridgeConfig.load(configFile));
|
Bridge bridge = new Bridge(BridgeConfig.load(configFile), new ConfigUpdater(configFile));
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(bridge::stop, "shutdown"));
|
Runtime.getRuntime().addShutdownHook(new Thread(bridge::stop, "shutdown"));
|
||||||
bridge.start();
|
bridge.start();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.mhsl.voicebridge.bridge.discord;
|
|||||||
|
|
||||||
import club.minnced.discord.jdave.interop.JDaveSessionFactory;
|
import club.minnced.discord.jdave.interop.JDaveSessionFactory;
|
||||||
import eu.mhsl.voicebridge.bridge.BridgeConfig;
|
import eu.mhsl.voicebridge.bridge.BridgeConfig;
|
||||||
|
import eu.mhsl.voicebridge.bridge.ConfigUpdater;
|
||||||
import eu.mhsl.voicebridge.bridge.Room;
|
import eu.mhsl.voicebridge.bridge.Room;
|
||||||
import eu.mhsl.voicebridge.bridge.audio.Opus;
|
import eu.mhsl.voicebridge.bridge.audio.Opus;
|
||||||
import io.github.jaredmdobson.concentus.OpusEncoder;
|
import io.github.jaredmdobson.concentus.OpusEncoder;
|
||||||
@@ -27,6 +28,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@@ -39,6 +41,7 @@ public final class DiscordRoomBot implements AudioSendHandler, AudioReceiveHandl
|
|||||||
private static final Logger LOG = LoggerFactory.getLogger(DiscordRoomBot.class);
|
private static final Logger LOG = LoggerFactory.getLogger(DiscordRoomBot.class);
|
||||||
|
|
||||||
private final BridgeConfig.Discord config;
|
private final BridgeConfig.Discord config;
|
||||||
|
private final ConfigUpdater updater;
|
||||||
private final Room room;
|
private final Room room;
|
||||||
private final OpusEncoder encoder = Opus.encoder(2, 64000);
|
private final OpusEncoder encoder = Opus.encoder(2, 64000);
|
||||||
private final ExecutorService executor;
|
private final ExecutorService executor;
|
||||||
@@ -48,8 +51,9 @@ public final class DiscordRoomBot implements AudioSendHandler, AudioReceiveHandl
|
|||||||
private volatile long channelId;
|
private volatile long channelId;
|
||||||
private volatile boolean joined;
|
private volatile boolean joined;
|
||||||
|
|
||||||
public DiscordRoomBot(BridgeConfig.Discord config, Room room) {
|
public DiscordRoomBot(BridgeConfig.Discord config, Room room, ConfigUpdater updater) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.updater = updater;
|
||||||
this.room = room;
|
this.room = room;
|
||||||
this.executor = Executors.newSingleThreadExecutor(
|
this.executor = Executors.newSingleThreadExecutor(
|
||||||
Thread.ofPlatform().daemon().name("discord-" + room.key()).factory());
|
Thread.ofPlatform().daemon().name("discord-" + room.key()).factory());
|
||||||
@@ -104,28 +108,34 @@ public final class DiscordRoomBot implements AudioSendHandler, AudioReceiveHandl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raum-Channel auflösen: eine ID (nur Ziffern) wird direkt genommen, ein Name (leer = Raumname) auf dem
|
||||||
|
* ganzen Server gesucht und sonst in {@code category} angelegt. Die ID landet danach in der Config.
|
||||||
|
*/
|
||||||
private VoiceChannel resolveChannel(Guild guild) {
|
private VoiceChannel resolveChannel(Guild guild) {
|
||||||
String configured = room.config().discordChannel();
|
String configured = room.config().discordChannel();
|
||||||
if (!configured.isBlank()) {
|
String ref = configured.isBlank() ? room.name() : configured;
|
||||||
VoiceChannel channel = guild.getVoiceChannelById(configured);
|
if (ref.chars().allMatch(Character::isDigit)) {
|
||||||
if (channel == null) throw new IllegalStateException("Voice-Channel " + configured + " gibt es nicht");
|
VoiceChannel channel = guild.getVoiceChannelById(ref);
|
||||||
|
if (channel == null) throw new IllegalStateException("Voice-Channel " + ref + " gibt es nicht");
|
||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<VoiceChannel> matches = guild.getVoiceChannelsByName(ref, false);
|
||||||
|
if (matches.size() > 1) {
|
||||||
|
throw new IllegalStateException("mehrere Voice-Channels heißen \"" + ref + "\", bitte die ID eintragen");
|
||||||
|
}
|
||||||
|
VoiceChannel channel;
|
||||||
|
if (matches.size() == 1) {
|
||||||
|
channel = matches.getFirst();
|
||||||
|
} else {
|
||||||
Category category = config.category().isBlank() ? null : guild.getCategoryById(config.category());
|
Category category = config.category().isBlank() ? null : guild.getCategoryById(config.category());
|
||||||
if (!config.category().isBlank() && category == null) {
|
LOG.info("{}: lege Discord-Voice-Channel {} an", room.name(), ref);
|
||||||
throw new IllegalStateException("Kategorie " + config.category() + " gibt es nicht");
|
channel = category != null ? category.createVoiceChannel(ref).complete() : guild.createVoiceChannel(ref).complete();
|
||||||
}
|
}
|
||||||
for (VoiceChannel channel : category != null ? category.getVoiceChannels() : guild.getVoiceChannels()) {
|
updater.setRoomValue(room.key(), "discordChannel", "\"" + channel.getId() + "\"", ref);
|
||||||
if (channel.getName().equals(room.name()) && (category != null || channel.getParentCategory() == null)) {
|
|
||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
LOG.info("{}: lege Discord-Voice-Channel an", room.name());
|
|
||||||
return category != null
|
|
||||||
? category.createVoiceChannel(room.name()).complete()
|
|
||||||
: guild.createVoiceChannel(room.name()).complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Menschen im Raum-Channel (Bots zählen nicht). */
|
/** Menschen im Raum-Channel (Bots zählen nicht). */
|
||||||
public int humans() {
|
public int humans() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.mhsl.voicebridge.bridge.teamspeak;
|
|||||||
|
|
||||||
import com.github.manevolent.ts3j.identity.LocalIdentity;
|
import com.github.manevolent.ts3j.identity.LocalIdentity;
|
||||||
import eu.mhsl.voicebridge.bridge.BridgeConfig;
|
import eu.mhsl.voicebridge.bridge.BridgeConfig;
|
||||||
|
import eu.mhsl.voicebridge.bridge.ConfigUpdater;
|
||||||
import eu.mhsl.voicebridge.bridge.Room;
|
import eu.mhsl.voicebridge.bridge.Room;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -37,6 +38,7 @@ public final class TeamspeakSide {
|
|||||||
private record TsChannel(int parent, String name) {}
|
private record TsChannel(int parent, String name) {}
|
||||||
|
|
||||||
private final BridgeConfig.Teamspeak config;
|
private final BridgeConfig.Teamspeak config;
|
||||||
|
private final ConfigUpdater updater;
|
||||||
private final Path dataDir;
|
private final Path dataDir;
|
||||||
private final List<Room> rooms;
|
private final List<Room> rooms;
|
||||||
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(
|
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(
|
||||||
@@ -55,8 +57,9 @@ public final class TeamspeakSide {
|
|||||||
/** Channel, unter dem die Raum-Channels liegen (0 = oberste Ebene). */
|
/** Channel, unter dem die Raum-Channels liegen (0 = oberste Ebene). */
|
||||||
private volatile int parentChannel;
|
private volatile int parentChannel;
|
||||||
|
|
||||||
public TeamspeakSide(BridgeConfig.Teamspeak config, Path dataDir, List<Room> rooms) {
|
public TeamspeakSide(BridgeConfig.Teamspeak config, Path dataDir, List<Room> rooms, ConfigUpdater updater) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.updater = updater;
|
||||||
this.dataDir = dataDir;
|
this.dataDir = dataDir;
|
||||||
this.rooms = rooms;
|
this.rooms = rooms;
|
||||||
}
|
}
|
||||||
@@ -214,28 +217,42 @@ public final class TeamspeakSide {
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raum-Channels auflösen: eine ID wird direkt genommen, ein Name (leer = Raumname) auf dem ganzen
|
||||||
|
* Server gesucht und sonst unter {@code parentChannel} angelegt. Die ID landet danach in der Config.
|
||||||
|
*/
|
||||||
private void resolveChannels(TsQuery q) {
|
private void resolveChannels(TsQuery q) {
|
||||||
for (Room room : rooms) {
|
for (Room room : rooms) {
|
||||||
if (roomChannels.containsKey(room.key())) continue;
|
if (roomChannels.containsKey(room.key())) continue;
|
||||||
int configured = room.config().teamspeakChannel();
|
String ref = room.config().teamspeakChannel().isBlank() ? room.name() : room.config().teamspeakChannel();
|
||||||
if (configured > 0) {
|
int id = parse(ref, -1);
|
||||||
roomChannels.put(room.key(), configured);
|
if (id > 0) {
|
||||||
|
roomChannels.put(room.key(), id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Integer found = findChannel(parentChannel, room.name());
|
|
||||||
if (found == null) {
|
List<Integer> matches = channels.entrySet().stream()
|
||||||
|
.filter(e -> e.getValue().name().equals(ref)).map(Map.Entry::getKey).toList();
|
||||||
|
if (matches.size() > 1) {
|
||||||
|
LOG.error("{}: mehrere TeamSpeak-Channels heißen \"{}\" ({}), bitte die ID eintragen", room.name(), ref, matches);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (matches.size() == 1) {
|
||||||
|
id = matches.getFirst();
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
found = parse(q.send("channelcreate", "channel_name", room.name(), "cpid", Integer.toString(parentChannel),
|
id = parse(q.send("channelcreate", "channel_name", ref, "cpid", Integer.toString(parentChannel),
|
||||||
"channel_flag_permanent", "1", "channel_codec", "4").getFirst().get("cid"), -1);
|
"channel_flag_permanent", "1", "channel_codec", "4").getFirst().get("cid"), -1);
|
||||||
channels.put(found, new TsChannel(parentChannel, room.name()));
|
channels.put(id, new TsChannel(parentChannel, ref));
|
||||||
LOG.info("TeamSpeak-Channel angelegt: {} ({})", room.name(), found);
|
LOG.info("TeamSpeak-Channel angelegt: {} ({})", ref, id);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.error("{}: TeamSpeak-Channel konnte nicht angelegt werden ({}). Fehlen dem Query-Login die "
|
LOG.error("{}: TeamSpeak-Channel konnte nicht angelegt werden ({}). Fehlen dem Query-Login die "
|
||||||
+ "Rechte zum Anlegen? Bis dahin ohne TeamSpeak.", room.name(), e.getMessage());
|
+ "Rechte zum Anlegen? Bis dahin ohne TeamSpeak.", room.name(), e.getMessage());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomChannels.put(room.key(), found);
|
roomChannels.put(room.key(), id);
|
||||||
|
updater.setRoomValue(room.key(), "teamspeakChannel", Integer.toString(id), ref);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package eu.mhsl.voicebridge.bridge;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ConfigUpdaterTest {
|
||||||
|
|
||||||
|
private static List<String> config() {
|
||||||
|
return new ArrayList<>(List.of(
|
||||||
|
"rooms:",
|
||||||
|
" - key: crosstalk-1",
|
||||||
|
" name: Crosstalk 1",
|
||||||
|
" teamspeakChannel: \"Crosstalk 1\" # Name reicht",
|
||||||
|
" discordToken: \"x\"",
|
||||||
|
"",
|
||||||
|
" # Kommentar",
|
||||||
|
" - key: crosstalk-2",
|
||||||
|
" name: Crosstalk 2",
|
||||||
|
" discordToken: \"y\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void replacesExistingField() {
|
||||||
|
List<String> lines = config();
|
||||||
|
assertTrue(ConfigUpdater.apply(lines, "crosstalk-1", "teamspeakChannel", "265", "Crosstalk 1"));
|
||||||
|
assertEquals(" teamspeakChannel: 265 # war: Crosstalk 1", lines.get(3));
|
||||||
|
assertEquals(10, lines.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addsMissingFieldOnlyInItsRoom() {
|
||||||
|
List<String> lines = config();
|
||||||
|
assertTrue(ConfigUpdater.apply(lines, "crosstalk-2", "discordChannel", "\"123\"", null));
|
||||||
|
assertEquals(" discordChannel: \"123\"", lines.get(8));
|
||||||
|
assertEquals(" teamspeakChannel: \"Crosstalk 1\" # Name reicht", lines.get(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unknownRoom() {
|
||||||
|
assertFalse(ConfigUpdater.apply(config(), "crosstalk-9", "teamspeakChannel", "1", null));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user