diff --git a/SharpQuark/Lightquark.cs b/SharpQuark/Lightquark.cs index 80eabf4f6ca16a59744a236db116afe4833abf9a..e4209d7909b5b6580449e32bff1cea8f6f6da627 100644 --- a/SharpQuark/Lightquark.cs +++ b/SharpQuark/Lightquark.cs @@ -1,37 +1,86 @@ -using System.Net.Http.Headers; +using System.Diagnostics; +using System.Net.Http.Headers; +using System.Net.WebSockets; using System.Reflection; using Newtonsoft.Json; using SharpQuark.Objects; using SharpQuark.Token; +using Websocket.Client; namespace SharpQuark; public class UnsupportedVersionException(string e) : Exception(e); -public partial class Lightquark +public partial class Lightquark : IDisposable { + private static int _sid; + private readonly int _id; private readonly HttpClient _http = new(); + private static readonly string[] UnsupportedVersions = ["v1", "v2"]; private readonly string _version; - private readonly Uri _baseUri; private readonly TokenCredential _tokenCredential; private readonly string _agent; - private List<Channel> _channels; + private WebsocketClient? _client; + public readonly NetworkInformation Network; + private Uri BaseUri => new (Network.BaseUrl ?? throw new Exception("Invalid network")); + private Timer? _heartbeatTimer; - public Lightquark(TokenCredential credential, NetworkInformation networkInformation, string? agent = null, string version = "v3", bool suppressStartupMessage = false) + public Lightquark(TokenCredential credential, NetworkInformation networkInformation, string? agent = null, string version = "v3", bool suppressStartupMessage = false, bool connectWebsocket = true) { + _id = _sid; + _sid++; _tokenCredential = credential; _version = version; _agent = agent ?? $"SharpQuark {Assembly.GetExecutingAssembly().GetName().Version}"; - if (networkInformation.BaseUrl == null) throw new Exception("Invalid network"); - _baseUri = new Uri(networkInformation.BaseUrl); - _channels = new List<Channel>(); + Network = networkInformation; if (!suppressStartupMessage) { - Console.WriteLine($"Running {_agent}"); + Console.WriteLine($"[{_id}] Running {_agent}"); } + + // Gateway + if (!connectWebsocket) return; + _connectGateway(); + + } + + private void _connectGateway() + { + if (Network.Gateway == null) throw new Exception("Network information missing gateway uri when trying to connect gateway."); + var clientFactory = new Func<ClientWebSocket>(() => + { + var client = new ClientWebSocket(); + client.Options.AddSubProtocol(_tokenCredential.AccessToken.ToString()!); + return client; + }); + _client = new WebsocketClient(new Uri(Network.Gateway), clientFactory); + + // Reconnections + _client.ReconnectTimeout = TimeSpan.FromSeconds(30); + _client.ReconnectionHappened.Subscribe(info => + Debug.WriteLine($"[{_id}] Reconnection happened, type: {info.Type}")); + + _client.DisconnectionHappened.Subscribe(info => + { + Debug.WriteLine($"[{_id}] Disconnection happened, type: {info.Type}"); + if (info.Exception != null) Console.Error.WriteLine(info.Exception); + }); + + // Start + _client.MessageReceived.Subscribe(msg => GatewayMessage(msg)); + _client.StartOrFail(); + + _heartbeatTimer = new Timer( + _ => + { + SendGateway(new GatewayMessage("heartbeat", "alive")); + Debug.WriteLine($"[{_id}] Sent heartbeat message."); + }, + null, + TimeSpan.Zero, + TimeSpan.FromSeconds(15)); } - private static readonly string[] UnsupportedVersions = ["v1", "v2"]; // ReSharper disable once MemberCanBePrivate.Global public async Task<string> Call(string endpoint, string method = "GET", ApiCallOptions? options = null) { @@ -53,7 +102,7 @@ public partial class Lightquark } // Use request specific version if available - var requestUri = new Uri(_baseUri, $"{options?.Version ?? _version}/{endpoint}"); + var requestUri = new Uri(BaseUri, $"{options?.Version ?? _version}/{endpoint}"); var request = new HttpRequestMessage { @@ -86,6 +135,15 @@ public partial class Lightquark return resultString; } + + public void Dispose() + { + Debug.WriteLine($"[{_id}] Goodbye :("); + _http.Dispose(); + _client?.Dispose(); + _heartbeatTimer?.Dispose(); + GC.SuppressFinalize(this); + } } public class ApiCallOptions diff --git a/SharpQuark/Methods/Gateway.cs b/SharpQuark/Methods/Gateway.cs new file mode 100644 index 0000000000000000000000000000000000000000..1f6c704ce599fbf6b3dc570676faa245fcdf2c87 --- /dev/null +++ b/SharpQuark/Methods/Gateway.cs @@ -0,0 +1,36 @@ +using System.Diagnostics; +using Newtonsoft.Json; +using SharpQuark.Objects; +using Websocket.Client; + +namespace SharpQuark; + +public partial class Lightquark +{ + private void GatewayMessage(ResponseMessage message) + { + Task.Run(() => + { + if (message.Text == null) return; + var baseMessage = JsonConvert.DeserializeObject<GatewayEventBase>(message.Text); + if (baseMessage == null) throw new Exception($"Invalid event from gateway {message.Text}"); + + switch (baseMessage.Event) + { + case "heartbeat": + Debug.WriteLine("Heartbeat received"); + return; + default: + Console.WriteLine($"Unimplemented event received {baseMessage.Event}"); + break; + } + }); + } + + private void SendGateway(GatewayMessage message) + { + if (_client == null) _connectGateway(); + _client!.Send(JsonConvert.SerializeObject(message)); + } + +} \ No newline at end of file diff --git a/SharpQuark/Methods/User.cs b/SharpQuark/Methods/User.cs index aecbf88f386facad5d5488cc884e02b815cd55db..dbce1823269a92694f566588de44f437bc88f986 100644 --- a/SharpQuark/Methods/User.cs +++ b/SharpQuark/Methods/User.cs @@ -1,13 +1,20 @@ using Newtonsoft.Json; using SharpQuark.ApiResult; +using SharpQuark.Objects; using SharpQuark.Objects.Id; namespace SharpQuark; public partial class Lightquark { + public async Task<User> UserMe() + { + var user = await UserMeRaw(); + return user.Response.User; + } + // /user/me - public async Task<UserMeApiResult> UserMe() + public async Task<UserMeApiResult> UserMeRaw() { var rawApiResult = await Call("/user/me"); @@ -16,8 +23,14 @@ public partial class Lightquark return parsedApiResult ?? throw new Exception("/user/me API Result is null"); } + public async Task<User> UserById(UserId userId) + { + var user = await UserByIdRaw(userId); + return user.Response.User; + } + // /user/:userId - public async Task<UserApiResult> UserById(UserId userId) + public async Task<UserApiResult> UserByIdRaw(UserId userId) { var rawApiResult = await Call($"/user/{userId}"); diff --git a/SharpQuark/Objects/GatewayMessage.cs b/SharpQuark/Objects/GatewayMessage.cs new file mode 100644 index 0000000000000000000000000000000000000000..5d1e9941b4f605bad5009c4c777d77f286fd1693 --- /dev/null +++ b/SharpQuark/Objects/GatewayMessage.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace SharpQuark.Objects; + +public struct GatewayMessage(string @event, string message, string? state = null) +{ + [JsonProperty("event")] public string Event = @event; + [JsonProperty("message")] public string Message = message; + [JsonProperty("state")] public string? State = state; +} + +public class GatewayEventBase +{ + [JsonProperty("eventId")] public required string Event; +} \ No newline at end of file diff --git a/SharpQuark/SharpQuark.csproj b/SharpQuark/SharpQuark.csproj index 334ae98f6ab1c955690b9de4ce69f029943f2bc6..3f11d464f52e30c04d681b6a8407bd1d3a1432ff 100644 --- a/SharpQuark/SharpQuark.csproj +++ b/SharpQuark/SharpQuark.csproj @@ -10,6 +10,7 @@ <ItemGroup> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> + <PackageReference Include="Websocket.Client" Version="5.1.1" /> </ItemGroup> </Project> diff --git a/SharpQuark/Token/TokenCredential.cs b/SharpQuark/Token/TokenCredential.cs index b8a707548ab7f427eef57f252184c0e1d6c497e7..e7bd788e7ca43f83a1490530c876d9be39332bec 100644 --- a/SharpQuark/Token/TokenCredential.cs +++ b/SharpQuark/Token/TokenCredential.cs @@ -1,4 +1,6 @@ -namespace SharpQuark.Token; +using SharpQuark.ApiResult; + +namespace SharpQuark.Token; public class TokenCredential(AccessToken accessToken, RefreshToken refreshToken) { @@ -24,18 +26,27 @@ public class TokenCredential(AccessToken accessToken, RefreshToken refreshToken) public static async Task<TokenCredential> Login(string email, string password, NetworkInformation networkInformation) { // Create temporary Lightquark instance - var tempLq = new Lightquark(new TokenCredential(new AccessToken(), new RefreshToken()), networkInformation, null, "v3", true); - var res = await tempLq.AuthToken(email, password); + AuthTokenApiResult res; + using (var tempLq = new Lightquark(new TokenCredential(new AccessToken(), new RefreshToken()), networkInformation, null, "v3", true, false)) + { + res = await tempLq.AuthToken(email, password); + } + var accessToken = (AccessToken)Token.From(res.Response.AccessToken); var refreshToken = (RefreshToken)Token.From(res.Response.RefreshToken); + return new TokenCredential(accessToken, refreshToken); } public static async Task<TokenCredential> Register(string email, string password, string username, NetworkInformation networkInformation) { // Create temporary Lightquark instance - var tempLq = new Lightquark(new TokenCredential(new AccessToken(), new RefreshToken()), networkInformation, null, "v3", true); - var res = await tempLq.AuthRegister(email, password, username); + AuthTokenApiResult res; + using (var tempLq = new Lightquark(new TokenCredential(new AccessToken(), new RefreshToken()), networkInformation, null, "v3", true)) + { + res = await tempLq.AuthRegister(email, password, username); + } + var accessToken = (AccessToken)Token.From(res.Response.AccessToken); var refreshToken = (RefreshToken)Token.From(res.Response.RefreshToken); return new TokenCredential(accessToken, refreshToken); diff --git a/readme.md b/readme.md index f8760f7ca1dbd988952baa264eca473abff3d0a2..8e39ec4e0dad512b86d213ffcf8be5dc1d05152c 100644 --- a/readme.md +++ b/readme.md @@ -26,11 +26,11 @@ var lq = new Lightquark(tokens, netInfo); // To get data about the logged in user var userMe = await lq.UserMe(); -Console.WriteLine(userMe.Response.User.Username); // Test_User +Console.WriteLine(userMe.Username); // Test_User // To get data about a specific user -var userById = await lq.UserById("62b3515989cdb45c9e06e010"); -Console.WriteLine($"{userById.Response.User.Status?.Type} {userById.Response.User.Status?.Content}"); // Playing Stardew Valley +var userById = await lq.UserById(new UserId("62b3515989cdb45c9e06e010")); +Console.WriteLine($"{userById.Status?.Type} {userById.Status?.Content}"); // Playing Stardew Valley // Get a channel var speakySpeaky = await lq.ChannelById(new ChannelId("643aa2e550c913775aec2057")); @@ -43,5 +43,4 @@ Console.WriteLine(speakySpeaky.Messages.Last().Content); var speakySpeaky2 = await lq.ChannelById(new ChannelId("643aa2e550c913775aec2057")); Console.WriteLine(speakySpeaky2.Messages.Last().Timestamp); - ``` \ No newline at end of file