diff --git a/Luski.net.sln b/Luski.net.sln new file mode 100755 index 0000000..f68549b --- /dev/null +++ b/Luski.net.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31717.71 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Luski.net", "Luski.net\Luski.net.csproj", "{3DF9B870-51B3-4338-84EC-75E4B8802F0C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Luski.net.tests", "Luski.net.tests\Luski.net.tests.csproj", "{FCA149C8-379B-454A-962A-856F30965C4E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3DF9B870-51B3-4338-84EC-75E4B8802F0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DF9B870-51B3-4338-84EC-75E4B8802F0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DF9B870-51B3-4338-84EC-75E4B8802F0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DF9B870-51B3-4338-84EC-75E4B8802F0C}.Release|Any CPU.Build.0 = Release|Any CPU + {FCA149C8-379B-454A-962A-856F30965C4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCA149C8-379B-454A-962A-856F30965C4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCA149C8-379B-454A-962A-856F30965C4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCA149C8-379B-454A-962A-856F30965C4E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {49AFEA24-10EC-4D2C-B99C-C3E70124E443} + EndGlobalSection +EndGlobal diff --git a/Luski.net/Encryption.cs b/Luski.net/Encryption.cs new file mode 100755 index 0000000..8d62272 --- /dev/null +++ b/Luski.net/Encryption.cs @@ -0,0 +1,515 @@ +using Luski.net.Enums; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Luski.net +{ + public static class Encryption + { + internal static string? MyPublicKey; + internal static readonly UnicodeEncoding Encoder = new(); + private static string? myPrivateKey; + internal static bool Generating = false; + internal static bool Generated = false; + private static string? _serverpublickey = null; + internal static string? ofkey = null; + internal static string? outofkey = null; + internal static string pw = ""; + public static int NewKeySize = 4096; + internal static string ServerPublicKey + { + get + { + if (_serverpublickey is null) _serverpublickey = new HttpClient().GetAsync($"https://{Server.InternalDomain}/{Server.API_Ver}/Keys/PublicKey").Result.Content.ReadAsStringAsync().Result; + return _serverpublickey; + } + } + + public static void GenerateKeys() + { + if (!Generating) + { + Generating = true; + GenerateNewKeys(out MyPublicKey, out myPrivateKey); + GenerateNewKeys(out outofkey, out ofkey); + Generated = true; + } + } + + internal static void GenerateNewKeys(out string Public, out string Private) + { + using RSACryptoServiceProvider r = new(NewKeySize); + Private = r.ToXmlString(true); + Public = r.ToXmlString(false); + return; + } + + public static class File + { + internal static void SetOfflineKey(string key) + { + MakeFile(Server.GetKeyFilePath, pw); + LuskiDataFile? fileLayout = JsonSerializer.Deserialize(FileString(Server.GetKeyFilePath, pw)); + fileLayout.OfflineKey = key; + fileLayout.Save(Server.GetKeyFilePath, pw); + } + + internal static string? GetOfflineKey() + { + MakeFile(Server.GetKeyFilePath, pw); + LuskiDataFile? fileLayout = JsonSerializer.Deserialize(FileString(Server.GetKeyFilePath, pw)); + return fileLayout?.OfflineKey; + } + + private static string FileString(string path, string password) + { + byte[] passwordBytes = Encoding.UTF8.GetBytes(password); + byte[] salt = new byte[100]; + FileStream fsCrypt = new(path, FileMode.Open); + fsCrypt.Read(salt, 0, salt.Length); + RijndaelManaged AES = new() + { + KeySize = 256, + BlockSize = 128 + }; + Rfc2898DeriveBytes key = new(passwordBytes, salt, 50000); + AES.Key = key.GetBytes(AES.KeySize / 8); + AES.IV = key.GetBytes(AES.BlockSize / 8); + AES.Padding = PaddingMode.PKCS7; + AES.Mode = CipherMode.CFB; + CryptoStream cs = new(fsCrypt, AES.CreateDecryptor(), CryptoStreamMode.Read); + MemoryStream fsOut = new(); + int read; + byte[] buffer = new byte[1048576]; + try + { + while ((read = cs.Read(buffer, 0, buffer.Length)) > 0) + { + fsOut.Write(buffer, 0, read); + } + } + catch (CryptographicException ex_CryptographicException) + { + Console.WriteLine("CryptographicException error: " + ex_CryptographicException.Message); + } + catch (Exception ex) + { + Console.WriteLine("Error: " + ex.Message); + } + fsOut.Seek(0, SeekOrigin.Begin); + using BinaryReader reader = new(fsOut); + byte[] by = reader.ReadBytes((int)fsOut.Length); + fsOut.Close(); + fsCrypt.Close(); + return Encoding.UTF8.GetString(by); + } + + public static class Channels + { + public static string GetKey(long channel) + { + LuskiDataFile? fileLayout; + IEnumerable? lis; + try + { +#pragma warning disable CS8603 // Possible null reference return. + if (channel == 0) return myPrivateKey; +#pragma warning restore CS8603 // Possible null reference return. + MakeFile(Server.GetKeyFilePath, pw); + fileLayout = JsonSerializer.Deserialize(FileString(Server.GetKeyFilePath, pw)); + lis = fileLayout?.channels?.Where(s => s.id == channel); + if (lis?.Count() > 0) + { + return lis.First().key; + } + foreach (Branch b in (Branch[])Enum.GetValues(typeof(Branch))) + { + if (b != Server.Branch) + { + try + { + string temp = GetKeyBranch(channel, b); + if (temp is not null) + { + AddKey(channel, temp); + return temp; + } + } + catch + { + + } + } + } + throw new Exception("You dont have a key for that channel"); + } + finally + { + fileLayout = null; + lis = null; + } + } + + internal static string GetKeyBranch(long channel, Branch branch) + { + LuskiDataFile? fileLayout; + IEnumerable? lis; + try + { +#pragma warning disable CS8603 // Possible null reference return. + if (channel == 0) return myPrivateKey; +#pragma warning restore CS8603 // Possible null reference return. + MakeFile(Server.GetKeyFilePathBr(branch.ToString()), pw); + fileLayout = JsonSerializer.Deserialize(FileString(Server.GetKeyFilePathBr(branch.ToString()), pw)); + lis = fileLayout?.channels?.Where(s => s.id == channel); + if (lis?.Count() > 0) + { + return lis.First().key; + } + throw new Exception("You dont have a key for that channel"); + } + finally + { + fileLayout = null; + lis = null; + } + } + + public static void AddKey(long channel, string key) + { + MakeFile(Server.GetKeyFilePath, pw); + LuskiDataFile? fileLayout = JsonSerializer.Deserialize(FileString(Server.GetKeyFilePath, pw)); + fileLayout?.Addchannelkey(channel, key); + fileLayout?.Save(Server.GetKeyFilePath, pw); + } + } + + private static void MakeFile(string dir, string password) + { + if (!System.IO.File.Exists(dir)) + { + LuskiDataFile? l = JsonSerializer.Deserialize("{\"channels\":[]}"); + l?.Save(dir, password); + } + } + + public class LuskiDataFile + { + public static LuskiDataFile GetDataFile(string path, string password) + { + MakeFile(path, password); + return JsonSerializer.Deserialize(FileString(path, password)); + } + + internal static LuskiDataFile GetDefualtDataFile() + { + return GetDataFile(Server.GetKeyFilePath, pw); + } + + public ChannelLayout[]? channels { get; set; } = default!; + + public string? OfflineKey { get; set; } = default!; + + public void Save(string file, string password) + { + byte[] salt = new byte[100]; + RandomNumberGenerator? provider = RandomNumberGenerator.Create(); + provider.GetBytes(salt); + FileStream fsCrypt = new(file, FileMode.Create); + byte[] passwordBytes = Encoding.UTF8.GetBytes(password); + RijndaelManaged AES = new() + { + KeySize = 256, + BlockSize = 128, + Padding = PaddingMode.PKCS7 + }; + Rfc2898DeriveBytes key = new(passwordBytes, salt, 50000); + AES.Key = key.GetBytes(AES.KeySize / 8); + AES.IV = key.GetBytes(AES.BlockSize / 8); + AES.Mode = CipherMode.CFB; + fsCrypt.Write(salt, 0, salt.Length); + CryptoStream cs = new(fsCrypt, AES.CreateEncryptor(), CryptoStreamMode.Write); + string tempp = JsonSerializer.Serialize(this); + MemoryStream fsIn = new(Encoding.UTF8.GetBytes(tempp)); + byte[] buffer = new byte[1048576]; + int read; + try + { + while ((read = fsIn.Read(buffer, 0, buffer.Length)) > 0) + { + cs.Write(buffer, 0, read); + } + fsIn.Close(); + } + catch (Exception ex) + { + Console.WriteLine("Error: " + ex.Message); + } + finally + { + cs.Close(); + fsCrypt.Close(); + } + } + + public void Addchannelkey(long chan, string Key) + { + List? chans = channels?.ToList(); + if (chans is null) chans = new(); + if (!(chans?.Where(s => s.id == chan).Count() > 0)) + { + ChannelLayout l = new() + { + id = chan, + key = Key + }; + chans?.Add(l); + channels = chans?.ToArray(); + } + } + } + + public class ChannelLayout + { + public long id { get; set; } = default!; + public string key { get; set; } = default!; + } + } + + public class AES + { + public static string Encrypt(string path, string Password) + { + string p = Path.GetTempFileName(); + byte[] salt = RandomNumberGenerator.GetBytes(100); + byte[] passwordBytes = Encoding.UTF8.GetBytes(Password); + Rfc2898DeriveBytes key = new(passwordBytes, salt, 50000); + byte[] data = System.IO.File.ReadAllBytes(path); + + using Aes aesAlg = Aes.Create(); + aesAlg.KeySize = 256; + aesAlg.BlockSize = 128; + aesAlg.Padding = PaddingMode.PKCS7; + aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8); + aesAlg.IV = key.GetBytes(aesAlg.BlockSize / 8); + + ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); + + using FileStream msEncrypt = new(p, FileMode.Open); + msEncrypt.Write(salt, 0, salt.Length); + using CryptoStream csEncrypt = new(msEncrypt, encryptor, CryptoStreamMode.Write); + csEncrypt.Write(data, 0, data.Length); + csEncrypt.Dispose(); + msEncrypt.Dispose(); + return p; + + /* + + + + + string p = Path.GetTempFileName(); + byte[] salt = new byte[100]; + RNGCryptoServiceProvider provider = new(); + provider.GetBytes(salt); + FileStream fsCrypt = new(p, FileMode.Open); + byte[] passwordBytes = Encoding.UTF8.GetBytes(Password); + Aes AES = Aes.Create(); + AES.KeySize = 256; + AES.BlockSize = 128; + AES.Padding = PaddingMode.PKCS7; + Rfc2898DeriveBytes key = new(passwordBytes, salt, 50000); + AES.Key = key.GetBytes(AES.KeySize / 8); + AES.IV = key.GetBytes(AES.BlockSize / 8); + AES.Mode = CipherMode.CFB; + fsCrypt.Write(salt, 0, salt.Length); + key.Dispose(); + CryptoStream cs = new(fsCrypt, AES.CreateEncryptor(), CryptoStreamMode.Write); + FileStream fsIn = new(path, FileMode.Open); + try + { + FileInfo FI = new(path); + byte[] buffer = new byte[FI.Length]; + int read; + while ((read = fsIn.Read(buffer, 0, buffer.Length)) > 0) + { + cs.Write(buffer, 0, read); + } + } + catch (OutOfMemoryException ex) + { + throw new Exception("Buffer", ex); + } + fsIn.Close(); + fsIn.Dispose(); + cs.Close(); + cs.Dispose(); + fsCrypt.Close(); + fsCrypt.Dispose(); + NewPath = p;*/ + } + + public static void Decrypt(byte[] data, string Password, string File) + { + byte[] salt = new byte[100]; + using MemoryStream fsCrypt = new(data); + fsCrypt.Read(salt, 0, salt.Length); + byte[] passwordBytes = Encoding.UTF8.GetBytes(Password); + Rfc2898DeriveBytes key = new(passwordBytes, salt, 50000); + byte[] decrypted = new byte[data.Length - salt.Length]; + + using Aes aesAlg = Aes.Create(); + aesAlg.KeySize = 256; + aesAlg.BlockSize = 128; + aesAlg.Padding = PaddingMode.PKCS7; + aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8); + aesAlg.IV = key.GetBytes(aesAlg.BlockSize / 8); + + ICryptoTransform encryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); + + using CryptoStream csEncrypt = new(fsCrypt, encryptor, CryptoStreamMode.Read); + FileStream fsOut = new(File, FileMode.Create); + int read; + byte[] buffer = new byte[data.Length]; + while ((read = csEncrypt.Read(buffer, 0, buffer.Length)) > 0) + { + fsOut.Write(buffer, 0, read); + } + csEncrypt.Dispose(); + fsCrypt.Dispose(); + fsOut.Dispose(); + } + } + + internal const int PasswordVersion = 0; + + internal static byte[] LocalPasswordEncrypt(byte[] Password, int PasswordVersion = PasswordVersion) + { + return PasswordVersion switch + { + 0 => SHA256.Create().ComputeHash(Password), + _ => throw new ArgumentException("The value provided was not accepted", nameof(PasswordVersion)), + }; + } + + internal static string RemotePasswordEncrypt(byte[] Password, int PasswordVersion = PasswordVersion) + { + return PasswordVersion switch + { + 0 => Convert.ToBase64String(Encrypt(LocalPasswordEncrypt(Password, PasswordVersion))), + _ => throw new ArgumentException("The value provided was not accepted", nameof(PasswordVersion)), + }; + } + + internal static byte[] Encrypt(string data) + { + return Encrypt(data, ServerPublicKey); + } + + internal static byte[] Encrypt(byte[] data) + { + return Encrypt(data, ServerPublicKey); + } + + internal static byte[] Encrypt(string data, string key, bool multithread = false) + { + return Encrypt(Encoder.GetBytes(data), key, multithread); + } + + internal static byte[] Encrypt(byte[] data, string key, bool multithread = false) + { + using RSACryptoServiceProvider rsa = new(); + rsa.FromXmlString(key); + int size = rsa.KeySize / 8; + double x = data.Length / (double)size; + int bbb = int.Parse(x.ToString().Split('.')[0]); + if (x.ToString().Contains('.')) bbb++; + byte[]? datasplitout = Array.Empty(); + if (multithread) + { + byte[][]? decccc = Array.Empty(); + Array.Resize(ref decccc, bbb); + int num = Convert.ToInt32(Math.Ceiling((Environment.ProcessorCount * Server.Percent) * 2.0)); + if (num == 0) num = 1; + Parallel.For(0, bbb, new ParallelOptions() + { + MaxDegreeOfParallelism = num + }, i => + { + decccc[i] = rsa.Encrypt(data.Skip(i * size).Take(size).ToArray(), false); + }); + foreach (byte[] dataa in decccc) + { + datasplitout = Combine(datasplitout, dataa); + } + } + else + { + for (int i = 0; i < bbb; i++) + { + datasplitout = Combine(datasplitout, rsa.Encrypt(data.Skip(i * size).Take(size).ToArray(), false)); + } + } + return datasplitout; + } + + private static byte[] Combine(byte[] first, byte[] second) + { + byte[]? bytes = new byte[first.Length + second.Length]; + Buffer.BlockCopy(first, 0, bytes, 0, first.Length); + Buffer.BlockCopy(second, 0, bytes, first.Length, second.Length); + return bytes; + } + + internal static byte[] Decrypt(byte[] EncryptedText, bool multithread = false) + { + return Decrypt(EncryptedText, myPrivateKey, multithread); + } + + internal static byte[] Decrypt(byte[]? EncryptedText, string? key, bool multithread = false) + { + if (key is null) throw new ArgumentNullException(nameof(key)); + if (EncryptedText is null) throw new ArgumentNullException(nameof(EncryptedText)); + using RSACryptoServiceProvider rsa = new(); + rsa.FromXmlString(key); + int size = rsa.KeySize / 8; + double x = EncryptedText.Length / (double)size; + int bbb = int.Parse(x.ToString().Split('.')[0]); + if (x.ToString().Contains('.')) bbb++; + byte[]? datasplitout = Array.Empty(); + if (multithread) + { + byte[][]? decccc = Array.Empty(); + Array.Resize(ref decccc, bbb); + int num = Convert.ToInt32(Math.Ceiling((Environment.ProcessorCount * Server.Percent) * 2.0)); + if (num == 0) num = 1; + Parallel.For(0, bbb, new ParallelOptions() + { + MaxDegreeOfParallelism = num + }, i => + { + decccc[i] = rsa.Decrypt(EncryptedText.Skip(i * size).Take(size).ToArray(), false); + }); + foreach (byte[] data in decccc) + { + datasplitout = Combine(datasplitout, data); + } + } + else + { + for (int i = 0; i < bbb; i++) + { + datasplitout = Combine(datasplitout, rsa.Decrypt(EncryptedText.Skip(i * size).Take(size).ToArray(), false)); + } + } + return datasplitout; + } + } +} diff --git a/Luski.net/Enums/Branch.cs b/Luski.net/Enums/Branch.cs new file mode 100755 index 0000000..ccc27b3 --- /dev/null +++ b/Luski.net/Enums/Branch.cs @@ -0,0 +1,8 @@ +namespace Luski.net.Enums; + +public enum Branch : short +{ + Dev, + Beta, + Master, +} diff --git a/Luski.net/Enums/ChannelType.cs b/Luski.net/Enums/ChannelType.cs new file mode 100755 index 0000000..c2bf1c5 --- /dev/null +++ b/Luski.net/Enums/ChannelType.cs @@ -0,0 +1,7 @@ +namespace Luski.net.Enums; + +public enum ChannelType : short +{ + DM, + GROUP, +} diff --git a/Luski.net/Enums/DataType.cs b/Luski.net/Enums/DataType.cs new file mode 100755 index 0000000..ff8ca21 --- /dev/null +++ b/Luski.net/Enums/DataType.cs @@ -0,0 +1,18 @@ +namespace Luski.net.Enums; + +internal enum DataType +{ + Message_Create, + Status_Update, + Friend_Request_Result, + Friend_Request, + Change_Channel, + Join_Call, + Leave_Call, + Call_Info, + Call_Data, + Login, + Error, + Key_Exchange, + MAX +} diff --git a/Luski.net/Enums/ErrorCode.cs b/Luski.net/Enums/ErrorCode.cs new file mode 100755 index 0000000..70139d1 --- /dev/null +++ b/Luski.net/Enums/ErrorCode.cs @@ -0,0 +1,14 @@ +namespace Luski.net.Enums; + +public enum ErrorCode +{ + MissingToken, + InvalidToken, + MissingPostData, + InvalidPostData, + Forbidden, + ServerError, + MissingHeader, + InvalidHeader, + InvalidURL +} diff --git a/Luski.net/Enums/FriendStatus.cs b/Luski.net/Enums/FriendStatus.cs new file mode 100755 index 0000000..1651479 --- /dev/null +++ b/Luski.net/Enums/FriendStatus.cs @@ -0,0 +1,9 @@ +namespace Luski.net.Enums; + +public enum FriendStatus +{ + NotFriends, + Friends, + PendingOut, + PendingIn +} diff --git a/Luski.net/Enums/PictureType.cs b/Luski.net/Enums/PictureType.cs new file mode 100755 index 0000000..ca5686b --- /dev/null +++ b/Luski.net/Enums/PictureType.cs @@ -0,0 +1,13 @@ +namespace Luski.net.Enums; + +public enum PictureType : short +{ + png, + jpeg, + bmp, + gif, + ico, + svg, + tif, + webp +} diff --git a/Luski.net/Enums/UserFlag.cs b/Luski.net/Enums/UserFlag.cs new file mode 100755 index 0000000..d4d0a35 --- /dev/null +++ b/Luski.net/Enums/UserFlag.cs @@ -0,0 +1,11 @@ +using System; + +namespace Luski.net.Enums; + +[Flags] +public enum UserFlag : short +{ + Dev = 0b_001, + Early = 0b_010, + Tester = 0b_100 +} diff --git a/Luski.net/Enums/UserStatus.cs b/Luski.net/Enums/UserStatus.cs new file mode 100755 index 0000000..439d9fb --- /dev/null +++ b/Luski.net/Enums/UserStatus.cs @@ -0,0 +1,10 @@ +namespace Luski.net.Enums; + +public enum UserStatus : short +{ + Offline, + Online, + Idle, + DoNotDisturb, + Invisible +} diff --git a/Luski.net/Exceptions.cs b/Luski.net/Exceptions.cs new file mode 100755 index 0000000..8b26354 --- /dev/null +++ b/Luski.net/Exceptions.cs @@ -0,0 +1,30 @@ +using System; + +namespace Luski.net +{ + public class Exceptions + { + + [Serializable] + public class MissingEventException : Exception + { + public string EventName; + public MissingEventException(string Event) : base(Event) + { + EventName = Event; + } + } + + + [Serializable] + public class NotConnectedException : Exception + { + public NotConnectedException(object sender, string message) : base(message) + { + Sender = sender; + } + + public object Sender { get; } + } + } +} diff --git a/Luski.net/Interfaces/IAudioClient.cs b/Luski.net/Interfaces/IAudioClient.cs new file mode 100755 index 0000000..dc21ee5 --- /dev/null +++ b/Luski.net/Interfaces/IAudioClient.cs @@ -0,0 +1,48 @@ +using Luski.net.Sound; +using System; +using System.Threading.Tasks; +using static Luski.net.Exceptions; + +namespace Luski.net.Interfaces; + +public interface IAudioClient +{ + /// + /// the event is fired when your has joined the call + /// + event Func Connected; + /// + /// Tells you if you are muted + /// + bool Muted { get; } + /// + /// Tells you if you are deafned + /// + bool Deafened { get; } + /// + /// Toggles if you are speaking to your friends + /// + void ToggleMic(); + /// + /// Toggles if you can hear audio + /// + void ToggleAudio(); + /// + /// Changes what the call gets its data from + /// + /// This is the you want to recored from + /// + void RecordSoundFrom(RecordingDevice Device); + /// + /// Changes what the call gets its data from + /// + /// This is the you want to heare outhers + /// + void PlaySoundTo(PlaybackDevice Device); + /// + /// Joins the Voice call + /// + /// + void JoinCall(); + void LeaveCall(); +} diff --git a/Luski.net/Interfaces/IChannel.cs b/Luski.net/Interfaces/IChannel.cs new file mode 100755 index 0000000..98ab035 --- /dev/null +++ b/Luski.net/Interfaces/IChannel.cs @@ -0,0 +1,32 @@ +using Luski.net.Enums; +using Luski.net.JsonTypes; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Luski.net.Interfaces; + +/// +/// contains a list of variables that all channels from luski have +/// +public interface ITextChannel +{ + long Id { get; } + string Title { get; } + string Description { get; } + string Key { get; } + /// + /// returns the current of the + /// + ChannelType Type { get; } + /// + /// Sends a to the server for the currently selected + /// + /// The messate you want to send to the server + Task SendMessage(string Message, params File[] Files); + Task SendKeysToUsers(); + Task GetMessage(long ID); + Task> GetMessages(long Message_Id, int count = 50); + Task> GetMessages(int count = 50); + Task GetPicture(); + IReadOnlyList Members { get; } +} diff --git a/Luski.net/Interfaces/IUser.cs b/Luski.net/Interfaces/IUser.cs new file mode 100755 index 0000000..1355fea --- /dev/null +++ b/Luski.net/Interfaces/IUser.cs @@ -0,0 +1,45 @@ +using Luski.net.Enums; +using System.Threading.Tasks; + +namespace Luski.net.Interfaces; + +/// +/// Represents the curent user +/// +public interface IUser +{ + /// + /// The current Id of the user + /// + long Id { get; } + /// + /// The cerrent username of the user + /// + string Username { get; } + /// + /// The current tag for the user + /// Ex: #1234 + /// + //short Tag { get; } + /// + /// The current status of the user + /// + UserStatus Status { get; } + /// + /// will returen the picture type of the user + /// + PictureType PictureType { get; } + /// + /// the current flags of a user + /// + UserFlag Flags { get; } + /// + /// Gets the current avatar of the user + /// + Task GetAvatar(); + /// + /// Gets the current user key + /// + /// + Task GetUserKey(); +} diff --git a/Luski.net/JsonRequest.cs b/Luski.net/JsonRequest.cs new file mode 100755 index 0000000..4a898e3 --- /dev/null +++ b/Luski.net/JsonRequest.cs @@ -0,0 +1,28 @@ +using Luski.net.Enums; +using System; + +namespace Luski.net +{ + internal static class JsonRequest + { + internal static string SendCallData(byte[] Data, long channel) + { + return $"{{\"data\": \"{Convert.ToBase64String(Data)}\", \"id\": {channel}}}"; + } + + internal static string JoinCall(long Channel) + { + return $"{{\"id\": {Channel}}}"; + } + + internal static string Send(DataType Request, string Data) + { + return $"{{\"type\": {(int)Request}, \"data\": {Data}}}"; + } + + internal static string FriendRequestResult(long User, bool Result) + { + return $"{{\"id\": {User},\"result\": {Result.ToString().ToLower()}}}"; + } + } +} diff --git a/Luski.net/JsonTypes/BaseTypes/HTTPRequest.cs b/Luski.net/JsonTypes/BaseTypes/HTTPRequest.cs new file mode 100755 index 0000000..f539e17 --- /dev/null +++ b/Luski.net/JsonTypes/BaseTypes/HTTPRequest.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; +using Luski.net.Enums; + +namespace Luski.net.JsonTypes.BaseTypes; + +internal class HTTPRequest +{ + [JsonPropertyName("data_type")] + [JsonInclude] + public DataType Type { get; set; } = default!; +} diff --git a/Luski.net/JsonTypes/BaseTypes/IncomingHTTP.cs b/Luski.net/JsonTypes/BaseTypes/IncomingHTTP.cs new file mode 100755 index 0000000..ff6c3f8 --- /dev/null +++ b/Luski.net/JsonTypes/BaseTypes/IncomingHTTP.cs @@ -0,0 +1,32 @@ +using Luski.net.Enums; +using System.ComponentModel; +using System.Net; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes.BaseTypes; + +[Browsable(false)] +[EditorBrowsable(EditorBrowsableState.Never)] +public class IncomingHTTP +{ + [JsonPropertyName("error")] + [JsonInclude] + public ErrorCode? Error { get; internal set; } = default!; +#pragma warning disable SYSLIB1037 // Deserialization of init-only properties is currently not supported in source generation mode. + [JsonIgnore] + public HttpStatusCode StatusCode { get; init; } +#pragma warning restore SYSLIB1037 // Deserialization of init-only properties is currently not supported in source generation mode. + [JsonPropertyName("error_message")] + [JsonInclude] + public string? ErrorMessage { get; internal set; } = default!; +} + +[JsonSerializable(typeof(IncomingHTTP))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class IncomingHTTPContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/BaseTypes/IncomingWSS.cs b/Luski.net/JsonTypes/BaseTypes/IncomingWSS.cs new file mode 100755 index 0000000..ec2ba6f --- /dev/null +++ b/Luski.net/JsonTypes/BaseTypes/IncomingWSS.cs @@ -0,0 +1,24 @@ +using Luski.net.Enums; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes.BaseTypes; + +internal class IncomingWSS +{ + [JsonPropertyName("type")] + [JsonInclude] + public DataType? Type { get; set; } = default!; + [JsonPropertyName("error")] + [JsonInclude] + public string Error { get; set; } = default!; +} + +[JsonSerializable(typeof(IncomingWSS))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified, + WriteIndented = false)] +internal partial class IncomingWSSContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/BaseTypes/SocketUserBase.cs b/Luski.net/JsonTypes/BaseTypes/SocketUserBase.cs new file mode 100755 index 0000000..4577c75 --- /dev/null +++ b/Luski.net/JsonTypes/BaseTypes/SocketUserBase.cs @@ -0,0 +1,118 @@ +using Luski.net.Enums; +using Luski.net.Interfaces; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; + +namespace Luski.net.JsonTypes.BaseTypes; + +[Browsable(false)] +[EditorBrowsable(EditorBrowsableState.Never)] +public abstract class SocketUserBase : IncomingHTTP, IUser +{ + [JsonPropertyName("id")] + [JsonInclude] + public long Id { get; internal set; } = default!; + [JsonPropertyName("username")] + [JsonInclude] + public string Username { get; internal set; } = default!; + // [JsonPropertyName("tag")] + //[JsonInclude] + //public short Tag { get; internal set; } = default!; + [JsonPropertyName("status")] + [JsonInclude] + public UserStatus Status { get; internal set; } = default!; + [JsonPropertyName("picture_type")] + [JsonInclude] + public PictureType PictureType { get; internal set; } = default!; + + [JsonPropertyName("flags")] + [JsonInclude] + public UserFlag Flags { get; internal set; } = default!; + + public async Task GetAvatar() + { + if (Server.Cache != null) + { + bool isc = System.IO.File.Exists($"{Server.Cache}/avatars/{Id}"); + if (!isc) await Server.GetFromServer($"socketuserprofile/Avatar/{Id}", $"{Server.Cache}/avatars/{Id}"); + } + return System.IO.File.ReadAllBytes($"{Server.Cache}/avatars/{Id}"); + } + + public Task GetUserKey() + { + if (Server._user is null) throw new Exception("you are not loged in"); + if (Id == Server._user.Id && Encryption.MyPublicKey is not null) return Task.FromResult(Encryption.MyPublicKey); + string data = Server.GetFromServer($"Keys/GetUserKey/{Id}").Content.ReadAsStringAsync().Result; + return Task.FromResult(data); + } + + internal static async Task GetUser(long UserId, JsonTypeInfo Json) where TUser : SocketUserBase, new() + { + TUser user; + if (Server.poeople is null) Server.poeople = new(); + if (Server.poeople.Count > 0 && Server.poeople.Any(s => s.Id == UserId)) + { + TUser temp = Server.poeople.Where(s => s is TUser && s.Id == UserId).Cast().FirstOrDefault()!; + if (temp is SocketRemoteUser && (temp as SocketRemoteUser)!.Channel == null) + { + foreach (SocketDMChannel chan in Server.chans.Where(s => s is SocketDMChannel).Cast()) + { + if (chan.Type == ChannelType.DM && chan.Id != 0 && chan.MemberIdList is not null) + { + if (chan.MemberIdList.Any(s => s == UserId)) (temp as SocketRemoteUser)!.Channel = chan; + } + } + } + return temp!; + } + while (true) + { + if (Server.CanRequest) + { + user = await Server.GetFromServer("socketuser", + Json, + new System.Collections.Generic.KeyValuePair("id", UserId.ToString())); + break; + } + } + + if (user is null) throw new Exception("Server did not return a user"); + if (Server.poeople.Count > 0 && Server.poeople.Any(s => s.Id == UserId)) + { + foreach (IUser? p in Server.poeople.Where(s => s.Id == UserId)) + { + Server.poeople.Remove(p); + } + } + if (Server._user is not null && UserId != 0 && UserId != Server._user.Id) + { + foreach (SocketDMChannel chan in Server.chans.Where(s => s is SocketDMChannel).Cast()) + { + if (chan.Id != 0 && chan.MemberIdList is not null) + { + if (chan.MemberIdList.Any(s => s == UserId)) (user as SocketRemoteUser)!.Channel = chan; + } + } + } + Server.poeople.Add(user); + return user; + } +} + +[JsonSerializable(typeof(SocketUserBase))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.Never)] +internal partial class SocketUserBaseContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/File.cs b/Luski.net/JsonTypes/File.cs new file mode 100755 index 0000000..901cd14 --- /dev/null +++ b/Luski.net/JsonTypes/File.cs @@ -0,0 +1,107 @@ +using Luski.net.Enums; +using Luski.net.JsonTypes.BaseTypes; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Luski.net.JsonTypes; + +public class File : IncomingHTTP +{ + [JsonInclude] + [JsonPropertyName("name")] + public string Name { get; internal set; } = default!; + [JsonInclude] + [JsonPropertyName("size")] + public ulong Size { get; internal set; } = default!; + [JsonInclude] + [JsonPropertyName("id")] + public long Id { get; internal set; } = default!; + [JsonIgnore] + internal string? key { get; set; } = default!; + [JsonIgnore] + internal string loc { get; set; } = default!; + + public async void DownloadBytes(string Loc, long key) + { + //using HttpClient web = new(); + //web.DefaultRequestHeaders.Add("token", Server.Token); + //web.DefaultRequestHeaders.Add("id", id.ToString()); + //IncomingHTTP? request = JsonSerializer.Deserialize(web.GetAsync($"https://{Server.Domain}/{Server.API_Ver}/SocketMessage/GetFile").Result.Content.ReadAsStringAsync().Result, IncomingHTTPContext.Default.IncomingHTTP); + string path = Path.GetTempFileName(); + + await Server.GetFromServer($"SocketMessage/GetFile/{Id}", path); + string Key = (key == 0 ? Encryption.MyPublicKey : Encryption.File.Channels.GetKey(key))!; + Encryption.AES.Decrypt(System.IO.File.ReadAllBytes(path), Key, Loc); + /* + if (request is not null && request.Error is not null) + { + switch (request.Error) + { + case ErrorCode.InvalidToken: + throw new Exception("Your current token is no longer valid"); + case ErrorCode.ServerError: + throw new Exception("Error from server: " + request.ErrorMessage); + case ErrorCode.Forbidden: + throw new Exception("Your request was denied by the server"); + default: + MemoryStream? ms = new(); + JsonSerializer.Serialize(new Utf8JsonWriter(ms), + request, + IncomingHTTPContext.Default.IncomingHTTP); + throw new Exception(Encoding.UTF8.GetString(ms.ToArray())); + } + } + + + if (request?.data is not null) + { + foreach (string raw in request.data) + { + Encryption.AES.Decrypt(Convert.FromBase64String(raw), Encryption.File.Channels.GetKey(key), Loc); + } + }*/ + } + + public void SetFile(string path) + { + FileInfo fi = new(path); + Name = fi.Name; + Size = (ulong)fi.Length; + loc = path; + } + + internal async Task Upload(string keyy) + { + if (Name != null) Name = Convert.ToBase64String(Encryption.Encrypt(Name, keyy)); + Debug.WriteLine("uploading"); + string NPath = Encryption.AES.Encrypt(loc, keyy); + File sf = await Server.SendServer( + "SocketMessage/UploadFile", + NPath, + FileContext.Default.File, + new KeyValuePair("name", Name)); + try { System.IO.File.Delete(NPath); } catch { } + Debug.WriteLine("done uploading"); + if (sf.Error is not null) throw new Exception(sf.ErrorMessage); + return sf.Id; + } + + internal void decrypt() + { + if (Name is not null) Name = Encryption.Encoder.GetString(Encryption.Decrypt(Convert.FromBase64String(Name), key)); + } +} + +[JsonSerializable(typeof(File))] +internal partial class FileContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/FriendRequestResult.cs b/Luski.net/JsonTypes/FriendRequestResult.cs new file mode 100755 index 0000000..2d78f98 --- /dev/null +++ b/Luski.net/JsonTypes/FriendRequestResult.cs @@ -0,0 +1,27 @@ +using Luski.net.JsonTypes.BaseTypes; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes; + +internal class FriendRequestResult : IncomingHTTP +{ + [JsonPropertyName("channel")] + [JsonInclude] + public long? Channel { get; set; } + [JsonPropertyName("id")] + [JsonInclude] + public long? Id { get; set; } + [JsonPropertyName("result")] + [JsonInclude] + public bool? Result { get; set; } +} + +[JsonSerializable(typeof(FriendRequestResult))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class FriendRequestResultContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/HTTP/Channel.cs b/Luski.net/JsonTypes/HTTP/Channel.cs new file mode 100755 index 0000000..1150e5f --- /dev/null +++ b/Luski.net/JsonTypes/HTTP/Channel.cs @@ -0,0 +1,21 @@ +using Luski.net.JsonTypes.BaseTypes; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes.HTTP; + +internal class Channel : HTTPRequest +{ + [JsonPropertyName("id")] + [JsonInclude] + public long Id { get; set; } = default!; +} + +[JsonSerializable(typeof(Channel))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class ChannelContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/HTTP/FriendRequest.cs b/Luski.net/JsonTypes/HTTP/FriendRequest.cs new file mode 100755 index 0000000..528d4c4 --- /dev/null +++ b/Luski.net/JsonTypes/HTTP/FriendRequest.cs @@ -0,0 +1,30 @@ +using Luski.net.JsonTypes.BaseTypes; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes.HTTP; + +internal class FriendRequest : HTTPRequest +{ + [JsonPropertyName("id")] + [JsonInclude] + public long Id { get; set; } = default!; + [JsonPropertyName("subtype")] + [JsonInclude] + public int SubType { get; set; } = default!; + [JsonPropertyName("username")] + [JsonInclude] + public string Username { get; set; } = default!; + [JsonPropertyName("tag")] + [JsonInclude] + public short Tag { get; set; } = default!; +} + +[JsonSerializable(typeof(FriendRequest))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class FriendRequestContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/HTTP/FriendRequestResultOut.cs b/Luski.net/JsonTypes/HTTP/FriendRequestResultOut.cs new file mode 100755 index 0000000..7517c7a --- /dev/null +++ b/Luski.net/JsonTypes/HTTP/FriendRequestResultOut.cs @@ -0,0 +1,24 @@ +using Luski.net.JsonTypes.BaseTypes; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes.HTTP; + +internal class FriendRequestResultOut : HTTPRequest +{ + [JsonPropertyName("id")] + [JsonInclude] + public long? Id { get; set; } + [JsonPropertyName("result")] + [JsonInclude] + public bool? Result { get; set; } +} + +[JsonSerializable(typeof(FriendRequestResultOut))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class FriendRequestResultOutContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/HTTP/Message.cs b/Luski.net/JsonTypes/HTTP/Message.cs new file mode 100755 index 0000000..ee6f9a5 --- /dev/null +++ b/Luski.net/JsonTypes/HTTP/Message.cs @@ -0,0 +1,27 @@ +using Luski.net.JsonTypes.BaseTypes; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes.HTTP; + +internal class Message : HTTPRequest +{ + [JsonPropertyName("channel_id")] + [JsonInclude] + public long Channel { get; set; } = default!; + [JsonPropertyName("content")] + [JsonInclude] + public string Context { get; set; } = default!; + [JsonPropertyName("files")] + [JsonInclude] + public long[] Files { get; set; } = default!; +} + +[JsonSerializable(typeof(Message))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class MessageContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/HTTP/Status.cs b/Luski.net/JsonTypes/HTTP/Status.cs new file mode 100755 index 0000000..f447753 --- /dev/null +++ b/Luski.net/JsonTypes/HTTP/Status.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; +using Luski.net.Enums; +using Luski.net.JsonTypes.BaseTypes; + +namespace Luski.net.JsonTypes.HTTP; + +internal class Status : HTTPRequest +{ + [JsonPropertyName("status")] + [JsonInclude] + public UserStatus UserStatus { get; set; } = default!; +} + +[JsonSerializable(typeof(Status))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class StatusContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/KeyExchange.cs b/Luski.net/JsonTypes/KeyExchange.cs new file mode 100755 index 0000000..310f846 --- /dev/null +++ b/Luski.net/JsonTypes/KeyExchange.cs @@ -0,0 +1,10 @@ +namespace Luski.net.JsonTypes +{ + internal class KeyExchange + { + public long channel { get; set; } = default!; + public string key { get; set; } = default!; + + public long? to { get; set; } = default!; + } +} diff --git a/Luski.net/JsonTypes/Login.cs b/Luski.net/JsonTypes/Login.cs new file mode 100755 index 0000000..8492d70 --- /dev/null +++ b/Luski.net/JsonTypes/Login.cs @@ -0,0 +1,20 @@ +using Luski.net.JsonTypes.BaseTypes; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes; + +internal class Login : IncomingHTTP +{ + [JsonPropertyName("login_token")] + public string? Token { get; set; } = default!; +} + +[JsonSerializable(typeof(Login))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class LoginContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/OfflineKeyData.cs b/Luski.net/JsonTypes/OfflineKeyData.cs new file mode 100755 index 0000000..1f4b978 --- /dev/null +++ b/Luski.net/JsonTypes/OfflineKeyData.cs @@ -0,0 +1,19 @@ +using Luski.net.JsonTypes.BaseTypes; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes; + +internal class OfflineKeyData : IncomingHTTP +{ + public KeyExchange[]? keys { get; internal set; } = default!; +} + +[JsonSerializable(typeof(OfflineKeyData))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class OfflineKeyDataContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/SocketAppUser.cs b/Luski.net/JsonTypes/SocketAppUser.cs new file mode 100755 index 0000000..15b5e60 --- /dev/null +++ b/Luski.net/JsonTypes/SocketAppUser.cs @@ -0,0 +1,178 @@ +using Luski.net.Interfaces; +using Luski.net.JsonTypes.BaseTypes; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes; + +public class SocketAppUser : SocketUserBase +{ + [JsonPropertyName("email")] + [JsonInclude] + public string Email { get; internal set; } = default!; + [JsonIgnore] + public IReadOnlyList Channels + { + get + { + if (_Channels is null || ChannelIdList is not null) + { + if (ChannelIdList.Length != 0) + { + _Channels = new List(); + foreach (long channel in ChannelIdList) + { + SocketChannel s = SocketChannel.GetChannel(channel, SocketChannelContext.Default.SocketChannel).Result; + Server.chans.Remove(s); + switch (s.Type) + { + case Enums.ChannelType.GROUP: + _Channels.Add(SocketChannel.GetChannel(channel, SocketGroupChannelContext.Default.SocketGroupChannel).Result); + break; + case Enums.ChannelType.DM: + _Channels.Add(SocketChannel.GetChannel(channel, SocketDMChannelContext.Default.SocketDMChannel).Result); + break; + } + } + } + else _Channels = new List(); + } + return _Channels.AsReadOnly(); + } + } + [JsonIgnore] + public IReadOnlyList FriendRequests + { + get + { + if (_FriendRequests is null || FriendRequestsRaw is not null) + { + _FriendRequests = new(); + if (ChannelIdList.Length != 0 && FriendRequestsRaw is not null) + { + foreach (FR person in FriendRequestsRaw) + { + //_Friends.Add(SocketRemoteUser.GetUser(person)); + long id = person.user_id == Id ? person.from : person.user_id; + SocketRemoteUser frq = GetUser(id, SocketRemoteUserContext.Default.SocketRemoteUser).Result; + _FriendRequests.Add(frq); + } + } + else _FriendRequests = new(); + } + return _FriendRequests.AsReadOnly(); + } + } + [JsonIgnore] + public IReadOnlyList Friends + { + get + { + if (_Friends is null || FriendIdList is not null) + { + if (ChannelIdList.Length != 0) + { + _Friends = new List(); + foreach (long person in FriendIdList) + { + _Friends.Add(GetUser(person, SocketRemoteUserContext.Default.SocketRemoteUser).Result); + } + } + else _Friends = new List(); + } + return _Friends.AsReadOnly(); + } + } + [JsonPropertyName("selected_channel")] + [JsonInclude] + public long SelectedChannel { get; internal set; } = default!; + [JsonPropertyName("channels")] + [JsonInclude] + public long[] ChannelIdList { get; internal set; } = default!; + [JsonPropertyName("friends")] + [JsonInclude] + public long[] FriendIdList { get; internal set; } = default!; + [JsonPropertyName("friend_requests")] + [JsonInclude] + public FR[] FriendRequestsRaw { get; internal set; } = default!; + [JsonIgnore] + private List _Channels = default!; + [JsonIgnore] + private List _Friends = default!; + [JsonIgnore] + private List _FriendRequests = default!; + + public class FR + { + public long from { get; set; } = default!; + public long user_id { get; set; } = default!; + } + + internal void AddFriend(SocketRemoteUser User) + { + if (Server.poeople.Any(s => s.Id == User.Id)) + { + IEnumerable b = Server.poeople.Where(s => s.Id == User.Id); + foreach (IUser item in b) + { + Server.poeople.Remove(item); + } + Server.poeople.Add(User); + } + else + { + Server.poeople.Add(User); + } + _Friends.Add(User); + } + + internal void RemoveFriendRequest(SocketRemoteUser User) + { + if (Server.poeople.Any(s => s.Id == User.Id)) + { + IEnumerable b = Server.poeople.Where(s => s.Id == User.Id); + foreach (IUser item in b) + { + Server.poeople.Remove(item); + } + } + Server.poeople.Add(User); + foreach (SocketRemoteUser user in _FriendRequests) + { + if (User.Id == user.Id) + { + _FriendRequests.Remove(User); + } + } + } + + internal void AddFriendRequest(SocketRemoteUser User) + { + if (Server.poeople.Any(s => s.Id == User.Id)) + { + IEnumerable b = Server.poeople.Where(s => s.Id == User.Id); + foreach (IUser item in b) + { + Server.poeople.Remove(item); + } + Server.poeople.Add(User); + } + else + { + Server.poeople.Add(User); + } + _FriendRequests.Add(User); + } +} + +[JsonSerializable(typeof(SocketAppUser))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.Never)] +internal partial class SocketAppUserContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/SocketBulkMessage.cs b/Luski.net/JsonTypes/SocketBulkMessage.cs new file mode 100755 index 0000000..7ca8518 --- /dev/null +++ b/Luski.net/JsonTypes/SocketBulkMessage.cs @@ -0,0 +1,23 @@ +using Luski.net.JsonTypes.BaseTypes; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes; + +internal class SocketBulkMessage : IncomingHTTP +{ + [JsonPropertyName("messages")] + [JsonInclude] + public SocketMessage[]? Messages { get; set; } = default!; +} + +[JsonSerializable(typeof(SocketBulkMessage))] +[JsonSerializable(typeof(SocketAppUser))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.Never)] +internal partial class SocketBulkMessageContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/SocketChannel.cs b/Luski.net/JsonTypes/SocketChannel.cs new file mode 100755 index 0000000..95af482 --- /dev/null +++ b/Luski.net/JsonTypes/SocketChannel.cs @@ -0,0 +1,173 @@ +using Luski.net.Enums; +using Luski.net.Interfaces; +using Luski.net.JsonTypes.BaseTypes; +using System.Collections.Generic; +using System; +using System.Text.Json.Serialization; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Luski.net.JsonTypes.WSS; +using System.Text.Json.Serialization.Metadata; + +namespace Luski.net.JsonTypes; + +public class SocketChannel : IncomingHTTP +{ + [JsonInclude] + [JsonPropertyName("id")] + public long Id { get; internal set; } = default!; + [JsonInclude] + [JsonPropertyName("title")] + public string Title { get; internal set; } = default!; + [JsonInclude] + [JsonPropertyName("description")] + public string Description { get; internal set; } = default!; + [JsonInclude] + [JsonPropertyName("key")] + public string Key { get; internal set; } = default!; + [JsonPropertyName("type")] + [JsonInclude] + public ChannelType Type { get; internal set; } = default!; + [JsonPropertyName("members")] + [JsonInclude] + public long[] MemberIdList { get; internal set; } = default!; + [JsonIgnore] + public IReadOnlyList Members + { + get + { + if (MemberIdList is null || MemberIdList.Length == 0) return Array.Empty().ToList().AsReadOnly(); + if (_members is null || !_members.Any()) + { + _members = new(); + foreach (long member in MemberIdList) + { + if (member != Server._user!.Id) _members.Add(SocketUserBase.GetUser(member, SocketRemoteUserContext.Default.SocketRemoteUser).Result); + else _members.Add(Server._user); + } + } + return _members.AsReadOnly(); + } + } + [JsonIgnore] + private List _members = new(); + + public async Task SendKeysToUsers() + { + if (Key is null) + { + StartKeyProcessAsync().Wait(); + return Task.CompletedTask; + } + int num = Convert.ToInt32(Math.Ceiling((Environment.ProcessorCount * Server.Percent) * 2.0)); + if (num == 0) num = 1; + string? lkey = Encryption.File.Channels.GetKey(Id); + Parallel.ForEach(Members, new ParallelOptions() + { + MaxDegreeOfParallelism = num + }, async i => + { + if (i.Id != Server._user?.Id) + { + string key = await i.GetUserKey(); + if (!string.IsNullOrEmpty(key)) + { + WSSKeyExchange send = new() + { + to = i.Id, + channel = Id, + key = Convert.ToBase64String(Encryption.Encrypt(lkey, key)) + }; + Server.SendServer(send, WSSKeyExchangeContext.Default.WSSKeyExchange); + } + } + }); + return Task.CompletedTask; + } + + internal async Task StartKeyProcessAsync() + { + Encryption.GenerateNewKeys(out string Public, out string Private); + Key = Public; + HttpResponseMessage b; + using (HttpClient web = new()) + { + web.DefaultRequestHeaders.Add("token", Server.Token); + b = web.PostAsync($"https://{Server.InternalDomain}/{Server.API_Ver}/SocketChannel/SetKey/{Id}", new StringContent(Key)).Result; + } + int num = Convert.ToInt32(Math.Ceiling((Environment.ProcessorCount * Server.Percent) * 2.0)); + if (num == 0) num = 1; + Encryption.File.Channels.AddKey(Id, Private); + Parallel.ForEach(Members, new ParallelOptions() + { + MaxDegreeOfParallelism = num + }, i => + { + if (i.Id != Server._user?.Id) + { + string key = i.GetUserKey().Result; + if (!string.IsNullOrEmpty(key)) + { + WSSKeyExchange send = new() + { + to = i.Id, + channel = Id, + key = Convert.ToBase64String(Encryption.Encrypt(Private, key)) + }; + Server.SendServer(send, WSSKeyExchangeContext.Default.WSSKeyExchange); + } + } + }); + } + + internal static async Task GetChannel(long id, JsonTypeInfo Json) where TChannel : SocketChannel, new() + { + TChannel request; + if (Server.chans is null) Server.chans = new(); + if (Server.chans.Count > 0 && Server.chans.Any(s => s.Id == id)) + { + return Server.chans.Where(s => s is TChannel && s.Id == id).Cast().FirstOrDefault()!; + } + while (true) + { + if (Server.CanRequest) + { + request = await Server.GetFromServer($"SocketChannel/Get/{id}", Json); + break; + } + } + if (request is null) throw new Exception("Something was wrong with the server responce"); + if (request.Error is null) + { + if (Server.chans is null) Server.chans = new(); + if (Server.chans.Count > 0 && Server.chans.Any(s => s.Id == request.Id)) + { + foreach (SocketChannel? p in Server.chans.Where(s => s.Id == request.Id)) + { + Server.chans.Remove(p); + } + } + Server.chans.Add(request); + return request; + } + throw request.Error switch + { + ErrorCode.InvalidToken => new Exception("Your current token is no longer valid"), + ErrorCode.Forbidden => new Exception("The server rejected your request"), + ErrorCode.ServerError => new Exception("Error from server: " + request.ErrorMessage), + ErrorCode.InvalidURL or ErrorCode.MissingHeader => new Exception(request.ErrorMessage), + _ => new Exception($"Unknown data: '{request.ErrorMessage}'"), + }; + } +} + +[JsonSerializable(typeof(SocketChannel))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class SocketChannelContext : JsonSerializerContext +{ + +} \ No newline at end of file diff --git a/Luski.net/JsonTypes/SocketDMChannel.cs b/Luski.net/JsonTypes/SocketDMChannel.cs new file mode 100755 index 0000000..2c6ea6a --- /dev/null +++ b/Luski.net/JsonTypes/SocketDMChannel.cs @@ -0,0 +1,33 @@ +using Luski.net.JsonTypes.BaseTypes; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes; + +public class SocketDMChannel : SocketTextChannel +{ + public SocketRemoteUser User + { + get + { + if (_user is null) + { + var list = MemberIdList.ToList(); + list.Remove(Server._user!.Id); + _user = SocketUserBase.GetUser(list.FirstOrDefault(), SocketRemoteUserContext.Default.SocketRemoteUser).Result; + } + return _user; + } + } + public SocketRemoteUser _user = null!; +} + +[JsonSerializable(typeof(SocketDMChannel))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class SocketDMChannelContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/SocketGroupChannel.cs b/Luski.net/JsonTypes/SocketGroupChannel.cs new file mode 100755 index 0000000..f7a3300 --- /dev/null +++ b/Luski.net/JsonTypes/SocketGroupChannel.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes; + +public class SocketGroupChannel : SocketTextChannel +{ + [JsonPropertyName("owner")] + [JsonInclude] + public long Owner { get; internal set; } = default!; +} + +[JsonSerializable(typeof(SocketGroupChannel))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class SocketGroupChannelContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/SocketMessage.cs b/Luski.net/JsonTypes/SocketMessage.cs new file mode 100755 index 0000000..aa4c948 --- /dev/null +++ b/Luski.net/JsonTypes/SocketMessage.cs @@ -0,0 +1,87 @@ +using Luski.net.Interfaces; +using Luski.net.JsonTypes.BaseTypes; +using System; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Luski.net.JsonTypes; + +public class SocketMessage : IncomingHTTP +{ + [JsonPropertyName("id")] + [JsonInclude] + public long Id { get; internal set; } = default!; + [JsonPropertyName("user_id")] + [JsonInclude] + public long AuthorID { get; internal set; } = default!; + [JsonPropertyName("content")] + [JsonInclude] + public string Context { get; internal set; } = default!; + [JsonPropertyName("channel_id")] + [JsonInclude] + public long ChannelID { get; internal set; } = default!; + [JsonPropertyName("files")] + [JsonInclude] + public File[]? Files { get; internal set; } = default!; + public async Task GetChannel() + { + if (Server.chans.Any(s => s.Id == ChannelID)) + { + return (SocketTextChannel)Server.chans.Where(s => s.Id == ChannelID).First(); + } + else + { + SocketTextChannel ch = await SocketChannel.GetChannel(ChannelID, SocketTextChannelContext.Default.SocketTextChannel); + Server.chans.Add(ch); + return ch; + } + } + public async Task GetAuthor() + { + if (Server._user!.Id != AuthorID) return await SocketUserBase.GetUser(AuthorID, SocketRemoteUserContext.Default.SocketRemoteUser); + else return Server._user; + } + + internal void decrypt(string? key) + { + if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); + Context = Encryption.Encoder.GetString(Encryption.Decrypt(Convert.FromBase64String(Context), key)); + if (Files is not null && Files.Length > 0) + { + for (int i = 0; i < Files.Length; i++) + { + Files[i].key = key; + Files[i].decrypt(); + } + } + } + internal static async Task GetMessage(long id) + { + SocketMessage message; + while (true) + { + if (Server.CanRequest) + { + message = await Server.GetFromServer("socketmessage", + SocketMessageContext.Default.SocketMessage, + new System.Collections.Generic.KeyValuePair("msg_id", id.ToString())); + break; + } + } + if (message is not null) return message; + throw new Exception("Server did not return a message"); + } +} + +[JsonSerializable(typeof(SocketMessage))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +public partial class SocketMessageContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/SocketRemoteUser.cs b/Luski.net/JsonTypes/SocketRemoteUser.cs new file mode 100755 index 0000000..11277b4 --- /dev/null +++ b/Luski.net/JsonTypes/SocketRemoteUser.cs @@ -0,0 +1,35 @@ +using Luski.net.Enums; +using Luski.net.Interfaces; +using Luski.net.JsonTypes.BaseTypes; +using System; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Luski.net.JsonTypes; + +public class SocketRemoteUser : SocketUserBase +{ + [JsonPropertyName("friend_status")] + [JsonInclude] + public FriendStatus FriendStatus { get; internal set; } = default!; + [JsonIgnore] + public SocketDMChannel Channel { get; internal set; } = default!; + + internal SocketRemoteUser Clone() + { + return (SocketRemoteUser)MemberwiseClone(); + } +} + +[JsonSerializable(typeof(SocketRemoteUser))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class SocketRemoteUserContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/SocketTextChannel.cs b/Luski.net/JsonTypes/SocketTextChannel.cs new file mode 100755 index 0000000..5b61339 --- /dev/null +++ b/Luski.net/JsonTypes/SocketTextChannel.cs @@ -0,0 +1,168 @@ +using Luski.net.Enums; +using Luski.net.Interfaces; +using Luski.net.JsonTypes.BaseTypes; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Luski.net.JsonTypes; + +public class SocketTextChannel : SocketChannel, ITextChannel +{ + public async Task GetMessage(long ID) + { + return await SocketMessage.GetMessage(ID); + } + + public async Task GetPicture() + { + if (Type == ChannelType.DM) return Members.First().GetAvatar().Result; + else + { + if (Server.Cache != null) + { + bool isc = System.IO.File.Exists($"{Server.Cache}/channels/{Id}"); + if (!isc) await Server.GetFromServer($"SocketChannel/GetPicture/{Id}", $"{Server.Cache}/channels/{Id}"); + } + return System.IO.File.ReadAllBytes($"{Server.Cache}/channels/{Id}"); + } + } + + public async Task> GetMessages(long Message_Id, int count = 50) + { + if (count > 200) + { + throw new Exception("You can not request more than 200 messages at a time"); + } + else if (count < 1) + { + throw new Exception("You must request at least 1 message"); + } + else + { + SocketBulkMessage data = await Server.GetFromServer("SocketBulkMessage", + SocketBulkMessageContext.Default.SocketBulkMessage, + new KeyValuePair("channel_id", Id.ToString()), + new KeyValuePair("messages", count.ToString()), + new KeyValuePair("mostrecentid", Message_Id.ToString())); + if (data.Error is null) + { + int num = Convert.ToInt32(Math.Ceiling((Environment.ProcessorCount * Server.Percent) * 2.0)); + if (num == 0) num = 1; + + string? key = Encryption.File.Channels.GetKey(Id); + if (data is null) throw new Exception("Invalid data from server"); + if (data.Messages is null) data.Messages = Array.Empty(); + Parallel.ForEach(data.Messages, new ParallelOptions() + { + MaxDegreeOfParallelism = num + }, i => + { + i.decrypt(key); + }); + key = null; + return await Task.FromResult(data.Messages.ToList().AsReadOnly() as IReadOnlyList); + } + else + { + throw new Exception(data.ErrorMessage); + } + } + } + + public async Task> GetMessages(int count = 50) + { + try + { + if (count > 200) + { + throw new Exception("You can not request more than 200 messages at a time"); + } + else if (count < 1) + { + throw new Exception("You must request at least 1 message"); + } + else + { + SocketBulkMessage data = await Server.GetFromServer("SocketBulkMessage", + SocketBulkMessageContext.Default.SocketBulkMessage, + new KeyValuePair("id", Id.ToString()), + new KeyValuePair("messages", count.ToString())); + if (data is not null && !data.Error.HasValue) + { + int num = Convert.ToInt32(Math.Ceiling((Environment.ProcessorCount * Server.Percent) * 2.0)); + if (num == 0) num = 1; + string? key = Encryption.File.Channels.GetKey(Id); + if (data.Messages is null) data.Messages = Array.Empty(); + Parallel.ForEach(data.Messages, new ParallelOptions() + { + MaxDegreeOfParallelism = num + }, i => + { + i.decrypt(key); + }); + key = null; + return await Task.FromResult(data.Messages.ToList().AsReadOnly() as IReadOnlyList); + } + else + { + throw data?.Error switch + { + ErrorCode.InvalidToken => new Exception("Your current token is no longer valid"), + ErrorCode.ServerError => new Exception($"Error from server: {data.ErrorMessage}"), + ErrorCode.InvalidHeader => new Exception(data.ErrorMessage), + ErrorCode.MissingHeader => new Exception("The header sent to the server was not found. This may be because you app is couropt or you are using the wron API version"), + ErrorCode.Forbidden => new Exception("You are not allowed to do this request"), + _ => new Exception(data?.Error.ToString()), + }; + } + } + } + catch (Exception) + { + throw; + } + } + + public async Task SendMessage(string Message, params File?[] Files) + { + string key = Encryption.File.Channels.GetKey(Id); + if (Id == 0) key = Encryption.ServerPublicKey; + HTTP.Message m = new() + { + Context = Convert.ToBase64String(Encryption.Encrypt(Message, key)), + Channel = Id, + }; + if (Files is not null && Files.Length > 0) + { + List bb = new(); + for (int i = 0; i < Files.Length; i++) + { + File? ff = Files[i]; + if (ff is not null) + { + bb.Add(await ff.Upload(key)); + Files[i] = null; + } + } + m.Files = bb.ToArray(); + } + IncomingHTTP data = await Server.SendServer("socketmessage", m, HTTP.MessageContext.Default.Message, IncomingHTTPContext.Default.IncomingHTTP); + if (data.Error is not null && data.ErrorMessage != "Server responded with empty data") throw new Exception(data.ErrorMessage); + return Task.CompletedTask; + } +} + +[JsonSerializable(typeof(SocketTextChannel))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class SocketTextChannelContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/StatusUpdate.cs b/Luski.net/JsonTypes/StatusUpdate.cs new file mode 100755 index 0000000..7719a7d --- /dev/null +++ b/Luski.net/JsonTypes/StatusUpdate.cs @@ -0,0 +1,17 @@ +using Luski.net.Enums; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes; + +internal class StatusUpdate +{ + [JsonInclude] + [JsonPropertyName("id")] + public long id { get; set; } = default!; + [JsonInclude] + [JsonPropertyName("before")] + public UserStatus before { get; set; } = default!; + [JsonInclude] + [JsonPropertyName("after")] + public UserStatus after { get; set; } = default!; +} diff --git a/Luski.net/JsonTypes/WSS/WSSKeyExchange.cs b/Luski.net/JsonTypes/WSS/WSSKeyExchange.cs new file mode 100755 index 0000000..fd18e13 --- /dev/null +++ b/Luski.net/JsonTypes/WSS/WSSKeyExchange.cs @@ -0,0 +1,31 @@ +using Luski.net.Enums; +using Luski.net.JsonTypes.BaseTypes; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes.WSS; + +internal class WSSKeyExchange : IncomingWSS +{ + [JsonPropertyName("type")] + [JsonInclude] + new public DataType? Type { get; set; } = DataType.Key_Exchange; + [JsonPropertyName("channel")] + [JsonInclude] + public long channel { get; set; } = default!; + [JsonPropertyName("key")] + [JsonInclude] + public string key { get; set; } = default!; + [JsonPropertyName("to")] + [JsonInclude] + public long? to { get; set; } = default!; +} + +[JsonSerializable(typeof(WSSKeyExchange))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class WSSKeyExchangeContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/JsonTypes/WSS/WSSLogin.cs b/Luski.net/JsonTypes/WSS/WSSLogin.cs new file mode 100755 index 0000000..d50b8bc --- /dev/null +++ b/Luski.net/JsonTypes/WSS/WSSLogin.cs @@ -0,0 +1,25 @@ +using Luski.net.Enums; +using Luski.net.JsonTypes.BaseTypes; +using System.Text.Json.Serialization; + +namespace Luski.net.JsonTypes.WSS; + +internal class WSSLogin : IncomingWSS +{ + [JsonPropertyName("token")] + [JsonInclude] + public string Token { get; set; } = default!; + [JsonPropertyName("type")] + [JsonInclude] + new public DataType? Type { get; set; } = DataType.Login; +} + +[JsonSerializable(typeof(WSSLogin))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Default, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +internal partial class WSSLoginContext : JsonSerializerContext +{ + +} diff --git a/Luski.net/Luski.net.csproj b/Luski.net/Luski.net.csproj new file mode 100755 index 0000000..87b0f19 --- /dev/null +++ b/Luski.net/Luski.net.csproj @@ -0,0 +1,33 @@ + + + + net6.0 + disable + enable + true + Luski.net + JacobTech + JacobTech, LLC + A wrapper for the luski API + https://www.jacobtech.com/Luski/Documentation + https://github.com/JacobTech-com/Luski.net + True + 1.0.0 + 1.1.4-alpha + + + + + + + + + + + + + + + + + diff --git a/Luski.net/Luski.net.csproj.user b/Luski.net/Luski.net.csproj.user new file mode 100755 index 0000000..c5a8f00 --- /dev/null +++ b/Luski.net/Luski.net.csproj.user @@ -0,0 +1,6 @@ + + + + <_LastSelectedProfileId>C:\Users\techn\source\repos\JacobTech-com\Luski.net\Luski.net\Luski.net\Properties\PublishProfiles\FolderProfile.pubxml + + \ No newline at end of file diff --git a/Luski.net/Properties/PublishProfiles/FolderProfile.pubxml b/Luski.net/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100755 index 0000000..154e035 --- /dev/null +++ b/Luski.net/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,13 @@ + + + + + Release + Any CPU + bin\Release\net6.0\publish\ + FileSystem + <_TargetId>Folder + + \ No newline at end of file diff --git a/Luski.net/Properties/PublishProfiles/FolderProfile.pubxml.user b/Luski.net/Properties/PublishProfiles/FolderProfile.pubxml.user new file mode 100755 index 0000000..d26c1f6 --- /dev/null +++ b/Luski.net/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -0,0 +1,10 @@ + + + + + True|2022-11-23T16:09:01.7347068Z;True|2022-11-23T11:07:47.9880607-05:00;True|2022-11-23T11:07:08.8325322-05:00;True|2022-11-23T11:05:40.5859900-05:00;True|2022-09-21T18:57:48.1433890-04:00;False|2022-09-21T18:56:37.2624157-04:00;True|2022-07-05T22:55:54.9271108-04:00; + + + \ No newline at end of file diff --git a/Luski.net/Server.Cleanup.cs b/Luski.net/Server.Cleanup.cs new file mode 100755 index 0000000..8fbc84c --- /dev/null +++ b/Luski.net/Server.Cleanup.cs @@ -0,0 +1,17 @@ +using System; +using System.IO; + +namespace Luski.net; + +public sealed partial class Server : IDisposable +{ + ~Server() + { + try { if (Directory.Exists(Cache)) Directory.Delete(Cache, true); } catch { } + } + + public void Dispose() + { + try { if (Directory.Exists(Cache)) Directory.Delete(Cache, true); } catch { } + } +} diff --git a/Luski.net/Server.Constructors.cs b/Luski.net/Server.Constructors.cs new file mode 100755 index 0000000..6cce45f --- /dev/null +++ b/Luski.net/Server.Constructors.cs @@ -0,0 +1,129 @@ +using Luski.net.Enums; +using Luski.net.JsonTypes; +using Luski.net.JsonTypes.BaseTypes; +using Luski.net.JsonTypes.WSS; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using WebSocketSharp; +using File = System.IO.File; + +namespace Luski.net; + +public sealed partial class Server +{ + internal Server(string Email, string Password, Branch branch = Branch.Master, string? Username = null, string? pfp = null) + { + if (!Encryption.Generating) + { + Encryption.GenerateKeys(); + } + while (!Encryption.Generated) { } + InternalDomain = $"api.{branch}.luski.JacobTech.com"; + Branch = branch; + login = true; + Login json; + List> heads = new() + { + new("key", Encryption.MyPublicKey), + new("email", Convert.ToBase64String(Encryption.Encrypt(Email))), + new("password", Encryption.RemotePasswordEncrypt(Encryption.Encoder.GetBytes(Password))) + }; + if (File.Exists("LastPassVer.txt") && int.TryParse(File.ReadAllText("LastPassVer.txt"), out int lpv) && lpv < Encryption.PasswordVersion && lpv >= 0) + { + heads.Add(new("old_password", Encryption.RemotePasswordEncrypt(Encryption.Encoder.GetBytes(Password), lpv))); + heads.Add(new("old_version", lpv.ToString())); + } + if (pfp is not null) + { + heads.Add(new("username", Username)); + json = SendServer( + "CreateAccount", + pfp, + LoginContext.Default.Login, + heads.ToArray()).Result; + } + else + { + json = GetFromServer( + "Login", + LoginContext.Default.Login, + heads.ToArray()).Result; + } + + if (json.Error is not null) throw new Exception($"Luski appears to be down at the current moment: {json.ErrorMessage}"); + if (Encryption.ofkey is null || Encryption.outofkey is null) throw new Exception("Something went wrong generating the offline keys"); + login = false; + if (json is not null && json.Error is null) + { + ServerOut = new WebSocket($"wss://{InternalDomain}/WSS/{API_Ver}"); + ServerOut.SslConfiguration.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls13; + ServerOut.OnMessage += DataFromServer; + ServerOut.WaitTime = new TimeSpan(0, 0, 5); + ServerOut.OnError += ServerOut_OnError; + ServerOut.Connect(); + string Infermation = $"{{\"token\": \"{json.Token}\"}}"; + SendServer(new WSSLogin() { Token = json.Token! }, WSSLoginContext.Default.WSSLogin); + while (Token is null && Error is null) + { + + } + if (Error is not null) + { + throw new Exception(Error); + } + if (Token is null) throw new Exception("Server did not send a token"); + CanRequest = true; + _user = SocketUserBase.GetUser(long.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(Token.Split('.')[0]))), SocketAppUserContext.Default.SocketAppUser).Result; + if (_user is null || _user.Error is not null) throw new Exception("Something went wrong getting your user infermation"); + _ = _user.Channels; + foreach (var ch in chans) + { + _ = ch.Members; + } + _user.Email = Email; + _ = UpdateStatus(UserStatus.Online); + + try + { + Encryption.pw = Email.ToLower() + Password; + _ = Encryption.File.GetOfflineKey(); + } + catch + { + try + { + Encryption.pw = Email + Password; + var temp222 = Encryption.File.LuskiDataFile.GetDefualtDataFile(); + Encryption.pw = Email.ToLower() + Password; + if (temp222 is not null) temp222.Save(GetKeyFilePath, Encryption.pw); + } + catch + { + Token = null; + Error = null; + ServerOut.Close(); + throw new Exception("The key file you have is getting the wrong pasword. Type your Email in the same way you creaated your account to fix this error."); + } + } + OfflineKeyData offlinedata = GetFromServer("Keys/GetOfflineData", OfflineKeyDataContext.Default.OfflineKeyData).Result; + if (string.IsNullOrEmpty(Encryption.File.GetOfflineKey())) Encryption.File.SetOfflineKey(Encryption.ofkey); + if (offlinedata is not null && offlinedata.Error is null && offlinedata.keys is not null) + { + foreach (KeyExchange key in offlinedata.keys) + { + Encryption.File.Channels.AddKey(key.channel, Encryption.Encoder.GetString(Encryption.Decrypt(Convert.FromBase64String(key.key), Encryption.File.GetOfflineKey()))); + } + } + System.IO.File.WriteAllText("LastPassVer.txt", Encryption.PasswordVersion.ToString()); + Encryption.File.SetOfflineKey(Encryption.ofkey); + using HttpClient setkey = new(); + setkey.DefaultRequestHeaders.Add("token", Token); + _ = setkey.PostAsync($"https://{InternalDomain}/{API_Ver}/Keys/SetOfflineKey", new StringContent(Encryption.outofkey)).Result; + Encryption.outofkey = null; + Encryption.ofkey = null; + } + else throw new Exception(json?.ErrorMessage); + } +} diff --git a/Luski.net/Server.CreateAccount.cs b/Luski.net/Server.CreateAccount.cs new file mode 100755 index 0000000..8dd53b7 --- /dev/null +++ b/Luski.net/Server.CreateAccount.cs @@ -0,0 +1,93 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using Luski.net.Enums; +using Luski.net.JsonTypes; +using Luski.net.JsonTypes.WSS; +using WebSocketSharp; + +namespace Luski.net; + +public sealed partial class Server +{ + internal Server(string Email, string Password, string Username, byte[] PFP, Branch branch = Branch.Master) + { + Encryption.pw = Email.ToLower() + Password; + if (!Encryption.Generating) + { + Encryption.GenerateKeys(); + } + while (!Encryption.Generated) { } + if (Encryption.ofkey is null || Encryption.outofkey is null) throw new Exception("Something went wrong generating the offline keys"); + string Result; + InternalDomain = $"api.{branch}.luski.JacobTech.com"; + Branch = branch; + using (HttpClient web = new()) + { + web.DefaultRequestHeaders.Add("key", Encryption.MyPublicKey); + web.DefaultRequestHeaders.Add("email", Convert.ToBase64String(Encryption.Encrypt(Email))); + web.DefaultRequestHeaders.Add("password", Convert.ToBase64String(Encryption.Encrypt(Password))); + web.DefaultRequestHeaders.Add("username", Username); + HttpResponseMessage? d = web.PostAsync($"https://{InternalDomain}/{API_Ver}/CreateAccount", new StringContent(Convert.ToBase64String(PFP))).Result; + if (d is null || !d.IsSuccessStatusCode) throw new Exception("Luski appears to be down at the current moment"); + Result = d.Content.ReadAsStringAsync().Result; + web.DefaultRequestHeaders.Clear(); + } + Login? json = JsonSerializer.Deserialize(Result, LoginContext.Default.Login); + if (json is not null && json.Error is null) + { + ServerOut = new WebSocket($"wss://{InternalDomain}/{API_Ver}"); + ServerOut.SslConfiguration.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls13; + ServerOut.OnMessage += DataFromServer; + ServerOut.WaitTime = new TimeSpan(0, 0, 5); + ServerOut.OnError += ServerOut_OnError; + ServerOut.Connect(); + string Infermation = $"{{\"token\": \"{json.Token}\"}}"; + SendServer(new WSSLogin() { Token = json.Token}, WSSLoginContext.Default.WSSLogin); + while (Token is null && Error is null) + { + + } + if (Error is not null) + { + throw new Exception(Error); + } + if (Token is null) throw new Exception("Server did not send a token"); + CanRequest = true; + string data; + using (HttpClient web = new()) + { + web.DefaultRequestHeaders.Add("token", Token); + web.DefaultRequestHeaders.Add("id", Encoding.UTF8.GetString(Convert.FromBase64String(Token.Split('.')[0]))); + data = web.GetAsync($"https://{InternalDomain}/{API_Ver}/SocketUser").Result.Content.ReadAsStringAsync().Result; + } + _user = JsonSerializer.Deserialize(data); + if (_user is null || _user.Error is not null) throw new Exception("Something went wrong getting your user infermation"); + _ = _user.Channels; + foreach (var ch in chans) + { + _ = ch.Members; + } + _user.Email = Email; + UpdateStatus(UserStatus.Online); + Encryption.File.SetOfflineKey(Encryption.ofkey); + using HttpClient setkey = new(); + setkey.DefaultRequestHeaders.Add("token", Token); + _ = setkey.PostAsync($"https://{InternalDomain}/{API_Ver}/Keys/SetOfflineKey", new StringContent(Encryption.outofkey)).Result; + Encryption.outofkey = null; + Encryption.ofkey = null; + } + else throw new Exception(json?.ErrorMessage); + } + + public static Server CreateAccount(string Email, string Password, string Username, byte[] PFP, Branch branch = Branch.Master) + { + return new Server(Email, Password, Username, PFP, branch); + } + + public static Server CreateAccount(string Email, string Password, string Username, string PFP, Branch branch = Branch.Master) + { + return new Server(Email, Password, branch, Username, PFP); + } +} diff --git a/Luski.net/Server.Events.cs b/Luski.net/Server.Events.cs new file mode 100755 index 0000000..8ed8955 --- /dev/null +++ b/Luski.net/Server.Events.cs @@ -0,0 +1,21 @@ +using Luski.net.Interfaces; +using Luski.net.JsonTypes; +using System; +using System.Threading.Tasks; + +namespace Luski.net; + +public sealed partial class Server +{ + public event Func? MessageReceived; + + public event Func? UserStatusUpdate; + + public event Func? ReceivedFriendRequest; + + public event Func? FriendRequestResult; + + public event Func? IncommingCall; + + public event Func? OnError; +} diff --git a/Luski.net/Server.Globals.cs b/Luski.net/Server.Globals.cs new file mode 100755 index 0000000..0832213 --- /dev/null +++ b/Luski.net/Server.Globals.cs @@ -0,0 +1,83 @@ +using Luski.net.Enums; +using Luski.net.Interfaces; +using Luski.net.JsonTypes; +using Luski.net.Sockets; +using System; +using System.Collections.Generic; +using System.IO; +using WebSocketSharp; + +namespace Luski.net; + +public sealed partial class Server +{ + internal static string JT { get { return Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "/JacobTech"; } } + internal static SocketAudioClient? AudioClient = null; + internal static string? Token = null, Error = null; + internal static bool CanRequest = false; + internal static SocketAppUser? _user; + internal static string InternalDomain = "api.master.luski.jacobtech.com", platform = "win-x64"; + internal static Branch Branch; + internal static double Percent = 0.5; + private static WebSocket? ServerOut; + private static string? gen = null; + private static bool login = false; + + public string Domain { get { return InternalDomain; } } + + internal static string Cache + { + get + { + if (gen is null) + { + if (!Directory.Exists(JT)) Directory.CreateDirectory(JT); + string path = JT + "/Luski/"; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + path += Branch.ToString() + "/"; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + path += platform + "/"; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + path += "Data/"; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + path += _user?.Id + "/"; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + path += "Cache/"; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + path += Path.GetRandomFileName() + "/"; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + gen = path; + } + if (!Directory.Exists($"{gen}/avatars")) Directory.CreateDirectory($"{gen}/avatars"); + if (!Directory.Exists($"{gen}/channels")) Directory.CreateDirectory($"{gen}/channels"); + return gen; + } + } + internal const string API_Ver = "v1"; + internal static List poeople = new(); + internal static List chans { get; set; } = new(); + internal static string GetKeyFilePath + { + get + { + return GetKeyFilePathBr(Branch.ToString()); + } + } + + internal static string GetKeyFilePathBr(string br) + { + if (!Directory.Exists(JT)) Directory.CreateDirectory(JT); + string path = JT + "/Luski/"; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + path += br + "/"; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + path += platform + "/"; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + path += "Data/"; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + path += _user?.Id + "/"; + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + path += "keys.lsk"; + return path; + } +} diff --git a/Luski.net/Server.Incoming.cs b/Luski.net/Server.Incoming.cs new file mode 100755 index 0000000..cb5aae3 --- /dev/null +++ b/Luski.net/Server.Incoming.cs @@ -0,0 +1,118 @@ +using Luski.net.Enums; +using Luski.net.JsonTypes; +using Luski.net.JsonTypes.BaseTypes; +using Luski.net.JsonTypes.HTTP; +using Luski.net.JsonTypes.WSS; +using System; +using System.Text.Json; +using WebSocketSharp; + +namespace Luski.net +{ + public sealed partial class Server + { + private void DataFromServer(object? sender, MessageEventArgs e) + { + if (e.IsPing) return; + IncomingWSS? data = JsonSerializer.Deserialize(e.Data, IncomingWSSContext.Default.IncomingWSS); + switch (data?.Type) + { + case DataType.Login: + WSSLogin n = JsonSerializer.Deserialize(e.Data, WSSLoginContext.Default.WSSLogin)!; + Token = n.Token; + break; + case DataType.Error: + if (Token is null) + { + Error = data.Error; + } + else + { + if (OnError is not null) + { + _ = OnError.Invoke(new Exception(data.Error)); + } + } + break; + case DataType.Message_Create: + if (MessageReceived is not null) + { + SocketMessage? m = JsonSerializer.Deserialize(e.Data); + if (m is not null) + { + m.decrypt(Encryption.File.Channels.GetKey(m.ChannelID)); + _ = MessageReceived.Invoke(m); + } + } + break; + case DataType.Status_Update: + if (UserStatusUpdate is not null) + { + StatusUpdate? SU = JsonSerializer.Deserialize(e.Data); + if (SU is not null) + { + SocketRemoteUser after = SocketUserBase.GetUser(SU.id, SocketRemoteUserContext.Default.SocketRemoteUser).Result; + after.Status = SU.after; + SocketRemoteUser before = after.Clone(); + before.Status = SU.before; + _ = UserStatusUpdate.Invoke(before, after); + } + } + break; + case DataType.Friend_Request: + if (ReceivedFriendRequest is not null) + { + FriendRequest? request = JsonSerializer.Deserialize(e.Data, FriendRequestContext.Default.FriendRequest); + if (request is not null) _ = ReceivedFriendRequest.Invoke(SocketUserBase.GetUser(request.Id, SocketRemoteUserContext.Default.SocketRemoteUser).Result); + } + break; + case DataType.Friend_Request_Result: + if (FriendRequestResult is not null) + { + FriendRequestResult? FRR = JsonSerializer.Deserialize(e.Data); + if (FRR is not null && FRR.Channel is not null && FRR.Id is not null && FRR.Result is not null) + { + SocketDMChannel chan = SocketChannel.GetChannel((long)FRR.Channel, SocketDMChannelContext.Default.SocketDMChannel).Result; + chans.Add(chan); + SocketRemoteUser from1 = SocketUserBase.GetUser((long)FRR.Id, SocketRemoteUserContext.Default.SocketRemoteUser).Result; + from1.Channel = chan; + _ = FriendRequestResult.Invoke(from1, (bool)FRR.Result); + } + } + break; + case DataType.Call_Info: + if (IncommingCall is not null) + { + callinfoinc? ci = JsonSerializer.Deserialize(e.Data); + if (ci is not null) _ = IncommingCall.Invoke(SocketChannel.GetChannel(ci.channel, SocketTextChannelContext.Default.SocketTextChannel).Result, SocketUserBase.GetUser(ci.from, SocketRemoteUserContext.Default.SocketRemoteUser).Result); + } + break; + case DataType.Call_Data: + if (AudioClient is not null) + { + AudioClient.Givedata(e.Data); + } + break; + case DataType.Key_Exchange: + try + { + KeyExchange? KE = JsonSerializer.Deserialize(e.Data); + if (KE is not null) Encryption.File.Channels.AddKey(KE.channel, Encryption.Encoder.GetString(Encryption.Decrypt(Convert.FromBase64String(KE.key)))); + } + catch (Exception ex) + { + if (OnError is not null) OnError.Invoke(ex); + } + break; + default: + break; + } + } + + private class callinfoinc + { + public long channel { get; set; } = default!; + public long from { get; set; } = default!; + } + } +} diff --git a/Luski.net/Server.Login.cs b/Luski.net/Server.Login.cs new file mode 100755 index 0000000..e0b1596 --- /dev/null +++ b/Luski.net/Server.Login.cs @@ -0,0 +1,12 @@ +using Luski.net.Enums; +using System.Threading.Tasks; + +namespace Luski.net; + +public sealed partial class Server +{ + public static async Task Login(string Email, string Password, Branch branch = Branch.Master) + { + return new Server(Email, Password, branch); + } +} diff --git a/Luski.net/Server.cs b/Luski.net/Server.cs new file mode 100755 index 0000000..b6cfbe9 --- /dev/null +++ b/Luski.net/Server.cs @@ -0,0 +1,298 @@ +using Luski.net.Enums; +using Luski.net.Interfaces; +using Luski.net.JsonTypes; +using Luski.net.JsonTypes.BaseTypes; +using Luski.net.JsonTypes.HTTP; +using Luski.net.Sockets; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; + +namespace Luski.net; + +public sealed partial class Server +{ +#pragma warning disable CA1822 // Mark members as static + /// + /// Creates an audio client for the you want to talk on + /// + /// The channel you want to talk on + /// + public IAudioClient CreateAudioClient(long channel_id) + { + // if (AudioClient != null) throw new Exception("audio client alread created"); + SocketAudioClient client = new(channel_id, OnError); + AudioClient = client; + return client; + } + + public async Task SendFriendResult(long user, bool answer) + { + + FriendRequestResult json = await SendServer("FriendRequestResult", + new FriendRequestResultOut() + { + Id = user, + Result = answer + }, + FriendRequestResultOutContext.Default.FriendRequestResultOut, + FriendRequestResultContext.Default.FriendRequestResult); + + if (json is not null && json.Error is null && json.ErrorMessage is null && answer && json.Channel is not null) + { + SocketDMChannel chan = await SocketChannel.GetChannel((long)json.Channel, SocketDMChannelContext.Default.SocketDMChannel); + _ = chan.StartKeyProcessAsync(); + chans.Add(chan); + } + else + { + throw new Exception(json?.Error.ToString()); + } + return SocketUserBase.GetUser(user, SocketRemoteUserContext.Default.SocketRemoteUser).Result; + } + + public async Task SendFriendRequest(long user) + { + FriendRequestResult? json = await SendServer("FriendRequest", new FriendRequest() { Id = user, SubType = 0 }, FriendRequestContext.Default.FriendRequest, FriendRequestResultContext.Default.FriendRequestResult); + + if (json.StatusCode != HttpStatusCode.Accepted) + { + if (json is not null && json.Error is not null) + { + switch ((ErrorCode)(int)json.Error) + { + case ErrorCode.InvalidToken: + throw new Exception("Your current token is no longer valid"); + case ErrorCode.ServerError: + throw new Exception($"Error from server: {json.ErrorMessage}"); + case ErrorCode.InvalidPostData: + throw new Exception("The post data dent to the server is not the correct format. This may be because you app is couropt or you are using the wron API version"); + case ErrorCode.Forbidden: + throw new Exception("You already have an outgoing request or the persone is not real"); + } + } + + if (json is not null && json.Channel is not null) + { + SocketDMChannel chan = await SocketChannel.GetChannel((long)json.Channel, (JsonTypeInfo)SocketDMChannelContext.Default.SocketDMChannel); + _ = chan.StartKeyProcessAsync(); + chans.Add(chan); + } + } + + SocketRemoteUser b = await SocketUserBase.GetUser(user, SocketRemoteUserContext.Default.SocketRemoteUser); + b.FriendStatus = FriendStatus.PendingOut; + return b; + } + + public async Task SendFriendRequest(string username, short tag) + { + FriendRequestResult json = await SendServer("FriendRequest", new FriendRequest() { Username = username, Tag = tag, SubType = 1 }, FriendRequestContext.Default.FriendRequest, FriendRequestResultContext.Default.FriendRequestResult); + + if (json is not null && json.Error is not null) + { + throw (ErrorCode)(int)json.Error switch + { + ErrorCode.InvalidToken => new Exception("Your current token is no longer valid"), + ErrorCode.ServerError => new Exception("Error from server: " + json.ErrorMessage), + ErrorCode.InvalidPostData => new Exception("The post data dent to the server is not the correct format. This may be because you app is couropt or you are using the wron API version"), + ErrorCode.Forbidden => new Exception("You already have an outgoing request or the persone is not real"), + _ => new Exception(JsonSerializer.Serialize(json)), + }; + } + else if (json is not null && json.Channel is not null && json.Id is not null) + { + SocketDMChannel chan = await SocketChannel.GetChannel(json.Channel.Value, (JsonTypeInfo)SocketDMChannelContext.Default.SocketDMChannel); + _ = chan.StartKeyProcessAsync(); + chans.Add(chan); + return await SocketUserBase.GetUser((long)json.Id, SocketRemoteUserContext.Default.SocketRemoteUser); + } + else throw new Exception("missing data from server"); + } + + /// + /// Sends the server a request to update the of you account + /// + /// The you want to set your status to + /// + public async Task UpdateStatus(UserStatus Status) + { + if (_user is null) throw new Exception("You must login to make a request"); + IncomingHTTP? data = await SendServer("SocketUserProfile/Status", new Status() { UserStatus = Status }, StatusContext.Default.Status, IncomingHTTPContext.Default.IncomingHTTP); + if (data.Error is not null && ((int)data.StatusCode < 200 || (int)data.StatusCode > 299)) + { + if (data?.ErrorMessage is not null) throw new Exception(data.ErrorMessage); + if (data?.Error is not null) throw new Exception(((int)data.Error).ToString()); + else throw new Exception("Something went worng"); + } + + _user.Status = Status; + return Task.CompletedTask; + } + + public async Task ChangeChannel(long Channel) + { + if (_user is null) throw new Exception("You must login to make a request"); + IncomingHTTP? data = await SendServer("ChangeChannel", new Channel() { Id = Channel }, ChannelContext.Default.Channel, IncomingHTTPContext.Default.IncomingHTTP); + if (data.StatusCode != HttpStatusCode.Accepted) + { + if (data?.Error is not null) + { + switch (data.Error) + { + case ErrorCode.InvalidToken: + throw new Exception("Your current token is no longer valid"); + case ErrorCode.ServerError: + throw new Exception("Error from server: " + data.ErrorMessage); + } + } + else throw new Exception("Something went worng"); + } + + _user.SelectedChannel = Channel; + } + + public async Task SendMessage(string Message, long Channel, params JsonTypes.File[] Files) => (await GetChannel(Channel)).SendMessage(Message, Files); + + public void SetMultiThreadPercent(double num) + { + if (num < 1 || num > 100) throw new Exception("Number must be from 1 - 100"); + Percent = num / 100; + } + + public async Task GetMessage(long MessageId) => await SocketMessage.GetMessage(MessageId); + + public async Task GetUser(long UserID) => await SocketUserBase.GetUser(UserID, SocketRemoteUserContext.Default.SocketRemoteUser); + + public async Task GetChannel(long Channel) where TChannel : SocketChannel, new() + { + TChannel Return = new(); + switch (Return) + { + case SocketDMChannel: + Return = (await SocketChannel.GetChannel(Channel, SocketDMChannelContext.Default.SocketDMChannel) as TChannel)!; + break; + case SocketGroupChannel: + Return = (await SocketChannel.GetChannel(Channel, SocketGroupChannelContext.Default.SocketGroupChannel) as TChannel)!; + break; + case SocketTextChannel: + Return = (await SocketChannel.GetChannel(Channel, SocketTextChannelContext.Default.SocketTextChannel) as TChannel)!; + break; + case SocketChannel: + Return = (await SocketChannel.GetChannel(Channel, SocketChannelContext.Default.SocketChannel) as TChannel)!; + break; + case null: + throw new NullReferenceException(nameof(TChannel)); + default: + throw new Exception("Unknown channel type"); + } + return Return; + } + + + public SocketAppUser CurrentUser + { + get + { + if (_user is null) throw new Exception("You must Login first"); + return _user; + } + } +#pragma warning restore CA1822 // Mark members as static + + private void ServerOut_OnError(object? sender, WebSocketSharp.ErrorEventArgs e) + { + if (OnError is not null) OnError.Invoke(new Exception(e.Message)); + } + + [Obsolete("Move to new Data layout")] + internal static void SendServer(string data) + { + ServerOut?.Send(data); + } + + internal static void SendServer(Tvalue Payload, JsonTypeInfo jsonTypeInfo) where Tvalue : IncomingWSS + { + ServerOut?.Send(JsonSerializer.Serialize(Payload, jsonTypeInfo)); + } + + internal static HttpResponseMessage GetFromServer(string Path, params KeyValuePair[] Headers) + { + using HttpClient web = new(); + web.Timeout = TimeSpan.FromSeconds(10); + if (!login) web.DefaultRequestHeaders.Add("token", Token); + if (Headers is not null && Headers.Length > 0) foreach (KeyValuePair header in Headers) web.DefaultRequestHeaders.Add(header.Key, header.Value); + return web.GetAsync($"https://{InternalDomain}/{API_Ver}/{Path}").Result; + } + + internal static Task GetFromServer(string Path, string File, params KeyValuePair[] Headers) + { + using HttpClient web = new(); + web.Timeout = TimeSpan.FromMinutes(10); + if (!login) web.DefaultRequestHeaders.Add("token", Token); + if (Headers is not null && Headers.Length > 0) foreach (KeyValuePair header in Headers) web.DefaultRequestHeaders.Add(header.Key, header.Value); + HttpResponseMessage Response = web.GetAsync($"https://{InternalDomain}/{API_Ver}/{Path}").Result; + Stream stream = Response.Content.ReadAsStreamAsync().Result; + using FileStream fs = System.IO.File.Create(File); + stream.CopyTo(fs); + return Task.CompletedTask; + } + + internal static async Task GetFromServer(string Path, JsonTypeInfo Type, params KeyValuePair[] Headers) where Tresult : IncomingHTTP, new() + { + HttpResponseMessage ServerResponce = GetFromServer(Path, Headers); + if (!ServerResponce.IsSuccessStatusCode) return new Tresult() { StatusCode = ServerResponce.StatusCode, Error = ErrorCode.ServerError, ErrorMessage = $"Server responded with status code {(int)ServerResponce.StatusCode}:{ServerResponce.StatusCode}" }; + Tresult? temp = JsonSerializer.Deserialize(ServerResponce.Content.ReadAsStreamAsync().Result, Type); + if (temp is null) return new Tresult() { StatusCode = ServerResponce.StatusCode, Error = ErrorCode.ServerError, ErrorMessage = $"Server responded with empty data" }; + return temp; + } + + internal static async Task SendServer(string Path, Tvalue Payload, JsonTypeInfo jsonTypeInfo, JsonTypeInfo ReturnjsonTypeInfo, params KeyValuePair[] Headers) where Tvalue : HTTPRequest where Tresult : IncomingHTTP, new() + { + using HttpClient web = new(); + if (!login) web.DefaultRequestHeaders.Add("token", Token); + if (Headers is not null && Headers.Length > 0) foreach (KeyValuePair header in Headers) web.DefaultRequestHeaders.Add(header.Key, header.Value); + HttpResponseMessage ServerResponce = web.PostAsJsonAsync($"https://{InternalDomain}/{API_Ver}/{Path}", Payload, jsonTypeInfo).Result; + if (!ServerResponce.IsSuccessStatusCode) return new Tresult() { StatusCode = ServerResponce.StatusCode, Error = ErrorCode.ServerError, ErrorMessage = $"Server responded with status code {(int)ServerResponce.StatusCode}:{ServerResponce.StatusCode}" }; + Tresult error = new() { StatusCode = ServerResponce.StatusCode, Error = ErrorCode.ServerError, ErrorMessage = $"Server responded with empty data" }; + if (string.IsNullOrWhiteSpace(ServerResponce.Content.ReadAsStringAsync().Result)) return error; + try + { + Tresult? temp = JsonSerializer.Deserialize(ServerResponce.Content.ReadAsStreamAsync().Result, ReturnjsonTypeInfo); + if (temp is null) return error; + return temp; + } + catch { return error; } + } + + internal static async Task SendServer(string Path, string File, JsonTypeInfo ReturnjsonTypeInfo, params KeyValuePair[] Headers) where Tresult : IncomingHTTP, new() + { + var fs = System.IO.File.OpenRead(File); + try + { + using HttpClient web = new(); + if (!login) web.DefaultRequestHeaders.Add("token", Token); + web.Timeout = new TimeSpan(0, 10, 0); + if (Headers is not null && Headers.Length > 0) foreach (KeyValuePair header in Headers) web.DefaultRequestHeaders.Add(header.Key, header.Value); + HttpResponseMessage ServerResponce = web.PostAsync($"https://{InternalDomain}/{API_Ver}/{Path}", new StreamContent(fs)).Result; + if (!ServerResponce.IsSuccessStatusCode) return new Tresult() { StatusCode = ServerResponce.StatusCode, Error = ErrorCode.ServerError, ErrorMessage = $"Server responded with status code {(int)ServerResponce.StatusCode}:{ServerResponce.StatusCode}" }; + try + { + Tresult? temp = JsonSerializer.Deserialize(ServerResponce.Content.ReadAsStreamAsync().Result, ReturnjsonTypeInfo); + if (temp is null) return new Tresult() { StatusCode = ServerResponce.StatusCode, Error = ErrorCode.ServerError, ErrorMessage = $"Server responded with empty data" }; + return temp; + } + catch { return new Tresult() { StatusCode = ServerResponce.StatusCode, Error = ErrorCode.ServerError, ErrorMessage = $"Server responded with empty data" }; } + } + finally + { + fs.Close(); + } + } +} diff --git a/Luski.net/Sockets/SocketAudioClient.cs b/Luski.net/Sockets/SocketAudioClient.cs new file mode 100755 index 0000000..7388aaa --- /dev/null +++ b/Luski.net/Sockets/SocketAudioClient.cs @@ -0,0 +1,388 @@ +using Luski.net.Enums; +using Luski.net.Interfaces; +using Luski.net.JsonTypes.BaseTypes; +using Luski.net.Sound; +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using static Luski.net.Exceptions; + +namespace Luski.net.Sockets +{ + internal class SocketAudioClient : IAudioClient + { + internal SocketAudioClient(long Channel, Func? error) + { + this.Channel = Channel; + errorin = error; + Muted = false; + PrototolClient.DataComplete += new Protocol.DelegateDataComplete(OnProtocolClient_DataComplete); + DataRecived += SocketAudioClient_DataRecived; + } + + public event Func? Connected; + + public bool Muted { get; private set; } + + public bool Deafened { get; private set; } + + public void ToggleMic() + { + if (Muted == true) + { + Muted = false; + } + else + { + Muted = true; + } + } + + public void ToggleAudio() + { + if (Deafened == true) + { + Deafened = false; + } + else + { + Deafened = true; + } + } + + public void RecordSoundFrom(RecordingDevice Device) + { + if (Connectedb) + { + StartRecordingFromSounddevice_Client(Device); + } + else + { + throw new NotConnectedException(this, "The call has not been connected yet!"); + } + } + + public void PlaySoundTo(PlaybackDevice Device) + { + if (Connectedb) + { + StartPlayingToSounddevice_Client(Device); + } + else + { + throw new NotConnectedException(this, "The call has not been connected yet!"); + } + } + + public void JoinCall() + { + if (Connected == null) + { + throw new MissingEventException("Connected"); + } + else + { + //get info + string data; + while (true) + { + if (Server.CanRequest) + { + using HttpClient web = new(); + web.DefaultRequestHeaders.Add("token", Server.Token); + web.DefaultRequestHeaders.Add("id", Channel.ToString()); + data = web.GetAsync($"https://{Server.InternalDomain}/{Server.API_Ver}/GetCallInfo").Result.Content.ReadAsStringAsync().Result; + break; + } + } + call? json = JsonSerializer.Deserialize(data); + Server.SendServer(JsonRequest.Send(DataType.Join_Call, JsonRequest.JoinCall(Channel)).ToString()); + Samples = json.samples; + } + } + + private class call : IncomingHTTP + { + public int samples { get; set; } = default!; + public string[] members { get; set; } = default!; + } + + public void LeaveCall() + { + Server.SendServer(JsonRequest.Send(DataType.Leave_Call, JsonRequest.JoinCall(Channel)).ToString()); + StopRecordingFromSounddevice_Client(); + } + + private readonly Protocol PrototolClient = new(ProtocolTypes.LH, Encoding.Default); + private JitterBuffer RecordingJitterBuffer = new(null, JitterBuffer, 20); + private JitterBuffer PlayingJitterBuffer = new(null, JitterBuffer, 20); + private readonly Func? errorin; + private event Func DataRecived; + private static readonly uint JitterBuffer = 5; + private readonly int BitsPerSample = 16; + private long SequenceNumber = 4596; + private readonly int Channels = 1; + private Recorder? RecorderClient; + private bool Connectedb = false; + private bool recording = false; + private long m_TimeStamp = 0; + private Player? PlayerClient; + private readonly long Channel; + + private void StopPlayingToSounddevice_Client() + { + if (PlayerClient != null) + { + PlayerClient.Close(); + PlayerClient = null; + } + + if (PlayingJitterBuffer != null) + { + PlayingJitterBuffer.Stop(); + } + } + + private async Task SocketAudioClient_DataRecived(string arg) + { + cdata d = JsonSerializer.Deserialize(arg); + byte[] data = Convert.FromBase64String(d.data); + PrototolClient.Receive_LH(this, data); + } + + private class cdata + { + public string data { get; set; } = default!; + public long from { get; set; } = default!; + } + + private void SendData(byte[] data) + { + if (!Connectedb) + { + return; + } + + Server.SendServer(JsonRequest.Send(DataType.Call_Data, JsonRequest.SendCallData(PrototolClient.ToBytes(data), Channel))); + } + + internal void Givedata(dynamic data) + { + DataRecived.Invoke(((object)data).ToString()); + } + + private int _samp; + + internal int Samples + { + get => _samp; + set + { + _samp = value; + Connectedb = true; + if (Connected is not null) Connected.Invoke(); + PlaySoundTo(Devices.GetDefaltPlaybackDevice()); + RecordSoundFrom(Devices.GetDefaltRecordingDevice()); + } + } + + private bool playing = false; + + private void StartPlayingToSounddevice_Client(PlaybackDevice device) + { + if (playing) + { + StopPlayingToSounddevice_Client(); + } + playing = true; + if (PlayingJitterBuffer != null) + { + PlayingJitterBuffer.DataAvailable -= new JitterBuffer.DelegateDataAvailable(OnJitterBufferClientDataAvailablePlaying); + + PlayingJitterBuffer = new JitterBuffer(null, JitterBuffer, 20); + PlayingJitterBuffer.DataAvailable += new JitterBuffer.DelegateDataAvailable(OnJitterBufferClientDataAvailablePlaying); + PlayingJitterBuffer.Start(); + } + + if (PlayerClient == null) + { + PlayerClient = new Player(); + PlayerClient.Open(device.Name, Samples, BitsPerSample, Channels, (int)JitterBuffer); + } + } + + private void OnJitterBufferClientDataAvailablePlaying(object sender, RTPPacket rtp) + { + try + { + if (PlayerClient != null) + { + if (PlayerClient.Opened) + { + if (Deafened == false) + { + byte[] linearBytes = Utils.MuLawToLinear(rtp.Data, BitsPerSample, Channels); + PlayerClient.PlayData(linearBytes, false); + } + } + } + } + catch (Exception ex) + { + System.Diagnostics.StackFrame sf = new(true); + errorin?.Invoke(new Exception(string.Format("Exception: {0} StackTrace: {1}. FileName: {2} Method: {3} Line: {4}", ex.Message, ex.StackTrace, sf.GetFileName(), sf.GetMethod(), sf.GetFileLineNumber()))); + } + } + + private void StartRecordingFromSounddevice_Client(RecordingDevice device) + { + try + { + if (recording) + { + StopRecordingFromSounddevice_Client(); + } + recording = true; + InitJitterBufferClientRecording(); + int bufferSize = 0; + bufferSize = Utils.GetBytesPerInterval((uint)Samples, BitsPerSample, Channels) * 4; + + if (bufferSize > 0) + { + RecorderClient = new Recorder(); + RecorderClient.DataRecorded += new Recorder.DelegateDataRecorded(OnDataReceivedFromSoundcard_Client); + + if (RecorderClient.Start(device.Name, Samples, BitsPerSample, Channels, 8, bufferSize)) + { + + RecordingJitterBuffer.Start(); + } + } + } + catch (Exception ex) + { + errorin.Invoke(ex); + } + } + + private void StopRecordingFromSounddevice_Client() + { + RecorderClient.Stop(); + + RecorderClient.DataRecorded -= new Recorder.DelegateDataRecorded(OnDataReceivedFromSoundcard_Client); + RecorderClient = null; + + RecordingJitterBuffer.Stop(); + } + + private void InitJitterBufferClientRecording() + { + if (RecordingJitterBuffer != null) + { + RecordingJitterBuffer.DataAvailable -= new JitterBuffer.DelegateDataAvailable(OnJitterBufferClientDataAvailableRecording); + } + + RecordingJitterBuffer = new JitterBuffer(null, 8, 20); + RecordingJitterBuffer.DataAvailable += new JitterBuffer.DelegateDataAvailable(OnJitterBufferClientDataAvailableRecording); + } + + private void OnJitterBufferClientDataAvailableRecording(object sender, RTPPacket rtp) + { + if (Muted == false && rtp != null && rtp.Data != null && rtp.Data.Length > 0) + { + byte[] rtpBytes = rtp.ToBytes(); + SendData(rtpBytes); + } + } + + private void OnDataReceivedFromSoundcard_Client(byte[] data) + { + try + { + lock (this) + { + int bytesPerInterval = Utils.GetBytesPerInterval((uint)Samples, BitsPerSample, Channels); + int count = data.Length / bytesPerInterval; + int currentPos = 0; + for (int i = 0; i < count; i++) + { + byte[] partBytes = new byte[bytesPerInterval]; + Array.Copy(data, currentPos, partBytes, 0, bytesPerInterval); + currentPos += bytesPerInterval; + RTPPacket rtp = ToRTPPacket(partBytes, BitsPerSample, Channels); + + RecordingJitterBuffer.AddData(rtp); + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex.Message); + } + } + + private RTPPacket ToRTPPacket(byte[] linearData, int bitsPerSample, int channels) + { + byte[] mulaws = Utils.LinearToMulaw(linearData, bitsPerSample, channels); + + RTPPacket rtp = new() + { + Data = mulaws, + CSRCCount = 0, + Extension = false, + HeaderLength = RTPPacket.MinHeaderLength, + Marker = false, + Padding = false, + PayloadType = 0, + Version = 2, + SourceId = 0 + }; + + try + { + rtp.SequenceNumber = Convert.ToUInt16(SequenceNumber); + SequenceNumber++; + } + catch (Exception) + { + SequenceNumber = 0; + } + try + { + rtp.Timestamp = Convert.ToUInt32(m_TimeStamp); + m_TimeStamp += mulaws.Length; + } + catch (Exception) + { + m_TimeStamp = 0; + } + + return rtp; + } + + private void OnProtocolClient_DataComplete(object sender, byte[] data) + { + try + { + if (PlayerClient != null && PlayerClient.Opened) + { + RTPPacket rtp = new(data); + + if (rtp.Data != null) + { + if (PlayingJitterBuffer != null) + { + PlayingJitterBuffer.AddData(rtp); + } + } + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + } + } +} diff --git a/Luski.net/Sound/Devices.cs b/Luski.net/Sound/Devices.cs new file mode 100755 index 0000000..8bf90f2 --- /dev/null +++ b/Luski.net/Sound/Devices.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; + +namespace Luski.net.Sound +{ + public static class Devices + { + public static RecordingDevice GetDefaltRecordingDevice() + { + return GetRecordingDevices()[0]; + } + + public static PlaybackDevice GetDefaltPlaybackDevice() + { + return GetPlaybackDevices()[0]; + } + + public static IReadOnlyList GetRecordingDevices() + { + List RecordingNames = WinSound.GetRecordingNames(); + List RecordingDevices = new(); + foreach (string Device in RecordingNames) + { + RecordingDevices.Add(new RecordingDevice(Device)); + } + return RecordingDevices.AsReadOnly(); + } + public static IReadOnlyList GetPlaybackDevices() + { + List PlaybackName = WinSound.GetPlaybackNames(); + List PlaybackDevices = new(); + foreach (string Device in PlaybackName) + { + PlaybackDevices.Add(new PlaybackDevice(Device)); + } + return PlaybackDevices.AsReadOnly(); + } + } + + public class RecordingDevice + { + internal RecordingDevice(string name) + { + Name = name; + } + + public string Name { get; } + } + + public class PlaybackDevice + { + internal PlaybackDevice(string name) + { + Name = name; + } + + public string Name { get; } + } +} diff --git a/Luski.net/Sound/JitterBuffer.cs b/Luski.net/Sound/JitterBuffer.cs new file mode 100755 index 0000000..27348ab --- /dev/null +++ b/Luski.net/Sound/JitterBuffer.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; + +namespace Luski.net.Sound +{ + internal class JitterBuffer + { + internal JitterBuffer(object? sender, uint maxRTPPackets, uint timerIntervalInMilliseconds) + { + if (maxRTPPackets < 2) + { + throw new Exception("Wrong Arguments. Minimum maxRTPPackets is 2"); + } + + m_Sender = sender; + Maximum = maxRTPPackets; + IntervalInMilliseconds = timerIntervalInMilliseconds; + + Init(); + } + + private readonly object? m_Sender = null; + private readonly EventTimer m_Timer = new(); + private readonly Queue m_Buffer = new(); + private RTPPacket m_LastRTPPacket = new(); + private bool m_Underflow = true; + private bool m_Overflow = false; + + internal delegate void DelegateDataAvailable(object sender, RTPPacket packet); + internal event DelegateDataAvailable? DataAvailable; + + internal uint Maximum { get; } = 10; + internal uint IntervalInMilliseconds { get; } = 20; + private void Init() + { + InitTimer(); + } + private void InitTimer() + { + m_Timer.TimerTick += new EventTimer.DelegateTimerTick(OnTimerTick); + } + internal void Start() + { + m_Timer.Start(IntervalInMilliseconds); + m_Underflow = true; + } + internal void Stop() + { + m_Timer.Stop(); + m_Buffer.Clear(); + } + private void OnTimerTick() + { + try + { + if (DataAvailable != null) + { + if (m_Buffer.Count > 0) + { + if (m_Overflow) + { + if (m_Buffer.Count <= Maximum / 2) + { + m_Overflow = false; + } + } + + if (m_Underflow) + { + if (m_Buffer.Count < Maximum / 2) + { + return; + } + else + { + m_Underflow = false; + } + } + + m_LastRTPPacket = m_Buffer.Dequeue(); + DataAvailable(m_Sender, m_LastRTPPacket); + } + else + { + m_Overflow = false; + + if (m_LastRTPPacket != null && m_Underflow == false) + { + if (m_LastRTPPacket.Data != null) + { + m_Underflow = true; + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine(string.Format("JitterBuffer.cs | OnTimerTick() | {0}", ex.Message)); + } + } + internal void AddData(RTPPacket packet) + { + try + { + if (m_Overflow == false) + { + if (m_Buffer.Count <= Maximum) + { + m_Buffer.Enqueue(packet); + } + else + { + m_Overflow = true; + } + } + } + catch (Exception ex) + { + Console.WriteLine(string.Format("JitterBuffer.cs | AddData() | {0}", ex.Message)); + } + } + } +} diff --git a/Luski.net/Sound/Player.cs b/Luski.net/Sound/Player.cs new file mode 100755 index 0000000..173594d --- /dev/null +++ b/Luski.net/Sound/Player.cs @@ -0,0 +1,417 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Luski.net.Sound +{ + internal unsafe class Player + { + internal Player() + { + + delegateWaveOutProc = new Win32.DelegateWaveOutProc(WaveOutProc); + } + + private readonly LockerClass Locker = new(); + private readonly LockerClass LockerCopy = new(); + private IntPtr hWaveOut = IntPtr.Zero; + private string WaveOutDeviceName = ""; + private bool IsWaveOutOpened = false; + private bool IsThreadPlayWaveOutRunning = false; + private bool IsClosed = false; + private bool IsPaused = false; + private bool IsStarted = false; + private bool IsBlocking = false; + private int SamplesPerSecond = 8000; + private int BitsPerSample = 16; + private int Channels = 1; + private int BufferCount = 8; + private readonly int BufferLength = 1024; + private Win32.WAVEHDR*[] WaveOutHeaders; + private readonly Win32.DelegateWaveOutProc delegateWaveOutProc; + private Thread? ThreadPlayWaveOut; + private readonly AutoResetEvent AutoResetEventDataPlayed = new(false); + + internal delegate void DelegateStopped(); + internal event DelegateStopped? PlayerClosed; + internal event DelegateStopped? PlayerStopped; + + internal bool Opened => IsWaveOutOpened & IsClosed == false; + + internal bool Playing + { + get + { + if (Opened && IsStarted) + { + foreach (Win32.WAVEHDR* pHeader in WaveOutHeaders) + { + if (IsHeaderInqueue(*pHeader)) + { + return true; + } + } + } + return false; + } + } + + private bool CreateWaveOutHeaders() + { + WaveOutHeaders = new Win32.WAVEHDR*[BufferCount]; + int createdHeaders = 0; + + for (int i = 0; i < BufferCount; i++) + { + WaveOutHeaders[i] = (Win32.WAVEHDR*)Marshal.AllocHGlobal(sizeof(Win32.WAVEHDR)); + + WaveOutHeaders[i]->dwLoops = 0; + WaveOutHeaders[i]->dwUser = IntPtr.Zero; + WaveOutHeaders[i]->lpNext = IntPtr.Zero; + WaveOutHeaders[i]->reserved = IntPtr.Zero; + WaveOutHeaders[i]->lpData = Marshal.AllocHGlobal(BufferLength); + WaveOutHeaders[i]->dwBufferLength = (uint)BufferLength; + WaveOutHeaders[i]->dwBytesRecorded = 0; + WaveOutHeaders[i]->dwFlags = 0; + + Win32.MMRESULT hr = Win32.waveOutPrepareHeader(hWaveOut, WaveOutHeaders[i], sizeof(Win32.WAVEHDR)); + if (hr == Win32.MMRESULT.MMSYSERR_NOERROR) + { + createdHeaders++; + } + } + + return (createdHeaders == BufferCount); + } + + private void FreeWaveOutHeaders() + { + try + { + if (WaveOutHeaders != null) + { + for (int i = 0; i < WaveOutHeaders.Length; i++) + { + Win32.MMRESULT hr = Win32.waveOutUnprepareHeader(hWaveOut, WaveOutHeaders[i], sizeof(Win32.WAVEHDR)); + + int count = 0; + while (count <= 100 && (WaveOutHeaders[i]->dwFlags & Win32.WaveHdrFlags.WHDR_INQUEUE) == Win32.WaveHdrFlags.WHDR_INQUEUE) + { + Thread.Sleep(20); + count++; + } + + if ((WaveOutHeaders[i]->dwFlags & Win32.WaveHdrFlags.WHDR_INQUEUE) != Win32.WaveHdrFlags.WHDR_INQUEUE) + { + if (WaveOutHeaders[i]->lpData != IntPtr.Zero) + { + Marshal.FreeHGlobal(WaveOutHeaders[i]->lpData); + WaveOutHeaders[i]->lpData = IntPtr.Zero; + } + } + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.Write(ex.Message); + } + } + + private void StartThreadPlayWaveOut() + { + if (IsThreadPlayWaveOutRunning == false) + { + ThreadPlayWaveOut = new System.Threading.Thread(new System.Threading.ThreadStart(OnThreadPlayWaveOut)); + IsThreadPlayWaveOutRunning = true; + ThreadPlayWaveOut.Name = "PlayWaveOut"; + ThreadPlayWaveOut.Priority = System.Threading.ThreadPriority.Highest; + ThreadPlayWaveOut.Start(); + } + } + + private bool OpenWaveOut() + { + if (hWaveOut == IntPtr.Zero) + { + if (IsWaveOutOpened == false) + { + Win32.WAVEFORMATEX waveFormatEx = new() + { + wFormatTag = (ushort)Win32.WaveFormatFlags.WAVE_FORMAT_PCM, + nChannels = (ushort)Channels, + nSamplesPerSec = (ushort)SamplesPerSecond, + wBitsPerSample = (ushort)BitsPerSample + }; + waveFormatEx.nBlockAlign = (ushort)((waveFormatEx.wBitsPerSample * waveFormatEx.nChannels) >> 3); + waveFormatEx.nAvgBytesPerSec = waveFormatEx.nBlockAlign * waveFormatEx.nSamplesPerSec; + + int deviceId = WinSound.GetWaveOutDeviceIdByName(WaveOutDeviceName); + Win32.MMRESULT hr = Win32.waveOutOpen(ref hWaveOut, deviceId, ref waveFormatEx, delegateWaveOutProc, 0, (int)Win32.WaveProcFlags.CALLBACK_FUNCTION); + + if (hr != Win32.MMRESULT.MMSYSERR_NOERROR) + { + IsWaveOutOpened = false; + return false; + } + + GCHandle.Alloc(hWaveOut, GCHandleType.Pinned); + } + } + + IsWaveOutOpened = true; + return true; + } + + internal bool Open(string waveOutDeviceName, int samplesPerSecond, int bitsPerSample, int channels, int bufferCount) + { + try + { + lock (Locker) + { + if (Opened == false) + { + + WaveOutDeviceName = waveOutDeviceName; + SamplesPerSecond = samplesPerSecond; + BitsPerSample = bitsPerSample; + Channels = channels; + BufferCount = Math.Max(bufferCount, 1); + + if (OpenWaveOut()) + { + if (CreateWaveOutHeaders()) + { + StartThreadPlayWaveOut(); + IsClosed = false; + return true; + } + } + } + + return false; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(string.Format("Start | {0}", ex.Message)); + return false; + } + } + + internal bool PlayData(byte[] datas, bool isBlocking) + { + try + { + if (Opened) + { + int index = GetNextFreeWaveOutHeaderIndex(); + if (index != -1) + { + IsBlocking = isBlocking; + + if (WaveOutHeaders[index]->dwBufferLength != datas.Length) + { + Marshal.FreeHGlobal(WaveOutHeaders[index]->lpData); + WaveOutHeaders[index]->lpData = Marshal.AllocHGlobal(datas.Length); + WaveOutHeaders[index]->dwBufferLength = (uint)datas.Length; + } + + WaveOutHeaders[index]->dwBufferLength = (uint)datas.Length; + WaveOutHeaders[index]->dwUser = (IntPtr)index; + Marshal.Copy(datas, 0, WaveOutHeaders[index]->lpData, datas.Length); + + IsStarted = true; + Win32.MMRESULT hr = Win32.waveOutWrite(hWaveOut, WaveOutHeaders[index], sizeof(Win32.WAVEHDR)); + if (hr == Win32.MMRESULT.MMSYSERR_NOERROR) + { + if (isBlocking) + { + AutoResetEventDataPlayed.WaitOne(); + AutoResetEventDataPlayed.Set(); + } + return true; + } + else + { + AutoResetEventDataPlayed.Set(); + return false; + } + } + else + { + System.Diagnostics.Debug.WriteLine(string.Format("No free WaveOut Buffer found | {0}", DateTime.Now.ToLongTimeString())); + return false; + } + } + else + { + return false; + } + + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(string.Format("PlayData | {0}", ex.Message)); + return false; + } + } + + internal bool Close() + { + try + { + lock (Locker) + { + if (Opened) + { + IsClosed = true; + + int count = 0; + while (Win32.waveOutReset(hWaveOut) != Win32.MMRESULT.MMSYSERR_NOERROR && count <= 100) + { + Thread.Sleep(50); + count++; + } + + FreeWaveOutHeaders(); + + count = 0; + while (Win32.waveOutClose(hWaveOut) != Win32.MMRESULT.MMSYSERR_NOERROR && count <= 100) + { + Thread.Sleep(50); + count++; + } + + IsWaveOutOpened = false; + AutoResetEventDataPlayed.Set(); + return true; + } + return false; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(string.Format("Close | {0}", ex.Message)); + return false; + } + } + + private int GetNextFreeWaveOutHeaderIndex() + { + for (int i = 0; i < WaveOutHeaders.Length; i++) + { + if (IsHeaderPrepared(*WaveOutHeaders[i]) && !IsHeaderInqueue(*WaveOutHeaders[i])) + { + return i; + } + } + return -1; + } + + private static bool IsHeaderPrepared(Win32.WAVEHDR header) + { + return (header.dwFlags & Win32.WaveHdrFlags.WHDR_PREPARED) > 0; + } + + private static bool IsHeaderInqueue(Win32.WAVEHDR header) + { + return (header.dwFlags & Win32.WaveHdrFlags.WHDR_INQUEUE) > 0; + } + + private void WaveOutProc(IntPtr hWaveOut, Win32.WOM_Messages msg, IntPtr dwInstance, Win32.WAVEHDR* pWaveHeader, IntPtr lParam) + { + try + { + switch (msg) + { + //Open + case Win32.WOM_Messages.OPEN: + break; + + //Done + case Win32.WOM_Messages.DONE: + IsStarted = true; + AutoResetEventDataPlayed.Set(); + break; + + //Close + case Win32.WOM_Messages.CLOSE: + IsStarted = false; + IsWaveOutOpened = false; + IsPaused = false; + IsClosed = true; + AutoResetEventDataPlayed.Set(); + hWaveOut = IntPtr.Zero; + break; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(string.Format("Player.cs | waveOutProc() | {0}", ex.Message)); + AutoResetEventDataPlayed.Set(); + } + } + + private void OnThreadPlayWaveOut() + { + while (Opened && !IsClosed) + { + AutoResetEventDataPlayed.WaitOne(); + + lock (Locker) + { + if (Opened && !IsClosed) + { + IsThreadPlayWaveOutRunning = true; + + if (!Playing) + { + if (IsStarted) + { + IsStarted = false; + if (PlayerStopped != null) + { + try + { + PlayerStopped(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(string.Format("Player Stopped | {0}", ex.Message)); + } + finally + { + AutoResetEventDataPlayed.Set(); + } + } + } + } + } + } + + if (IsBlocking) + { + AutoResetEventDataPlayed.Set(); + } + } + + lock (Locker) + { + IsThreadPlayWaveOutRunning = false; + } + + if (PlayerClosed != null) + { + try + { + PlayerClosed(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(string.Format("Player Closed | {0}", ex.Message)); + } + } + } + } +} diff --git a/Luski.net/Sound/Protocol.cs b/Luski.net/Sound/Protocol.cs new file mode 100755 index 0000000..2ba7550 --- /dev/null +++ b/Luski.net/Sound/Protocol.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Luski.net.Sound +{ + internal enum ProtocolTypes + { + LH + } + + internal class Protocol + { + internal Protocol(ProtocolTypes type, Encoding encoding) + { + m_ProtocolType = type; + m_Encoding = encoding; + } + + private readonly List m_DataBuffer = new(); + private const int m_MaxBufferLength = 10000; + private readonly ProtocolTypes m_ProtocolType = ProtocolTypes.LH; + private readonly Encoding m_Encoding = Encoding.Default; + internal object m_LockerReceive = new(); + + internal delegate void DelegateDataComplete(object sender, byte[] data); + internal delegate void DelegateExceptionAppeared(object sender, Exception ex); + internal event DelegateDataComplete DataComplete; + internal event DelegateExceptionAppeared ExceptionAppeared; + + internal byte[] ToBytes(byte[] data) + { + try + { + byte[] bytesLength = BitConverter.GetBytes(data.Length); + + byte[] allBytes = new byte[bytesLength.Length + data.Length]; + Array.Copy(bytesLength, allBytes, bytesLength.Length); + Array.Copy(data, 0, allBytes, bytesLength.Length, data.Length); + + return allBytes; + } + catch (Exception ex) + { + ExceptionAppeared(null, ex); + } + + return data; + } + + internal void Receive_LH(object sender, byte[] data) + { + lock (m_LockerReceive) + { + try + { + m_DataBuffer.AddRange(data); + + if (m_DataBuffer.Count > m_MaxBufferLength) + { + m_DataBuffer.Clear(); + } + + byte[] bytes = m_DataBuffer.Take(4).ToArray(); + int length = BitConverter.ToInt32(bytes.ToArray(), 0); + + if (length > m_MaxBufferLength) + { + m_DataBuffer.Clear(); + } + + while (m_DataBuffer.Count >= length + 4) + { + byte[] message = m_DataBuffer.Skip(4).Take(length).ToArray(); + + DataComplete?.Invoke(sender, message); + m_DataBuffer.RemoveRange(0, length + 4); + + if (m_DataBuffer.Count > 4) + { + bytes = m_DataBuffer.Take(4).ToArray(); + length = BitConverter.ToInt32(bytes.ToArray(), 0); + } + } + } + catch (Exception ex) + { + m_DataBuffer.Clear(); + ExceptionAppeared(null, ex); + } + } + } + } +} diff --git a/Luski.net/Sound/RTPPacket.cs b/Luski.net/Sound/RTPPacket.cs new file mode 100755 index 0000000..c446114 --- /dev/null +++ b/Luski.net/Sound/RTPPacket.cs @@ -0,0 +1,139 @@ +using System; +using System.Linq; + +namespace Luski.net.Sound +{ + internal class RTPPacket + { + internal RTPPacket() + { + + } + + internal RTPPacket(byte[] data) + { + Parse(data); + } + + internal static int MinHeaderLength = 12; + internal int HeaderLength = MinHeaderLength; + internal int Version = 0; + internal bool Padding = false; + internal bool Extension = false; + internal int CSRCCount = 0; + internal bool Marker = false; + internal int PayloadType = 0; + internal ushort SequenceNumber = 0; + internal uint Timestamp = 0; + internal uint SourceId = 0; + internal byte[]? Data; + internal ushort ExtensionHeaderId = 0; + internal ushort ExtensionLengthAsCount = 0; + internal int ExtensionLengthInBytes = 0; + + private void Parse(byte[] data) + { + if (data.Length >= MinHeaderLength) + { + Version = ValueFromByte(data[0], 6, 2); + Padding = Convert.ToBoolean(ValueFromByte(data[0], 5, 1)); + Extension = Convert.ToBoolean(ValueFromByte(data[0], 4, 1)); + CSRCCount = ValueFromByte(data[0], 0, 4); + Marker = Convert.ToBoolean(ValueFromByte(data[1], 7, 1)); + PayloadType = ValueFromByte(data[1], 0, 7); + HeaderLength = MinHeaderLength + (CSRCCount * 4); + + //Sequence Nummer + byte[] seqNum = new byte[2]; + seqNum[0] = data[3]; + seqNum[1] = data[2]; + SequenceNumber = BitConverter.ToUInt16(seqNum, 0); + + //TimeStamp + byte[] timeStmp = new byte[4]; + timeStmp[0] = data[7]; + timeStmp[1] = data[6]; + timeStmp[2] = data[5]; + timeStmp[3] = data[4]; + Timestamp = BitConverter.ToUInt32(timeStmp, 0); + + //SourceId + byte[] srcId = new byte[4]; + srcId[0] = data[8]; + srcId[1] = data[9]; + srcId[2] = data[10]; + srcId[3] = data[11]; + SourceId = BitConverter.ToUInt32(srcId, 0); + + if (Extension) + { + byte[] extHeaderId = new byte[2]; + extHeaderId[1] = data[HeaderLength + 0]; + extHeaderId[0] = data[HeaderLength + 1]; + ExtensionHeaderId = BitConverter.ToUInt16(extHeaderId, 0); + + byte[] extHeaderLength16 = new byte[2]; + extHeaderLength16[1] = data[HeaderLength + 2]; + extHeaderLength16[0] = data[HeaderLength + 3]; + ExtensionLengthAsCount = BitConverter.ToUInt16(extHeaderLength16.ToArray(), 0); + + ExtensionLengthInBytes = ExtensionLengthAsCount * 4; + HeaderLength += ExtensionLengthInBytes + 4; + } + + Data = new byte[data.Length - HeaderLength]; + Array.Copy(data, HeaderLength, Data, 0, data.Length - HeaderLength); + } + } + + private static int ValueFromByte(byte value, int startPos, int length) + { + byte mask = 0; + for (int i = 0; i < length; i++) + { + mask = (byte)(mask | 0x1 << startPos + i); + } + + byte result = (byte)((value & mask) >> startPos); + return Convert.ToInt32(result); + } + + internal byte[] ToBytes() + { + byte[] bytes = new byte[HeaderLength + Data.Length]; + + //Byte 0 + bytes[0] = (byte)(Version << 6); + bytes[0] |= (byte)(Convert.ToInt32(Padding) << 5); + bytes[0] |= (byte)(Convert.ToInt32(Extension) << 4); + bytes[0] |= (byte)(Convert.ToInt32(CSRCCount)); + + //Byte 1 + bytes[1] = (byte)(Convert.ToInt32(Marker) << 7); + bytes[1] |= (byte)(Convert.ToInt32(PayloadType)); + + //Byte 2 + 3 + byte[] bytesSequenceNumber = BitConverter.GetBytes(SequenceNumber); + bytes[2] = bytesSequenceNumber[1]; + bytes[3] = bytesSequenceNumber[0]; + + //Byte 4 bis 7 + byte[] bytesTimeStamp = BitConverter.GetBytes(Timestamp); + bytes[4] = bytesTimeStamp[3]; + bytes[5] = bytesTimeStamp[2]; + bytes[6] = bytesTimeStamp[1]; + bytes[7] = bytesTimeStamp[0]; + + //Byte 8 bis 11 + byte[] bytesSourceId = BitConverter.GetBytes(SourceId); + bytes[8] = bytesSourceId[3]; + bytes[9] = bytesSourceId[2]; + bytes[10] = bytesSourceId[1]; + bytes[11] = bytesSourceId[0]; + + Array.Copy(Data, 0, bytes, HeaderLength, Data.Length); + + return bytes; + } + } +} diff --git a/Luski.net/Sound/Recorder.cs b/Luski.net/Sound/Recorder.cs new file mode 100755 index 0000000..93a0093 --- /dev/null +++ b/Luski.net/Sound/Recorder.cs @@ -0,0 +1,340 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Luski.net.Sound +{ + internal unsafe class Recorder + { + internal Recorder() + { + delegateWaveInProc = new Win32.DelegateWaveInProc(WaveInProc); + } + + private readonly LockerClass Locker = new(); + private readonly LockerClass LockerCopy = new(); + private IntPtr hWaveIn = IntPtr.Zero; + private string WaveInDeviceName = ""; + private bool IsWaveInOpened = false; + private bool IsWaveInStarted = false; + private bool IsThreadRecordingRunning = false; + private bool IsDataIncomming = false; + private bool Stopped = false; + private int SamplesPerSecond = 8000; + private int BitsPerSample = 16; + private int Channels = 1; + private int BufferCount = 8; + private int BufferSize = 1024; + private Win32.WAVEHDR*[] WaveInHeaders; + private Win32.WAVEHDR* CurrentRecordedHeader; + private readonly Win32.DelegateWaveInProc delegateWaveInProc; + private Thread ThreadRecording; + private readonly AutoResetEvent AutoResetEventDataRecorded = new(false); + + internal delegate void DelegateStopped(); + internal delegate void DelegateDataRecorded(byte[] bytes); + internal event DelegateStopped RecordingStopped; + internal event DelegateDataRecorded DataRecorded; + + internal bool Started => IsWaveInStarted && IsWaveInOpened && IsThreadRecordingRunning; + + private bool CreateWaveInHeaders() + { + WaveInHeaders = new Win32.WAVEHDR*[BufferCount]; + int createdHeaders = 0; + + for (int i = 0; i < BufferCount; i++) + { + WaveInHeaders[i] = (Win32.WAVEHDR*)Marshal.AllocHGlobal(sizeof(Win32.WAVEHDR)); + + WaveInHeaders[i]->dwLoops = 0; + WaveInHeaders[i]->dwUser = IntPtr.Zero; + WaveInHeaders[i]->lpNext = IntPtr.Zero; + WaveInHeaders[i]->reserved = IntPtr.Zero; + WaveInHeaders[i]->lpData = Marshal.AllocHGlobal(BufferSize); + WaveInHeaders[i]->dwBufferLength = (uint)BufferSize; + WaveInHeaders[i]->dwBytesRecorded = 0; + WaveInHeaders[i]->dwFlags = 0; + + Win32.MMRESULT hr = Win32.waveInPrepareHeader(hWaveIn, WaveInHeaders[i], sizeof(Win32.WAVEHDR)); + if (hr == Win32.MMRESULT.MMSYSERR_NOERROR) + { + if (i == 0) + { + hr = Win32.waveInAddBuffer(hWaveIn, WaveInHeaders[i], sizeof(Win32.WAVEHDR)); + } + createdHeaders++; + } + } + + return (createdHeaders == BufferCount); + } + + private void FreeWaveInHeaders() + { + try + { + if (WaveInHeaders != null) + { + for (int i = 0; i < WaveInHeaders.Length; i++) + { + Win32.MMRESULT hr = Win32.waveInUnprepareHeader(hWaveIn, WaveInHeaders[i], sizeof(Win32.WAVEHDR)); + + int count = 0; + while (count <= 100 && (WaveInHeaders[i]->dwFlags & Win32.WaveHdrFlags.WHDR_INQUEUE) == Win32.WaveHdrFlags.WHDR_INQUEUE) + { + Thread.Sleep(20); + count++; + } + + if ((WaveInHeaders[i]->dwFlags & Win32.WaveHdrFlags.WHDR_INQUEUE) != Win32.WaveHdrFlags.WHDR_INQUEUE) + { + if (WaveInHeaders[i]->lpData != IntPtr.Zero) + { + Marshal.FreeHGlobal(WaveInHeaders[i]->lpData); + WaveInHeaders[i]->lpData = IntPtr.Zero; + } + } + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.Write(ex.Message); + } + } + + private void StartThreadRecording() + { + if (Started == false) + { + ThreadRecording = new Thread(new ThreadStart(OnThreadRecording)); + IsThreadRecordingRunning = true; + ThreadRecording.Name = "Recording"; + ThreadRecording.Priority = ThreadPriority.Highest; + ThreadRecording.Start(); + } + } + + private bool OpenWaveIn() + { + if (hWaveIn == IntPtr.Zero) + { + if (IsWaveInOpened == false) + { + Win32.WAVEFORMATEX waveFormatEx = new() + { + wFormatTag = (ushort)Win32.WaveFormatFlags.WAVE_FORMAT_PCM, + nChannels = (ushort)Channels, + nSamplesPerSec = (ushort)SamplesPerSecond, + wBitsPerSample = (ushort)BitsPerSample + }; + waveFormatEx.nBlockAlign = (ushort)((waveFormatEx.wBitsPerSample * waveFormatEx.nChannels) >> 3); + waveFormatEx.nAvgBytesPerSec = waveFormatEx.nBlockAlign * waveFormatEx.nSamplesPerSec; + + int deviceId = WinSound.GetWaveInDeviceIdByName(WaveInDeviceName); + Win32.MMRESULT hr = Win32.waveInOpen(ref hWaveIn, deviceId, ref waveFormatEx, delegateWaveInProc, 0, (int)Win32.WaveProcFlags.CALLBACK_FUNCTION); + + if (hWaveIn == IntPtr.Zero) + { + IsWaveInOpened = false; + return false; + } + + GCHandle.Alloc(hWaveIn, GCHandleType.Pinned); + } + } + + IsWaveInOpened = true; + return true; + } + + internal bool Start(string waveInDeviceName, int samplesPerSecond, int bitsPerSample, int channels, int bufferCount, int bufferSize) + { + try + { + lock (Locker) + { + if (Started == false) + { + WaveInDeviceName = waveInDeviceName; + SamplesPerSecond = samplesPerSecond; + BitsPerSample = bitsPerSample; + Channels = channels; + BufferCount = bufferCount; + BufferSize = bufferSize; + + if (OpenWaveIn()) + { + if (CreateWaveInHeaders()) + { + Win32.MMRESULT hr = Win32.waveInStart(hWaveIn); + if (hr == Win32.MMRESULT.MMSYSERR_NOERROR) + { + IsWaveInStarted = true; + StartThreadRecording(); + Stopped = false; + return true; + } + else + { + return false; + } + } + } + } + + return false; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(string.Format("Start | {0}", ex.Message)); + return false; + } + } + + internal bool Stop() + { + try + { + lock (Locker) + { + if (Started) + { + Stopped = true; + IsThreadRecordingRunning = false; + + CloseWaveIn(); + + AutoResetEventDataRecorded.Set(); + return true; + } + return false; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(string.Format("Stop | {0}", ex.Message)); + return false; + } + } + + private void CloseWaveIn() + { + Win32.MMRESULT hr = Win32.waveInStop(hWaveIn); + + int resetCount = 0; + while (IsAnyWaveInHeaderInState(Win32.WaveHdrFlags.WHDR_INQUEUE) & resetCount < 20) + { + hr = Win32.waveInReset(hWaveIn); + Thread.Sleep(50); + resetCount++; + } + + FreeWaveInHeaders(); + hr = Win32.waveInClose(hWaveIn); + } + + private bool IsAnyWaveInHeaderInState(Win32.WaveHdrFlags state) + { + for (int i = 0; i < WaveInHeaders.Length; i++) + { + if ((WaveInHeaders[i]->dwFlags & state) == state) + { + return true; + } + } + return false; + } + + private void WaveInProc(IntPtr hWaveIn, Win32.WIM_Messages msg, IntPtr dwInstance, Win32.WAVEHDR* pWaveHdr, IntPtr lParam) + { + switch (msg) + { + //Open + case Win32.WIM_Messages.OPEN: + break; + + //Data + case Win32.WIM_Messages.DATA: + IsDataIncomming = true; + CurrentRecordedHeader = pWaveHdr; + AutoResetEventDataRecorded.Set(); + break; + + //Close + case Win32.WIM_Messages.CLOSE: + IsDataIncomming = false; + IsWaveInOpened = false; + AutoResetEventDataRecorded.Set(); + this.hWaveIn = IntPtr.Zero; + break; + } + } + + private void OnThreadRecording() + { + while (Started && !Stopped) + { + AutoResetEventDataRecorded.WaitOne(); + + try + { + if (Started && !Stopped) + { + if (CurrentRecordedHeader->dwBytesRecorded > 0) + { + if (DataRecorded != null && IsDataIncomming) + { + try + { + byte[] bytes = new byte[CurrentRecordedHeader->dwBytesRecorded]; + Marshal.Copy(CurrentRecordedHeader->lpData, bytes, 0, (int)CurrentRecordedHeader->dwBytesRecorded); + + DataRecorded(bytes); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(string.Format("Recorder.cs | OnThreadRecording() | {0}", ex.Message)); + } + } + + for (int i = 0; i < WaveInHeaders.Length; i++) + { + if ((WaveInHeaders[i]->dwFlags & Win32.WaveHdrFlags.WHDR_INQUEUE) == 0) + { + Win32.MMRESULT hr = Win32.waveInAddBuffer(hWaveIn, WaveInHeaders[i], sizeof(Win32.WAVEHDR)); + } + } + + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex.Message); + } + } + + + lock (Locker) + { + IsWaveInStarted = false; + IsThreadRecordingRunning = false; + } + + if (RecordingStopped != null) + { + try + { + RecordingStopped(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(string.Format("Recording Stopped | {0}", ex.Message)); + } + } + } + } +} diff --git a/Luski.net/Sound/Timer.cs b/Luski.net/Sound/Timer.cs new file mode 100755 index 0000000..3884ad9 --- /dev/null +++ b/Luski.net/Sound/Timer.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.InteropServices; + +namespace Luski.net.Sound +{ + internal class EventTimer + { + internal EventTimer() + { + m_DelegateTimeEvent = new Win32.TimerEventHandler(OnTimer); + } + + private bool m_IsRunning = false; + private uint m_Milliseconds = 20; + private uint m_TimerId = 0; + private GCHandle m_GCHandleTimer; + private uint m_UserData = 0; + private uint m_ResolutionInMilliseconds = 0; + + private readonly Win32.TimerEventHandler m_DelegateTimeEvent; + internal delegate void DelegateTimerTick(); + internal event DelegateTimerTick? TimerTick; + + internal void Start(uint milliseconds) + { + m_Milliseconds = milliseconds; + + Win32.TimeCaps tc = new(); + Win32.TimeGetDevCaps(ref tc, (uint)Marshal.SizeOf(typeof(Win32.TimeCaps))); + m_ResolutionInMilliseconds = Math.Max(tc.wPeriodMin, 0); + + Win32.TimeBeginPeriod(m_ResolutionInMilliseconds); + + m_TimerId = Win32.TimeSetEvent(m_Milliseconds, m_ResolutionInMilliseconds, m_DelegateTimeEvent, ref m_UserData, Win32.TIME_PERIODIC); + if (m_TimerId > 0) + { + m_GCHandleTimer = GCHandle.Alloc(m_TimerId, GCHandleType.Pinned); + m_IsRunning = true; + } + } + + internal void Stop() + { + if (m_TimerId > 0) + { + _ = Win32.TimeKillEvent(m_TimerId); + Win32.TimeEndPeriod(m_ResolutionInMilliseconds); + + if (m_GCHandleTimer.IsAllocated) + { + m_GCHandleTimer.Free(); + } + + m_TimerId = 0; + m_IsRunning = false; + } + } + + private void OnTimer(uint id, uint msg, ref uint userCtx, uint rsv1, uint rsv2) + { + TimerTick?.Invoke(); + } + } +} diff --git a/Luski.net/Sound/Utils.cs b/Luski.net/Sound/Utils.cs new file mode 100755 index 0000000..2ccb7ae --- /dev/null +++ b/Luski.net/Sound/Utils.cs @@ -0,0 +1,195 @@ +using System; + +namespace Luski.net.Sound +{ + internal class Utils + { + internal Utils() + { + + } + + private const int SIGN_BIT = 0x80; + private const int QUANT_MASK = 0xf; + private const int SEG_SHIFT = 4; + private const int SEG_MASK = 0x70; + private const int BIAS = 0x84; + private const int CLIP = 8159; + private static readonly short[] seg_uend = new short[] { 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF }; + + internal static int GetBytesPerInterval(uint SamplesPerSecond, int BitsPerSample, int Channels) + { + int blockAlign = ((BitsPerSample * Channels) >> 3); + int bytesPerSec = (int)(blockAlign * SamplesPerSecond); + uint sleepIntervalFactor = 1000 / 20; + int bytesPerInterval = (int)(bytesPerSec / sleepIntervalFactor); + + return bytesPerInterval; + } + + internal static int MulawToLinear(int ulaw) + { + ulaw = ~ulaw; + int t = ((ulaw & QUANT_MASK) << 3) + BIAS; + t <<= (ulaw & SEG_MASK) >> SEG_SHIFT; + return ((ulaw & SIGN_BIT) > 0 ? (BIAS - t) : (t - BIAS)); + } + + private static short Search(short val, short[] table, short size) + { + short i; + int index = 0; + for (i = 0; i < size; i++) + { + if (val <= table[index]) + { + return (i); + } + index++; + } + return (size); + } + + internal static byte Linear2ulaw(short pcm_val) + { + + /* Get the sign and the magnitude of the value. */ + pcm_val = (short)(pcm_val >> 2); + short mask; + if (pcm_val < 0) + { + pcm_val = (short)-pcm_val; + mask = 0x7F; + } + else + { + mask = 0xFF; + } + /* clip the magnitude */ + if (pcm_val > CLIP) + { + pcm_val = CLIP; + } + pcm_val += (BIAS >> 2); + + /* Convert the scaled magnitude to segment number. */ + short seg = Search(pcm_val, seg_uend, 8); + + /* + * Combine the sign, segment, quantization bits; + * and complement the code word. + */ + /* out of range, return maximum value. */ + if (seg >= 8) + { + return (byte)(0x7F ^ mask); + } + else + { + byte uval = (byte)((seg << 4) | ((pcm_val >> (seg + 1)) & 0xF)); + return ((byte)(uval ^ mask)); + } + } + + internal static byte[] MuLawToLinear(byte[] bytes, int bitsPerSample, int channels) + { + int blockAlign = channels * bitsPerSample / 8; + + byte[] result = new byte[bytes.Length * blockAlign]; + for (int i = 0, counter = 0; i < bytes.Length; i++, counter += blockAlign) + { + int value = MulawToLinear(bytes[i]); + byte[] values = BitConverter.GetBytes(value); + + switch (bitsPerSample) + { + case 8: + switch (channels) + { + //8 Bit 1 Channel + case 1: + result[counter] = values[0]; + break; + + //8 Bit 2 Channel + case 2: + result[counter] = values[0]; + result[counter + 1] = values[0]; + break; + } + break; + + case 16: + switch (channels) + { + //16 Bit 1 Channel + case 1: + result[counter] = values[0]; + result[counter + 1] = values[1]; + break; + + //16 Bit 2 Channels + case 2: + result[counter] = values[0]; + result[counter + 1] = values[1]; + result[counter + 2] = values[0]; + result[counter + 3] = values[1]; + break; + } + break; + } + } + + return result; + } + + internal static byte[] LinearToMulaw(byte[] bytes, int bitsPerSample, int channels) + { + int blockAlign = channels * bitsPerSample / 8; + + byte[] result = new byte[bytes.Length / blockAlign]; + int resultIndex = 0; + for (int i = 0; i < result.Length; i++) + { + switch (bitsPerSample) + { + case 8: + switch (channels) + { + //8 Bit 1 Channel + case 1: + result[i] = Linear2ulaw(bytes[resultIndex]); + resultIndex += 1; + break; + + //8 Bit 2 Channel + case 2: + result[i] = Linear2ulaw(bytes[resultIndex]); + resultIndex += 2; + break; + } + break; + + case 16: + switch (channels) + { + //16 Bit 1 Channel + case 1: + result[i] = Linear2ulaw(BitConverter.ToInt16(bytes, resultIndex)); + resultIndex += 2; + break; + + //16 Bit 2 Channels + case 2: + result[i] = Linear2ulaw(BitConverter.ToInt16(bytes, resultIndex)); + resultIndex += 4; + break; + } + break; + } + } + + return result; + } + } +} diff --git a/Luski.net/Sound/WaveFile.cs b/Luski.net/Sound/WaveFile.cs new file mode 100755 index 0000000..398c5e3 --- /dev/null +++ b/Luski.net/Sound/WaveFile.cs @@ -0,0 +1,104 @@ +using System; +using System.IO; +using System.Text; + +namespace Luski.net.Sound +{ + internal class WaveFile + { + internal WaveFile() + { + + } + + internal const int WAVE_FORMAT_PCM = 1; + + + internal static WaveFileHeader Read(string fileName) + { + WaveFileHeader header = ReadHeader(fileName); + + return header; + } + + private static WaveFileHeader ReadHeader(string fileName) + { + WaveFileHeader header = new(); + + if (File.Exists(fileName)) + { + FileStream fs = new(fileName, FileMode.Open, FileAccess.Read); + BinaryReader rd = new(fs, Encoding.UTF8); + + if (fs.CanRead) + { + header.RIFF = rd.ReadChars(4); + header.RiffSize = (uint)rd.ReadInt32(); + header.RiffFormat = rd.ReadChars(4); + + header.FMT = rd.ReadChars(4); + header.FMTSize = (uint)rd.ReadInt32(); + header.FMTPos = fs.Position; + header.AudioFormat = rd.ReadInt16(); + header.Channels = rd.ReadInt16(); + header.SamplesPerSecond = (uint)rd.ReadInt32(); + header.BytesPerSecond = (uint)rd.ReadInt32(); + header.BlockAlign = rd.ReadInt16(); + header.BitsPerSample = rd.ReadInt16(); + + fs.Seek(header.FMTPos + header.FMTSize, SeekOrigin.Begin); + + header.DATA = rd.ReadChars(4); + header.DATASize = (uint)rd.ReadInt32(); + header.DATAPos = (int)fs.Position; + + if (new string(header.DATA).ToUpper() != "DATA") + { + uint DataChunkSize = header.DATASize + 8; + fs.Seek(DataChunkSize, SeekOrigin.Current); + header.DATASize = (uint)(fs.Length - header.DATAPos - DataChunkSize); + } + + if (header.DATASize <= fs.Length - header.DATAPos) + { + header.Payload = rd.ReadBytes((int)header.DATASize); + } + } + + rd.Close(); + fs.Close(); + } + + return header; + } + } + + internal class WaveFileHeader + { + internal WaveFileHeader() + { + + } + + internal char[] RIFF = new char[4]; + internal uint RiffSize = 8; + internal char[] RiffFormat = new char[4]; + + internal char[] FMT = new char[4]; + internal uint FMTSize = 16; + internal short AudioFormat; + internal short Channels; + internal uint SamplesPerSecond; + internal uint BytesPerSecond; + internal short BlockAlign; + internal short BitsPerSample; + + internal char[] DATA = new char[4]; + internal uint DATASize; + + internal byte[] Payload = Array.Empty(); + + internal int DATAPos = 44; + internal long FMTPos = 20; + } +} diff --git a/Luski.net/Sound/Win32.cs b/Luski.net/Sound/Win32.cs new file mode 100755 index 0000000..0275eaf --- /dev/null +++ b/Luski.net/Sound/Win32.cs @@ -0,0 +1,241 @@ +using System; +using System.Runtime.InteropServices; + +namespace Luski.net.Sound +{ + internal unsafe class Win32 + { + internal Win32() + { + + } + + internal const int WAVE_MAPPER = -1; + + internal const int WT_EXECUTEDEFAULT = 0x00000000; + internal const int WT_EXECUTEINIOTHREAD = 0x00000001; + internal const int WT_EXECUTEINTIMERTHREAD = 0x00000020; + internal const int WT_EXECUTEINPERSISTENTTHREAD = 0x00000080; + + internal const int TIME_ONESHOT = 0; + internal const int TIME_PERIODIC = 1; + + [StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Auto)] + internal struct WAVEOUTCAPS + { + internal short wMid; + internal short wPid; + internal int vDriverVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + internal string szPname; + internal uint dwFormats; + internal short wChannels; + internal short wReserved; + internal int dwSupport; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Auto)] + internal struct WAVEINCAPS + { + internal short wMid; + internal short wPid; + internal int vDriverVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + internal string szPname; + internal uint dwFormats; + internal short wChannels; + internal short wReserved; + internal int dwSupport; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct WAVEFORMATEX + { + internal ushort wFormatTag; + internal ushort nChannels; + internal uint nSamplesPerSec; + internal uint nAvgBytesPerSec; + internal ushort nBlockAlign; + internal ushort wBitsPerSample; + internal ushort cbSize; + } + + internal enum MMRESULT : uint + { + MMSYSERR_NOERROR = 0, + MMSYSERR_ERROR = 1, + MMSYSERR_BADDEVICEID = 2, + MMSYSERR_NOTENABLED = 3, + MMSYSERR_ALLOCATED = 4, + MMSYSERR_INVALHANDLE = 5, + MMSYSERR_NODRIVER = 6, + MMSYSERR_NOMEM = 7, + MMSYSERR_NOTSUPPORTED = 8, + MMSYSERR_BADERRNUM = 9, + MMSYSERR_INVALFLAG = 10, + MMSYSERR_INVALPARAM = 11, + MMSYSERR_HANDLEBUSY = 12, + MMSYSERR_INVALIDALIAS = 13, + MMSYSERR_BADDB = 14, + MMSYSERR_KEYNOTFOUND = 15, + MMSYSERR_READERROR = 16, + MMSYSERR_WRITEERROR = 17, + MMSYSERR_DELETEERROR = 18, + MMSYSERR_VALNOTFOUND = 19, + MMSYSERR_NODRIVERCB = 20, + WAVERR_BADFORMAT = 32, + WAVERR_STILLPLAYING = 33, + WAVERR_UNPREPARED = 34 + } + + [Flags] + internal enum WaveHdrFlags : uint + { + WHDR_DONE = 1, + WHDR_PREPARED = 2, + WHDR_BEGINLOOP = 4, + WHDR_ENDLOOP = 8, + WHDR_INQUEUE = 16 + } + + [Flags] + internal enum WaveProcFlags : int + { + CALLBACK_NULL = 0, + CALLBACK_FUNCTION = 0x30000, + CALLBACK_EVENT = 0x50000, + CALLBACK_WINDOW = 0x10000, + CALLBACK_THREAD = 0x20000, + WAVE_FORMAT_QUERY = 1, + WAVE_MAPPED = 4, + WAVE_FORMAT_DIRECT = 8 + } + + [Flags] + internal enum HRESULT : long + { + S_OK = 0L, + S_FALSE = 1L + } + + [Flags] + internal enum WaveFormatFlags : int + { + WAVE_FORMAT_PCM = 0x0001 + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + internal struct WAVEHDR + { + internal IntPtr lpData; + internal uint dwBufferLength; + internal uint dwBytesRecorded; + internal IntPtr dwUser; + internal WaveHdrFlags dwFlags; + internal uint dwLoops; + internal IntPtr lpNext; + internal IntPtr reserved; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct TimeCaps + { + internal uint wPeriodMin; + internal uint wPeriodMax; + }; + + internal enum WOM_Messages : int + { + OPEN = 0x03BB, + CLOSE = 0x03BC, + DONE = 0x03BD + } + + internal enum WIM_Messages : int + { + OPEN = 0x03BE, + CLOSE = 0x03BF, + DATA = 0x03C0 + } + + internal delegate void DelegateWaveOutProc(IntPtr hWaveOut, WOM_Messages msg, IntPtr dwInstance, WAVEHDR* pWaveHdr, IntPtr lParam); + internal delegate void DelegateWaveInProc(IntPtr hWaveIn, WIM_Messages msg, IntPtr dwInstance, WAVEHDR* pWaveHdr, IntPtr lParam); + internal delegate void DelegateTimerProc(IntPtr lpParameter, bool TimerOrWaitFired); + internal delegate void TimerEventHandler(uint id, uint msg, ref uint userCtx, uint rsv1, uint rsv2); + + [DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeSetEvent")] + internal static extern uint TimeSetEvent(uint msDelay, uint msResolution, TimerEventHandler handler, ref uint userCtx, uint eventType); + + [DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeKillEvent")] + internal static extern uint TimeKillEvent(uint timerId); + + [DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeGetDevCaps")] + internal static extern MMRESULT TimeGetDevCaps(ref TimeCaps timeCaps, uint sizeTimeCaps); + + [DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeBeginPeriod")] + internal static extern MMRESULT TimeBeginPeriod(uint uPeriod); + + [DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeEndPeriod")] + internal static extern MMRESULT TimeEndPeriod(uint uPeriod); + + [DllImport("winmm.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern MMRESULT waveOutOpen(ref IntPtr hWaveOut, int uDeviceID, ref WAVEFORMATEX lpFormat, DelegateWaveOutProc dwCallBack, int dwInstance, int dwFlags); + + [DllImport("winmm.dll")] + internal static extern MMRESULT waveInOpen(ref IntPtr hWaveIn, int deviceId, ref WAVEFORMATEX wfx, DelegateWaveInProc dwCallBack, int dwInstance, int dwFlags); + + [DllImport("winmm.dll", SetLastError = true)] + internal static extern MMRESULT waveInStart(IntPtr hWaveIn); + + [DllImport("winmm.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern uint waveInGetDevCaps(int index, ref WAVEINCAPS pwic, int cbwic); + + [DllImport("winmm.dll", SetLastError = true)] + internal static extern uint waveInGetNumDevs(); + + [DllImport("winmm.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern uint waveOutGetDevCaps(int index, ref WAVEOUTCAPS pwoc, int cbwoc); + + [DllImport("winmm.dll", SetLastError = true)] + internal static extern uint waveOutGetNumDevs(); + + [DllImport("winmm.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern MMRESULT waveOutWrite(IntPtr hWaveOut, WAVEHDR* pwh, int cbwh); + + [DllImport("winmm.dll", SetLastError = true, EntryPoint = "waveOutPrepareHeader", CharSet = CharSet.Auto)] + internal static extern MMRESULT waveOutPrepareHeader(IntPtr hWaveOut, WAVEHDR* lpWaveOutHdr, int uSize); + + [DllImport("winmm.dll", SetLastError = true, EntryPoint = "waveOutUnprepareHeader", CharSet = CharSet.Auto)] + internal static extern MMRESULT waveOutUnprepareHeader(IntPtr hWaveOut, WAVEHDR* lpWaveOutHdr, int uSize); + + [DllImport("winmm.dll", EntryPoint = "waveInStop", SetLastError = true)] + internal static extern MMRESULT waveInStop(IntPtr hWaveIn); + + [DllImport("winmm.dll", EntryPoint = "waveInReset", SetLastError = true)] + internal static extern MMRESULT waveInReset(IntPtr hWaveIn); + + [DllImport("winmm.dll", EntryPoint = "waveOutReset", SetLastError = true)] + internal static extern MMRESULT waveOutReset(IntPtr hWaveOut); + + [DllImport("winmm.dll", SetLastError = true)] + internal static extern MMRESULT waveInPrepareHeader(IntPtr hWaveIn, WAVEHDR* pwh, int cbwh); + + [DllImport("winmm.dll", SetLastError = true)] + internal static extern MMRESULT waveInUnprepareHeader(IntPtr hWaveIn, WAVEHDR* pwh, int cbwh); + + [DllImport("winmm.dll", EntryPoint = "waveInAddBuffer", SetLastError = true)] + internal static extern MMRESULT waveInAddBuffer(IntPtr hWaveIn, WAVEHDR* pwh, int cbwh); + + [DllImport("winmm.dll", SetLastError = true)] + internal static extern MMRESULT waveInClose(IntPtr hWaveIn); + + [DllImport("winmm.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern MMRESULT waveOutClose(IntPtr hWaveOut); + + [DllImport("winmm.dll")] + internal static extern MMRESULT waveOutPause(IntPtr hWaveOut); + + [DllImport("winmm.dll", EntryPoint = "waveOutRestart", SetLastError = true)] + internal static extern MMRESULT waveOutRestart(IntPtr hWaveOut); + } +} diff --git a/Luski.net/Sound/WinSound.cs b/Luski.net/Sound/WinSound.cs new file mode 100755 index 0000000..c5d48ce --- /dev/null +++ b/Luski.net/Sound/WinSound.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Luski.net.Sound +{ + internal class LockerClass + { + + } + + internal class WinSound + { + internal WinSound() + { + + } + + internal static List GetPlaybackNames() + { + List list = new(); + Win32.WAVEOUTCAPS waveOutCap = new(); + + uint num = Win32.waveOutGetNumDevs(); + for (int i = 0; i < num; i++) + { + uint hr = Win32.waveOutGetDevCaps(i, ref waveOutCap, Marshal.SizeOf(typeof(Win32.WAVEOUTCAPS))); + if (hr == (int)Win32.HRESULT.S_OK) + { + list.Add(waveOutCap.szPname); + } + } + + return list; + } + + internal static List GetRecordingNames() + { + List list = new(); + Win32.WAVEINCAPS waveInCap = new(); + + uint num = Win32.waveInGetNumDevs(); + for (int i = 0; i < num; i++) + { + uint hr = Win32.waveInGetDevCaps(i, ref waveInCap, Marshal.SizeOf(typeof(Win32.WAVEINCAPS))); + if (hr == (int)Win32.HRESULT.S_OK) + { + list.Add(waveInCap.szPname); + } + } + + return list; + } + + internal static int GetWaveInDeviceIdByName(string name) + { + uint num = Win32.waveInGetNumDevs(); + + Win32.WAVEINCAPS caps = new(); + for (int i = 0; i < num; i++) + { + Win32.HRESULT hr = (Win32.HRESULT)Win32.waveInGetDevCaps(i, ref caps, Marshal.SizeOf(typeof(Win32.WAVEINCAPS))); + if (hr == Win32.HRESULT.S_OK) + { + if (caps.szPname == name) + { + return i; + } + } + } + + return Win32.WAVE_MAPPER; + } + + internal static int GetWaveOutDeviceIdByName(string name) + { + uint num = Win32.waveOutGetNumDevs(); + + Win32.WAVEOUTCAPS caps = new(); + for (int i = 0; i < num; i++) + { + Win32.HRESULT hr = (Win32.HRESULT)Win32.waveOutGetDevCaps(i, ref caps, Marshal.SizeOf(typeof(Win32.WAVEOUTCAPS))); + if (hr == Win32.HRESULT.S_OK) + { + if (caps.szPname == name) + { + return i; + } + } + } + + return Win32.WAVE_MAPPER; + } + } +}