using axXez.Threading;
using axXez.TS3.Protocol;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace axXez.TS3
{
/// Manages the lifecycle of a TeamSpeak ServerQuery connection: authentication, event routing, channel/client cache, and watcher tasks.
public class ServerConnection
{
readonly QueryClient query;
readonly int serverID;
readonly List _voiceConnections = new();
string nickname;
int channelUpdateInterval;
int clientUpdateInterval;
string defaultChannelName;
bool autoDisableQueryFloodProtection;
VersionInfo versionInfo;
HostInfo hostInfo;
InstanceInfo instanceInfo;
ServerInfo serverInfo;
Client queryClient;
Dictionary serverGroups;
Dictionary channelGroups;
Dictionary channels;
Dictionary clients;
ImmutableDictionary _serverGroupsSnapshot = ImmutableDictionary.Empty;
ImmutableDictionary _channelGroupsSnapshot = ImmutableDictionary.Empty;
ImmutableDictionary _channelsSnapshot = ImmutableDictionary.Empty;
ImmutableDictionary _clientsSnapshot = ImmutableDictionary.Empty;
//Dictionary> pendingChannelCreations = new();
LoopTask channelWatcherTask;
LoopTask clientWatcherTask;
/// The underlying used for all ServerQuery communication.
public QueryClient Query => query;
/// Whether the ServerQuery connection is currently active.
public bool Connected => query.Connected; //todo: use own connected bool? set true after logged in
/// Human-readable connection identifier in the form login@host:port.
public string ConnectionString => $"{query.Transport}://{query.LoginName}@{query.Host}:{query.Port}";
/// Connect-time snapshot of server binary: version string, build number, and OS platform.
public VersionInfo Binary => versionInfo;
/// Connect-time snapshot of instance runtime stats: uptime, total clients/channels across all virtual servers, and bandwidth counters.
public HostInfo Host => hostInfo;
/// Connect-time snapshot of instance configuration: flood protection thresholds, file transfer port, template group IDs, and DB schema version.
public InstanceInfo HostConfiguration => instanceInfo;
/// Connect-time snapshot of virtual server: name, password, limits, group defaults, anti-flood settings, banners, log flags, and per-server bandwidth stats.
public ServerInfo Configuration => serverInfo;
/// The bot's own client entry on the server, representing the query session itself.
public Client QueryClient => queryClient;
/// The channel the bot moved to on connect, or null if no default channel was configured.
public Channel QueryClientDefaultChannel { get; private set; }
/// Live snapshot of all regular server groups, keyed by group ID.
public IReadOnlyDictionary ServerGroups => _serverGroupsSnapshot;
/// Live snapshot of all regular channel groups, keyed by group ID.
public IReadOnlyDictionary ChannelGroups => _channelGroupsSnapshot;
/// Live snapshot of all channels currently on the server, keyed by channel ID.
public IReadOnlyDictionary Channels => _channelsSnapshot;
/// Live snapshot of all voice clients currently on the server, keyed by client ID.
public IReadOnlyDictionary Clients => _clientsSnapshot;
/// Handler for connection-level events (connected / disconnected).
public delegate void ServerConnectionHandler(ServerConnection sender);
/// Handler raised when a channel is created.
public delegate void ServerConnectionChannelCreateHandler(ServerConnection sender, Channel channel, Channel parent, Client invoker);
/// Handler raised when a channel is deleted.
public delegate void ServerConnectionChannelDeleteHandler(ServerConnection sender, Channel channel, Client invoker);
/// Handler raised when a channel's properties are updated by the watcher poll.
public delegate void ServerConnectionChannelUpdateHandler(ServerConnection sender, Channel channel, TsEntityChangeInfo changes);
/// Handler raised when a channel's properties are edited by a client.
public delegate void ServerConnectionChannelEditHandler(ServerConnection sender, Channel channel, TsEntityChangeInfo changes, Client invoker);
/// Handler raised when a client connects and enters view.
public delegate void ServerConnectionClientJoinHandler(ServerConnection sender, Client client, Channel source, Channel target, EventReason reason);
/// Handler raised when a client disconnects or leaves view.
public delegate void ServerConnectionClientLeaveHandler(ServerConnection sender, Client client, Channel source, Channel target, EventReason reason, Client invoker, string reasonMsg, TimeSpan banTime);
/// Handler raised when a client's properties are updated by the watcher poll.
public delegate void ServerConnectionClientUpdateHandler(ServerConnection sender, Client client, TsEntityChangeInfo changes);
/// Handler raised when a client is moved to a different channel.
public delegate void ServerConnectionClientMoveHandler(ServerConnection sender, Client client, Channel target, EventReason reason, Client invoker);
/// Handler raised when a text message is received.
public delegate void ServerConnectionTextMessageHandler(ServerConnection sender, ITeamspeakClient source, string message, Client invoker, MessageTargetMode targetMode, Client target);
/// Handler raised when the virtual server's properties are edited.
public delegate void ServerConnectionServerEditHandler(ServerConnection sender, ServerInfo serverInfo, TsEntityChangeInfo changes, EventReason reason, Client invoker);
/// Raised after the ServerQuery connection is established and initial data is loaded.
public event ServerConnectionHandler OnConnected;
/// Raised when the ServerQuery connection is lost.
public event ServerConnectionHandler OnDisconnected;
/// Raised when a channel is created on the server.
public event ServerConnectionChannelCreateHandler OnChannelCreated;
/// Raised when a channel is deleted from the server.
public event ServerConnectionChannelDeleteHandler OnChannelDeleted;
/// Raised when a channel's properties change (watcher poll or description update).
public event ServerConnectionChannelUpdateHandler OnChannelUpdated;
/// Raised when a client explicitly edits a channel's properties.
public event ServerConnectionChannelEditHandler OnChannelEdited;
/// Raised when a client joins the server or enters view.
public event ServerConnectionClientJoinHandler OnClientJoined;
/// Raised when a client leaves the server or exits view.
public event ServerConnectionClientLeaveHandler OnClientLeaved;
/// Raised when a client's properties are updated by the watcher poll.
public event ServerConnectionClientUpdateHandler OnClientUpdated;
/// Raised when a client is moved to a different channel.
public event ServerConnectionClientMoveHandler OnClientMoved;
/// Raised when a text message is received in any registered scope.
public event ServerConnectionTextMessageHandler OnTextMessage;
/// Raised when the virtual server's properties are edited.
public event ServerConnectionServerEditHandler OnServerEdited;
/// The detected TeamSpeak server version (TS3 or TS6). Set on connect, before first connect.
public ServerVersion Version { get; private set; } = ServerVersion.Unknown;
/// Nickname the bot will use on the server.
public string Nickname { get => nickname; set => SetNickname(value); }
/// Name of the channel the bot will move to on connect.
public string DefaultChannelName { get => defaultChannelName; set => MoveToChannel(value); }
/// Whether to disable the server's query flood protection on connect.
public bool AutoDisableQueryFloodProtection { get => autoDisableQueryFloodProtection; set => autoDisableQueryFloodProtection = value; }
/// Interval in milliseconds between channel list refresh polls. Set to 0 to disable.
public int ChannelUpdateInterval { get => channelUpdateInterval; set => SetChannelUpdateInterval(value); }
/// Interval in milliseconds between client list refresh polls. Set to 0 to disable.
public int ClientUpdateInterval { get => clientUpdateInterval; set => SetClientUpdateInterval(value); }
/// Initializes a new with the given connection parameters. Call to begin connecting.
public ServerConnection(string ip, int port, string loginName, string password, int serverID, QueryTransportType transport = QueryTransportType.SSH)
{
this.serverID = serverID;
query = new QueryClient(ip, port, transport)
{
LoginName = loginName,
LoginPassword = password
};
query.OnConnected += Query_OnConnected;
query.OnDisconnected += Query_OnDisconnected;
query.OnError += Query_OnError;
channelWatcherTask = new LoopTask(ChannelWatcher, stopCallback: ChannelWatcherTask_OnStop, autoRestart: true);
clientWatcherTask = new LoopTask(ClientWatcher, stopCallback: ClientWatcherTask_OnStop, autoRestart: true);
}
/// Starts the underlying query client and begins the connection process.
public void Start()
{
query.Start();
}
/// Disconnects from the server, stops all watchers, stops all voice connections, and clears the channel/client cache.
public void Stop()
{
StopChannelWatcher();
StopClientWatcher();
foreach (var conn in _voiceConnections.ToList())
RemoveVoiceConnection(conn);
if (query.Connected)
query.Disconnect();
lock (channels)
{
channels.Clear();
_channelsSnapshot = ImmutableDictionary.Empty;
}
lock (clients)
{
clients.Clear();
_clientsSnapshot = ImmutableDictionary.Empty;
}
}
/// Forces the underlying query client to drop the current connection and reconnect.
public void Reconnect()
{
query.ForceReconnect();
}
/// Creates a new targeting this server, registers it for text-message routing, and tracks it for lifecycle management.
/// If is null, a fresh identity is generated at the server's required security level.
public VoiceConnection CreateVoiceConnection(string nickname, VoiceIdentity identity = null, VoiceClientOptions options = null)
{
identity ??= VoiceIdentity.Generate(serverInfo.NeededIdentitySecurityLevel);
var conn = new VoiceConnection(query.Host, nickname: nickname, identity: identity, options: options);
_voiceConnections.Add(conn);
conn.OnTextMessage += Client_OnTextMessage;
return conn;
}
/// Unregisters, stops, and disposes a previously created by .
public void RemoveVoiceConnection(VoiceConnection conn)
{
_voiceConnections.Remove(conn);
conn.OnTextMessage -= Client_OnTextMessage;
conn.Dispose();
}
/// Changes the bot's nickname on the server. Returns false if the rename command fails while connected.
public bool SetNickname(string name)
{
if (query.Connected && !query.UpdateClient(new EditClientProperties() { Name = name }))
return false;
nickname = name;
return true;
}
/// Moves the bot to the channel matching and saves it as the default. Returns false if the channel is not found or the move command fails.
public bool MoveToChannel(string channelName)
{
if (query.Connected)
{
if (!FindChannel(channelName, out var channel))
return false;
if (!query.MoveClient(queryClient.ID, channel.ID))
return false;
QueryClientDefaultChannel = channel;
}
defaultChannelName = channelName;
return true;
}
/// Changes the channel watcher poll interval. Pass 0 to stop the watcher entirely.
public void SetChannelUpdateInterval(int intervalMs)
{
channelUpdateInterval = intervalMs;
if (!query.Connected)
return;
if (intervalMs == 0)
StopChannelWatcher();
else
StartChannelWatcher();
}
/// Changes the client watcher poll interval. Pass 0 to stop the watcher entirely.
public void SetClientUpdateInterval(int intervalMs)
{
clientUpdateInterval = intervalMs;
if (!query.Connected)
return;
if (intervalMs == 0)
StopClientWatcher();
else
StartClientWatcher();
}
#region find methods
/// Finds a server group by ID (numeric string) or by name (case-insensitive substring). Returns false if not found.
public bool FindServerGroup(string input, out ServerGroupInfo serverGroup)
{
if (string.IsNullOrWhiteSpace(input))
{
serverGroup = null;
return false;
}
if (int.TryParse(input, out int id) && FindServerGroup(id, out serverGroup))
return true;
serverGroup = _serverGroupsSnapshot.Values.FirstOrDefault(x => x.Name.Contains(input, StringComparison.InvariantCultureIgnoreCase));
return serverGroup != null;
}
/// Finds a server group by its numeric ID. Returns false if not found.
public bool FindServerGroup(int id, out ServerGroupInfo serverGroup)
=> _serverGroupsSnapshot.TryGetValue(id, out serverGroup);
/// Finds a channel group by ID (numeric string) or by name (case-insensitive substring). Returns false if not found.
public bool FindChannelGroup(string input, out ChannelGroupInfo channelGroup)
{
if (string.IsNullOrWhiteSpace(input))
{
channelGroup = null;
return false;
}
if (int.TryParse(input, out int id) && FindChannelGroup(id, out channelGroup))
return true;
channelGroup = _channelGroupsSnapshot.Values.FirstOrDefault(x => x.Name.Contains(input, StringComparison.InvariantCultureIgnoreCase));
return channelGroup != null;
}
/// Finds a channel group by its numeric ID. Returns false if not found.
public bool FindChannelGroup(int id, out ChannelGroupInfo channelGroup)
=> _channelGroupsSnapshot.TryGetValue(id, out channelGroup);
/// Finds a channel by ID (numeric string) or by name (case-insensitive substring). Returns false if not found.
public bool FindChannel(string input, out Channel channel)
{
if (string.IsNullOrWhiteSpace(input))
{
channel = null;
return false;
}
if (int.TryParse(input, out int id) && FindChannel(id, out channel))
return true;
channel = _channelsSnapshot.Values.FirstOrDefault(x => x.Name.Contains(input, StringComparison.InvariantCultureIgnoreCase));
return channel != null;
}
/// Finds a channel by its numeric ID. Returns false if not found.
public bool FindChannel(int id, out Channel channel)
=> _channelsSnapshot.TryGetValue(id, out channel);
/// Returns all direct sub-channels of the given parent channel ID. Returns false if none exist.
public bool FindSubChannels(int parentId, out IEnumerable subChannels)
{
subChannels = _channelsSnapshot.Values.Where(c => parentId.Equals(c.ParentID)).ToImmutableList();
return subChannels.Any();
}
/// Finds a client by ID (numeric string), UID, or name (case-insensitive substring). Returns false if not found.
public bool FindClient(string input, out Client client)
{
if (string.IsNullOrWhiteSpace(input))
{
client = null;
return false;
}
if (int.TryParse(input, out int id) && FindClient(id, out client))
return true;
client = _clientsSnapshot.Values.FirstOrDefault(x => x.UID == input || x.Name.Contains(input, StringComparison.InvariantCultureIgnoreCase));
return client != null;
}
/// Finds a client by their numeric client ID. Returns false if not found.
public bool FindClient(int id, out Client client)
=> _clientsSnapshot.TryGetValue(id, out client);
#endregion
//#region channel creation
//public Channel CreateChannel(string name, EditChannelProperties properties)
//{
// int cid = query.CreateChannel(properties);
// return WaitForChannelCreated(cid).Result; // deadlock when called from event handler
//}
//public Task WaitForChannelCreated(int channelID, int timeoutMs = 5000)
//{
// var tcs = new TaskCompletionSource();
// pendingChannelCreations[channelID] = tcs;
// _ = Task.Delay(timeoutMs).ContinueWith(_ =>
// {
// channels.TryGetValue(channelID, out var channel);
// if (pendingChannelCreations.Remove(channelID))
// tcs.TrySetResult(channel);
// });
// return tcs.Task;
//}
//#endregion
#region query connection handler
private void Query_OnConnected(ITeamspeakClient sender)
{
if (!query.Use(serverID, clientNickname: nickname))
throw new Exception("Server instance not found.");
//if (!query.UpdateClient(new EditClientProperties() { Name = string.IsNullOrWhiteSpace(Config.Name) ? BOT_DEFAULT_NAME : Config.Name }))
// throw new Exception("ServerQuery client renaming failed.");
//get actual server info
versionInfo = query.Version() ?? throw new Exception("Failed to retrieve server version.");
hostInfo = query.Hostinfo() ?? throw new Exception("Failed to retrieve host info.");
instanceInfo = query.InstanceInfo() ?? throw new Exception("Failed to retrieve instance info.");
serverInfo = query.ServerInfo() ?? throw new Exception("Failed to retrieve server info.");
Version = versionInfo?.Version?.StartsWith("3.") == true ? ServerVersion.TS3
: versionInfo?.Version?.StartsWith("6.") == true ? ServerVersion.TS6
: ServerVersion.Unknown;
queryClient = new Client(this, query.GetMyself()); // TODO: replace with tracked instance... track all clients!?
GlobalLogger.Log("ServerConnection", $"Server running {versionInfo.Version} ({versionInfo.Build}) on {versionInfo.Platform}, Uptime: {hostInfo.HostTime}, DB: {instanceInfo.DatabaseVersion}");
if (autoDisableQueryFloodProtection && instanceInfo.IsQueryFloodProtectionEnabled)
{
query.DisableQueryFloodProtection();
instanceInfo = query.InstanceInfo();
}
//else auto configurate flood protection in query client
GlobalLogger.Log("ServerConnection", $"QueryFloodProtection: Local={(autoDisableQueryFloodProtection ? "enabled" : "disabled")} Server={(instanceInfo.IsQueryFloodProtectionEnabled ? "enabled" : "disabled")}");
//get actual server data
serverGroups = query.ListServerGroups()?.Where(c => c.Type == GroupType.Regular).ToDictionary(g => g.ID);
channelGroups = query.ListChannelGroups()?.Where(c => c.Type == GroupType.Regular).ToDictionary(g => g.ID);
_serverGroupsSnapshot = serverGroups.ToImmutableDictionary();
_channelGroupsSnapshot = channelGroups.ToImmutableDictionary();
channels = query.ListChannels(true, true, true, true, true, true)?.Select(c => new Channel(this, c)).ToDictionary(c => c.ID);
clients = query.ListClients()?.Where(c => c.Type == ClientType.Voice && c.UID != queryClient.UID).Select(c => new Client(this, c)).ToDictionary(c => c.ID); //todo: track every client
//todo: add error handling
_channelsSnapshot = channels.ToImmutableDictionary();
_clientsSnapshot = clients.ToImmutableDictionary();
GlobalLogger.Log("ServerConnection", $"ServerGroups={serverGroups.Count} ChannelGroups={channelGroups.Count} Channels={channels.Count}");
if (!string.IsNullOrWhiteSpace(defaultChannelName))
{
if (FindChannel(defaultChannelName, out var defChannel))
{
QueryClientDefaultChannel = defChannel;
query.MoveClient(queryClient.ID, defChannel.ID);
}
else
GlobalLogger.Log("ServerConnection", $"Default channel '{defaultChannelName}' not found.", LogType.Warning);
}
//register events to detect changes
query.OnChannelCreated += Query_OnChannelCreated;
query.OnChannelDeleted += Query_OnChannelDeleted;
query.OnChannelEdited += Query_OnChannelEdited;
query.OnChannelDescriptionChanged += Query_OnChannelDescriptionChanged;
query.OnChannelMoved += Query_OnChannelMoved;
query.OnClientEnteredView += Query_OnClientEnteredView;
query.OnClientLeftView += Query_OnClientLeftView;
query.OnClientMoved += Query_OnClientMoved;
query.OnTextMessage += Client_OnTextMessage;
query.OnServerEdited += Query_OnServerEdited;
query.ServerNotifyRegister("server");
query.ServerNotifyRegister("channel", 0);
// after channel/server events so channels created in OnStart are tracked via notify events
// (safe because events aren't dispatched to modules until Running is set after OnStart)
OnConnected?.Invoke(this);
query.ServerNotifyRegister("textserver");
query.ServerNotifyRegister("textchannel");
query.ServerNotifyRegister("textprivate");
GlobalLogger.Log("ServerConnection", "ServerQuery events registered.");
//start channel and client watcher to detect detailed variable updates
StartChannelWatcher();
StartClientWatcher();
}
private void Query_OnDisconnected(ITeamspeakClient sender) //actual its connection lost -> not called when disconnect intentionally
{
StopChannelWatcher();
StopClientWatcher();
query.OnChannelCreated -= Query_OnChannelCreated;
query.OnChannelDeleted -= Query_OnChannelDeleted;
query.OnChannelEdited -= Query_OnChannelEdited;
query.OnChannelDescriptionChanged -= Query_OnChannelDescriptionChanged;
query.OnChannelMoved -= Query_OnChannelMoved;
query.OnClientEnteredView -= Query_OnClientEnteredView;
query.OnClientLeftView -= Query_OnClientLeftView;
query.OnClientMoved -= Query_OnClientMoved;
query.OnTextMessage -= Client_OnTextMessage;
query.OnServerEdited -= Query_OnServerEdited;
GlobalLogger.Log("ServerConnection", "Connection lost.", LogType.Warning);
//call disconnect event here so stop stuff can happen before cache is cleared
OnDisconnected?.Invoke(this);
lock (channels)
{
channels.Clear();
_channelsSnapshot = ImmutableDictionary.Empty;
}
lock (clients)
{
clients.Clear();
_clientsSnapshot = ImmutableDictionary.Empty;
}
}
private void Query_OnError(QueryClient sender, QueryResponse response)
{
}
#endregion
#region channel handler
private void StartChannelWatcher()
{
if (channelWatcherTask.Running || channelUpdateInterval <= 0)
return;
channelWatcherTask.Start();
GlobalLogger.Log("ServerConnection", $"ChannelWatcher started. (Interval: {channelUpdateInterval})");
}
private void StopChannelWatcher()
{
if (!channelWatcherTask.Running)
return;
channelWatcherTask.Stop();
GlobalLogger.Log("ServerConnection", $"ChannelWatcher stopped.");
}
private async Task ChannelWatcher(CancellationToken ct)
{
if (!query.Connected)
return;
await Task.Delay(channelUpdateInterval, ct);
if (ct.IsCancellationRequested)
return;
List channelUpdates = query.ListChannels(true, true, true, true, true, true);
if (channelUpdates == null)
return;
lock (channels)
{
foreach (ChannelInfo channelUpdate in channelUpdates)
{
if (!channels.TryGetValue(channelUpdate.ID, out var channel))
continue;
//update existing channel
var changes = channel.Apply(channelUpdate);
if (!changes.Any)
continue;
OnChannelUpdated?.Invoke(this, channel, changes);
}
}
}
private void ChannelWatcherTask_OnStop(LoopTask sender, Exception exception)
{
if (exception == null)
return;
GlobalLogger.Log("ServerConnection", $"Error in channelWatcherTask: {exception.Message}\n{exception}", LogType.Error);
}
private void Query_OnChannelCreated(object sender, ChannelCreatedArgs e)
{
Channel channel;
lock (channels)
{
if (channels.ContainsKey(e.ChannelID))
{
GlobalLogger.Log("ServerConnection", $"OnChannelCreated: channel [{e.ChannelID}] already in cache.", LogType.Warning);
return;
}
var channelInfo = query.GetChannelInfo(e.ChannelID); //needed for description and more
if (channelInfo == null)
{
GlobalLogger.Log("ServerConnection", $"OnChannelCreated: GetChannelInfo [{e.ChannelID}] failed, falling back to event data.", LogType.Warning);
channelInfo = e.ChannelInfo;
}
channel = new Channel(this, channelInfo);
channels.Add(channel.ID, channel);
_channelsSnapshot = channels.ToImmutableDictionary();
}
//if (pendingChannelCreations.Remove(e.ChannelID, out var tcs))
// tcs.TrySetResult(channel);
channels.TryGetValue(e.ParentChannelID, out var parent);
clients.TryGetValue(e.InvokerInfo.InvokerID, out var invoker);
OnChannelCreated?.Invoke(this, channel, parent, invoker);
}
private void Query_OnChannelDeleted(object sender, ChannelDeletedArgs e)
{
Channel channel;
lock (channels)
{
if (!channels.TryGetValue(e.ChannelID, out channel))
{
GlobalLogger.Log("ServerConnection", $"OnChannelDeleted: channel [{e.ChannelID}] not found in cache.", LogType.Warning);
return;
}
channels.Remove(e.ChannelID);
_channelsSnapshot = channels.ToImmutableDictionary();
}
clients.TryGetValue(e.InvokerInfo.InvokerID, out Client invoker);
OnChannelDeleted?.Invoke(this, channel, invoker);
}
private void Query_OnChannelEdited(object sender, ChannelEditedArgs e)
{
if (!channels.TryGetValue(e.ChannelID, out var channel))
{
GlobalLogger.Log("ServerConnection", $"OnChannelEdited: channel [{e.ChannelID}] not found in cache.", LogType.Warning);
return;
}
//update channel object
var changes = channel.Apply(e);
//invoke events only if changes (on description edit -> no changes)
if (!changes.Any)
return;
OnChannelUpdated?.Invoke(this, channel, changes);
clients.TryGetValue(e.InvokerInfo.InvokerID, out var invoker);
OnChannelEdited?.Invoke(this, channel, changes, invoker);
}
private void Query_OnChannelDescriptionChanged(QueryClient sender, int channelId)
{
//update description
if (!channels.TryGetValue(channelId, out var channel))
{
GlobalLogger.Log("ServerConnection", $"OnChannelDescriptionChanged: channel [{channelId}] not found in cache.", LogType.Warning);
return;
}
var channelInfo = query.GetChannelInfo(channelId);
if (channelInfo == null)
{
GlobalLogger.Log("ServerConnection", $"OnChannelDescriptionChanged: GetChannelInfo [{channelId}] failed.", LogType.Warning);
return;
}
//update channel object
var changes = channel.Apply(channelInfo);
if (!changes.Any)
return;
OnChannelUpdated?.Invoke(this, channel, changes);
//todo: need to get invoker from edit event
//clients.TryGetValue(e.Invoker.InvokerID, out var invoker);
//OnChannelEdited?.Invoke(this, channel, invoker, changes);
}
private void Query_OnChannelMoved(object sender, ChannelMovedArgs e) //note: not triggered on just a order change
{
if (!channels.TryGetValue(e.ChannelID, out var channel))
{
GlobalLogger.Log("ServerConnection", $"OnChannelMoved: channel [{e.ChannelID}] not found in cache.", LogType.Warning);
return;
}
//update channel object
var changes = channel.Apply(e);
if (!changes.Any)
return;
OnChannelUpdated?.Invoke(this, channel, changes);
clients.TryGetValue(e.InvokerInfo.InvokerID, out var invoker);
OnChannelEdited?.Invoke(this, channel, changes, invoker);
}
#endregion
#region client handler
private void StartClientWatcher()
{
if (clientWatcherTask.Running || clientUpdateInterval <= 0)
return;
clientWatcherTask.Start();
GlobalLogger.Log("ServerConnection", $"ClientWatcher started. (Interval: {clientUpdateInterval})");
}
private void StopClientWatcher()
{
if (!clientWatcherTask.Running)
return;
clientWatcherTask.Stop();
GlobalLogger.Log("ServerConnection", $"ClientWatcher stopped.");
}
private async Task ClientWatcher(CancellationToken ct)
{
if (!query.Connected)
return;
await Task.Delay(clientUpdateInterval, ct);
if (ct.IsCancellationRequested)
return;
if (clients.Count == 0) //no need to update lul
return;
List clientUpdates = query.GetClientInfo(clients.Select(c => c.Key).ToArray());
if (clientUpdates == null)
return;
lock (clients)
{
foreach (ClientInfo clientUpdate in clientUpdates)
{
if (!clients.TryGetValue(clientUpdate.ID, out var client))
continue;
//update existing client
var changes = client.Apply(clientUpdate);
if (!changes.Any)
continue;
#if DEBUG
//if (!changes.Entries.All(p => p == "IdleTime" || p == "ConnectedTime" || p == "PacketsSent" || p == "PacketsReceived" || p == "BytesSent" || p == "BytesReceived"))
//{
// GlobalLogger.Log("ServerConnection", $"clientWatcher: client updated [{client.ID}] {client.Name} ({client.UID}), changes: {string.Join(", ", changes.Values.Select(c => $"{c.Property.Name}: {c.OldValue} -> {c.NewValue}"))}");
//}
#endif
OnClientUpdated?.Invoke(this, client, changes);
}
}
}
private void ClientWatcherTask_OnStop(LoopTask sender, Exception exception)
{
if (exception == null)
return;
GlobalLogger.Log("ServerConnection", $"Error in clientWatcherTask: {exception.Message}\n{exception}", LogType.Error);
}
private void Query_OnClientEnteredView(object sender, ClientEnteredViewArgs e)
{
if (e.ClientInfo.Type == ClientType.Query) return; //todo: maybe remove? -> track all clients?
Client client;
lock (clients)
{
if (clients.ContainsKey(e.ClientInfo.ID))
{
GlobalLogger.Log("ServerConnection", $"OnClientEnteredView: client [{e.ClientInfo.ID}] {e.ClientInfo.Name} already in cache.", LogType.Warning);
return;
}
//collect full client info before adding to clients list
var clientInfo = query.GetClientInfo(e.ClientInfo.ID);
if (clientInfo == null)
{
GlobalLogger.Log("ServerConnection", $"OnClientEnteredView: GetClientInfo [{e.ClientInfo.ID}] failed, falling back to event data.", LogType.Warning);
clientInfo = e.ClientInfo;
}
client = new Client(this, clientInfo);
clients.Add(client.ID, client);
_clientsSnapshot = clients.ToImmutableDictionary();
}
GlobalLogger.Log("ServerConnection", $"client joined [{e.ClientInfo.ID}] {e.ClientInfo.Name} ({e.ClientInfo.UID})");
channels.TryGetValue(e.SourceChannelID, out var source);
channels.TryGetValue(e.TargetChannelID, out var target);
OnClientJoined?.Invoke(this, client, source, target, e.ReasonID);
}
private void Query_OnClientLeftView(object sender, ClientLeftViewArgs e)
{
Client client;
lock (clients)
{
if (!clients.TryGetValue(e.ClientID, out client))
{
GlobalLogger.Log("ServerConnection", $"client left [{e.ClientID}] (NO CLIENTINFO), Reason: [{e.ReasonID}] {e.ReasonMessage}, Invoker: {e.InvokerInfo.InvokerName}", LogType.Warning);
return;
}
clients.Remove(e.ClientID);
_clientsSnapshot = clients.ToImmutableDictionary();
}
GlobalLogger.Log("ServerConnection", $"client left [{e.ClientID}] {client.Name} ({client.UID}), Reason: [{e.ReasonID}] {e.ReasonMessage}, Invoker: {e.InvokerInfo.InvokerName}");
channels.TryGetValue(e.SourceChannelID, out var source);
channels.TryGetValue(e.TargetChannelID, out var target);
clients.TryGetValue(e.InvokerInfo.InvokerID, out var invoker);
OnClientLeaved?.Invoke(this, client, source, target, e.ReasonID, invoker, e.ReasonMessage, e.BanTime);
}
private void Query_OnClientMoved(object sender, ClientMovedArgs e)
{
if (!clients.TryGetValue(e.ClientID, out var client))
{
GlobalLogger.Log("ServerConnection", $"OnClientMoved: client [{e.ClientID}] not found in cache.", LogType.Warning);
return;
}
//update client object
var changes = client.Apply(e);
if (!changes.Any)
return;
OnClientUpdated?.Invoke(this, client, changes);
channels.TryGetValue(e.TargetChannelID, out var target);
clients.TryGetValue(e.InvokerInfo.InvokerID, out var invoker);
OnClientMoved?.Invoke(this, client, target, e.ReasonID, invoker);
}
#endregion
#region query event handler
private void Client_OnTextMessage(ITeamspeakClient sender, TextMessageArgs args)
{
Client target = null;
if (args.TargetMode == MessageTargetMode.Private)
clients.TryGetValue(args.Target, out target);
clients.TryGetValue(args.InvokerInfo.InvokerID, out Client invoker);
if (invoker == null && args.InvokerInfo.InvokerID == QueryClient.ID)
invoker = QueryClient;
OnTextMessage?.Invoke(this, sender, args.Message, invoker, args.TargetMode, target);
}
private void Query_OnServerEdited(QueryClient sender, ServerEditedArgs e)
{
var changes = serverInfo.Apply(e.ServerInfo);
if (!changes.Any)
return;
clients.TryGetValue(e.InvokerInfo.InvokerID, out var invoker);
OnServerEdited?.Invoke(this, serverInfo, changes, e.ReasonID, invoker);
}
#endregion
///
public override string ToString() => $"Connection[{ConnectionString}] {(Connected ? $"connected ({versionInfo.Version} on {versionInfo.Platform})" : "disconnected")}";
///
public override int GetHashCode() => ConnectionString.GetHashCode();
///
public override bool Equals(object obj) => obj is ServerConnection && ConnectionString.Equals((obj as ServerConnection).ConnectionString);
}
}