diff --git a/README.md b/README.md index c2992e4..7ce75e8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # C# TeamSpeak3Query API [![Travis Build Status](https://travis-ci.org/nikeee/TeamSpeak3QueryAPI.svg?branch=master)](https://travis-ci.org/nikeee/TeamSpeak3QueryAPI) ![NuGet Downloads](https://img.shields.io/nuget/dt/TeamSpeak3QueryApi.svg) -An API wrapper for the TeamSpeak 3 Query API written in C#. Still work in progress. +An API wrapper for the TeamSpeak 3 Query API written in C#. **Still work in progress**. Key features of this library: - Built entirely with the .NET TAP pattern for perfect async/await usage opportunities - Robust library architecture - Query responses are fully mapped to .NET objects, including the naming style - Usable via Middleware/Rich Client +- SSH and Telnet protocol will be supported ## Contents 1. [Documentation](#documentation) @@ -26,9 +27,10 @@ The TeamSpeak 3 Query API is documented [here](http://media.teamspeak.com/ts3_li This library has an online documentation which was created using [sharpDox](http://sharpdox.de). You can find the documentation on the [GitHub Page of this repository](https://nikeee.github.io/TeamSpeak3QueryAPI). ## Compatibility -This library requires .NET Standard `1.3`. You can look at [this table](https://docs.microsoft.com/en-us/dotnet/standard/net-standard#net-implementation-support) to see whether your platform is supported. If you find something that is missing (espacially in the `TeamSpeakClient` class), just submit a PR or an issue! +This library requires .NET Core `3.0`. You can look at [this table](https://docs.microsoft.com/en-us/dotnet/standard/net-standard#net-implementation-support) to see whether your platform is supported. If you find something that is missing (espacially in the `TeamSpeakClient` class), just submit a PR or an issue! ### NuGet +*This is currently not possible.* ```Shell Install-Package TeamSpeak3QueryApi # or @@ -39,11 +41,18 @@ dotnet add package TeamSpeak3QueryApi Using the rich client, you can connect to a TeamSpeak Query server like this: ### Connect and Login -```C# +```C# Telnet query var rc = new TeamSpeakClient(host, port); // Create rich client instance -await rc.Connect(); // connect to the server -await rc.Login(user, password); // login to do some stuff that requires permission -await rc.UseServer(1); // Use the server with id '1' +await rc.ConnectAsync(); // connect to the server +await rc.LoginAsync(user, password); // login to do some stuff that requires permission +await rc.UseServerAsync(1); // Use the server with id '1' +var me = await rc.WhoAmIAsync(); // Get information about yourself! +``` + +```C# SSH query +var rc = new TeamSpeakClient(host, 10022, Protocol.SSH); // Create rich client instance +rc.Connect(user, password); // connect to the server with login data +await rc.UseServerAsync(1); // Use the server with id '1' var me = await rc.WhoAmI(); // Get information about yourself! ``` @@ -52,10 +61,10 @@ You can receive notifications. The notification data is fully typed, so you can ```C# // assuming connected -await rc.RegisterServerNotification(); // register notifications to receive server notifications +await rc.RegisterServerNotificationAsync(); // register notifications to receive server notifications // register channel notifications to receive notifications for channel with id '30' -await rc.RegisterChannelNotification(30); +await rc.RegisterChannelNotificationAsync(30); //Subscribe a callback to a notification: rc.Subscribe(data => { @@ -70,7 +79,7 @@ rc.Subscribe(data => { Getting all clients and moving them to a specific channel is as simple as: ```C# -var currentClients = await rc.GetClients(); +var currentClients = await rc.GetClientsAsync(); await rc.MoveClient(currentClients, 30); // Where 30 is the channel id ``` ...and kick someone whose name is "Foobar". @@ -78,7 +87,7 @@ await rc.MoveClient(currentClients, 30); // Where 30 is the channel id ```C# var fooBar = currentClients.SingleOrDefault(c => c.NickName == "Foobar"); // Using linq to find our dude if(fooBar != null) // Make sure we pass a valid reference - await rc.KickClient(fooBar, 30); + await rc.KickClientAsync(fooBar, 30); ``` ### Exceptions @@ -100,16 +109,16 @@ If you want to work more loose-typed, you can do this. This is possible using th ```C# var qc = new QueryClient(host, port); -await qc.Connect(); +await qc.ConnectAsync(); -await qc.Send("login", new Parameter("client_login_name", userName), new Parameter("client_login_password", password)); +await qc.SendAsync("login", new Parameter("client_login_name", userName), new Parameter("client_login_password", password)); -await qc.Send("use", new Parameter("sid", "1")); +await qc.SendAsync("use", new Parameter("sid", "1")); -var me = await qc.Send("whoami"); +var me = await qc.SendAsync("whoami"); -await qc.Send("servernotifyregister", new Parameter("event", "server")); -await qc.Send("servernotifyregister", new Parameter("event", "channel"), new Parameter("id", channelId)); +await qc.SendAsync("servernotifyregister", new Parameter("event", "server")); +await qc.SendAsync("servernotifyregister", new Parameter("event", "channel"), new Parameter("id", channelId)); // and so on. ``` diff --git a/src/TeamSpeak3QueryApi.Demo/Program.cs b/src/TeamSpeak3QueryApi.Demo/Program.cs index 6520838..0399ec8 100644 --- a/src/TeamSpeak3QueryApi.Demo/Program.cs +++ b/src/TeamSpeak3QueryApi.Demo/Program.cs @@ -4,8 +4,8 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using TeamSpeak3QueryApi.Net.Specialized; -using TeamSpeak3QueryApi.Net.Specialized.Notifications; +using TeamSpeak3QueryApi.Net.Query.Enums; +using TeamSpeak3QueryApi.Net.Query.Notifications; namespace TeamSpeak3QueryApi.Net.Demo { @@ -19,29 +19,34 @@ static async Task Main(string[] args) var user = loginData[1].Trim(); var password = loginData[2].Trim(); - var rc = new TeamSpeakClient(host); - - await rc.Connect(); + // Ssh connection + //var rc = new TeamSpeakClient(host, 10022, Protocol.SSH); + //rc.Connect(user, password); - await rc.Login(user, password); - await rc.UseServer(1); + // Telnet connection + var rc = new TeamSpeakClient(host); + await rc.ConnectAsync(); + await rc.LoginAsync(user, password); - await rc.WhoAmI(); + await rc.LoginAsync(user, password); + await rc.UseServerAsync(1); - await rc.RegisterServerNotification(); - await rc.RegisterChannelNotification(30); + await rc.WhoAmIAsync(); - var serverGroups = await rc.GetServerGroups(); - var firstNormalGroup = serverGroups?.FirstOrDefault(s => s.ServerGroupType == ServerGroupType.NormalGroup); - var groupClients = await rc.GetServerGroupClientList(firstNormalGroup.Id); + await rc.RegisterServerNotificationAsync(); + await rc.RegisterAllChannelNotificationAsync(); + + var serverGroups = await rc.GetServerGroupsAsync(); + var firstNormalGroup = serverGroups?.FirstOrDefault(s => s.ServerGroupType == ServerGroupType.NormalGroup && s.Id != 46); // Id 46 is default Servergroup - var currentClients = await rc.GetClients(); + var groupClients = await rc.GetServerGroupClientListAsync(firstNormalGroup.Id); + var currentClients = await rc.GetClientsAsync(); var fullClients = currentClients.Where(c => c.Type == ClientType.FullClient).ToList(); - await rc.KickClient(fullClients, KickOrigin.Channel); + await rc.KickClientAsync(fullClients, KickOrigin.Channel, "You're kicked from channel"); - // await rc.MoveClient(1, 1); - // await rc.KickClient(1, KickTarget.Server); + // await rc.MoveClientAsync(1, 1); + // await rc.KickClientAsync(1, KickTarget.Server); rc.Subscribe(data => data.ForEach(c => Debug.WriteLine($"Client {c.NickName} joined."))); rc.Subscribe(data => data.ForEach(c => Debug.WriteLine($"Client with id {c.Id} left (kicked/banned/left)."))); diff --git a/src/TeamSpeak3QueryApi.Demo/TeamSpeak3QueryApi.Demo.csproj b/src/TeamSpeak3QueryApi.Demo/TeamSpeak3QueryApi.Demo.csproj index 10ece9a..8566bf3 100644 --- a/src/TeamSpeak3QueryApi.Demo/TeamSpeak3QueryApi.Demo.csproj +++ b/src/TeamSpeak3QueryApi.Demo/TeamSpeak3QueryApi.Demo.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp1.1 + netcoreapp3.1 diff --git a/src/TeamSpeak3QueryApi/Specialized/DataProxy.cs b/src/TeamSpeak3QueryApi/DataProxy.cs similarity index 95% rename from src/TeamSpeak3QueryApi/Specialized/DataProxy.cs rename to src/TeamSpeak3QueryApi/DataProxy.cs index 847827a..926189e 100644 --- a/src/TeamSpeak3QueryApi/Specialized/DataProxy.cs +++ b/src/TeamSpeak3QueryApi/DataProxy.cs @@ -4,8 +4,12 @@ using System.Diagnostics; using System.Linq; using System.Reflection; +using TeamSpeak3QueryApi.Net.Enums; +using TeamSpeak3QueryApi.Net.Interfaces; +using TeamSpeak3QueryApi.Net.Query; +using TeamSpeak3QueryApi.Net.Query.Enums; -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net { internal static class DataProxy { diff --git a/src/TeamSpeak3QueryApi/Specialized/NotificationEventTarget.cs b/src/TeamSpeak3QueryApi/Enums/NotificationEventTarget.cs similarity index 77% rename from src/TeamSpeak3QueryApi/Specialized/NotificationEventTarget.cs rename to src/TeamSpeak3QueryApi/Enums/NotificationEventTarget.cs index 68b6cba..c730746 100644 --- a/src/TeamSpeak3QueryApi/Specialized/NotificationEventTarget.cs +++ b/src/TeamSpeak3QueryApi/Enums/NotificationEventTarget.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Enums { internal enum NotificationEventTarget { diff --git a/src/TeamSpeak3QueryApi/Specialized/NotificationType.cs b/src/TeamSpeak3QueryApi/Enums/NotificationType.cs similarity index 92% rename from src/TeamSpeak3QueryApi/Specialized/NotificationType.cs rename to src/TeamSpeak3QueryApi/Enums/NotificationType.cs index ae4539c..d6f6de6 100644 --- a/src/TeamSpeak3QueryApi/Specialized/NotificationType.cs +++ b/src/TeamSpeak3QueryApi/Enums/NotificationType.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Enums { ///http://redeemer.biz/medien/artikel/ts3-query-notify/ public enum NotificationType diff --git a/src/TeamSpeak3QueryApi/Enums/Protocol.cs b/src/TeamSpeak3QueryApi/Enums/Protocol.cs new file mode 100644 index 0000000..8177580 --- /dev/null +++ b/src/TeamSpeak3QueryApi/Enums/Protocol.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TeamSpeak3QueryApi.Net.Enums +{ + public enum Protocol + { + Telnet = 0, + SSH = 1 + } +} diff --git a/src/TeamSpeak3QueryApi/Specialized/EnumExtensions.cs b/src/TeamSpeak3QueryApi/Extensions/EnumExtensions.cs similarity index 92% rename from src/TeamSpeak3QueryApi/Specialized/EnumExtensions.cs rename to src/TeamSpeak3QueryApi/Extensions/EnumExtensions.cs index bbae776..10de798 100644 --- a/src/TeamSpeak3QueryApi/Specialized/EnumExtensions.cs +++ b/src/TeamSpeak3QueryApi/Extensions/EnumExtensions.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Collections.Generic; -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Extensions { internal static class EnumExtensions { diff --git a/src/TeamSpeak3QueryApi/ListExtensions.cs b/src/TeamSpeak3QueryApi/Extensions/ListExtensions.cs similarity index 84% rename from src/TeamSpeak3QueryApi/ListExtensions.cs rename to src/TeamSpeak3QueryApi/Extensions/ListExtensions.cs index a247d6e..0094c3b 100644 --- a/src/TeamSpeak3QueryApi/ListExtensions.cs +++ b/src/TeamSpeak3QueryApi/Extensions/ListExtensions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Extensions { internal static class ListExtensions { diff --git a/src/TeamSpeak3QueryApi/StringExtensions.cs b/src/TeamSpeak3QueryApi/Extensions/StringExtensions.cs similarity index 97% rename from src/TeamSpeak3QueryApi/StringExtensions.cs rename to src/TeamSpeak3QueryApi/Extensions/StringExtensions.cs index a11db9c..6280f91 100644 --- a/src/TeamSpeak3QueryApi/StringExtensions.cs +++ b/src/TeamSpeak3QueryApi/Extensions/StringExtensions.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Extensions { internal static class StringExtensions { diff --git a/src/TeamSpeak3QueryApi/TaskExtensions.cs b/src/TeamSpeak3QueryApi/Extensions/TaskExtensions.cs similarity index 95% rename from src/TeamSpeak3QueryApi/TaskExtensions.cs rename to src/TeamSpeak3QueryApi/Extensions/TaskExtensions.cs index efb4544..347723e 100644 --- a/src/TeamSpeak3QueryApi/TaskExtensions.cs +++ b/src/TeamSpeak3QueryApi/Extensions/TaskExtensions.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Extensions { internal static class TaskExtensions { diff --git a/src/TeamSpeak3QueryApi/FileTransferClient.cs b/src/TeamSpeak3QueryApi/FileTransfer/FileTransferClient.cs similarity index 82% rename from src/TeamSpeak3QueryApi/FileTransferClient.cs rename to src/TeamSpeak3QueryApi/FileTransfer/FileTransferClient.cs index 18fa8b5..f9569c2 100644 --- a/src/TeamSpeak3QueryApi/FileTransferClient.cs +++ b/src/TeamSpeak3QueryApi/FileTransfer/FileTransferClient.cs @@ -3,7 +3,7 @@ using System.Text; using System.Threading.Tasks; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.FileTransfer { internal class FileTransferClient { @@ -20,43 +20,43 @@ public int GetFileTransferId() return _currentFileTransferId++; } - public async Task SendFile(byte[] data, int port, string key) + public async Task SendFileAsync(byte[] data, int port, string key) { using var client = new TcpClient(); await client.ConnectAsync(_host, port).ConfigureAwait(false); using var ns = client.GetStream(); - // Send key + // SendAsync key var keyBytes = Encoding.ASCII.GetBytes(key); await ns.WriteAsync(keyBytes, 0, keyBytes.Length).ConfigureAwait(false); await ns.FlushAsync().ConfigureAwait(false); - // Send data + // SendAsync data await ns.WriteAsync(data, 0, data.Length).ConfigureAwait(false); } - public async Task SendFile(Stream dataStream, int port, string key) + public async Task SendFileAsync(Stream dataStream, int port, string key) { using var client = new TcpClient(); await client.ConnectAsync(_host, port).ConfigureAwait(false); using var ns = client.GetStream(); - // Send key + // SendAsync key var keyBytes = Encoding.ASCII.GetBytes(key); await ns.WriteAsync(keyBytes, 0, keyBytes.Length).ConfigureAwait(false); await ns.FlushAsync().ConfigureAwait(false); - // Send data + // SendAsync data await dataStream.CopyToAsync(ns).ConfigureAwait(false); } - public async Task ReceiveFile(int size, int port, string key) + public async Task ReceiveFileAsync(int size, int port, string key) { using var client = new TcpClient(); await client.ConnectAsync(_host, port).ConfigureAwait(false); using var ns = client.GetStream(); - // Send key + // SendAsync key var keyBytes = Encoding.ASCII.GetBytes(key); await ns.WriteAsync(keyBytes, 0, keyBytes.Length).ConfigureAwait(false); await ns.FlushAsync().ConfigureAwait(false); diff --git a/src/TeamSpeak3QueryApi/FileTransferException.cs b/src/TeamSpeak3QueryApi/FileTransfer/FileTransferException.cs similarity index 96% rename from src/TeamSpeak3QueryApi/FileTransferException.cs rename to src/TeamSpeak3QueryApi/FileTransfer/FileTransferException.cs index 21ce6e4..00c48b0 100644 --- a/src/TeamSpeak3QueryApi/FileTransferException.cs +++ b/src/TeamSpeak3QueryApi/FileTransfer/FileTransferException.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.FileTransfer { /// Represents errors that occur during file transfers. [Serializable] diff --git a/src/TeamSpeak3QueryApi/Specialized/ITeamSpeakSerializable.cs b/src/TeamSpeak3QueryApi/Interfaces/ITeamSpeakSerializable.cs similarity index 68% rename from src/TeamSpeak3QueryApi/Specialized/ITeamSpeakSerializable.cs rename to src/TeamSpeak3QueryApi/Interfaces/ITeamSpeakSerializable.cs index 2042c29..614e7c6 100644 --- a/src/TeamSpeak3QueryApi/Specialized/ITeamSpeakSerializable.cs +++ b/src/TeamSpeak3QueryApi/Interfaces/ITeamSpeakSerializable.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Interfaces { interface ITeamSpeakSerializable { diff --git a/src/TeamSpeak3QueryApi/Specialized/ITypeCaster.cs b/src/TeamSpeak3QueryApi/Interfaces/ITypeCaster.cs similarity index 97% rename from src/TeamSpeak3QueryApi/Specialized/ITypeCaster.cs rename to src/TeamSpeak3QueryApi/Interfaces/ITypeCaster.cs index 789acdb..4c6386d 100644 --- a/src/TeamSpeak3QueryApi/Specialized/ITypeCaster.cs +++ b/src/TeamSpeak3QueryApi/Interfaces/ITypeCaster.cs @@ -2,8 +2,9 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using TeamSpeak3QueryApi.Net.Extensions; -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Interfaces { interface ITypeCaster { diff --git a/src/TeamSpeak3QueryApi/Specialized/ClientType.cs b/src/TeamSpeak3QueryApi/Query/Enums/ClientType.cs similarity index 65% rename from src/TeamSpeak3QueryApi/Specialized/ClientType.cs rename to src/TeamSpeak3QueryApi/Query/Enums/ClientType.cs index f685ac2..aef817d 100644 --- a/src/TeamSpeak3QueryApi/Specialized/ClientType.cs +++ b/src/TeamSpeak3QueryApi/Query/Enums/ClientType.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Query.Enums { public enum ClientType { diff --git a/src/TeamSpeak3QueryApi/Specialized/GetChannelOptions.cs b/src/TeamSpeak3QueryApi/Query/Enums/GetChannelOptions.cs similarity index 81% rename from src/TeamSpeak3QueryApi/Specialized/GetChannelOptions.cs rename to src/TeamSpeak3QueryApi/Query/Enums/GetChannelOptions.cs index 6c55f8e..169a5be 100644 --- a/src/TeamSpeak3QueryApi/Specialized/GetChannelOptions.cs +++ b/src/TeamSpeak3QueryApi/Query/Enums/GetChannelOptions.cs @@ -1,6 +1,6 @@ using System; -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Query.Enums { [Flags] public enum GetChannelOptions diff --git a/src/TeamSpeak3QueryApi/Specialized/GetClientOptions.cs b/src/TeamSpeak3QueryApi/Query/Enums/GetClientOptions.cs similarity index 80% rename from src/TeamSpeak3QueryApi/Specialized/GetClientOptions.cs rename to src/TeamSpeak3QueryApi/Query/Enums/GetClientOptions.cs index 90ae501..6830a23 100644 --- a/src/TeamSpeak3QueryApi/Specialized/GetClientOptions.cs +++ b/src/TeamSpeak3QueryApi/Query/Enums/GetClientOptions.cs @@ -1,6 +1,6 @@ using System; -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Query.Enums { [Flags] public enum GetClientOptions @@ -13,5 +13,6 @@ public enum GetClientOptions Info = 1 << 5, Icon = 1 << 6, Country = 1 << 7, + Ip = 1 << 8 } } diff --git a/src/TeamSpeak3QueryApi/Specialized/GetServerOptions.cs b/src/TeamSpeak3QueryApi/Query/Enums/GetServerOptions.cs similarity index 79% rename from src/TeamSpeak3QueryApi/Specialized/GetServerOptions.cs rename to src/TeamSpeak3QueryApi/Query/Enums/GetServerOptions.cs index 72ff8b0..6f320b7 100644 --- a/src/TeamSpeak3QueryApi/Specialized/GetServerOptions.cs +++ b/src/TeamSpeak3QueryApi/Query/Enums/GetServerOptions.cs @@ -1,6 +1,6 @@ using System; -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Query.Enums { [Flags] public enum GetServerOptions diff --git a/src/TeamSpeak3QueryApi/Specialized/KickOrigin.cs b/src/TeamSpeak3QueryApi/Query/Enums/KickOrigin.cs similarity index 64% rename from src/TeamSpeak3QueryApi/Specialized/KickOrigin.cs rename to src/TeamSpeak3QueryApi/Query/Enums/KickOrigin.cs index 5779d00..95c5ac9 100644 --- a/src/TeamSpeak3QueryApi/Specialized/KickOrigin.cs +++ b/src/TeamSpeak3QueryApi/Query/Enums/KickOrigin.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Query.Enums { public enum KickOrigin { diff --git a/src/TeamSpeak3QueryApi/ManualEnums.cs b/src/TeamSpeak3QueryApi/Query/Enums/ManualEnums.cs similarity index 98% rename from src/TeamSpeak3QueryApi/ManualEnums.cs rename to src/TeamSpeak3QueryApi/Query/Enums/ManualEnums.cs index 6d92b9a..b3b54ae 100644 --- a/src/TeamSpeak3QueryApi/ManualEnums.cs +++ b/src/TeamSpeak3QueryApi/Query/Enums/ManualEnums.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query.Enums { public enum HostMessageMode { diff --git a/src/TeamSpeak3QueryApi/Specialized/MessageTarget.cs b/src/TeamSpeak3QueryApi/Query/Enums/MessageTarget.cs similarity index 70% rename from src/TeamSpeak3QueryApi/Specialized/MessageTarget.cs rename to src/TeamSpeak3QueryApi/Query/Enums/MessageTarget.cs index cd8983c..a584b65 100644 --- a/src/TeamSpeak3QueryApi/Specialized/MessageTarget.cs +++ b/src/TeamSpeak3QueryApi/Query/Enums/MessageTarget.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Query.Enums { public enum MessageTarget { diff --git a/src/TeamSpeak3QueryApi/Specialized/ReasonId.cs b/src/TeamSpeak3QueryApi/Query/Enums/ReasonId.cs similarity index 86% rename from src/TeamSpeak3QueryApi/Specialized/ReasonId.cs rename to src/TeamSpeak3QueryApi/Query/Enums/ReasonId.cs index b4fdfb5..495eb92 100644 --- a/src/TeamSpeak3QueryApi/Specialized/ReasonId.cs +++ b/src/TeamSpeak3QueryApi/Query/Enums/ReasonId.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Query.Enums { public enum ReasonId { diff --git a/src/TeamSpeak3QueryApi/Specialized/ServerGroupType.cs b/src/TeamSpeak3QueryApi/Query/Enums/ServerGroupType.cs similarity index 73% rename from src/TeamSpeak3QueryApi/Specialized/ServerGroupType.cs rename to src/TeamSpeak3QueryApi/Query/Enums/ServerGroupType.cs index 175effa..7d3052e 100644 --- a/src/TeamSpeak3QueryApi/Specialized/ServerGroupType.cs +++ b/src/TeamSpeak3QueryApi/Query/Enums/ServerGroupType.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net.Query.Enums { public enum ServerGroupType { diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelCreated.cs b/src/TeamSpeak3QueryApi/Query/Notifications/ChannelCreated.cs similarity index 95% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelCreated.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/ChannelCreated.cs index 9f1367a..272bdf3 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelCreated.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/ChannelCreated.cs @@ -1,6 +1,7 @@ using System; +using TeamSpeak3QueryApi.Net.Query.Enums; -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public class ChannelCreated : InvokerInformation { diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelDeleted.cs b/src/TeamSpeak3QueryApi/Query/Notifications/ChannelDeleted.cs similarity index 78% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelDeleted.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/ChannelDeleted.cs index 85f6b64..32d72d4 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelDeleted.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/ChannelDeleted.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public class ChannelDeleted : InvokerInformation { diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelDescriptionChanged.cs b/src/TeamSpeak3QueryApi/Query/Notifications/ChannelDescriptionChanged.cs similarity index 69% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelDescriptionChanged.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/ChannelDescriptionChanged.cs index 99e8da8..4e64e89 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelDescriptionChanged.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/ChannelDescriptionChanged.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public class ChannelDescriptionChanged : Notification { diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelEdited.cs b/src/TeamSpeak3QueryApi/Query/Notifications/ChannelEdited.cs similarity index 95% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelEdited.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/ChannelEdited.cs index ac7d75b..ae281de 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelEdited.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/ChannelEdited.cs @@ -1,6 +1,7 @@ using System; +using TeamSpeak3QueryApi.Net.Query.Enums; -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public class ChannelEdited : InvokerInformation { diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelMoved.cs b/src/TeamSpeak3QueryApi/Query/Notifications/ChannelMoved.cs similarity index 81% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelMoved.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/ChannelMoved.cs index e2bc1ce..751ebf8 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelMoved.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/ChannelMoved.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public class ChannelMoved : InvokerInformation { diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelPasswordChanged.cs b/src/TeamSpeak3QueryApi/Query/Notifications/ChannelPasswordChanged.cs similarity index 69% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelPasswordChanged.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/ChannelPasswordChanged.cs index efd9ae7..098c1c0 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/ChannelPasswordChanged.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/ChannelPasswordChanged.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public class ChannelPasswordChanged : Notification { diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/ClientEnterView.cs b/src/TeamSpeak3QueryApi/Query/Notifications/ClientEnterView.cs similarity index 97% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/ClientEnterView.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/ClientEnterView.cs index 0a31352..11fde38 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/ClientEnterView.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/ClientEnterView.cs @@ -1,6 +1,7 @@ using System; +using TeamSpeak3QueryApi.Net.Query.Enums; -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public class ClientEnterView : Notification { diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/ClientLeftView.cs b/src/TeamSpeak3QueryApi/Query/Notifications/ClientLeftView.cs similarity index 88% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/ClientLeftView.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/ClientLeftView.cs index 0cdc6a1..788f1b0 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/ClientLeftView.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/ClientLeftView.cs @@ -1,6 +1,7 @@ using System; +using TeamSpeak3QueryApi.Net.Query.Enums; -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public class ClientLeftView : Notification { diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/ClientMoved.cs b/src/TeamSpeak3QueryApi/Query/Notifications/ClientMoved.cs similarity index 77% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/ClientMoved.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/ClientMoved.cs index 449bedc..612829b 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/ClientMoved.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/ClientMoved.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public class ClientMoved : InvokerInformation { diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/InvokerInformation.cs b/src/TeamSpeak3QueryApi/Query/Notifications/InvokerInformation.cs similarity index 79% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/InvokerInformation.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/InvokerInformation.cs index 7d02567..cd06b3b 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/InvokerInformation.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/InvokerInformation.cs @@ -1,4 +1,6 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +using TeamSpeak3QueryApi.Net.Query.Enums; + +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public abstract class InvokerInformation : Notification { diff --git a/src/TeamSpeak3QueryApi/Query/Notifications/Notification.cs b/src/TeamSpeak3QueryApi/Query/Notifications/Notification.cs new file mode 100644 index 0000000..eb2c631 --- /dev/null +++ b/src/TeamSpeak3QueryApi/Query/Notifications/Notification.cs @@ -0,0 +1,7 @@ +using TeamSpeak3QueryApi.Net.Interfaces; + +namespace TeamSpeak3QueryApi.Net.Query.Notifications +{ + public abstract class Notification : ITeamSpeakSerializable + { } +} diff --git a/src/TeamSpeak3QueryApi/NotificationData.cs b/src/TeamSpeak3QueryApi/Query/Notifications/NotificationData.cs similarity index 88% rename from src/TeamSpeak3QueryApi/NotificationData.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/NotificationData.cs index 9afab69..629dd4c 100644 --- a/src/TeamSpeak3QueryApi/NotificationData.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/NotificationData.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using TeamSpeak3QueryApi.Net.Query; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query.Notifications { /// Provides data that was retrieved by a notification. public class NotificationData diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/ServerEdited.cs b/src/TeamSpeak3QueryApi/Query/Notifications/ServerEdited.cs similarity index 95% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/ServerEdited.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/ServerEdited.cs index 190fb18..b5b8375 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/ServerEdited.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/ServerEdited.cs @@ -1,6 +1,7 @@ using System; +using TeamSpeak3QueryApi.Net.Query.Enums; -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public class ServerEdited : Notification { diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/TextMessage.cs b/src/TeamSpeak3QueryApi/Query/Notifications/TextMessage.cs similarity index 55% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/TextMessage.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/TextMessage.cs index 371749c..c61d67d 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/TextMessage.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/TextMessage.cs @@ -1,4 +1,6 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +using TeamSpeak3QueryApi.Net.Query.Enums; + +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public class TextMessage : InvokerInformation { @@ -9,6 +11,6 @@ public class TextMessage : InvokerInformation public string Message; [QuerySerialize("target")] - public int TargetClientId; // (clid des Empfängers; Parameter nur bei textprivate vorhanden) + public int TargetClientId; // (clid des Empfängers; Parameter nur bei textprivate vorhanden) } } diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/TokenUsed.cs b/src/TeamSpeak3QueryApi/Query/Notifications/TokenUsed.cs similarity index 90% rename from src/TeamSpeak3QueryApi/Specialized/Notifications/TokenUsed.cs rename to src/TeamSpeak3QueryApi/Query/Notifications/TokenUsed.cs index 46675d4..b269f21 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/TokenUsed.cs +++ b/src/TeamSpeak3QueryApi/Query/Notifications/TokenUsed.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications +namespace TeamSpeak3QueryApi.Net.Query.Notifications { public class TokenUsed : InvokerInformation { diff --git a/src/TeamSpeak3QueryApi/IParameterValue.cs b/src/TeamSpeak3QueryApi/Query/Parameters/IParameterValue.cs similarity index 88% rename from src/TeamSpeak3QueryApi/IParameterValue.cs rename to src/TeamSpeak3QueryApi/Query/Parameters/IParameterValue.cs index 5541fd5..e613f50 100644 --- a/src/TeamSpeak3QueryApi/IParameterValue.cs +++ b/src/TeamSpeak3QueryApi/Query/Parameters/IParameterValue.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query.Parameters { /// Represents an abstraction of a parameter value. public interface IParameterValue diff --git a/src/TeamSpeak3QueryApi/Parameter.cs b/src/TeamSpeak3QueryApi/Query/Parameters/Parameter.cs similarity index 98% rename from src/TeamSpeak3QueryApi/Parameter.cs rename to src/TeamSpeak3QueryApi/Query/Parameters/Parameter.cs index 20c484e..eac68fd 100644 --- a/src/TeamSpeak3QueryApi/Parameter.cs +++ b/src/TeamSpeak3QueryApi/Query/Parameters/Parameter.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query.Parameters { /// Represents a Query API parameter. public class Parameter diff --git a/src/TeamSpeak3QueryApi/ParameterValue.cs b/src/TeamSpeak3QueryApi/Query/Parameters/ParameterValue.cs similarity index 97% rename from src/TeamSpeak3QueryApi/ParameterValue.cs rename to src/TeamSpeak3QueryApi/Query/Parameters/ParameterValue.cs index 80fcd8a..8782dbe 100644 --- a/src/TeamSpeak3QueryApi/ParameterValue.cs +++ b/src/TeamSpeak3QueryApi/Query/Parameters/ParameterValue.cs @@ -1,6 +1,7 @@ using System.Globalization; +using TeamSpeak3QueryApi.Net.Extensions; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query.Parameters { /// Represents the value of a parameter which consits of a single value. public class ParameterValue : IParameterValue diff --git a/src/TeamSpeak3QueryApi/ParameterValueArray.cs b/src/TeamSpeak3QueryApi/Query/Parameters/ParameterValueArray.cs similarity index 97% rename from src/TeamSpeak3QueryApi/ParameterValueArray.cs rename to src/TeamSpeak3QueryApi/Query/Parameters/ParameterValueArray.cs index e04635e..625391b 100644 --- a/src/TeamSpeak3QueryApi/ParameterValueArray.cs +++ b/src/TeamSpeak3QueryApi/Query/Parameters/ParameterValueArray.cs @@ -1,6 +1,6 @@ using System.Linq; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query.Parameters { /// Represents the value of a parameter which consits of a multiple values. public class ParameterValueArray : IParameterValue diff --git a/src/TeamSpeak3QueryApi/QueryClient.cs b/src/TeamSpeak3QueryApi/Query/QueryClient.cs similarity index 68% rename from src/TeamSpeak3QueryApi/QueryClient.cs rename to src/TeamSpeak3QueryApi/Query/QueryClient.cs index f47c778..3a203b2 100644 --- a/src/TeamSpeak3QueryApi/QueryClient.cs +++ b/src/TeamSpeak3QueryApi/Query/QueryClient.cs @@ -9,114 +9,87 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Renci.SshNet; +using TeamSpeak3QueryApi.Net.Enums; +using TeamSpeak3QueryApi.Net.Extensions; +using TeamSpeak3QueryApi.Net.Query.Notifications; +using TeamSpeak3QueryApi.Net.Query.Parameters; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query { /// Represents a client that can be used to access the TeamSpeak Query API on a remote server. - public class QueryClient : IDisposable + public abstract class QueryClient : IDisposable { + /// + /// Events for handling in the correct moment + /// + public EventHandler OnConnectionLost; + public EventHandler OnConnected; + public EventHandler OnDisconnected; + /// Gets the remote host of the Query API client. /// The remote host of the Query API client. - public string Host { get; } + public string Host { get; internal set; } /// Gets the remote port of the Query API client. /// The remote port of the Query API client. - public int Port { get; } + public int Port { get; internal set; } + + public Protocol ConnectionType { get; internal set; } - public bool IsConnected { get; private set; } + public bool IsConnected { get; internal set; } /// The default host which is used when no host is provided. public const string DefaultHost = "localhost"; - /// The default port which is used when no port is provided. - public const short DefaultPort = 10011; - - public TcpClient Client { get; } - private StreamReader _reader; - private StreamWriter _writer; - private NetworkStream _ns; - private CancellationTokenSource _cts; + public TcpClient Client { get; internal set; } + protected StreamReader _reader; + protected StreamWriter _writer; + protected NetworkStream _ns; + protected CancellationTokenSource _cts; private readonly Queue _queue = new Queue(); private readonly ConcurrentDictionary>> _subscriptions = new ConcurrentDictionary>>(); - #region Ctors - - /// Creates a new instance of using the and . - public QueryClient() - : this(DefaultHost, DefaultPort) - { } - - /// Creates a new instance of using the provided host and the . - /// The host name of the remote server. - public QueryClient(string hostName) - : this(hostName, DefaultPort) - { } - /// Creates a new instance of using the provided host TCP port. - /// The host name of the remote server. - /// The TCP port of the Query API server. - public QueryClient(string hostName, int port) - { - if (string.IsNullOrWhiteSpace(hostName)) - throw new ArgumentNullException(nameof(hostName)); - if (!ValidationHelper.ValidateTcpPort(port)) - throw new ArgumentOutOfRangeException(nameof(port)); - - Host = hostName; - Port = port; - IsConnected = false; - Client = new TcpClient(); - } - - #endregion - - /// Connects to the Query API server. - /// An awaitable . - public async Task Connect() - { - await Client.ConnectAsync(Host, Port).ConfigureAwait(false); - if (!Client.Connected) - throw new InvalidOperationException("Could not connect."); - - _ns = Client.GetStream(); - _reader = new StreamReader(_ns); - _writer = new StreamWriter(_ns) { NewLine = "\n" }; - - IsConnected = true; - - await _reader.ReadLineAsync().ConfigureAwait(false); - await _reader.ReadLineAsync().ConfigureAwait(false); // Ignore welcome message - await _reader.ReadLineAsync().ConfigureAwait(false); - - return ResponseProcessingLoop(); - } + protected SshClient _sshClient; + protected ShellStream _shell; + protected string username; public void Disconnect() { if (_cts == null) return; + OnDisconnected?.Invoke(this, EventArgs.Empty); _cts.Cancel(); } + /// Connects to the Query API server. + /// An awaitable . + public abstract Task ConnectAsync(); + + /// Connects to the Query API server. + /// An awaitable . + public abstract CancellationTokenSource Connect(string username, string password); + #region Send /// Sends a Query API command wihtout parameters to the server. /// The command. /// An awaitable . - public Task Send(string cmd) => Send(cmd, null); + public Task SendAsync(string cmd) => SendAsync(cmd, null); /// Sends a Query API command with parameters to the server. /// The command. /// The parameters of the command. /// An awaitable . - public Task Send(string cmd, params Parameter[] parameters) => Send(cmd, parameters, null); + public Task SendAsync(string cmd, params Parameter[] parameters) => SendAsync(cmd, parameters, null); /// Sends a Query API command with parameters and options to the server. /// The command. /// The parameters of the command. /// The options of the command. /// An awaitable . - public async Task Send(string cmd, Parameter[] parameters, string[] options) + public async Task SendAsync(string cmd, Parameter[] parameters, string[] options) { if (string.IsNullOrWhiteSpace(cmd)) throw new ArgumentNullException(nameof(cmd)); //return Task.Run( () => throw new ArgumentNullException("cmd")); @@ -141,11 +114,10 @@ public async Task Send(string cmd, Parameter[] parame var d = new TaskCompletionSource(); var newItem = new QueryCommand(cmd, ps.AsReadOnly(), options, d, toSend.ToString()); - + _queue.Enqueue(newItem); - await CheckQueue().ConfigureAwait(false); - + await CheckQueueAsync().ConfigureAwait(false); return await d.Task.ConfigureAwait(false); } @@ -210,7 +182,7 @@ public void Unsubscribe(string notificationName, Action callba #endregion #region Parsing - private static QueryResponseDictionary[] ParseResponse(string rawResponse) + protected static QueryResponseDictionary[] ParseResponse(string rawResponse) { var records = rawResponse.Split('|'); var response = records.Select(s => @@ -242,7 +214,7 @@ private static QueryResponseDictionary[] ParseResponse(string rawResponse) return response.ToArray(); } - private static QueryError ParseError(string errorString) + protected static QueryError ParseError(string errorString) { // Ex: // error id=2568 msg=insufficient\sclient\spermissions failed_permid=27 @@ -276,6 +248,9 @@ private static QueryError ParseError(string errorString) case "FAILED_PERMID": parsedError.FailedPermissionId = errData.Length > 1 ? int.Parse(errData[1], NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, CultureInfo.CurrentCulture) : -1; continue; + case "EXTRA_MSG": + parsedError.ExtraMessage = errData.Length > 1 ? errData[1].TeamSpeakUnescape() : null; + continue; default: throw new QueryProtocolException(); } @@ -283,7 +258,7 @@ private static QueryError ParseError(string errorString) return parsedError; } - private static QueryNotification ParseNotification(string notificationString) + protected static QueryNotification ParseNotification(string notificationString) { Debug.Assert(!string.IsNullOrWhiteSpace(notificationString)); @@ -301,7 +276,7 @@ private static QueryNotification ParseNotification(string notificationString) return new QueryNotification(notificationName, notData); } - private static void InvokeResponse(QueryCommand forCommand) + protected static void InvokeResponse(QueryCommand forCommand) { Debug.Assert(forCommand != null); Debug.Assert(forCommand.Defer != null); @@ -320,7 +295,7 @@ private static void InvokeResponse(QueryCommand forCommand) #endregion #region Invocation - private void InvokeNotification(QueryNotification notification) + protected void InvokeNotification(QueryNotification notification) { Debug.Assert(notification != null); Debug.Assert(notification.Name != null); @@ -340,70 +315,14 @@ private void InvokeNotification(QueryNotification notification) } } - private CancellationTokenSource ResponseProcessingLoop() - { - var cts = _cts = new CancellationTokenSource(); - Task.Run(async () => - { - while (!cts.Token.IsCancellationRequested) - { - string line = null; - try - { - line = await _reader.ReadLineAsync().WithCancellation(cts.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - break; - } - - Debug.WriteLine(line); - - if (line == null) - { - cts.Cancel(); - continue; - } - - if (string.IsNullOrWhiteSpace(line)) - continue; - - var s = line.Trim(); - if (s.StartsWith("error", StringComparison.OrdinalIgnoreCase)) - { - Debug.Assert(_currentCommand != null); - - var error = ParseError(s); - _currentCommand.Error = error; - InvokeResponse(_currentCommand); - } - else if (s.StartsWith("notify", StringComparison.OrdinalIgnoreCase)) - { - s = s.Remove(0, "notify".Length); - var not = ParseNotification(s); - InvokeNotification(not); - } - else - { - Debug.Assert(_currentCommand != null); - _currentCommand.RawResponse = s; - _currentCommand.ResponseDictionary = ParseResponse(s); - } - } - - IsConnected = false; - }); - return cts; - } - - private QueryCommand _currentCommand; - private async Task CheckQueue() + protected QueryCommand _currentCommand; + private async Task CheckQueueAsync() { if (_queue.Count > 0) { _currentCommand = _queue.Dequeue(); - Debug.WriteLine(_currentCommand.SentText); - await _writer.WriteLineAsync(_currentCommand.SentText).ConfigureAwait(false); + Debug.WriteLine($"{ConnectionType}: {_currentCommand.SentText}"); + await _writer.WriteLineAsync((ConnectionType == Protocol.Telnet ? _currentCommand.SentText : _currentCommand.SentText + "\n")).ConfigureAwait(false); await _writer.FlushAsync().ConfigureAwait(false); } } @@ -436,9 +355,10 @@ protected virtual void Dispose(bool disposing) _ns?.Dispose(); _reader?.Dispose(); _writer?.Dispose(); + _shell?.Dispose(); + _sshClient?.Dispose(); } } - #endregion } } diff --git a/src/TeamSpeak3QueryApi/QueryCommand.cs b/src/TeamSpeak3QueryApi/Query/QueryCommand.cs similarity index 88% rename from src/TeamSpeak3QueryApi/QueryCommand.cs rename to src/TeamSpeak3QueryApi/Query/QueryCommand.cs index aae8ac9..ea8042c 100644 --- a/src/TeamSpeak3QueryApi/QueryCommand.cs +++ b/src/TeamSpeak3QueryApi/Query/QueryCommand.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; using System.Threading.Tasks; +using TeamSpeak3QueryApi.Net.Query.Parameters; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query { - internal class QueryCommand + public class QueryCommand { public string Command { get; } public string[] Options { get; } diff --git a/src/TeamSpeak3QueryApi/QueryError.cs b/src/TeamSpeak3QueryApi/Query/QueryError.cs similarity index 80% rename from src/TeamSpeak3QueryApi/QueryError.cs rename to src/TeamSpeak3QueryApi/Query/QueryError.cs index 069f90d..c5a0637 100644 --- a/src/TeamSpeak3QueryApi/QueryError.cs +++ b/src/TeamSpeak3QueryApi/Query/QueryError.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query { /// Represents a query error that comes with every API response. public class QueryError @@ -11,6 +11,10 @@ public class QueryError /// The error message. public string Message { get; internal set; } + /// The extra error message. Is only set if there is an extra error message. + /// The error message. + public string ExtraMessage { get; internal set; } + /// If the cause of the error was a missing permission, this property represents the ID of the permission the client does not have. A value of 0 means that there was no permission error. /// The ID of the missing permission. If there is none, 0. /// Check the of the to determine if there was a permission error. diff --git a/src/TeamSpeak3QueryApi/QueryException.cs b/src/TeamSpeak3QueryApi/Query/QueryException.cs similarity index 94% rename from src/TeamSpeak3QueryApi/QueryException.cs rename to src/TeamSpeak3QueryApi/Query/QueryException.cs index 188ee10..5719fef 100644 --- a/src/TeamSpeak3QueryApi/QueryException.cs +++ b/src/TeamSpeak3QueryApi/Query/QueryException.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.Serialization; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query { /// Represents errors that occur during query execution. [Serializable] @@ -27,7 +27,7 @@ public QueryException(string message, Exception innerException) /// Initializes a new instance of the class with a specified error returned by the Query API. /// The that was returned by the Query API. public QueryException(QueryError error) - : this("An error occurred during the query.") + : this("An error occurred during the teamspeak query.") { Error = error; } diff --git a/src/TeamSpeak3QueryApi/QueryNotification.cs b/src/TeamSpeak3QueryApi/Query/QueryNotification.cs similarity index 71% rename from src/TeamSpeak3QueryApi/QueryNotification.cs rename to src/TeamSpeak3QueryApi/Query/QueryNotification.cs index 5d3cd7e..ceb69ea 100644 --- a/src/TeamSpeak3QueryApi/QueryNotification.cs +++ b/src/TeamSpeak3QueryApi/Query/QueryNotification.cs @@ -1,8 +1,9 @@ using System.Diagnostics; +using TeamSpeak3QueryApi.Net.Query.Notifications; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query { - internal class QueryNotification + public class QueryNotification { public string Name { get; set; } public NotificationData Data { get; set; } diff --git a/src/TeamSpeak3QueryApi/QueryProtocolException.cs b/src/TeamSpeak3QueryApi/Query/QueryProtocolException.cs similarity index 93% rename from src/TeamSpeak3QueryApi/QueryProtocolException.cs rename to src/TeamSpeak3QueryApi/Query/QueryProtocolException.cs index 23c081a..ec76b1c 100644 --- a/src/TeamSpeak3QueryApi/QueryProtocolException.cs +++ b/src/TeamSpeak3QueryApi/Query/QueryProtocolException.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.Serialization; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query { /// Represents errors that occur during queries caused by protcol violations. [Serializable] @@ -9,7 +9,7 @@ public class QueryProtocolException : Exception { /// Initializes a new instance of the class. public QueryProtocolException() - : this("An error occurred during the query.") + : this("An error occurred during the teamspeak query.") { } /// Initializes a new instance of the class with a specified error message. diff --git a/src/TeamSpeak3QueryApi/QueryResponseDictionary.cs b/src/TeamSpeak3QueryApi/Query/QueryResponseDictionary.cs similarity index 91% rename from src/TeamSpeak3QueryApi/QueryResponseDictionary.cs rename to src/TeamSpeak3QueryApi/Query/QueryResponseDictionary.cs index 8aae0b8..36b4a8c 100644 --- a/src/TeamSpeak3QueryApi/QueryResponseDictionary.cs +++ b/src/TeamSpeak3QueryApi/Query/QueryResponseDictionary.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace TeamSpeak3QueryApi.Net +namespace TeamSpeak3QueryApi.Net.Query { /// Represents the data of a query response. [Serializable] diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/ClientBan.cs b/src/TeamSpeak3QueryApi/Query/Responses/ClientBan.cs similarity index 66% rename from src/TeamSpeak3QueryApi/Specialized/Responses/ClientBan.cs rename to src/TeamSpeak3QueryApi/Query/Responses/ClientBan.cs index 491c9fb..8528ceb 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/ClientBan.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/ClientBan.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class ClientBan : Response { diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/CreatedChannel.cs b/src/TeamSpeak3QueryApi/Query/Responses/CreatedChannel.cs similarity index 67% rename from src/TeamSpeak3QueryApi/Specialized/Responses/CreatedChannel.cs rename to src/TeamSpeak3QueryApi/Query/Responses/CreatedChannel.cs index b2b3c23..7ebdc36 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/CreatedChannel.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/CreatedChannel.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class CreatedChannel : Response { diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/EditChannelInfo.cs b/src/TeamSpeak3QueryApi/Query/Responses/EditChannelInfo.cs similarity index 96% rename from src/TeamSpeak3QueryApi/Specialized/Responses/EditChannelInfo.cs rename to src/TeamSpeak3QueryApi/Query/Responses/EditChannelInfo.cs index e8bcae2..3812515 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/EditChannelInfo.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/EditChannelInfo.cs @@ -1,6 +1,7 @@ using System; +using TeamSpeak3QueryApi.Net.Query.Enums; -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class EditChannelInfo : Response { diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/FoundChannel.cs b/src/TeamSpeak3QueryApi/Query/Responses/FoundChannel.cs similarity index 76% rename from src/TeamSpeak3QueryApi/Specialized/Responses/FoundChannel.cs rename to src/TeamSpeak3QueryApi/Query/Responses/FoundChannel.cs index a2c08b3..7e6f9f3 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/FoundChannel.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/FoundChannel.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class FoundChannel : Response { diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/GetChannelInfo.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetChannelInfo.cs similarity index 96% rename from src/TeamSpeak3QueryApi/Specialized/Responses/GetChannelInfo.cs rename to src/TeamSpeak3QueryApi/Query/Responses/GetChannelInfo.cs index dcd3633..8e9979c 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/GetChannelInfo.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetChannelInfo.cs @@ -1,6 +1,7 @@ using System; +using TeamSpeak3QueryApi.Net.Query.Enums; -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class GetChannelInfo : Response { diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/GetChannelListInfo.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetChannelListInfo.cs similarity index 94% rename from src/TeamSpeak3QueryApi/Specialized/Responses/GetChannelListInfo.cs rename to src/TeamSpeak3QueryApi/Query/Responses/GetChannelListInfo.cs index f3518ba..621028f 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/GetChannelListInfo.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetChannelListInfo.cs @@ -1,6 +1,7 @@ using System; +using TeamSpeak3QueryApi.Net.Query.Enums; -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class GetChannelListInfo : Response { diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/GetClientDetailedInfo.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetClientDetailedInfo.cs similarity index 96% rename from src/TeamSpeak3QueryApi/Specialized/Responses/GetClientDetailedInfo.cs rename to src/TeamSpeak3QueryApi/Query/Responses/GetClientDetailedInfo.cs index 20c2d6a..b91ef70 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/GetClientDetailedInfo.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetClientDetailedInfo.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using TeamSpeak3QueryApi.Net.Query.Enums; -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class GetClientDetailedInfo : Response { diff --git a/src/TeamSpeak3QueryApi/Query/Responses/GetClientIds.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetClientIds.cs new file mode 100644 index 0000000..19727c7 --- /dev/null +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetClientIds.cs @@ -0,0 +1,14 @@ +namespace TeamSpeak3QueryApi.Net.Query.Responses +{ + public class GetClientIds : Response + { + [QuerySerialize("clid")] + public int Id; + + [QuerySerialize("cluid")] + public string UniqueIdentifier; + + [QuerySerialize("name")] + public string NickName; + } +} diff --git a/src/TeamSpeak3QueryApi/Query/Responses/GetClientInfo.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetClientInfo.cs new file mode 100644 index 0000000..0bc6926 --- /dev/null +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetClientInfo.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using TeamSpeak3QueryApi.Net.Query.Enums; + +namespace TeamSpeak3QueryApi.Net.Query.Responses +{ + public class GetClientInfo : Response + { + [QuerySerialize("clid")] + public int Id; + + [QuerySerialize("cid")] + public int ChannelId; + + [QuerySerialize("client_database_id")] + public int DatabaseId; + + [QuerySerialize("client_nickname")] + public string NickName; + + [QuerySerialize("client_type")] + public ClientType Type; + + [QuerySerialize("client_unique_identifier")] + public string UniqueIdentifier; + + [QuerySerialize("client_version")] + public string Version; + + [QuerySerialize("connection_client_ip")] + public string ConnectionIp; + + [QuerySerialize("client_platform")] + public string Plattform; + + [QuerySerialize("client_input_muted")] + public bool InputMuted; + + [QuerySerialize("client_output_muted")] + public bool OutputMuted; + + [QuerySerialize("client_is_recording")] + public bool IsRecording; + + [QuerySerialize("client_servergroups")] + public IReadOnlyList ServerGroupIds; + + [QuerySerialize("client_channel_group_id")] + public IReadOnlyList ChannelGroupsIds; + + [QuerySerialize("client_created")] + public DateTime Created; + + [QuerySerialize("client_lastconnected")] + public DateTime LastConnected; + + [QuerySerialize("client_away")] + public bool Away; + + [QuerySerialize("client_away_message")] + public string AwayMessage; + + [QuerySerialize("client_idle_time")] + private long _idleTime; //Because it is in ms instead if s defined in Typecaster + public TimeSpan IdleTime => TimeSpan.FromMilliseconds(_idleTime); + } +} diff --git a/src/TeamSpeak3QueryApi/Query/Responses/GetClientUniqueIdFromClientId.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetClientUniqueIdFromClientId.cs new file mode 100644 index 0000000..f08e2cf --- /dev/null +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetClientUniqueIdFromClientId.cs @@ -0,0 +1,14 @@ +namespace TeamSpeak3QueryApi.Net.Query.Responses +{ + public class GetClientUniqueIdFromClientId : Response + { + [QuerySerialize("clid")] + public int Id; + + [QuerySerialize("cluid")] + public string UniqueIdentifier; + + [QuerySerialize("nickname")] + public string NickName; + } +} diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/GetCurrentFileTransfer.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetCurrentFileTransfer.cs similarity index 94% rename from src/TeamSpeak3QueryApi/Specialized/Responses/GetCurrentFileTransfer.cs rename to src/TeamSpeak3QueryApi/Query/Responses/GetCurrentFileTransfer.cs index 9dfbd39..877cce4 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/GetCurrentFileTransfer.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetCurrentFileTransfer.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class GetCurrentFileTransfer : Response { diff --git a/src/TeamSpeak3QueryApi/Query/Responses/GetDatabaseIdFromClientUniqueId.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetDatabaseIdFromClientUniqueId.cs new file mode 100644 index 0000000..79ed891 --- /dev/null +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetDatabaseIdFromClientUniqueId.cs @@ -0,0 +1,11 @@ +namespace TeamSpeak3QueryApi.Net.Query.Responses +{ + public class GetDatabaseIdFromClientUniqueId : Response + { + [QuerySerialize("cluid")] + public string UniqueIdentifier; + + [QuerySerialize("cldbid")] + public int DatabaseId; + } +} diff --git a/src/TeamSpeak3QueryApi/Query/Responses/GetDbClientInfo.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetDbClientInfo.cs new file mode 100644 index 0000000..a6df532 --- /dev/null +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetDbClientInfo.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace TeamSpeak3QueryApi.Net.Query.Responses +{ + public class GetDbClientInfo : Response + { + [QuerySerialize("cldbid")] + public int DatabaseId; + + [QuerySerialize("client_nickname")] + public string NickName; + + [QuerySerialize("client_unique_identifier")] + public string UniqueIdentifier; + + [QuerySerialize("client_created")] + public DateTime Created; + + [QuerySerialize("client_lastconnected")] + public DateTime LastConnected; + + [QuerySerialize("client_totalconnections")] + public int TotalConnectionCount; + + [QuerySerialize("client_description")] + public string Description; + + [QuerySerialize("client_login_name")] + public string LoginName; + + [QuerySerialize("client_lastip")] + public string LastConnectionIp; + } +} diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/GetFileInfo.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetFileInfo.cs similarity index 85% rename from src/TeamSpeak3QueryApi/Specialized/Responses/GetFileInfo.cs rename to src/TeamSpeak3QueryApi/Query/Responses/GetFileInfo.cs index 45f0709..abb12dc 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/GetFileInfo.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetFileInfo.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class GetFileInfo : Response { diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/GetFiles.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetFiles.cs similarity index 87% rename from src/TeamSpeak3QueryApi/Specialized/Responses/GetFiles.cs rename to src/TeamSpeak3QueryApi/Query/Responses/GetFiles.cs index 773797e..168e322 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/GetFiles.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetFiles.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class GetFiles : Response { diff --git a/src/TeamSpeak3QueryApi/Query/Responses/GetNameFromClientDatabaseId.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetNameFromClientDatabaseId.cs new file mode 100644 index 0000000..0f36cf5 --- /dev/null +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetNameFromClientDatabaseId.cs @@ -0,0 +1,14 @@ +namespace TeamSpeak3QueryApi.Net.Query.Responses +{ + public class GetNameFromClientDatabaseId : Response + { + [QuerySerialize("cluid")] + public string UniqueIdentifier; + + [QuerySerialize("name")] + public string NickName; + + [QuerySerialize("cldbid")] + public int DatabaseId; + } +} diff --git a/src/TeamSpeak3QueryApi/Query/Responses/GetNameFromClientUniqueId.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetNameFromClientUniqueId.cs new file mode 100644 index 0000000..b5ccbd4 --- /dev/null +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetNameFromClientUniqueId.cs @@ -0,0 +1,14 @@ +namespace TeamSpeak3QueryApi.Net.Query.Responses +{ + public class GetNameFromClientUniqueId : Response + { + [QuerySerialize("cluid")] + public string UniqueIdentifier; + + [QuerySerialize("cldbid")] + public int DatabaseId; + + [QuerySerialize("name")] + public string NickName; + } +} diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/GetServerGroup.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetServerGroup.cs similarity index 81% rename from src/TeamSpeak3QueryApi/Specialized/Responses/GetServerGroup.cs rename to src/TeamSpeak3QueryApi/Query/Responses/GetServerGroup.cs index 2774918..757fe07 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/GetServerGroup.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetServerGroup.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class GetServerGroup : Response { diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/GetServerGroupClientList.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetServerGroupClientList.cs similarity index 71% rename from src/TeamSpeak3QueryApi/Specialized/Responses/GetServerGroupClientList.cs rename to src/TeamSpeak3QueryApi/Query/Responses/GetServerGroupClientList.cs index f987ff9..add70b4 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/GetServerGroupClientList.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetServerGroupClientList.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class GetServerGroupClientList : Response { diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/GetServerGroupListInfo.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetServerGroupListInfo.cs similarity index 89% rename from src/TeamSpeak3QueryApi/Specialized/Responses/GetServerGroupListInfo.cs rename to src/TeamSpeak3QueryApi/Query/Responses/GetServerGroupListInfo.cs index f92f58a..39b2880 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/GetServerGroupListInfo.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetServerGroupListInfo.cs @@ -1,4 +1,6 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +using TeamSpeak3QueryApi.Net.Query.Enums; + +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class GetServerGroupListInfo : Response { diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/GetServerListInfo.cs b/src/TeamSpeak3QueryApi/Query/Responses/GetServerListInfo.cs similarity index 94% rename from src/TeamSpeak3QueryApi/Specialized/Responses/GetServerListInfo.cs rename to src/TeamSpeak3QueryApi/Query/Responses/GetServerListInfo.cs index 0d01187..efc8756 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/GetServerListInfo.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/GetServerListInfo.cs @@ -1,6 +1,6 @@ using System; -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class GetServerListInfo : Response { diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/InitDownload.cs b/src/TeamSpeak3QueryApi/Query/Responses/InitDownload.cs similarity index 89% rename from src/TeamSpeak3QueryApi/Specialized/Responses/InitDownload.cs rename to src/TeamSpeak3QueryApi/Query/Responses/InitDownload.cs index 624f805..b1ff8ce 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/InitDownload.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/InitDownload.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class InitDownload : Response { diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/InitUpload.cs b/src/TeamSpeak3QueryApi/Query/Responses/InitUpload.cs similarity index 88% rename from src/TeamSpeak3QueryApi/Specialized/Responses/InitUpload.cs rename to src/TeamSpeak3QueryApi/Query/Responses/InitUpload.cs index 092396e..b057f3f 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/InitUpload.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/InitUpload.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class InitUpload : Response { diff --git a/src/TeamSpeak3QueryApi/Query/Responses/Response.cs b/src/TeamSpeak3QueryApi/Query/Responses/Response.cs new file mode 100644 index 0000000..bc34bd9 --- /dev/null +++ b/src/TeamSpeak3QueryApi/Query/Responses/Response.cs @@ -0,0 +1,7 @@ +using TeamSpeak3QueryApi.Net.Interfaces; + +namespace TeamSpeak3QueryApi.Net.Query.Responses +{ + public abstract class Response : ITeamSpeakSerializable + { } +} diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/WhoAmI.cs b/src/TeamSpeak3QueryApi/Query/Responses/WhoAmI.cs similarity index 95% rename from src/TeamSpeak3QueryApi/Specialized/Responses/WhoAmI.cs rename to src/TeamSpeak3QueryApi/Query/Responses/WhoAmI.cs index 1e0d0c7..5d54ac1 100644 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/WhoAmI.cs +++ b/src/TeamSpeak3QueryApi/Query/Responses/WhoAmI.cs @@ -1,4 +1,4 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Responses +namespace TeamSpeak3QueryApi.Net.Query.Responses { public class WhoAmI : Response { diff --git a/src/TeamSpeak3QueryApi/Query/SshQueryClient.cs b/src/TeamSpeak3QueryApi/Query/SshQueryClient.cs new file mode 100644 index 0000000..4c82393 --- /dev/null +++ b/src/TeamSpeak3QueryApi/Query/SshQueryClient.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Renci.SshNet; +using Renci.SshNet.Common; +using TeamSpeak3QueryApi.Net.Enums; +using TeamSpeak3QueryApi.Net.Extensions; + +namespace TeamSpeak3QueryApi.Net.Query +{ + public class SshQueryClient : QueryClient + { + /// The default port which is used when no port is provided. + public const short DefaultPort = 10022; + + /// Creates a new instance of using the and . + public SshQueryClient() + : this(DefaultHost, DefaultPort) + { } + + /// Creates a new instance of using the provided host and the . + /// The host name of the remote server. + public SshQueryClient(string hostName) + : this(hostName, DefaultPort) + { } + /// Creates a new instance of using the provided host TCP port. + /// The host name of the remote server. + /// The TCP port of the Query API server. + public SshQueryClient(string hostName, int port) + { + if (string.IsNullOrWhiteSpace(hostName)) + throw new ArgumentNullException(nameof(hostName)); + if (!ValidationHelper.ValidateTcpPort(port)) + throw new ArgumentOutOfRangeException(nameof(port)); + + Host = hostName; + Port = port; + ConnectionType = Protocol.SSH; + IsConnected = false; + Client = new TcpClient(); + } + + /// Connects to the Query API server. + /// An awaitable . + public override CancellationTokenSource Connect(string username, string password) + { + this.username = username; + + _sshClient = new SshClient(Host, Port, username, password); + _sshClient.Connect(); + + var terminalMode = new Dictionary(); + terminalMode.Add(TerminalModes.ECHO, 53); + + _shell = _sshClient.CreateShellStream("", 0, 0, 0, 0, 4096); + + _reader = new StreamReader(_shell, Encoding.UTF8, true, 1024, true); + _writer = new StreamWriter(_shell) { NewLine = "\n", AutoFlush = true }; + + var headline = _shell.Expect("\r\n", new TimeSpan(0, 0, 3)); + if (!headline.Contains("TS3")) + { + throw new QueryProtocolException("Telnet Query isn't a valid Teamspeak Query"); + } + _shell.Expect("\n", new TimeSpan(0, 0, 3)); // Ignore welcome message + _shell.Expect("\n", new TimeSpan(0, 0, 3)); // Ignore welcome message + + return ResponseProcessingLoop(); + } + + /// Connects to the Query API server. + /// An awaitable . + public override Task ConnectAsync() + { + throw new InvalidOperationException("ConnectAsync Method is not supported in ssh query. Please use the telnet query."); + } + + private CancellationTokenSource ResponseProcessingLoop() + { + var cts = _cts = new CancellationTokenSource(); + Task.Run(async () => + { + while (!cts.Token.IsCancellationRequested) + { + string line = null; + try + { + line = _shell.ReadLine(); + } + catch (OperationCanceledException) + { + break; + } + + if (line == null) + { + cts.Cancel(); + continue; + } + + if (string.IsNullOrWhiteSpace(line) || string.IsNullOrEmpty(line) || line.StartsWith(username)) + continue; + + var s = line.Trim(); + Debug.WriteLine(line); + + if (s.StartsWith("error", StringComparison.OrdinalIgnoreCase)) + { + Debug.Assert(_currentCommand != null); + + var error = ParseError(s); + _currentCommand.Error = error; + InvokeResponse(_currentCommand); + } + else if (s.StartsWith("notify", StringComparison.OrdinalIgnoreCase)) + { + s = s.Remove(0, "notify".Length); + var not = ParseNotification(s); + InvokeNotification(not); + } + else + { + Debug.Assert(_currentCommand != null); + _currentCommand.RawResponse = s; + _currentCommand.ResponseDictionary = ParseResponse(s); + } + } + + IsConnected = false; + OnConnectionLost?.Invoke(this, EventArgs.Empty); + OnDisconnected?.Invoke(this, EventArgs.Empty); + + }); + return cts; + } + } +} diff --git a/src/TeamSpeak3QueryApi/Query/TelnetQueryClient.cs b/src/TeamSpeak3QueryApi/Query/TelnetQueryClient.cs new file mode 100644 index 0000000..44e7323 --- /dev/null +++ b/src/TeamSpeak3QueryApi/Query/TelnetQueryClient.cs @@ -0,0 +1,140 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using TeamSpeak3QueryApi.Net.Enums; +using TeamSpeak3QueryApi.Net.Extensions; + +namespace TeamSpeak3QueryApi.Net.Query +{ + public class TelnetQueryClient : QueryClient + { + /// The default port which is used when no port is provided. + public const short DefaultPort = 10011; + + /// Creates a new instance of using the and . + public TelnetQueryClient() + : this(DefaultHost, DefaultPort) + { } + + /// Creates a new instance of using the provided host and the . + /// The host name of the remote server. + public TelnetQueryClient(string hostName) + : this(hostName, DefaultPort) + { } + /// Creates a new instance of using the provided host TCP port. + /// The host name of the remote server. + /// The TCP port of the Query API server. + public TelnetQueryClient(string hostName, int port) + { + if (string.IsNullOrWhiteSpace(hostName)) + throw new ArgumentNullException(nameof(hostName)); + if (!ValidationHelper.ValidateTcpPort(port)) + throw new ArgumentOutOfRangeException(nameof(port)); + + Host = hostName; + Port = port; + ConnectionType = Protocol.Telnet; + IsConnected = false; + Client = new TcpClient(); + } + + /// Connects to the Query API server. + /// An awaitable . + public override CancellationTokenSource Connect(string username, string password) + { + throw new InvalidOperationException("Connect Method is not supported in telnet query. Please use the ssh query."); + } + + /// Connects to the Query API server. + /// An awaitable . + public override async Task ConnectAsync() + { + if (ConnectionType != Protocol.Telnet) + throw new InvalidOperationException("ConnectAsync Method without parameters can only be used with telnet Query. Please use Connect method."); + + await Client.ConnectAsync(Host, Port).ConfigureAwait(false); + if (!Client.Connected) + throw new InvalidOperationException("Could not connect."); + + _ns = Client.GetStream(); + _reader = new StreamReader(_ns); + _writer = new StreamWriter(_ns) { NewLine = "\n" }; + + IsConnected = true; + OnConnected?.Invoke(this, EventArgs.Empty); + + + var headline = await _reader.ReadLineAsync().ConfigureAwait(false); + if (headline != "TS3") + { + throw new QueryProtocolException("Telnet Query isn't a valid Teamspeak Query"); + } + await _reader.ReadLineAsync().ConfigureAwait(false); // Ignore welcome message + await _reader.ReadLineAsync().ConfigureAwait(false); + + return ResponseProcessingLoop(); + } + + private CancellationTokenSource ResponseProcessingLoop() + { + var cts = _cts = new CancellationTokenSource(); + Task.Run(async () => + { + while (!cts.Token.IsCancellationRequested) + { + string line = null; + try + { + line = await _reader.ReadLineAsync().WithCancellation(cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + + if (line == null) + { + cts.Cancel(); + continue; + } + + if (string.IsNullOrWhiteSpace(line) || string.IsNullOrEmpty(line)) + continue; + + var s = line.Trim(); + Debug.WriteLine(line); + + if (s.StartsWith("error", StringComparison.OrdinalIgnoreCase)) + { + Debug.Assert(_currentCommand != null); + + var error = ParseError(s); + _currentCommand.Error = error; + InvokeResponse(_currentCommand); + } + else if (s.StartsWith("notify", StringComparison.OrdinalIgnoreCase)) + { + s = s.Remove(0, "notify".Length); + var not = ParseNotification(s); + InvokeNotification(not); + } + else + { + Debug.Assert(_currentCommand != null); + _currentCommand.RawResponse = s; + _currentCommand.ResponseDictionary = ParseResponse(s); + } + } + + IsConnected = false; + OnConnectionLost?.Invoke(this, EventArgs.Empty); + OnDisconnected?.Invoke(this, EventArgs.Empty); + + }); + return cts; + } + } +} diff --git a/src/TeamSpeak3QueryApi/Specialized/QuerySerializeAttribute.cs b/src/TeamSpeak3QueryApi/QuerySerializeAttribute.cs similarity index 87% rename from src/TeamSpeak3QueryApi/Specialized/QuerySerializeAttribute.cs rename to src/TeamSpeak3QueryApi/QuerySerializeAttribute.cs index 006711d..f52f917 100644 --- a/src/TeamSpeak3QueryApi/Specialized/QuerySerializeAttribute.cs +++ b/src/TeamSpeak3QueryApi/QuerySerializeAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace TeamSpeak3QueryApi.Net.Specialized +namespace TeamSpeak3QueryApi.Net { [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] class QuerySerializeAttribute : Attribute diff --git a/src/TeamSpeak3QueryApi/Specialized/Notifications/Notification.cs b/src/TeamSpeak3QueryApi/Specialized/Notifications/Notification.cs deleted file mode 100644 index 5844791..0000000 --- a/src/TeamSpeak3QueryApi/Specialized/Notifications/Notification.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Notifications -{ - public abstract class Notification : ITeamSpeakSerializable - { } -} diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/GetClientInfo.cs b/src/TeamSpeak3QueryApi/Specialized/Responses/GetClientInfo.cs deleted file mode 100644 index 40acc11..0000000 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/GetClientInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Responses -{ - public class GetClientInfo : Response - { - [QuerySerialize("clid")] - public int Id; - - [QuerySerialize("cid")] - public int ChannelId; - - [QuerySerialize("client_database_id")] - public int DatabaseId; - - [QuerySerialize("client_nickname")] - public string NickName; - - [QuerySerialize("client_type")] - public ClientType Type; - } -} diff --git a/src/TeamSpeak3QueryApi/Specialized/Responses/Response.cs b/src/TeamSpeak3QueryApi/Specialized/Responses/Response.cs deleted file mode 100644 index 587cd21..0000000 --- a/src/TeamSpeak3QueryApi/Specialized/Responses/Response.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace TeamSpeak3QueryApi.Net.Specialized.Responses -{ - public abstract class Response : ITeamSpeakSerializable - { } -} diff --git a/src/TeamSpeak3QueryApi/Specialized/TeamSpeakClient.cs b/src/TeamSpeak3QueryApi/Specialized/TeamSpeakClient.cs deleted file mode 100644 index f57e000..0000000 --- a/src/TeamSpeak3QueryApi/Specialized/TeamSpeakClient.cs +++ /dev/null @@ -1,940 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using TeamSpeak3QueryApi.Net.Specialized.Notifications; -using TeamSpeak3QueryApi.Net.Specialized.Responses; - -namespace TeamSpeak3QueryApi.Net.Specialized -{ - public class TeamSpeakClient : IDisposable - { - public QueryClient Client { get; } - - // TODO: Migrate to ValueTuples - private readonly List>> _callbacks = new List>>(); - - private readonly FileTransferClient _fileTransferClient; - - #region Ctors - - /// Creates a new instance of using the and . - public TeamSpeakClient() - : this(QueryClient.DefaultHost, QueryClient.DefaultPort) - { } - - /// Creates a new instance of using the provided host and the . - /// The host name of the remote server. - public TeamSpeakClient(string hostName) - : this(hostName, QueryClient.DefaultPort) - { } - - /// Creates a new instance of using the provided host TCP port. - /// The host name of the remote server. - /// The TCP port of the Query API server. - public TeamSpeakClient(string hostName, int port) - { - Client = new QueryClient(hostName, port); - _fileTransferClient = new FileTransferClient(hostName); - } - - #endregion - - public Task Connect() => Client.Connect(); - - #region Subscriptions - - public void Subscribe(Action> callback) - where T : Notification - { - var notification = GetNotificationType(); - - Action cb = data => callback(DataProxy.SerializeGeneric(data.Payload)); - - _callbacks.Add(Tuple.Create(notification, callback as object, cb)); - Client.Subscribe(notification.ToString(), cb); - } - public void Unsubscribe() - where T : Notification - { - var notification = GetNotificationType(); - var cbts = _callbacks.Where(tp => tp.Item1 == notification).ToList(); - cbts.ForEach(k => _callbacks.Remove(k)); - Client.Unsubscribe(notification.ToString()); - } - public void Unsubscribe(Action> callback) - where T : Notification - { - var notification = GetNotificationType(); - var cbt = _callbacks.SingleOrDefault(t => t.Item1 == notification && t.Item2 == callback as object); - if (cbt != null) - Client.Unsubscribe(notification.ToString(), cbt.Item3); - } - - private static NotificationType GetNotificationType() - { - if (!Enum.TryParse(typeof(T).Name, out NotificationType notification)) // This may violate the generic pattern. May change this later. - throw new ArgumentException("The specified generic parameter is not a supported NotificationType."); // For this time, we only support class-internal types which are listed in NotificationType - return notification; - } - - #endregion - #region Implented api methods - - public Task Login(string userName, string password) - { - return Client.Send("login", new Parameter("client_login_name", userName), new Parameter("client_login_password", password)); - } - - public Task Logout() => Client.Send("logout"); - - public Task UseServer(int serverId) - { - return Client.Send("use", new Parameter("sid", serverId.ToString(CultureInfo.InvariantCulture))); - } - - public async Task WhoAmI() - { - var res = await Client.Send("whoami").ConfigureAwait(false); - var proxied = DataProxy.SerializeGeneric(res); - return proxied.FirstOrDefault(); - } - - #region Notification Methods - - public Task RegisterChannelNotification(int channelId) => RegisterNotification(NotificationEventTarget.Channel, channelId); - public Task RegisterServerNotification() => RegisterNotification(NotificationEventTarget.Server, -1); - public Task RegisterTextServerNotification() => RegisterNotification(NotificationEventTarget.TextServer, -1); - public Task RegisterTextChannelNotification() => RegisterNotification(NotificationEventTarget.TextChannel, -1); - public Task RegisterTextPrivateNotification() => RegisterNotification(NotificationEventTarget.TextPrivate, -1); - private Task RegisterNotification(NotificationEventTarget target, int channelId) - { - var ev = new Parameter("event", target.ToString().ToLowerInvariant()); - if (target == NotificationEventTarget.Channel) - return Client.Send("servernotifyregister", ev, new Parameter("id", channelId)); - return Client.Send("servernotifyregister", ev); - } - - #endregion - - #region Client Methods - - #region MoveClient - - public Task MoveClient(int clientId, int targetChannelId) => MoveClient(new[] { clientId }, targetChannelId); - public Task MoveClient(int clientId, int targetChannelId, string channelPassword) => MoveClient(new[] { clientId }, targetChannelId, channelPassword); - - public Task MoveClient(IEnumerable clients, int targetChannelId) - { - var clIds = clients.Select(c => c.Id).ToArray(); - return MoveClient(clIds, targetChannelId); - } - public Task MoveClient(IEnumerable clients, int targetChannelId, string channelPassword) - { - var clIds = clients.Select(c => c.Id).ToArray(); - return MoveClient(clIds, targetChannelId, channelPassword); - } - - public Task MoveClient(IList clientIds, int targetChannelId) - { - return Client.Send("clientmove", - new Parameter("clid", clientIds.Select(i => new ParameterValue(i)).ToArray()), - new Parameter("cid", targetChannelId)); - } - public Task MoveClient(IList clientIds, int targetChannelId, string channelPassword) - { - return Client.Send("clientmove", - new Parameter("clid", clientIds.Select(i => new ParameterValue(i)).ToArray()), - new Parameter("cid", targetChannelId), - new Parameter("cpw", channelPassword)); - } - - #endregion - #region KickClient - - public Task KickClient(int clientId, KickOrigin from) => KickClient(new[] { clientId }, from); - public Task KickClient(int clientId, KickOrigin from, string reasonMessage) => KickClient(new[] { clientId }, from, reasonMessage); - public Task KickClient(GetClientInfo client, KickOrigin from) => KickClient(client.Id, from); - public Task KickClient(IEnumerable clients, KickOrigin from) - { - var clIds = clients.Select(c => c.Id).ToArray(); - return KickClient(clIds, from); - } - public Task KickClient(IList clientIds, KickOrigin from) - { - return Client.Send("clientkick", - new Parameter("reasonid", (int)from), - new Parameter("clid", clientIds.Select(i => new ParameterValue(i)).ToArray())); - } - public Task KickClient(IList clientIds, KickOrigin from, string reasonMessage) - { - return Client.Send("clientkick", - new Parameter("reasonid", (int)from), - new Parameter("reasonmsg", reasonMessage), - new Parameter("clid", clientIds.Select(i => new ParameterValue(i)).ToArray())); - } - - #endregion - #region BanClient - - public Task> BanClient(GetClientInfo client) - { - if (client == null) - throw new ArgumentNullException(nameof(client)); - return BanClient(client.Id); - } - public Task> BanClient(GetClientInfo client, TimeSpan duration) - { - if (client == null) - throw new ArgumentNullException(nameof(client)); - return BanClient(client.Id, duration); - } - public Task> BanClient(GetClientInfo client, TimeSpan duration, string reason) - { - if (client == null) - throw new ArgumentNullException(nameof(client)); - return BanClient(client.Id, duration, reason); - } - - public async Task> BanClient(int clientId) - { - var res = await Client.Send("banclient", - new Parameter("clid", clientId)) - .ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - public async Task> BanClient(int clientId, TimeSpan duration) - { - var res = await Client.Send("banclient", - new Parameter("clid", clientId), - new Parameter("time", (int)Math.Ceiling(duration.TotalSeconds))) - .ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - public async Task> BanClient(int clientId, TimeSpan duration, string reason) - { - var res = await Client.Send("banclient", - new Parameter("clid", clientId), - new Parameter("time", (int)Math.Ceiling(duration.TotalSeconds)), - new Parameter("banreason", reason ?? string.Empty)) - .ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - - #endregion - #region GetClients - - public async Task> GetClients() - { - var res = await Client.Send("clientlist").ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - - public async Task> GetClients(GetClientOptions options) - { - var optionList = options.GetFlagsName(); - var res = await Client.Send("clientlist", null, optionList.ToArray()).ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - - public Task GetClientInfo(GetClientInfo client) => GetClientInfo(client.Id); - - public async Task GetClientInfo(int clientId) - { - var res = await Client.Send("clientinfo", - new Parameter("clid", clientId)) - .ConfigureAwait(false); - - return DataProxy.SerializeGeneric(res).FirstOrDefault(); - } - - #endregion - - #region GetServerGroups - - public async Task> GetServerGroups(int clientDatabaseId) - { - var res = await Client.Send("servergroupsbyclientid", new Parameter("cldbid", clientDatabaseId)).ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - - public Task> GetServerGroups(GetClientInfo clientInfo) => GetServerGroups(clientInfo.DatabaseId); - - public Task> GetServerGroups(WhoAmI clientInfo) => GetServerGroups(clientInfo.DatabaseId); - - #endregion - - #region AddServerGroup - - #region One User - - public Task AddServerGroup(int serverGroupId, int clientDatabaseId) => AddServerGroup(serverGroupId, new int[] { clientDatabaseId }); - - public Task AddServerGroup(int serverGroupId, GetClientInfo clientInfo) => AddServerGroup(serverGroupId, clientInfo.DatabaseId); - - public Task AddServerGroup(GetServerGroup serverGroup, int clientDatabaseId) => AddServerGroup(serverGroup.Id, clientDatabaseId); - - public Task AddServerGroup(GetServerGroup serverGroup, GetClientInfo clientInfo) => AddServerGroup(serverGroup.Id, clientInfo.DatabaseId); - - #endregion - - #region Multiple Users - - public Task AddServerGroup(int serverGroupId, IEnumerable clientInfo) => AddServerGroup(serverGroupId, clientInfo.Select(info => info.DatabaseId)); - - public Task AddServerGroup(GetServerGroup serverGroup, IEnumerable clientDatabaseIds) => AddServerGroup(serverGroup.Id, clientDatabaseIds); - - public Task AddServerGroup(GetServerGroup serverGroup, IEnumerable clientInfo) => AddServerGroup(serverGroup.Id, clientInfo.Select(info => info.DatabaseId)); - - public Task AddServerGroup(int serverGroupId, IEnumerable clientDatabaseIds) - { - return Client.Send("servergroupaddclient", - new Parameter("sgid", serverGroupId), - new Parameter("cldbid", clientDatabaseIds.Select(id => new ParameterValue(id)).ToArray())); - } - - #endregion - - #endregion - - #region RemoveServerGroup - - #region One User - - public Task RemoveServerGroup(int serverGroupId, int clientDatabaseId) => RemoveServerGroup(serverGroupId, new int[] { clientDatabaseId }); - - public Task RemoveServerGroup(int serverGroupId, GetClientInfo clientInfo) => RemoveServerGroup(serverGroupId, clientInfo.DatabaseId); - - public Task RemoveServerGroup(GetServerGroup serverGroup, int clientDatabaseId) => RemoveServerGroup(serverGroup.Id, clientDatabaseId); - - public Task RemoveServerGroup(GetServerGroup serverGroup, GetClientInfo clientInfo) => RemoveServerGroup(serverGroup.Id, clientInfo.DatabaseId); - - #endregion - - #region Multiple Users - - public Task RemoveServerGroup(int serverGroupId, IEnumerable clientInfo) => RemoveServerGroup(serverGroupId, clientInfo.Select(info => info.DatabaseId)); - - public Task RemoveServerGroup(GetServerGroup serverGroup, IEnumerable clientDatabaseIds) => RemoveServerGroup(serverGroup.Id, clientDatabaseIds); - - public Task RemoveServerGroup(GetServerGroup serverGroup, IEnumerable clientInfo) => RemoveServerGroup(serverGroup.Id, clientInfo.Select(info => info.DatabaseId)); - - public Task RemoveServerGroup(int serverGroupId, IEnumerable clientDatabaseIds) - { - return Client.Send("servergroupdelclient", - new Parameter("sgid", serverGroupId), - new Parameter("cldbid", clientDatabaseIds.Select(id => new ParameterValue(id)).ToArray())); - } - - #endregion - - #endregion - - #endregion - - #region Channel Methods - - #region GetChannels - - public async Task> GetChannels() - { - var res = await Client.Send("channellist").ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - - public async Task> GetChannels(GetChannelOptions options) - { - var optionList = options.GetFlagsName(); - var res = await Client.Send("channellist", null, optionList.ToArray()).ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - - #endregion - #region GetChannelInfo - - public Task GetChannelInfo(GetChannelListInfo channel) - { - if (channel == null) - throw new ArgumentNullException(nameof(channel)); - return GetChannelInfo(channel.Id); - } - - public async Task GetChannelInfo(int channelId) - { - var res = await Client.Send("channelinfo", - new Parameter("cid", channelId)) - .ConfigureAwait(false); - return DataProxy.SerializeGeneric(res).FirstOrDefault(); - } - - #endregion - #region FindChannel - - public async Task> FindChannel() - { - var res = await Client.Send("channelfind").ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - public async Task> FindChannel(string pattern) - { - var res = await Client.Send("channelfind", - new Parameter("pattern", pattern ?? string.Empty)) - .ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - - #endregion - #region MoveChannel - - public Task MoveChannel(GetChannelListInfo channel, GetChannelListInfo parent) - { - if (channel == null) - throw new ArgumentNullException(nameof(channel)); - if (parent == null) - throw new ArgumentNullException(nameof(parent)); - return MoveChannel(channel.Id, parent.Id); - } - public Task MoveChannel(GetChannelListInfo channel, GetChannelListInfo parent, int order) - { - if (channel == null) - throw new ArgumentNullException(nameof(channel)); - if (parent == null) - throw new ArgumentNullException(nameof(parent)); - return MoveChannel(channel.Id, parent.Id, order); - } - - public Task MoveChannel(int channelId, int parentChannelId) - { - return Client.Send("channelmove", - new Parameter("cid", channelId), - new Parameter("cpid", parentChannelId)); - } - public Task MoveChannel(int channelId, int parentChannelId, int order) - { - return Client.Send("channelmove", - new Parameter("cid", channelId), - new Parameter("cpid", parentChannelId), - new Parameter("order", order)); - } - - #endregion - #region CreateChannel - - // Region setting properties not supported yet - - public async Task CreateChannel(string name) - { - if (string.IsNullOrEmpty(name)) - throw new ArgumentNullException(nameof(name)); - - var res = await Client.Send("channelcreate", - new Parameter("channel_name", name)) - .ConfigureAwait(false); - return DataProxy.SerializeGeneric(res).FirstOrDefault(); - } - - #endregion - #region DeleteChannel - - public Task DeleteChannel(GetChannelListInfo channel) - { - if (channel == null) - throw new ArgumentNullException(nameof(channel)); - return DeleteChannel(channel.Id); - } - public Task DeleteChannel(GetChannelListInfo channel, bool force) - { - if (channel == null) - throw new ArgumentNullException(nameof(channel)); - return DeleteChannel(channel.Id, force); - } - - public Task DeleteChannel(int channelId) - { - return Client.Send("channeldelete", - new Parameter("cid", channelId)); - } - public Task DeleteChannel(int channelId, bool force) - { - return Client.Send("channeldelete", - new Parameter("cid", channelId), - new Parameter("force", force)); - } - - #endregion - #region EditChannel - - public Task EditChannel(int channelId, EditChannelInfo channel) - { - var updateParameters = new List - { - new Parameter("cid", channelId), - }; - - if (channel.Name != null) { updateParameters.Add(new Parameter("channel_name", channel.Name)); } - if (channel.Topic != null) { updateParameters.Add(new Parameter("channel_topic", channel.Topic)); } - if (channel.Description != null) { updateParameters.Add(new Parameter("channel_description", channel.Description)); } - if (channel.Password != null) { updateParameters.Add(new Parameter("channel_password", channel.Password)); } - if (channel.Codec != null) { updateParameters.Add(new Parameter("channel_codec", (int)channel.Codec)); } - if (channel.CodecQuality != null) { updateParameters.Add(new Parameter("channel_codec_quality", channel.CodecQuality)); } - if (channel.MaxClients != null) { updateParameters.Add(new Parameter("channel_maxclients", channel.MaxClients)); } - if (channel.MaxFamilyClients != null) { updateParameters.Add(new Parameter("channel_maxfamilyclients", channel.MaxFamilyClients)); } - if (channel.Order != null) { updateParameters.Add(new Parameter("channel_order", channel.Order)); } - if (channel.IsPermanent != null) { updateParameters.Add(new Parameter("channel_flag_permanent", channel.IsPermanent)); } - if (channel.IsSemiPermanent != null) { updateParameters.Add(new Parameter("channel_flag_semi_permanent", channel.IsSemiPermanent)); } - if (channel.IsTemporary != null) { updateParameters.Add(new Parameter("channel_flag_temporary", channel.IsTemporary)); } - if (channel.IsDefaultChannel != null) { updateParameters.Add(new Parameter("channel_flag_default", channel.IsDefaultChannel)); } - if (channel.IsMaxClientsUnlimited != null) { updateParameters.Add(new Parameter("channel_flag_maxclients_unlimited", channel.IsMaxClientsUnlimited)); } - if (channel.IsMaxFamilyClientsUnlimited != null) { updateParameters.Add(new Parameter("channel_flag_maxfamilyclients_unlimited", channel.IsMaxFamilyClientsUnlimited)); } - if (channel.IsMaxFamilyClientsInherited != null) { updateParameters.Add(new Parameter("channel_flag_maxfamilyclients_inherited", channel.IsMaxFamilyClientsInherited)); } - if (channel.NeededTalkPower != null) { updateParameters.Add(new Parameter("channel_needed_talk_power", channel.NeededTalkPower)); } - if (channel.PhoneticName != null) { updateParameters.Add(new Parameter("channel_name_phonetic", channel.PhoneticName)); } - if (channel.IconId != null) { updateParameters.Add(new Parameter("channel_icon_id", (int)channel.IconId)); } - if (channel.IsCodecUnencrypted != null) { updateParameters.Add(new Parameter("channel_codec_is_unencrypted", channel.IsCodecUnencrypted)); } - if (channel.ParentChannelId != null) { updateParameters.Add(new Parameter("channel_cpid", channel.ParentChannelId)); } - - return Client.Send("channeledit", updateParameters.ToArray()); - } - - #endregion - #region ChannelAddPerm - public Task ChannelAddPerm(int channelId, string permsId, int permValue) - { - return Client.Send("channeladdperm", - new Parameter("cid", channelId), - new Parameter("permsid", permsId), - new Parameter("permvalue", permValue)); - } - #endregion - - #endregion - - #region Server Methods - - #region GetServers - - public async Task> GetServers() - { - var res = await Client.Send("serverlist").ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - - public async Task> GetServers(GetServerOptions options) - { - var optionList = options.GetFlagsName(); - var res = await Client.Send("serverlist", null, optionList.ToArray()).ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - - public async Task> GetServerGroups() - { - var res = await Client.Send("servergrouplist").ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - - public async Task> GetServerGroupClientList(int serverGroupDatabaseId) - { - var res = await Client.Send("servergroupclientlist", new Parameter("sgid", serverGroupDatabaseId)).ConfigureAwait(false); - return DataProxy.SerializeGeneric(res); - } - - #endregion - - #endregion - - #region Message Methods - - #region SendTextMessage - - public Task SendMessage(string message, GetServerListInfo targetServer) - { - if (targetServer == null) - throw new ArgumentNullException(nameof(targetServer)); - return SendMessage(message, MessageTarget.Server, targetServer.Id); - } - public Task SendMessage(string message, GetChannelListInfo targetChannel) - { - if (targetChannel == null) - throw new ArgumentNullException(nameof(targetChannel)); - return SendMessage(message, MessageTarget.Channel, targetChannel.Id); - } - public Task SendMessage(string message, GetClientInfo targetClient) - { - if (targetClient == null) - throw new ArgumentNullException(nameof(targetClient)); - return SendMessage(message, MessageTarget.Private, targetClient.Id); - } - public Task SendMessage(string message, MessageTarget target, int targetId) - { - message = message ?? string.Empty; - return Client.Send("sendtextmessage", - new Parameter("targetmode", (int)target), - new Parameter("target", targetId), - new Parameter("msg", message)); - } - - #endregion - #region SendGlobalMessage - - public Task SendGlobalMessage(string message) - { - return Client.Send("gm", - new Parameter("msg", message ?? string.Empty)); - } - - #endregion - #region PokeClient - - public Task PokeClient(GetClientInfo client) - { - if (client == null) - throw new ArgumentNullException(nameof(client)); - return PokeClient(client.Id); - } - public Task PokeClient(int clientId) - { - return PokeClient(clientId, string.Empty); - } - - public Task PokeClient(GetClientInfo client, string message) - { - if (client == null) - throw new ArgumentNullException(nameof(client)); - return PokeClient(client.Id, message); - } - public Task PokeClient(int clientId, string message) - { - return Client.Send("clientpoke", - new Parameter("msg", message ?? string.Empty), - new Parameter("clid", clientId)); - } - - #endregion - - #region ChangeNickName - public Task ChangeNickName(string nickName) => ChangeNickName(nickName, default); - - public Task ChangeNickName(string nickName, WhoAmI whoAmI) - { - if (whoAmI != null) - whoAmI.NickName = nickName; - return Client.Send("clientupdate", - new Parameter("client_nickname", nickName)); - } - #endregion - - #endregion - - #region Filetransfer Methods - - #region CreateDirectory - - public Task CreateDirectory(int channelId, string dirPath) => CreateDirectory(channelId, string.Empty, dirPath); - - public Task CreateDirectory(int channelId, string channelPassword, string dirPath) - { - return Client.Send("ftcreatedir", - new Parameter("cid", channelId), - new Parameter("cpw", channelPassword), - new Parameter("dirname", NormalizePath(dirPath))); - } - - #endregion - - #region DeleteFile - - public Task DeleteFile(int channelId, string filePath) => DeleteFile(channelId, string.Empty, new string[] { filePath }); - - public Task DeleteFile(int channelId, string channelPassword, string filePath) => DeleteFile(channelId, channelPassword, new string[] { filePath }); - - public Task DeleteFile(int channelId, IEnumerable filePaths) => DeleteFile(channelId, string.Empty, filePaths); - - public Task DeleteFile(int channelId, string channelPassword, IEnumerable filePaths) - { - return Client.Send("ftdeletefile", - new Parameter("cid", channelId), - new Parameter("cpw", channelPassword), - new Parameter("name", filePaths.Select(path => new ParameterValue(NormalizePath(path))).ToArray())); - } - - #endregion - - #region GetFileInfo - - public Task GetFileInfo(int channelId, string filePath) => GetFileInfo(channelId, string.Empty, filePath); - - public async Task GetFileInfo(int channelId, string channelPassword, string filePath) - { - var res = await Client.Send("ftgetfileinfo", - new Parameter("cid", channelId), - new Parameter("cpw", channelPassword), - new Parameter("name", NormalizePath(filePath))).ConfigureAwait(false); - - return DataProxy.SerializeGeneric(res).FirstOrDefault(); - } - - #endregion - - #region GetFileList - - public Task> GetFiles(int channelId) => GetFiles(channelId, string.Empty, "/"); - - public Task> GetFiles(int channelId, string dirPath) => GetFiles(channelId, string.Empty, dirPath); - - public async Task> GetFiles(int channelId, string channelPassword, string dirPath) - { - var res = await Client.Send("ftgetfilelist", - new Parameter("cid", channelId), - new Parameter("cpw", channelPassword), - new Parameter("path", NormalizePath(dirPath))).ConfigureAwait(false); - - return DataProxy.SerializeGeneric(res); - } - - #endregion - - #region MoveFile - - #region Same Channel - - public Task MoveFile(int channelId, string oldFilePath, string newFilePath) => MoveFile(channelId, string.Empty, oldFilePath, newFilePath); - - public Task MoveFile(int channelId, string channelPassword, string oldFilePath, string newFilePath) - { - return Client.Send("ftrenamefile", - new Parameter("cid", channelId), - new Parameter("cpw", channelPassword), - new Parameter("oldname", NormalizePath(oldFilePath)), - new Parameter("newname", NormalizePath(newFilePath))); - } - - #endregion - - #region Other Channel - - public Task MoveFile(int channelId, string oldFilePath, int targetChannelId, string newFilePath) => MoveFile(channelId, string.Empty, oldFilePath, targetChannelId, string.Empty, newFilePath); - - public Task MoveFile(int channelId, string channelPassword, string oldFilePath, int targetChannelId, string newFilePath) => MoveFile(channelId, channelPassword, oldFilePath, targetChannelId, string.Empty, newFilePath); - - public Task MoveFile(int channelId, string oldFilePath, int targetChannelId, string targetChannelPassword, string newFilePath) => MoveFile(channelId, string.Empty, oldFilePath, targetChannelId, targetChannelPassword, newFilePath); - - public Task MoveFile(int channelId, string channelPassword, string oldFilePath, int targetChannelId, string targetChannelPassword, string newFilePath) - { - return Client.Send("ftrenamefile", - new Parameter("cid", channelId), - new Parameter("cpw", channelPassword), - new Parameter("tcid", targetChannelId), - new Parameter("tcpw", targetChannelPassword), - new Parameter("oldname", NormalizePath(oldFilePath)), - new Parameter("newname", NormalizePath(newFilePath))); - } - - #endregion - - #endregion - - #region UploadFile - - public Task UploadFile(int channelId, string filePath, byte[] data, bool overwrite = true, bool verify = true) => UploadFile(channelId, string.Empty, filePath, data, overwrite, verify); - - public async Task UploadFile(int channelId, string channelPassword, string filePath, byte[] data, bool overwrite = true, bool verify = true) - { - var res = await Client.Send("ftinitupload", - new Parameter("clientftfid", _fileTransferClient.GetFileTransferId()), - new Parameter("cid", channelId), - new Parameter("cpw", channelPassword), - new Parameter("name", NormalizePath(filePath)), - new Parameter("size", data.Length), - new Parameter("overwrite", overwrite), - new Parameter("resume", 0)).ConfigureAwait(false); - - var parsedRes = DataProxy.SerializeGeneric(res).First(); - - await _fileTransferClient.SendFile(data, parsedRes.Port, parsedRes.FileTransferKey).ConfigureAwait(false); - - if (verify) - { - await VerifyUpload(parsedRes.ServerFileTransferId).ConfigureAwait(false); - } - } - - public Task UploadFile(int channelId, string filePath, Stream dataStream, long size, bool overwrite = true, bool verify = true) => UploadFile(channelId, string.Empty, filePath, dataStream, size, overwrite, verify); - - public async Task UploadFile(int channelId, string channelPassword, string filePath, Stream dataStream, long size, bool overwrite = true, bool verify = true) - { - var res = await Client.Send("ftinitupload", - new Parameter("clientftfid", _fileTransferClient.GetFileTransferId()), - new Parameter("cid", channelId), - new Parameter("cpw", channelPassword), - new Parameter("name", NormalizePath(filePath)), - new Parameter("size", size), - new Parameter("overwrite", overwrite), - new Parameter("resume", 0)).ConfigureAwait(false); - - var parsedRes = DataProxy.SerializeGeneric(res).First(); - - await _fileTransferClient.SendFile(dataStream, parsedRes.Port, parsedRes.FileTransferKey).ConfigureAwait(false); - - if (verify) - { - await VerifyUpload(parsedRes.ServerFileTransferId).ConfigureAwait(false); - } - } - - /// Waits until the server fully receives the file or throws an exception when the upload times out. - private async Task VerifyUpload(int serverFileTransferId) - { - long arrivedBytes = 0; - var intervalMillis = 100; - var timeoutMillis = 3000; - var currentTimeoutMillis = intervalMillis * -1; - - while (true) - { - var transfers = await GetCurrentFileTransfers(); - var currentTransfer = transfers.Where(transfer => transfer.ServerFileTransferId == serverFileTransferId).FirstOrDefault(); - - if (currentTransfer == null) - { - // Download finished - return; - } - - if (currentTransfer.SizeDone == arrivedBytes) - { - // No upload progress - currentTimeoutMillis += intervalMillis; - - if (currentTimeoutMillis > timeoutMillis) - { - try - { - await StopFileTransfer(serverFileTransferId).ConfigureAwait(false); - } - catch - { } - - throw new FileTransferException("File upload timed out."); - } - } - else - { - // Upload progress - currentTimeoutMillis = 0; - arrivedBytes = currentTransfer.SizeDone; - } - - await Task.Delay(intervalMillis).ConfigureAwait(false); - } - } - - #endregion - - #region DownloadFile - - public Task DownloadFile(int channelId, string filePath) => DownloadFile(channelId, string.Empty, filePath); - - public async Task DownloadFile(int channelId, string channelPassword, string filePath) - { - var res = await Client.Send("ftinitdownload", - new Parameter("clientftfid", _fileTransferClient.GetFileTransferId()), - new Parameter("cid", channelId), - new Parameter("cpw", channelPassword), - new Parameter("name", NormalizePath(filePath)), - new Parameter("seekpos", 0)).ConfigureAwait(false); - - var parsedRes = DataProxy.SerializeGeneric(res).First(); - - if (parsedRes.Size > int.MaxValue) - { - throw new FileTransferException("The file is too big for a single byte array."); - } - - return await _fileTransferClient.ReceiveFile((int)parsedRes.Size, parsedRes.Port, parsedRes.FileTransferKey).ConfigureAwait(false); - } - - #endregion - - #region GetCurrentFileTransfers - - public async Task> GetCurrentFileTransfers() - { - try - { - var res = await Client.Send("ftlist").ConfigureAwait(false); - - return DataProxy.SerializeGeneric(res); - } - catch (QueryException ex) - { - if (ex.Error.Id == 1281) - { - // For some reason this error occurs when there are no active file transfers - return new ReadOnlyCollection(new GetCurrentFileTransfer[0]); - } - else - { - throw ex; - } - } - } - - #endregion - - private Task StopFileTransfer(int serverFileTransferId, bool delete = true) - { - return Client.Send("ftstop", - new Parameter("serverftfid", serverFileTransferId), - new Parameter("delete", delete)); - } - - private string NormalizePath(string path) - { - // Replace a sequence of backslashes with one forward slash - var result = Regex.Replace(path, @"\\+", "/"); - - // Make sure that the path starts with a slash - if (!result.StartsWith("/")) - { - result = "/" + result; - } - - return result; - } - - #endregion - - #endregion - - #region IDisposable support - - /// Finalizes the object. - ~TeamSpeakClient() - { - Dispose(false); - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// A value indicating whether the object is disposing or finalizing. - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - Client?.Dispose(); - } - } - - #endregion - } -} diff --git a/src/TeamSpeak3QueryApi/TeamSpeak3QueryApi.csproj b/src/TeamSpeak3QueryApi/TeamSpeak3QueryApi.csproj index 3e01ff1..6d58760 100644 --- a/src/TeamSpeak3QueryApi/TeamSpeak3QueryApi.csproj +++ b/src/TeamSpeak3QueryApi/TeamSpeak3QueryApi.csproj @@ -25,7 +25,8 @@ - + + diff --git a/src/TeamSpeak3QueryApi/TeamSpeakClient.cs b/src/TeamSpeak3QueryApi/TeamSpeakClient.cs new file mode 100644 index 0000000..8a23f0a --- /dev/null +++ b/src/TeamSpeak3QueryApi/TeamSpeakClient.cs @@ -0,0 +1,1044 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using TeamSpeak3QueryApi.Net.FileTransfer; +using TeamSpeak3QueryApi.Net.Query; +using TeamSpeak3QueryApi.Net.Enums; +using TeamSpeak3QueryApi.Net.Extensions; +using TeamSpeak3QueryApi.Net.Query.Parameters; +using TeamSpeak3QueryApi.Net.Query.Responses; +using TeamSpeak3QueryApi.Net.Query.Enums; +using TeamSpeak3QueryApi.Net.Query.Notifications; + +namespace TeamSpeak3QueryApi.Net +{ + public class TeamSpeakClient : IDisposable + { + public QueryClient Client { get; } + + // TODO: Migrate to ValueTuples + private readonly List>> _callbacks = new List>>(); + + private readonly FileTransferClient _fileTransferClient; + + #region Ctors + + /// Creates a new instance of using the and . + public TeamSpeakClient() + : this(QueryClient.DefaultHost, TelnetQueryClient.DefaultPort, Protocol.Telnet) + { } + + /// Creates a new instance of using the provided host TCP port. + /// The host name of the remote server. + /// The TCP port of the Query API server. + public TeamSpeakClient(string hostName, int port = TelnetQueryClient.DefaultPort, Protocol type = Protocol.Telnet) + { + switch (type) + { + case Protocol.Telnet: + Client = new TelnetQueryClient(hostName, port); + break; + case Protocol.SSH: + Client = new SshQueryClient(hostName, port); + break; + } + _fileTransferClient = new FileTransferClient(hostName); + } + + #endregion + + public Task ConnectAsync() => Client.ConnectAsync(); + public void Connect(string username, string password) => Client.Connect(username, password); + + + #region Subscriptions + + public void Subscribe(Action> callback) + where T : Notification + { + var notification = GetNotificationType(); + + Action cb = data => callback(DataProxy.SerializeGeneric(data.Payload)); + + _callbacks.Add(Tuple.Create(notification, callback as object, cb)); + Client.Subscribe(notification.ToString(), cb); + } + public void Unsubscribe() + where T : Notification + { + var notification = GetNotificationType(); + var cbts = _callbacks.Where(tp => tp.Item1 == notification).ToList(); + cbts.ForEach(k => _callbacks.Remove(k)); + Client.Unsubscribe(notification.ToString()); + } + public void Unsubscribe(Action> callback) + where T : Notification + { + var notification = GetNotificationType(); + var cbt = _callbacks.SingleOrDefault(t => t.Item1 == notification && t.Item2.Equals(callback)); + if (cbt != null) + { + _callbacks.Remove(cbt); + Client.Unsubscribe(notification.ToString(), cbt.Item3); + } + } + + private static NotificationType GetNotificationType() + { + if (!Enum.TryParse(typeof(T).Name, out NotificationType notification)) // This may violate the generic pattern. May change this later. + throw new ArgumentException("The specified generic parameter is not a supported NotificationType."); // For this time, we only support class-internal types which are listed in NotificationType + return notification; + } + + #endregion + #region Implented api methods + + public Task LoginAsync(string userName, string password) + { + if (Client.ConnectionType == Protocol.SSH) + throw new InvalidOperationException("Login is not needed in ssh protocol"); + + return Client.SendAsync("login", new Parameter("client_login_name", userName), new Parameter("client_login_password", password)); + } + + public Task LogoutAsync() => Client.SendAsync("logout"); + + public Task UseServerAsync(int serverId) + { + return Client.SendAsync("use", new Parameter("sid", serverId.ToString(CultureInfo.InvariantCulture))); + } + + public async Task WhoAmIAsync() + { + var res = await Client.SendAsync("whoami").ConfigureAwait(false); + var proxied = DataProxy.SerializeGeneric(res); + return proxied.FirstOrDefault(); + } + + #region Notification Methods + + public Task RegisterChannelNotificationAsync(int channelId) => RegisterNotificationAsync(NotificationEventTarget.Channel, channelId); + public Task RegisterAllChannelNotificationAsync() => RegisterNotificationAsync(NotificationEventTarget.Channel, 0); + public Task RegisterServerNotificationAsync() => RegisterNotificationAsync(NotificationEventTarget.Server, -1); + public Task RegisterTextServerNotificationAsync() => RegisterNotificationAsync(NotificationEventTarget.TextServer, -1); + public Task RegisterTextChannelNotificationAsync() => RegisterNotificationAsync(NotificationEventTarget.TextChannel, -1); + public Task RegisterTextPrivateNotificationAsync() => RegisterNotificationAsync(NotificationEventTarget.TextPrivate, -1); + private Task RegisterNotificationAsync(NotificationEventTarget target, int channelId) + { + var ev = new Parameter("event", target.ToString().ToLowerInvariant()); + if (target == NotificationEventTarget.Channel) + return Client.SendAsync("servernotifyregister", ev, new Parameter("id", channelId)); + return Client.SendAsync("servernotifyregister", ev); + } + + #endregion + + #region Client Methods + + #region MoveClient + + public Task MoveClientAsync(int clientId, int targetChannelId) => MoveClientAsync(new[] { clientId }, targetChannelId); + public Task MoveClientAsync(int clientId, int targetChannelId, string channelPassword) => MoveClientAsync(new[] { clientId }, targetChannelId, channelPassword); + + public Task MoveClientAsync(IEnumerable clients, int targetChannelId) + { + var clIds = clients.Select(c => c.Id).ToArray(); + return MoveClientAsync(clIds, targetChannelId); + } + public Task MoveClientAsync(IEnumerable clients, int targetChannelId, string channelPassword) + { + var clIds = clients.Select(c => c.Id).ToArray(); + return MoveClientAsync(clIds, targetChannelId, channelPassword); + } + + public Task MoveClientAsync(IList clientIds, int targetChannelId) + { + return Client.SendAsync("clientmove", + new Parameter("clid", clientIds.Select(i => new ParameterValue(i)).ToArray()), + new Parameter("cid", targetChannelId)); + } + public Task MoveClientAsync(IList clientIds, int targetChannelId, string channelPassword) + { + return Client.SendAsync("clientmove", + new Parameter("clid", clientIds.Select(i => new ParameterValue(i)).ToArray()), + new Parameter("cid", targetChannelId), + new Parameter("cpw", channelPassword)); + } + + #endregion + #region KickClient + + public Task KickClientAsync(int clientId, KickOrigin from) => KickClientAsync(new[] { clientId }, from); + public Task KickClientAsync(int clientId, KickOrigin from, string reasonMessage) => KickClientAsync(new[] { clientId }, from, reasonMessage); + public Task KickClientAsync(GetClientInfo client, KickOrigin from) => KickClientAsync(client.Id, from); + public Task KickClientAsync(IEnumerable clients, KickOrigin from, string v) + { + var clIds = clients.Select(c => c.Id).ToArray(); + return KickClientAsync(clIds, from); + } + public Task KickClientAsync(IList clientIds, KickOrigin from) + { + return Client.SendAsync("clientkick", + new Parameter("reasonid", (int)from), + new Parameter("clid", clientIds.Select(i => new ParameterValue(i)).ToArray())); + } + public Task KickClientAsync(IList clientIds, KickOrigin from, string reasonMessage) + { + return Client.SendAsync("clientkick", + new Parameter("reasonid", (int)from), + new Parameter("reasonmsg", reasonMessage), + new Parameter("clid", clientIds.Select(i => new ParameterValue(i)).ToArray())); + } + + #endregion + #region BanClient + + public Task> BanClientAsync(GetClientInfo client) + { + if (client == null) + throw new ArgumentNullException(nameof(client)); + return BanClientAsync(client.Id); + } + public Task> BanClientAsync(GetClientInfo client, TimeSpan duration) + { + if (client == null) + throw new ArgumentNullException(nameof(client)); + return BanClientAsync(client.Id, duration); + } + public Task> BanClientAsync(GetClientInfo client, TimeSpan duration, string reason) + { + if (client == null) + throw new ArgumentNullException(nameof(client)); + return BanClientAsync(client.Id, duration, reason); + } + + public async Task> BanClientAsync(int clientId) + { + var res = await Client.SendAsync("banclient", + new Parameter("clid", clientId)) + .ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + public async Task> BanClientAsync(int clientId, TimeSpan duration) + { + var res = await Client.SendAsync("banclient", + new Parameter("clid", clientId), + new Parameter("time", (int)Math.Ceiling(duration.TotalSeconds))) + .ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + public async Task> BanClientAsync(int clientId, TimeSpan duration, string reason) + { + var res = await Client.SendAsync("banclient", + new Parameter("clid", clientId), + new Parameter("time", (int)Math.Ceiling(duration.TotalSeconds)), + new Parameter("banreason", reason ?? string.Empty)) + .ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + + #endregion + #region GetClients + + public async Task> GetClientsAsync() + { + var res = await Client.SendAsync("clientlist").ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + + public async Task> GetClientsAsync(GetClientOptions options) + { + var optionList = options.GetFlagsName(); + var res = await Client.SendAsync("clientlist", null, optionList.ToArray()).ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + + public Task GetClientInfoAsync(GetClientInfo client) => GetClientInfoAsync(client.Id); + + public async Task GetClientInfoAsync(int clientId) + { + var res = await Client.SendAsync("clientinfo", + new Parameter("clid", clientId)) + .ConfigureAwait(false); + + return DataProxy.SerializeGeneric(res).FirstOrDefault(); + } + + public async Task> GetDbClientsAsync(int start = 0, long duration = 9999999999) // Duration must be that long cause without this parameters it won't work. Bug from Teamspeak... + { + var res = await Client.SendAsync("clientdblist", + new Parameter("start", start), + new Parameter("duration", duration)) + .ConfigureAwait(false); + + return DataProxy.SerializeGeneric(res); + } + + public async Task GetClientIdsAsync(string UniqueIdentifier) + { + var res = await Client.SendAsync("clientgetids", + new Parameter("cluid", UniqueIdentifier)) + .ConfigureAwait(false); + return DataProxy.SerializeGeneric(res).FirstOrDefault(); + } + + public async Task GetDatabaseIdFromClientUniqueIdAsync(string UniqueIdentifier) + { + var res = await Client.SendAsync("clientgetdbidfromuid", + new Parameter("cluid", UniqueIdentifier)) + .ConfigureAwait(false); + return DataProxy.SerializeGeneric(res).FirstOrDefault(); + } + + public async Task GetNameFromClientDatabaseIdAsync(int DatabaseId) + { + var res = await Client.SendAsync("clientgetnamefromdbid", + new Parameter("cldbid", DatabaseId)) + .ConfigureAwait(false); + return DataProxy.SerializeGeneric(res).FirstOrDefault(); + } + + public async Task NameFromClientUidAsync(string UniqueIdentifier) + { + var res = await Client.SendAsync("clientgetnamefromuid", + new Parameter("cluid", UniqueIdentifier)) + .ConfigureAwait(false); + return DataProxy.SerializeGeneric(res).FirstOrDefault(); + } + + public async Task ClientUidFromClientIdAsync(int id) + { + var res = await Client.SendAsync("clientgetuidfromclid", + new Parameter("clid", id)) + .ConfigureAwait(false); + return DataProxy.SerializeGeneric(res).FirstOrDefault(); + } + + #endregion + + #region GetServerGroups + + public async Task> GetServerGroupsAsync(int clientDatabaseId) + { + var res = await Client.SendAsync("servergroupsbyclientid", new Parameter("cldbid", clientDatabaseId)).ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + + public Task> GetServerGroupsAsync(GetClientInfo clientInfo) => GetServerGroupsAsync(clientInfo.DatabaseId); + + public Task> GetServerGroupsAsync(WhoAmI clientInfo) => GetServerGroupsAsync(clientInfo.DatabaseId); + + #endregion + + #region AddServerGroup + + #region One User + + public Task AddServerGroupAsync(int serverGroupId, int clientDatabaseId) => AddServerGroupAsync(serverGroupId, new int[] { clientDatabaseId }); + + public Task AddServerGroupAsync(int serverGroupId, GetClientInfo clientInfo) => AddServerGroupAsync(serverGroupId, clientInfo.DatabaseId); + + public Task AddServerGroupAsync(GetServerGroup serverGroup, int clientDatabaseId) => AddServerGroupAsync(serverGroup.Id, clientDatabaseId); + + public Task AddServerGroupAsync(GetServerGroup serverGroup, GetClientInfo clientInfo) => AddServerGroupAsync(serverGroup.Id, clientInfo.DatabaseId); + + #endregion + + #region Multiple Users + + public Task AddServerGroupAsync(int serverGroupId, IEnumerable clientInfo) => AddServerGroupAsync(serverGroupId, clientInfo.Select(info => info.DatabaseId)); + + public Task AddServerGroupAsync(GetServerGroup serverGroup, IEnumerable clientDatabaseIds) => AddServerGroupAsync(serverGroup.Id, clientDatabaseIds); + + public Task AddServerGroupAsync(GetServerGroup serverGroup, IEnumerable clientInfo) => AddServerGroupAsync(serverGroup.Id, clientInfo.Select(info => info.DatabaseId)); + + public Task AddServerGroupAsync(int serverGroupId, IEnumerable clientDatabaseIds) + { + return Client.SendAsync("servergroupaddclient", + new Parameter("sgid", serverGroupId), + new Parameter("cldbid", clientDatabaseIds.Select(id => new ParameterValue(id)).ToArray())); + } + + #endregion + + #endregion + + #region RemoveServerGroup + + #region One User + + public Task RemoveServerGroupAsync(int serverGroupId, int clientDatabaseId) => RemoveServerGroupAsync(serverGroupId, new int[] { clientDatabaseId }); + + public Task RemoveServerGroupAsync(int serverGroupId, GetClientInfo clientInfo) => RemoveServerGroupAsync(serverGroupId, clientInfo.DatabaseId); + + public Task RemoveServerGroupAsync(GetServerGroup serverGroup, int clientDatabaseId) => RemoveServerGroupAsync(serverGroup.Id, clientDatabaseId); + + public Task RemoveServerGroupAsync(GetServerGroup serverGroup, GetClientInfo clientInfo) => RemoveServerGroupAsync(serverGroup.Id, clientInfo.DatabaseId); + + #endregion + + #region Multiple Users + + public Task RemoveServerGroupAsync(int serverGroupId, IEnumerable clientInfo) => RemoveServerGroupAsync(serverGroupId, clientInfo.Select(info => info.DatabaseId)); + + public Task RemoveServerGroupAsync(GetServerGroup serverGroup, IEnumerable clientDatabaseIds) => RemoveServerGroupAsync(serverGroup.Id, clientDatabaseIds); + + public Task RemoveServerGroupAsync(GetServerGroup serverGroup, IEnumerable clientInfo) => RemoveServerGroupAsync(serverGroup.Id, clientInfo.Select(info => info.DatabaseId)); + + public Task RemoveServerGroupAsync(int serverGroupId, IEnumerable clientDatabaseIds) + { + return Client.SendAsync("servergroupdelclient", + new Parameter("sgid", serverGroupId), + new Parameter("cldbid", clientDatabaseIds.Select(id => new ParameterValue(id)).ToArray())); + } + + #endregion + + #endregion + + #endregion + + #region Channel Methods + + #region GetChannels + + public async Task> GetChannelsAsync() + { + var res = await Client.SendAsync("channellist").ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + + public async Task> GetChannelsAsync(GetChannelOptions options) + { + var optionList = options.GetFlagsName(); + var res = await Client.SendAsync("channellist", null, optionList.ToArray()).ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + + #endregion + #region GetChannelInfo + + public Task GetChannelInfoAsync(GetChannelListInfo channel) + { + if (channel == null) + throw new ArgumentNullException(nameof(channel)); + return GetChannelInfoAsync(channel.Id); + } + + public async Task GetChannelInfoAsync(int channelId) + { + var res = await Client.SendAsync("channelinfo", + new Parameter("cid", channelId)) + .ConfigureAwait(false); + return DataProxy.SerializeGeneric(res).FirstOrDefault(); + } + + #endregion + #region FindChannel + + public async Task> FindChannelAsync() + { + var res = await Client.SendAsync("channelfind").ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + public async Task> FindChannelAsync(string pattern) + { + var res = await Client.SendAsync("channelfind", + new Parameter("pattern", pattern ?? string.Empty)) + .ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + + #endregion + #region MoveChannel + + public Task MoveChannelAsync(GetChannelListInfo channel, GetChannelListInfo parent) + { + if (channel == null) + throw new ArgumentNullException(nameof(channel)); + if (parent == null) + throw new ArgumentNullException(nameof(parent)); + return MoveChannelAsync(channel.Id, parent.Id); + } + public Task MoveChannelAsync(GetChannelListInfo channel, GetChannelListInfo parent, int order) + { + if (channel == null) + throw new ArgumentNullException(nameof(channel)); + if (parent == null) + throw new ArgumentNullException(nameof(parent)); + return MoveChannelAsync(channel.Id, parent.Id, order); + } + + public Task MoveChannelAsync(int channelId, int parentChannelId) + { + return Client.SendAsync("channelmove", + new Parameter("cid", channelId), + new Parameter("cpid", parentChannelId)); + } + public Task MoveChannelAsync(int channelId, int parentChannelId, int order) + { + return Client.SendAsync("channelmove", + new Parameter("cid", channelId), + new Parameter("cpid", parentChannelId), + new Parameter("order", order)); + } + + #endregion + #region CreateChannel + + // Region setting properties not supported yet + + public async Task CreateChannelAsync(string name) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + + var res = await Client.SendAsync("channelcreate", + new Parameter("channel_name", name)) + .ConfigureAwait(false); + return DataProxy.SerializeGeneric(res).FirstOrDefault(); + } + + public async Task CreateChannelAsync(string name, EditChannelInfo channel) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + + var addParameters = new List + { + new Parameter("channel_name", name), + }; + + if (channel.Topic != null) { addParameters.Add(new Parameter("channel_topic", channel.Topic)); } + if (channel.Description != null) { addParameters.Add(new Parameter("channel_description", channel.Description)); } + if (channel.Password != null) { addParameters.Add(new Parameter("channel_password", channel.Password)); } + if (channel.Codec != null) { addParameters.Add(new Parameter("channel_codec", (int)channel.Codec)); } + if (channel.CodecQuality != null) { addParameters.Add(new Parameter("channel_codec_quality", channel.CodecQuality)); } + if (channel.MaxClients != null) { addParameters.Add(new Parameter("channel_maxclients", channel.MaxClients)); } + if (channel.MaxFamilyClients != null) { addParameters.Add(new Parameter("channel_maxfamilyclients", channel.MaxFamilyClients)); } + if (channel.Order != null) { addParameters.Add(new Parameter("channel_order", channel.Order)); } + if (channel.IsPermanent != null) { addParameters.Add(new Parameter("channel_flag_permanent", channel.IsPermanent)); } + if (channel.IsSemiPermanent != null) { addParameters.Add(new Parameter("channel_flag_semi_permanent", channel.IsSemiPermanent)); } + if (channel.IsTemporary != null) { addParameters.Add(new Parameter("channel_flag_temporary", channel.IsTemporary)); } + if (channel.IsDefaultChannel != null) { addParameters.Add(new Parameter("channel_flag_default", channel.IsDefaultChannel)); } + if (channel.IsMaxClientsUnlimited != null) { addParameters.Add(new Parameter("channel_flag_maxclients_unlimited", channel.IsMaxClientsUnlimited)); } + if (channel.IsMaxFamilyClientsUnlimited != null) { addParameters.Add(new Parameter("channel_flag_maxfamilyclients_unlimited", channel.IsMaxFamilyClientsUnlimited)); } + if (channel.IsMaxFamilyClientsInherited != null) { addParameters.Add(new Parameter("channel_flag_maxfamilyclients_inherited", channel.IsMaxFamilyClientsInherited)); } + if (channel.NeededTalkPower != null) { addParameters.Add(new Parameter("channel_needed_talk_power", channel.NeededTalkPower)); } + if (channel.PhoneticName != null) { addParameters.Add(new Parameter("channel_name_phonetic", channel.PhoneticName)); } + if (channel.IconId != null) { addParameters.Add(new Parameter("channel_icon_id", (int)channel.IconId)); } + if (channel.IsCodecUnencrypted != null) { addParameters.Add(new Parameter("channel_codec_is_unencrypted", channel.IsCodecUnencrypted)); } + if (channel.ParentChannelId != null) { addParameters.Add(new Parameter("cpid", channel.ParentChannelId)); } + + var res = await Client + .SendAsync("channelcreate", addParameters.ToArray()) + .ConfigureAwait(false); + return DataProxy.SerializeGeneric(res).FirstOrDefault(); + } + + #endregion + #region DeleteChannel + + public Task DeleteChannelAsync(GetChannelListInfo channel) + { + if (channel == null) + throw new ArgumentNullException(nameof(channel)); + return DeleteChannelAsync(channel.Id); + } + public Task DeleteChannelAsync(GetChannelListInfo channel, bool force) + { + if (channel == null) + throw new ArgumentNullException(nameof(channel)); + return DeleteChannelAsync(channel.Id, force); + } + + public Task DeleteChannelAsync(int channelId) + { + return Client.SendAsync("channeldelete", + new Parameter("cid", channelId)); + } + public Task DeleteChannelAsync(int channelId, bool force) + { + return Client.SendAsync("channeldelete", + new Parameter("cid", channelId), + new Parameter("force", force)); + } + + #endregion + #region EditChannel + + public Task EditChannelAsync(int channelId, EditChannelInfo channel) + { + var updateParameters = new List + { + new Parameter("cid", channelId), + }; + + if (channel.Name != null) { updateParameters.Add(new Parameter("channel_name", channel.Name)); } + if (channel.Topic != null) { updateParameters.Add(new Parameter("channel_topic", channel.Topic)); } + if (channel.Description != null) { updateParameters.Add(new Parameter("channel_description", channel.Description)); } + if (channel.Password != null) { updateParameters.Add(new Parameter("channel_password", channel.Password)); } + if (channel.Codec != null) { updateParameters.Add(new Parameter("channel_codec", (int)channel.Codec)); } + if (channel.CodecQuality != null) { updateParameters.Add(new Parameter("channel_codec_quality", channel.CodecQuality)); } + if (channel.MaxClients != null) { updateParameters.Add(new Parameter("channel_maxclients", channel.MaxClients)); } + if (channel.MaxFamilyClients != null) { updateParameters.Add(new Parameter("channel_maxfamilyclients", channel.MaxFamilyClients)); } + if (channel.Order != null) { updateParameters.Add(new Parameter("channel_order", channel.Order)); } + if (channel.IsPermanent != null) { updateParameters.Add(new Parameter("channel_flag_permanent", channel.IsPermanent)); } + if (channel.IsSemiPermanent != null) { updateParameters.Add(new Parameter("channel_flag_semi_permanent", channel.IsSemiPermanent)); } + if (channel.IsTemporary != null) { updateParameters.Add(new Parameter("channel_flag_temporary", channel.IsTemporary)); } + if (channel.IsDefaultChannel != null) { updateParameters.Add(new Parameter("channel_flag_default", channel.IsDefaultChannel)); } + if (channel.IsMaxClientsUnlimited != null) { updateParameters.Add(new Parameter("channel_flag_maxclients_unlimited", channel.IsMaxClientsUnlimited)); } + if (channel.IsMaxFamilyClientsUnlimited != null) { updateParameters.Add(new Parameter("channel_flag_maxfamilyclients_unlimited", channel.IsMaxFamilyClientsUnlimited)); } + if (channel.IsMaxFamilyClientsInherited != null) { updateParameters.Add(new Parameter("channel_flag_maxfamilyclients_inherited", channel.IsMaxFamilyClientsInherited)); } + if (channel.NeededTalkPower != null) { updateParameters.Add(new Parameter("channel_needed_talk_power", channel.NeededTalkPower)); } + if (channel.PhoneticName != null) { updateParameters.Add(new Parameter("channel_name_phonetic", channel.PhoneticName)); } + if (channel.IconId != null) { updateParameters.Add(new Parameter("channel_icon_id", (int)channel.IconId)); } + if (channel.IsCodecUnencrypted != null) { updateParameters.Add(new Parameter("channel_codec_is_unencrypted", channel.IsCodecUnencrypted)); } + if (channel.ParentChannelId != null) { updateParameters.Add(new Parameter("cpid", channel.ParentChannelId)); } + + return Client.SendAsync("channeledit", updateParameters.ToArray()); + } + + #endregion + #region ChannelAddPerm + public Task ChannelAddPermAsync(int channelId, string permsId, int permValue) + { + return Client.SendAsync("channeladdperm", + new Parameter("cid", channelId), + new Parameter("permsid", permsId), + new Parameter("permvalue", permValue)); + } + #endregion + + #endregion + + #region Server Methods + + #region GetServers + + public async Task> GetServersAsync() + { + var res = await Client.SendAsync("serverlist").ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + + public async Task> GetServersAsync(GetServerOptions options) + { + var optionList = options.GetFlagsName(); + var res = await Client.SendAsync("serverlist", null, optionList.ToArray()).ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + + public async Task> GetServerGroupsAsync() + { + var res = await Client.SendAsync("servergrouplist").ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + + public async Task> GetServerGroupClientListAsync(int serverGroupDatabaseId) + { + var res = await Client.SendAsync("servergroupclientlist", new Parameter("sgid", serverGroupDatabaseId)).ConfigureAwait(false); + return DataProxy.SerializeGeneric(res); + } + + #endregion + + #endregion + + #region Message Methods + + #region SendTextMessage + + public Task SendMessageAsync(string message, GetServerListInfo targetServer) + { + if (targetServer == null) + throw new ArgumentNullException(nameof(targetServer)); + return SendMessageAsync(message, MessageTarget.Server, targetServer.Id); + } + public Task SendMessageAsync(string message, GetChannelListInfo targetChannel) + { + if (targetChannel == null) + throw new ArgumentNullException(nameof(targetChannel)); + return SendMessageAsync(message, MessageTarget.Channel, targetChannel.Id); + } + public Task SendMessageAsync(string message, GetClientInfo targetClient) + { + if (targetClient == null) + throw new ArgumentNullException(nameof(targetClient)); + return SendMessageAsync(message, MessageTarget.Private, targetClient.Id); + } + public Task SendMessageAsync(string message, MessageTarget target, int targetId) + { + message = message ?? string.Empty; + return Client.SendAsync("sendtextmessage", + new Parameter("targetmode", (int)target), + new Parameter("target", targetId), + new Parameter("msg", message)); + } + + #endregion + #region SendGlobalMessage + + public Task SendGlobalMessageAsync(string message) + { + return Client.SendAsync("gm", + new Parameter("msg", message ?? string.Empty)); + } + + #endregion + #region PokeClient + + public Task PokeClientAsync(GetClientInfo client) + { + if (client == null) + throw new ArgumentNullException(nameof(client)); + return PokeClientAsync(client.Id); + } + public Task PokeClientAsync(int clientId) + { + return PokeClientAsync(clientId, string.Empty); + } + + public Task PokeClientAsync(GetClientInfo client, string message) + { + if (client == null) + throw new ArgumentNullException(nameof(client)); + return PokeClientAsync(client.Id, message); + } + public Task PokeClientAsync(int clientId, string message) + { + return Client.SendAsync("clientpoke", + new Parameter("msg", message ?? string.Empty), + new Parameter("clid", clientId)); + } + + #endregion + + #region ChangeNickName + public Task ChangeNickNameAsync(string nickName) => ChangeNickNameAsync(nickName, default); + + public Task ChangeNickNameAsync(string nickName, WhoAmI whoAmI) + { + if (whoAmI != null) + whoAmI.NickName = nickName; + return Client.SendAsync("clientupdate", + new Parameter("client_nickname", nickName)); + } + #endregion + + #endregion + + #region Filetransfer Methods + + #region CreateDirectory + + public Task CreateDirectoryAsync(int channelId, string dirPath) => CreateDirectoryAsync(channelId, string.Empty, dirPath); + + public Task CreateDirectoryAsync(int channelId, string channelPassword, string dirPath) + { + return Client.SendAsync("ftcreatedir", + new Parameter("cid", channelId), + new Parameter("cpw", channelPassword), + new Parameter("dirname", NormalizePath(dirPath))); + } + + #endregion + + #region DeleteFile + + public Task DeleteFileAsync(int channelId, string filePath) => DeleteFileAsync(channelId, string.Empty, new string[] { filePath }); + + public Task DeleteFileAsync(int channelId, string channelPassword, string filePath) => DeleteFileAsync(channelId, channelPassword, new string[] { filePath }); + + public Task DeleteFileAsync(int channelId, IEnumerable filePaths) => DeleteFileAsync(channelId, string.Empty, filePaths); + + public Task DeleteFileAsync(int channelId, string channelPassword, IEnumerable filePaths) + { + return Client.SendAsync("ftdeletefile", + new Parameter("cid", channelId), + new Parameter("cpw", channelPassword), + new Parameter("name", filePaths.Select(path => new ParameterValue(NormalizePath(path))).ToArray())); + } + + #endregion + + #region GetFileInfo + + public Task GetFileInfoAsync(int channelId, string filePath) => GetFileInfoAsync(channelId, string.Empty, filePath); + + public async Task GetFileInfoAsync(int channelId, string channelPassword, string filePath) + { + var res = await Client.SendAsync("ftgetfileinfo", + new Parameter("cid", channelId), + new Parameter("cpw", channelPassword), + new Parameter("name", NormalizePath(filePath))).ConfigureAwait(false); + + return DataProxy.SerializeGeneric(res).FirstOrDefault(); + } + + #endregion + + #region GetFileList + + public Task> GetFilesAsync(int channelId) => GetFilesAsync(channelId, string.Empty, "/"); + + public Task> GetFilesAsync(int channelId, string dirPath) => GetFilesAsync(channelId, string.Empty, dirPath); + + public async Task> GetFilesAsync(int channelId, string channelPassword, string dirPath) + { + var res = await Client.SendAsync("ftgetfilelist", + new Parameter("cid", channelId), + new Parameter("cpw", channelPassword), + new Parameter("path", NormalizePath(dirPath))).ConfigureAwait(false); + + return DataProxy.SerializeGeneric(res); + } + + #endregion + + #region MoveFile + + #region Same Channel + + public Task MoveFileAsync(int channelId, string oldFilePath, string newFilePath) => MoveFileAsync(channelId, string.Empty, oldFilePath, newFilePath); + + public Task MoveFileAsync(int channelId, string channelPassword, string oldFilePath, string newFilePath) + { + return Client.SendAsync("ftrenamefile", + new Parameter("cid", channelId), + new Parameter("cpw", channelPassword), + new Parameter("oldname", NormalizePath(oldFilePath)), + new Parameter("newname", NormalizePath(newFilePath))); + } + + #endregion + + #region Other Channel + + public Task MoveFileAsync(int channelId, string oldFilePath, int targetChannelId, string newFilePath) => MoveFileAsync(channelId, string.Empty, oldFilePath, targetChannelId, string.Empty, newFilePath); + + public Task MoveFileAsync(int channelId, string channelPassword, string oldFilePath, int targetChannelId, string newFilePath) => MoveFileAsync(channelId, channelPassword, oldFilePath, targetChannelId, string.Empty, newFilePath); + + public Task MoveFileAsync(int channelId, string oldFilePath, int targetChannelId, string targetChannelPassword, string newFilePath) => MoveFileAsync(channelId, string.Empty, oldFilePath, targetChannelId, targetChannelPassword, newFilePath); + + public Task MoveFileAsync(int channelId, string channelPassword, string oldFilePath, int targetChannelId, string targetChannelPassword, string newFilePath) + { + return Client.SendAsync("ftrenamefile", + new Parameter("cid", channelId), + new Parameter("cpw", channelPassword), + new Parameter("tcid", targetChannelId), + new Parameter("tcpw", targetChannelPassword), + new Parameter("oldname", NormalizePath(oldFilePath)), + new Parameter("newname", NormalizePath(newFilePath))); + } + + #endregion + + #endregion + + #region UploadFile + + public Task UploadFileAsync(int channelId, string filePath, byte[] data, bool overwrite = true, bool verify = true) => UploadFileAsync(channelId, string.Empty, filePath, data, overwrite, verify); + + public async Task UploadFileAsync(int channelId, string channelPassword, string filePath, byte[] data, bool overwrite = true, bool verify = true) + { + var res = await Client.SendAsync("ftinitupload", + new Parameter("clientftfid", _fileTransferClient.GetFileTransferId()), + new Parameter("cid", channelId), + new Parameter("cpw", channelPassword), + new Parameter("name", NormalizePath(filePath)), + new Parameter("size", data.Length), + new Parameter("overwrite", overwrite), + new Parameter("resume", 0)).ConfigureAwait(false); + + var parsedRes = DataProxy.SerializeGeneric(res).First(); + + await _fileTransferClient.SendFileAsync(data, parsedRes.Port, parsedRes.FileTransferKey).ConfigureAwait(false); + + if (verify) + { + await VerifyUploadAsync(parsedRes.ServerFileTransferId).ConfigureAwait(false); + } + } + + public Task UploadFileAsync(int channelId, string filePath, Stream dataStream, long size, bool overwrite = true, bool verify = true) => UploadFileAsync(channelId, string.Empty, filePath, dataStream, size, overwrite, verify); + + public async Task UploadFileAsync(int channelId, string channelPassword, string filePath, Stream dataStream, long size, bool overwrite = true, bool verify = true) + { + var res = await Client.SendAsync("ftinitupload", + new Parameter("clientftfid", _fileTransferClient.GetFileTransferId()), + new Parameter("cid", channelId), + new Parameter("cpw", channelPassword), + new Parameter("name", NormalizePath(filePath)), + new Parameter("size", size), + new Parameter("overwrite", overwrite), + new Parameter("resume", 0)).ConfigureAwait(false); + + var parsedRes = DataProxy.SerializeGeneric(res).First(); + + await _fileTransferClient.SendFileAsync(dataStream, parsedRes.Port, parsedRes.FileTransferKey).ConfigureAwait(false); + + if (verify) + { + await VerifyUploadAsync(parsedRes.ServerFileTransferId).ConfigureAwait(false); + } + } + + /// Waits until the server fully receives the file or throws an exception when the upload times out. + private async Task VerifyUploadAsync(int serverFileTransferId) + { + long arrivedBytes = 0; + var intervalMillis = 100; + var timeoutMillis = 3000; + var currentTimeoutMillis = intervalMillis * -1; + + while (true) + { + var transfers = await GetCurrentFileTransfersAsync(); + var currentTransfer = transfers.Where(transfer => transfer.ServerFileTransferId == serverFileTransferId).FirstOrDefault(); + + if (currentTransfer == null) + { + // Download finished + return; + } + + if (currentTransfer.SizeDone == arrivedBytes) + { + // No upload progress + currentTimeoutMillis += intervalMillis; + + if (currentTimeoutMillis > timeoutMillis) + { + try + { + await StopFileTransferAsync(serverFileTransferId).ConfigureAwait(false); + } + catch + { } + + throw new FileTransferException("File upload timed out."); + } + } + else + { + // Upload progress + currentTimeoutMillis = 0; + arrivedBytes = currentTransfer.SizeDone; + } + + await Task.Delay(intervalMillis).ConfigureAwait(false); + } + } + + #endregion + + #region DownloadFile + + public Task DownloadFileAsync(int channelId, string filePath) => DownloadFileAsync(channelId, string.Empty, filePath); + + public async Task DownloadFileAsync(int channelId, string channelPassword, string filePath) + { + var res = await Client.SendAsync("ftinitdownload", + new Parameter("clientftfid", _fileTransferClient.GetFileTransferId()), + new Parameter("cid", channelId), + new Parameter("cpw", channelPassword), + new Parameter("name", NormalizePath(filePath)), + new Parameter("seekpos", 0)).ConfigureAwait(false); + + var parsedRes = DataProxy.SerializeGeneric(res).First(); + + if (parsedRes.Size > int.MaxValue) + { + throw new FileTransferException("The file is too big for a single byte array."); + } + + return await _fileTransferClient.ReceiveFileAsync((int)parsedRes.Size, parsedRes.Port, parsedRes.FileTransferKey).ConfigureAwait(false); + } + + #endregion + + #region GetCurrentFileTransfers + + public async Task> GetCurrentFileTransfersAsync() + { + try + { + var res = await Client.SendAsync("ftlist").ConfigureAwait(false); + + return DataProxy.SerializeGeneric(res); + } + catch (QueryException ex) + { + if (ex.Error.Id == 1281) + { + // For some reason this error occurs when there are no active file transfers + return new ReadOnlyCollection(new GetCurrentFileTransfer[0]); + } + else + { + throw ex; + } + } + } + + #endregion + + private Task StopFileTransferAsync(int serverFileTransferId, bool delete = true) + { + return Client.SendAsync("ftstop", + new Parameter("serverftfid", serverFileTransferId), + new Parameter("delete", delete)); + } + + private string NormalizePath(string path) + { + // Replace a sequence of backslashes with one forward slash + var result = Regex.Replace(path, @"\\+", "/"); + + // Make sure that the path starts with a slash + if (!result.StartsWith("/")) + { + result = "/" + result; + } + + return result; + } + + #endregion + + #endregion + + #region IDisposable support + + /// Finalizes the object. + ~TeamSpeakClient() + { + Dispose(false); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// A value indicating whether the object is disposing or finalizing. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Client?.Dispose(); + } + } + + #endregion + } +} diff --git a/src/TeamSpeak3QueryApi/ValidationHelper.cs b/src/TeamSpeak3QueryApi/ValidationHelper.cs index 74a1a7a..9b87eac 100644 --- a/src/TeamSpeak3QueryApi/ValidationHelper.cs +++ b/src/TeamSpeak3QueryApi/ValidationHelper.cs @@ -1,17 +1,17 @@ using System; using System.Collections.Generic; -using System.Net; +using System.Net; using System.Text; namespace TeamSpeak3QueryApi { internal class ValidationHelper - { + { /// /// on false, API should throw new ArgumentOutOfRangeException("port"); /// /// - /// + /// public static bool ValidateTcpPort(int port) => port >= IPEndPoint.MinPort && port <= IPEndPoint.MaxPort; } }