Update connection and chat handling

This commit is contained in:
2026-09-06 19:14:42 +00:00
parent 62d5af7cf9
commit ad62f5fc92
6 changed files with 316 additions and 27 deletions

View File

@@ -586,8 +586,9 @@ final class ConnectionEventHandler implements TS3Listener {
notification = SoundEvent.CHAT_RECEIVED_MESSAGE_SERVER;
break;
}
conn.persistIncomingChat(scope, e.getInvokerId(), e.getInvokerUniqueId(), e.getInvokerName(), e.getMessage());
conn.sound(notification, conn.clientVars(e.getInvokerId(), e.getInvokerName()));
conn.ui.onChat(scope, e.getInvokerId(), e.getInvokerName(), e.getMessage());
conn.ui.onChat(scope, e.getInvokerId(), e.getInvokerUniqueId(), e.getInvokerName(), e.getMessage());
}
@Override

View File

@@ -1,5 +1,7 @@
package com.ts3client.net;
import java.util.List;
/**
* UI-facing callbacks fired by {@link TeamspeakConnection}. Implementations are
* responsible for marshalling to the Swing EDT.
@@ -24,7 +26,15 @@ public interface ConnectionListener {
/** A group icon finished downloading; repaint anything showing icons. */
void onIconsUpdated();
void onChat(ChatScope scope, int fromClientId, String fromName, String message);
void onChat(ChatScope scope, int fromClientId, String fromUniqueId, String fromName, String message);
/** Previously persisted TS3 HTML log entries for one chat scope. */
default void onChatHistory(ChatScope scope, List<String> htmlEntries) {
}
/** Previously persisted TS3 HTML log entries for a private chat. */
default void onPrivateChatHistory(int clientId, String nickname, List<String> htmlEntries) {
}
/** A client (possibly the local one) started or stopped talking. */
void onTalkStateChanged(int clientId, boolean talking);
@@ -40,4 +50,9 @@ public interface ConnectionListener {
* a group being (un)assigned, and so on.
*/
void onServerLog(String message);
/** A server-log line already rendered in TS3's HTML log format. */
default void onServerLogHtml(String html) {
onServerLog(html == null ? "" : html);
}
}

View File

@@ -4,6 +4,7 @@ import com.github.manevolent.ts3j.api.Channel;
import com.github.manevolent.ts3j.api.Client;
import com.github.manevolent.ts3j.command.MultiCommand;
import com.github.manevolent.ts3j.command.SingleCommand;
import com.github.manevolent.ts3j.command.parameter.CommandParameter;
import com.github.manevolent.ts3j.command.parameter.CommandSingleParameter;
import com.github.manevolent.ts3j.event.*;
import com.github.manevolent.ts3j.identity.LocalIdentity;
@@ -13,6 +14,8 @@ import com.github.manevolent.ts3j.util.Ts3Crypt;
import com.ts3client.audio.AudioBackend;
import com.ts3client.audio.VoiceInput;
import com.ts3client.audio.VoiceOutput;
import com.ts3client.chatlog.ChatLogScope;
import com.ts3client.chatlog.ChatLogStore;
import com.ts3client.config.Settings;
import com.ts3client.net.filetransfer.FileTransfer;
import com.ts3client.net.filetransfer.FileTransferManager;
@@ -22,8 +25,10 @@ import com.ts3client.sound.SoundNotifier;
import com.ts3client.text.TsLink;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -72,6 +77,7 @@ public final class TeamspeakConnection implements TS3Listener {
private VoiceOutput playback;
private LocalIdentity identity;
private FileTransferManager fileTransfers;
private ChatLogStore chatLogs;
/**
* Runs ts3j's event callbacks. Supplying it ourselves (rather than leaving ts3j to make
* its own) is what lets {@link #awaitEventsProcessed()} tell when everything the server
@@ -287,6 +293,7 @@ public final class TeamspeakConnection implements TS3Listener {
leaving = false;
disconnectAnnounced = false;
deafened = false;
loadChatLogs();
ui.onConnected();
requestPermissionNames();
@@ -517,6 +524,7 @@ public final class TeamspeakConnection implements TS3Listener {
playback = null;
client = null;
fileTransfers = null;
chatLogs = null;
selfClientId = -1;
stats.clear();
@@ -749,6 +757,7 @@ public final class TeamspeakConnection implements TS3Listener {
ClientEntry self = model.getClient(selfClientId);
int cid = self != null ? self.channelId : 0;
client.sendChannelMessage(cid, text);
persistOutgoingChat(ConnectionListener.ChatScope.CHANNEL, 0, "", text);
sound(SoundEvent.CHAT_SENT_MESSAGE_CHANNEL, channelVars(cid, null));
} catch (Exception e) {
error("Message failed: " + rootMessage(e));
@@ -760,6 +769,7 @@ public final class TeamspeakConnection implements TS3Listener {
new Thread(() -> {
try {
client.sendServerMessage(text);
persistOutgoingChat(ConnectionListener.ChatScope.SERVER, 0, "", text);
sound(SoundEvent.CHAT_SENT_MESSAGE_SERVER, serverVars());
} catch (Exception e) {
error("Message failed: " + rootMessage(e));
@@ -771,6 +781,16 @@ public final class TeamspeakConnection implements TS3Listener {
new Thread(() -> {
try {
client.sendPrivateMessage(clientId, text);
ClientEntry peer = model.getClient(clientId);
String peerUid = peer != null ? peer.uniqueId : "";
if (peerUid == null || peerUid.isBlank()) {
try {
Client info = client.getClientInfo(clientId);
if (info != null) peerUid = info.getUniqueIdentifier();
} catch (Exception ignored) {
}
}
persistOutgoingChat(ConnectionListener.ChatScope.PRIVATE, clientId, peerUid, text);
sound(SoundEvent.CHAT_SENT_MESSAGE_CLIENT, clientVars(clientId, null));
} catch (Exception e) {
error("Message failed: " + rootMessage(e));
@@ -1199,9 +1219,162 @@ public final class TeamspeakConnection implements TS3Listener {
sounds.fire(event, deafened, variables);
}
private void loadChatLogs() {
try {
String uniqueId = serverUniqueId();
if (uniqueId == null) {
chatLogs = null;
ui.onStatus("Chat logs unavailable: server unique id was not reported");
return;
}
ChatLogStore logs = new ChatLogStore(uniqueId);
chatLogs = logs;
logs.begin();
List<String> serverHistory = logs.serverHtmlHistory();
List<String> channelHistory = logs.channelHtmlHistory();
ui.onChatHistory(ConnectionListener.ChatScope.SERVER, serverHistory);
ui.onChatHistory(ConnectionListener.ChatScope.CHANNEL, channelHistory);
ui.onStatus("Loaded chat logs from " + logs.serverDir()
+ " (" + serverHistory.size() + " server, " + channelHistory.size() + " channel)");
} catch (Exception e) {
chatLogs = null;
ui.onStatus("Chat logs unavailable: " + rootMessage(e));
}
}
private String serverUniqueId() {
String uniqueId = validServerUniqueId(firstPresent(
client.getServerProperties().get("virtualserver_unique_identifier"),
client.getServerProperties().get("virtualserver_unique_id")));
if (uniqueId != null) return uniqueId;
uniqueId = serverUniqueIdFrom(new SingleCommand("serverinfo", ProtocolRole.CLIENT));
if (uniqueId != null) return uniqueId;
try {
SingleCommand command = new SingleCommand("servergetvariables", ProtocolRole.CLIENT);
String serverId = client.getServerProperties().get("virtualserver_id");
if (serverId != null && !serverId.isBlank()) {
command.add(new CommandSingleParameter("virtualserver_id", serverId));
}
return serverUniqueIdFrom(command);
} catch (Exception ignored) {
return null;
}
}
private String serverUniqueIdFrom(SingleCommand command) {
try {
for (SingleCommand response : client.executeCommand(command).get()) {
String uniqueId = validServerUniqueId(firstPresent(
parameterValue(response, "virtualserver_unique_identifier"),
parameterValue(response, "virtualserver_unique_id")));
if (uniqueId != null) return uniqueId;
}
return null;
} catch (Exception e) {
return null;
}
}
private static String validServerUniqueId(String value) {
if (value == null || value.isBlank()) return null;
try {
byte[] decoded = Base64.getDecoder().decode(value);
if (decoded.length != 20) return null;
// TS3 server UIDs are SHA-1-sized binary identifiers represented as base64.
// Plain host strings are rejected before they can become log folder names.
boolean printable = true;
for (byte b : decoded) {
int c = b & 0xff;
if (c < 0x20 || c > 0x7e) {
printable = false;
break;
}
}
return printable ? null : value;
} catch (IllegalArgumentException e) {
return null;
}
}
private static String firstPresent(String... values) {
for (String value : values) {
if (value != null && !value.isBlank()) return value;
}
return null;
}
private static String parameterValue(SingleCommand command, String key) {
CommandParameter parameter = command.get(key);
return parameter == null ? null : parameter.getValue();
}
public void loadPrivateChatHistory(int clientId, String uniqueId) {
ChatLogStore logs = chatLogs;
if (logs == null || uniqueId == null || uniqueId.isBlank()) return;
try {
ClientEntry c = model.getClient(clientId);
ui.onPrivateChatHistory(clientId, c != null ? c.nickname : "Client " + clientId,
logs.privateHtmlHistory(uniqueId));
} catch (IOException e) {
ui.onStatus("Private chat log unavailable: " + rootMessage(e));
}
}
void persistIncomingChat(ConnectionListener.ChatScope scope, int fromClientId, String fromUniqueId,
String fromName, String message) {
ChatLogStore logs = chatLogs;
if (logs == null) return;
try {
logs.appendMessage(toLogScope(scope), false, fromClientId, fromUniqueId, fromName, message);
} catch (IOException e) {
ui.onStatus("Could not write chat log: " + rootMessage(e));
}
}
private void persistOutgoingChat(ConnectionListener.ChatScope scope, int peerClientId, String peerUniqueId,
String message) {
ChatLogStore logs = chatLogs;
if (logs == null) return;
try {
ClientEntry self = model.getClient(selfClientId);
String uid = identity != null ? identity.getUid().toBase64() : "";
String name = self != null ? self.nickname : settings.nickname;
if (scope == ConnectionListener.ChatScope.PRIVATE) {
if (peerUniqueId == null || peerUniqueId.isBlank()) return;
logs.appendPrivateMessage(peerUniqueId, true, selfClientId, uid, name, message);
} else {
logs.appendMessage(toLogScope(scope), true, selfClientId, uid, name, message);
}
} catch (IOException e) {
ui.onStatus("Could not write chat log: " + rootMessage(e));
}
}
private static ChatLogScope toLogScope(ConnectionListener.ChatScope scope) {
return switch (scope) {
case SERVER -> ChatLogScope.SERVER;
case CHANNEL -> ChatLogScope.CHANNEL;
case PRIVATE -> ChatLogScope.PRIVATE;
};
}
/** Records a line in the server tab's log, the way native TS3 reports server activity. */
void log(String message) {
if (connected) ui.onServerLog(message);
if (!connected) return;
ChatLogStore logs = chatLogs;
if (logs == null) {
ui.onServerLog(message);
return;
}
try {
ui.onServerLogHtml(logs.appendServerEvent(message));
} catch (IOException e) {
ui.onServerLog(message);
ui.onStatus("Could not write server log: " + rootMessage(e));
}
}
String channelName(int channelId) {