From 24bcf3b73fccdc173b126af077f7295a09f69f90 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Sun, 5 Jan 2020 17:45:57 -0500 Subject: [PATCH 01/10] meta: destroy repo --- LICENSE | 2 +- README.md | 41 ++--------------------------------------- 2 files changed, 3 insertions(+), 40 deletions(-) diff --git a/LICENSE b/LICENSE index 3f78126e54..e8427ec2c0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015-2017 Discord.Net Contributors +Copyright (c) 2015-2020 Discord.Net Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7dc8cd7886..32ce5107f3 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,3 @@ -# Discord.Net -[![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) -[![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) -[![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) -[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR) +# Discord.Net 2020 -An unofficial .NET API Wrapper for the Discord client (http://discordapp.com). - -Check out the [documentation](https://discord.foxbot.me/docs/) or join the [Discord API Chat](https://discord.gg/jkrBmQR). - -## Installation -### Stable (NuGet) -Our stable builds available from NuGet through the Discord.Net metapackage: -- [Discord.Net](https://www.nuget.org/packages/Discord.Net/) - -The individual components may also be installed from NuGet: -- [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) -- [Discord.Net.Rest](https://www.nuget.org/packages/Discord.Net.Rest/) -- [Discord.Net.WebSocket](https://www.nuget.org/packages/Discord.Net.WebSocket/) -- [Discord.Net.Webhook](https://www.nuget.org/packages/Discord.Net.Webhook/) - -### Unstable (MyGet) -Nightly builds are available through our MyGet feed (`https://www.myget.org/F/discord-net/api/v3/index.json`). - -## Compiling -In order to compile Discord.Net, you require the following: - -### Using Visual Studio -- [Visual Studio 2017](https://www.microsoft.com/net/core#windowsvs2017) -- [.NET Core SDK](https://www.microsoft.com/net/download/core) - -The .NET Core workload must be selected during Visual Studio installation. - -### Using Command Line -- [.NET Core SDK](https://www.microsoft.com/net/download/core) - -## Known Issues - -### WebSockets (Win7 and earlier) -.NET Core 1.1 does not support WebSockets on Win7 and earlier. This issue has been fixed since the release of .NET Core 2.1. It is recommended to target .NET Core 2.1 or above for your project if you wish to run your bot on legacy platforms; alternatively, you may choose to install the [Discord.Net.Providers.WS4Net](https://www.nuget.org/packages/Discord.Net.Providers.WS4Net/) package. +Rewrite branch, work in progress. From 523da0be75d79646d665e26e5161828c90c95829 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Sun, 5 Jan 2020 18:11:14 -0500 Subject: [PATCH 02/10] meta: add project structure --- Discord.Net.sln | 39 ++++++++++++++++++++++++++++++ doc/.gitkeep | 0 sample/.gitkeep | 0 src/Discord.Net/Class1.cs | 8 ++++++ src/Discord.Net/Discord.Net.csproj | 7 ++++++ test/.gitkeep | 0 6 files changed, 54 insertions(+) create mode 100644 Discord.Net.sln create mode 100644 doc/.gitkeep create mode 100644 sample/.gitkeep create mode 100644 src/Discord.Net/Class1.cs create mode 100644 src/Discord.Net/Discord.Net.csproj create mode 100644 test/.gitkeep diff --git a/Discord.Net.sln b/Discord.Net.sln new file mode 100644 index 0000000000..6b31e4ec4b --- /dev/null +++ b/Discord.Net.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5DAC796B-0B77-4F84-B790-83DB78C6DFFE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net", "src\Discord.Net\Discord.Net.csproj", "{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Debug|x64.ActiveCfg = Debug|Any CPU + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Debug|x64.Build.0 = Debug|Any CPU + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Debug|x86.ActiveCfg = Debug|Any CPU + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Debug|x86.Build.0 = Debug|Any CPU + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|Any CPU.Build.0 = Release|Any CPU + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|x64.ActiveCfg = Release|Any CPU + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|x64.Build.0 = Release|Any CPU + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|x86.ActiveCfg = Release|Any CPU + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370} = {5DAC796B-0B77-4F84-B790-83DB78C6DFFE} + EndGlobalSection +EndGlobal diff --git a/doc/.gitkeep b/doc/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sample/.gitkeep b/sample/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Discord.Net/Class1.cs b/src/Discord.Net/Class1.cs new file mode 100644 index 0000000000..9a50619c74 --- /dev/null +++ b/src/Discord.Net/Class1.cs @@ -0,0 +1,8 @@ +using System; + +namespace Discord.Net +{ + public class Class1 + { + } +} diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj new file mode 100644 index 0000000000..d4c395e8cb --- /dev/null +++ b/src/Discord.Net/Discord.Net.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.1 + + + diff --git a/test/.gitkeep b/test/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 From f7c212d3c02857f412cd54411c55a72fb70aa3a6 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Sun, 5 Jan 2020 19:30:38 -0500 Subject: [PATCH 03/10] net: design socket incomplete, still needs receive handling gateway is yet to be designed --- src/Discord.Net/Class1.cs | 8 - src/Discord.Net/Discord.Net.csproj | 2 + src/Discord.Net/Socket/Gateway.cs | 32 +++ src/Discord.Net/Socket/ISocket.cs | 35 ++++ .../Socket/Providers/DefaultSocket.cs | 194 ++++++++++++++++++ 5 files changed, 263 insertions(+), 8 deletions(-) delete mode 100644 src/Discord.Net/Class1.cs create mode 100644 src/Discord.Net/Socket/Gateway.cs create mode 100644 src/Discord.Net/Socket/ISocket.cs create mode 100644 src/Discord.Net/Socket/Providers/DefaultSocket.cs diff --git a/src/Discord.Net/Class1.cs b/src/Discord.Net/Class1.cs deleted file mode 100644 index 9a50619c74..0000000000 --- a/src/Discord.Net/Class1.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Discord.Net -{ - public class Class1 - { - } -} diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj index d4c395e8cb..260238f05a 100644 --- a/src/Discord.Net/Discord.Net.csproj +++ b/src/Discord.Net/Discord.Net.csproj @@ -2,6 +2,8 @@ netstandard2.1 + 8.0 + enable diff --git a/src/Discord.Net/Socket/Gateway.cs b/src/Discord.Net/Socket/Gateway.cs new file mode 100644 index 0000000000..df4e1ab6d3 --- /dev/null +++ b/src/Discord.Net/Socket/Gateway.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Socket +{ + public class Gateway + { + static readonly Uri DefaultGatewayUri = new Uri("wss://gateway.discord.gg"); + + ISocket Socket { get; set; } + + public Gateway(SocketFactory socketFactory) + { + Socket = socketFactory(OnAborted, OnPacket); + } + + public async Task ConnectAsync(Uri? gatewayUri) + { + await Socket.ConnectAsync(gatewayUri ?? DefaultGatewayUri, CancellationToken.None).ConfigureAwait(false); + } + + public void OnAborted(Exception error) + { + // todo: log + } + public async Task OnPacket(object packet) + { + await Task.CompletedTask; + } + } +} diff --git a/src/Discord.Net/Socket/ISocket.cs b/src/Discord.Net/Socket/ISocket.cs new file mode 100644 index 0000000000..0c65e08285 --- /dev/null +++ b/src/Discord.Net/Socket/ISocket.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Socket +{ + public delegate ISocket SocketFactory(OnAbortionHandler abortionHandler, OnPacketHandler packetHandler); + + // A socket should only have one parent, so these do not need to be decoupled events. + public delegate Task OnPacketHandler(object packet); + public delegate void OnAbortionHandler(Exception error); + + public enum SocketState + { + Closed = default, + AcquiringOpenLock, + Opening, + Open, + AcquiringClosingLock, + Closing, + Aborted + } + + public interface ISocket : IDisposable + { + SocketState State { get; } + + Task ConnectAsync(Uri uri, CancellationToken token); + Task CloseAsync(int? code = null, string? reason = null); + Task SendAsync(ReadOnlyMemory payload); + + OnAbortionHandler OnAbortion { get; } + OnPacketHandler OnPacket { get; } + } +} diff --git a/src/Discord.Net/Socket/Providers/DefaultSocket.cs b/src/Discord.Net/Socket/Providers/DefaultSocket.cs new file mode 100644 index 0000000000..c4501d2a55 --- /dev/null +++ b/src/Discord.Net/Socket/Providers/DefaultSocket.cs @@ -0,0 +1,194 @@ +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Socket.Providers +{ + public static class DefaultSocketFactory + { + public static ISocket Create(OnAbortionHandler onAbortion, OnPacketHandler onPacket) + { + return new DefaultSocket(onAbortion, onPacket); + } + } + + internal class DefaultSocket : ISocket + { + public SocketState State { get; private set; } + public OnAbortionHandler OnAbortion { get; } + public OnPacketHandler OnPacket { get; } + + private ClientWebSocket _socket; + private Task? _receiveTask; + + private CancellationTokenSource _cancelTokenSource; + private SemaphoreSlim _sendLock; + private SemaphoreSlim _stateLock; + + public DefaultSocket(OnAbortionHandler onAbortion, OnPacketHandler onPacket) + { + _socket = new ClientWebSocket(); + + _cancelTokenSource = new CancellationTokenSource(); + _sendLock = new SemaphoreSlim(1); + _stateLock = new SemaphoreSlim(1); + + OnAbortion = onAbortion; + OnPacket = onPacket; + } + + public async Task ConnectAsync(Uri uri, CancellationToken connectCancelToken) + { + if (State == SocketState.Open + || State == SocketState.Opening + || State == SocketState.AcquiringOpenLock + || State == SocketState.Aborted) + { + // todo: evaluate how to handle a (redundant?) state operation + return; + } + + CancellationTokenSource openLock; // create a linked token in case the caller wants to cancel an opening connection + try + { + openLock = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, connectCancelToken); + } + catch (ObjectDisposedException e) + { + // Failed to link openLock, an expired cancellation token was passed + State = SocketState.Aborted; + OnAbortion(e); + return; + } + + State = SocketState.AcquiringOpenLock; + try + { + await _stateLock.WaitAsync(openLock.Token).ConfigureAwait(false); + } + catch (Exception e) + { + // Failed to acquire openLock + State = SocketState.Aborted; + OnAbortion(e); + } + State = SocketState.Opening; + + try + { + await _socket.ConnectAsync(uri, _cancelTokenSource.Token).ConfigureAwait(false); + } + catch (Exception e) + { + // Failed to open socket connection + State = SocketState.Aborted; + OnAbortion(e); + return; + } + State = SocketState.Open; + + // TODO: this should not be expected to fail + _stateLock.Release(); + openLock.Dispose(); + } + public async Task CloseAsync(int? code, string? reason) + { + if (State == SocketState.Closed + || State == SocketState.Closing + || State == SocketState.AcquiringClosingLock + || State == SocketState.Aborted) + { + // todo: evaluate how to handle a (redundant?) state operation; see OpenAsync + return; + } + + State = SocketState.AcquiringClosingLock; + try + { + await _stateLock.WaitAsync(); + } + catch (Exception e) + { + State = SocketState.Aborted; + OnAbortion(e); + return; + } + State = SocketState.Closing; + + // I think it is acceptable to use CancellationToken.None here, as no parallel operation should need to cancel the socket closure + await _socket.CloseAsync((WebSocketCloseStatus)(code ?? 1005), + reason ?? string.Empty, + CancellationToken.None + ).ConfigureAwait(false); + + // Wait until after .NET has been told to close the socket to cancel any pending sends/receives + // + // Presumably, sends/receives should have failed gracefully by this point, instead of aborting the underlying socket + try + { + _cancelTokenSource.Cancel(); + await (_receiveTask ?? Task.CompletedTask); + } + catch + { + // just log for now + } + + State = SocketState.Closed; + } + public async Task ReceiveAsync() + { + while (State == SocketState.Open && !_cancelTokenSource.IsCancellationRequested) + { + try + { + Memory buffer = new Memory(); + var res = await _socket.ReceiveAsync(buffer, _cancelTokenSource.Token).ConfigureAwait(false); + // todo: handle memory renting and ongoing messages + // todo: parse and OnPacket + } + catch (Exception err) + { + // log error + if (_socket.State != WebSocketState.Open) // detrimental error + { + State = SocketState.Aborted; + OnAbortion(err); + return; + } + } + } + } + public async Task SendAsync(ReadOnlyMemory data) + { + if (State != SocketState.Open) + { + // raise error? + return; + } + + await _sendLock.WaitAsync().ConfigureAwait(false); + try + { + // TODO: compression? who needs it + await _socket.SendAsync(data, WebSocketMessageType.Text, true, _cancelTokenSource.Token).ConfigureAwait(false); + } + finally + { + _sendLock.Release(); + } + } + + public void Dispose() + { + if (State != SocketState.Closed) + { + // log error? can this still proceed... + } + _socket.Dispose(); + _cancelTokenSource.Dispose(); + _stateLock.Dispose(); + } + } +} From 97ba033c1e040655716b28d25bc4a92cda0cf5d3 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Sun, 5 Jan 2020 21:38:39 -0500 Subject: [PATCH 04/10] api: design rest and core --- TODO | 32 ++++++++++++ src/Discord.Net/Discord.Net.csproj | 1 + src/Discord.Net/DiscordClient.cs | 25 +++++++++ src/Discord.Net/DiscordConfig.cs | 33 ++++++++++++ src/Discord.Net/IDiscordClient.cs | 23 ++++++++ src/Discord.Net/Rest/DiscordRestApi.cs | 28 ++++++++++ src/Discord.Net/Rest/IDiscordRestApi.cs | 12 +++++ src/Discord.Net/Rest/JsonContentSerializer.cs | 52 +++++++++++++++++++ src/Discord.Net/Rest/Models/GatewayInfo.cs | 25 +++++++++ src/Discord.Net/Rest/Requests/Test.cs | 0 .../{Gateway.cs => DiscordGatewayApi.cs} | 8 +-- src/Discord.Net/Socket/ISocket.cs | 2 +- .../Socket/Providers/DefaultSocket.cs | 2 +- 13 files changed, 237 insertions(+), 6 deletions(-) create mode 100644 TODO create mode 100644 src/Discord.Net/DiscordClient.cs create mode 100644 src/Discord.Net/DiscordConfig.cs create mode 100644 src/Discord.Net/IDiscordClient.cs create mode 100644 src/Discord.Net/Rest/DiscordRestApi.cs create mode 100644 src/Discord.Net/Rest/IDiscordRestApi.cs create mode 100644 src/Discord.Net/Rest/JsonContentSerializer.cs create mode 100644 src/Discord.Net/Rest/Models/GatewayInfo.cs create mode 100644 src/Discord.Net/Rest/Requests/Test.cs rename src/Discord.Net/Socket/{Gateway.cs => DiscordGatewayApi.cs} (77%) diff --git a/TODO b/TODO new file mode 100644 index 0000000000..43a38094d7 --- /dev/null +++ b/TODO @@ -0,0 +1,32 @@ +- REST + - Models + - Preconditions + - Endpoints + - Channel + - Emoji + - Guild + - Invite + - User + - Voice + - Webhook +- Gateway + - Models + - Client + - Socket + * Receive + * Compression + - Voice (long) +- Core + - CDN + - Datastore + - Entities + - Channel + - Emoji + - Guild + - User +- Tests + - Unit test Gateway stability / deadlockability? +- Extensions + - Commands + ? design - use finite's or quahu's + - Interactivity diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj index 260238f05a..806d2361b5 100644 --- a/src/Discord.Net/Discord.Net.csproj +++ b/src/Discord.Net/Discord.Net.csproj @@ -4,6 +4,7 @@ netstandard2.1 8.0 enable + Discord diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs new file mode 100644 index 0000000000..7fdd370457 --- /dev/null +++ b/src/Discord.Net/DiscordClient.cs @@ -0,0 +1,25 @@ +using Discord.Rest; +using Discord.Socket; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Discord +{ + internal class DiscordClient : IDiscordClient + { + public DiscordRestApi Rest => _restApi; + public DiscordGatewayApi Gateway => _gatewayApi; + + private readonly DiscordConfig _config; + private readonly DiscordRestApi _restApi; + private readonly DiscordGatewayApi _gatewayApi; + + public DiscordClient(DiscordConfig config, DiscordRestApi restApi, DiscordGatewayApi gatewayApi) + { + _config = config; + _restApi = restApi; + _gatewayApi = gatewayApi; + } + } +} diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs new file mode 100644 index 0000000000..2fb4de0902 --- /dev/null +++ b/src/Discord.Net/DiscordConfig.cs @@ -0,0 +1,33 @@ +using Discord.Socket; +using Discord.Socket.Providers; +using System; + +namespace Discord +{ + public class DiscordConfig + { + /// + /// Discord.Net version + /// + public const string Version = "3.0.0a0"; + /// + /// Discord.Net User-Agent + /// + public const string UserAgent = "DiscordBot (https://github.com/discord-net/Discord.Net, " + Version + ")"; + + /// + /// The default, fallback Gateway URI. This will generally be replaced by . + /// + public static readonly Uri DefaultGatewayUri = new Uri("wss://gateway.discord.gg"); + /// + /// The base URL for the Rest API. + /// + public string RestApiUrl { get; set; } = "https://discordapp.com/api/v6/"; + /// + /// The URI to use when connecting to the gateway. If specified, this will override the URI Discord instructs us to use. + /// + public Uri? GatewayUri = null; + + public SocketFactory SocketFactory { get; set; } = DefaultSocketFactory.Create; + } +} diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs new file mode 100644 index 0000000000..9137266766 --- /dev/null +++ b/src/Discord.Net/IDiscordClient.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Discord.Rest; +using Discord.Socket; + +namespace Discord +{ + internal interface IDiscordClient + { + static IDiscordClient Create(DiscordConfig config) + { + var rest = new DiscordRestApi(config); + var gateway = new DiscordGatewayApi(config); + + return new DiscordClient(config, rest, gateway); + } + + DiscordRestApi Rest { get; } + DiscordGatewayApi Gateway { get; } + } +} diff --git a/src/Discord.Net/Rest/DiscordRestApi.cs b/src/Discord.Net/Rest/DiscordRestApi.cs new file mode 100644 index 0000000000..052d4adc42 --- /dev/null +++ b/src/Discord.Net/Rest/DiscordRestApi.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using System.Threading.Tasks; +using Refit; +using Discord.Rest.Models; + +// This is essentially a reimplementation of Wumpus.Net.Rest +namespace Discord.Rest +{ + public class DiscordRestApi : IDiscordRestApi + { + private readonly IDiscordRestApi _api; + + public DiscordRestApi(DiscordConfig config) + { + var jsonOptions = new JsonSerializerOptions(); + var refitSettings = new RefitSettings + { + ContentSerializer = new JsonContentSerializer(jsonOptions), + }; + _api = RestService.For(config.RestApiUrl, refitSettings); + } + + public Task GetGatewayInfoAsync() + { + return _api.GetGatewayInfoAsync(); + } + } +} diff --git a/src/Discord.Net/Rest/IDiscordRestApi.cs b/src/Discord.Net/Rest/IDiscordRestApi.cs new file mode 100644 index 0000000000..3e58c1ce3d --- /dev/null +++ b/src/Discord.Net/Rest/IDiscordRestApi.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Refit; +using Discord.Rest.Models; + +namespace Discord.Rest +{ + public interface IDiscordRestApi + { + [Get("/gateway/bot")] + Task GetGatewayInfoAsync(); + } +} diff --git a/src/Discord.Net/Rest/JsonContentSerializer.cs b/src/Discord.Net/Rest/JsonContentSerializer.cs new file mode 100644 index 0000000000..dbbf0f0f4b --- /dev/null +++ b/src/Discord.Net/Rest/JsonContentSerializer.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Refit; + +// https://blog.martincostello.com/refit-and-system-text-json/ + +namespace Discord.Rest +{ + public class JsonContentSerializer : IContentSerializer + { + private static readonly MediaTypeHeaderValue _jsonMediaType = new MediaTypeHeaderValue("application/json") { CharSet = Encoding.UTF8.WebName }; + private readonly JsonSerializerOptions _serializerOptions; + + public JsonContentSerializer(JsonSerializerOptions serializerOptions) + { + _serializerOptions = serializerOptions; + } + + public async Task DeserializeAsync(HttpContent content) + { + using var json = await content.ReadAsStreamAsync().ConfigureAwait(false); + return await JsonSerializer.DeserializeAsync(json, _serializerOptions).ConfigureAwait(false); + } + + public async Task SerializeAsync(T data) + { + var stream = new MemoryStream(); + try + { + await JsonSerializer.SerializeAsync(stream, data, _serializerOptions).ConfigureAwait(false); + await stream.FlushAsync(); + + var content = new StreamContent(stream); + content.Headers.ContentType = _jsonMediaType; + + return content; + } + catch + { + await stream.DisposeAsync().ConfigureAwait(false); + throw; + } + } + } +} diff --git a/src/Discord.Net/Rest/Models/GatewayInfo.cs b/src/Discord.Net/Rest/Models/GatewayInfo.cs new file mode 100644 index 0000000000..e1497be06d --- /dev/null +++ b/src/Discord.Net/Rest/Models/GatewayInfo.cs @@ -0,0 +1,25 @@ +#pragma warning disable CS8618 // Uninitialized NRT expected in models +using System.Text.Json.Serialization; + +namespace Discord.Rest.Models +{ + public class GatewayInfo + { + [JsonPropertyName("url")] + public string Url { get; set; } + [JsonPropertyName("shards")] + public int Shards { get; set; } + [JsonPropertyName("session_start_limit")] + public GatewaySessionStartInfo SessionStartInfo { get; set; } + } + + public class GatewaySessionStartInfo + { + [JsonPropertyName("total")] + public int Total { get; set; } + [JsonPropertyName("remaining")] + public int Remaining { get; set; } + [JsonPropertyName("reset_after")] + public int ResetAfter { get; set; } + } +} diff --git a/src/Discord.Net/Rest/Requests/Test.cs b/src/Discord.Net/Rest/Requests/Test.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Discord.Net/Socket/Gateway.cs b/src/Discord.Net/Socket/DiscordGatewayApi.cs similarity index 77% rename from src/Discord.Net/Socket/Gateway.cs rename to src/Discord.Net/Socket/DiscordGatewayApi.cs index df4e1ab6d3..ad2305d296 100644 --- a/src/Discord.Net/Socket/Gateway.cs +++ b/src/Discord.Net/Socket/DiscordGatewayApi.cs @@ -2,17 +2,17 @@ using System.Threading; using System.Threading.Tasks; -namespace Discord.Net.Socket +namespace Discord.Socket { - public class Gateway + public class DiscordGatewayApi { static readonly Uri DefaultGatewayUri = new Uri("wss://gateway.discord.gg"); ISocket Socket { get; set; } - public Gateway(SocketFactory socketFactory) + public DiscordGatewayApi(DiscordConfig config) { - Socket = socketFactory(OnAborted, OnPacket); + Socket = config.SocketFactory(OnAborted, OnPacket); } public async Task ConnectAsync(Uri? gatewayUri) diff --git a/src/Discord.Net/Socket/ISocket.cs b/src/Discord.Net/Socket/ISocket.cs index 0c65e08285..98a0eacca8 100644 --- a/src/Discord.Net/Socket/ISocket.cs +++ b/src/Discord.Net/Socket/ISocket.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Discord.Net.Socket +namespace Discord.Socket { public delegate ISocket SocketFactory(OnAbortionHandler abortionHandler, OnPacketHandler packetHandler); diff --git a/src/Discord.Net/Socket/Providers/DefaultSocket.cs b/src/Discord.Net/Socket/Providers/DefaultSocket.cs index c4501d2a55..66fab72ce8 100644 --- a/src/Discord.Net/Socket/Providers/DefaultSocket.cs +++ b/src/Discord.Net/Socket/Providers/DefaultSocket.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Discord.Net.Socket.Providers +namespace Discord.Socket.Providers { public static class DefaultSocketFactory { From c51bb4cda3f12aa487eff05ec580ebdf80edd026 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Sun, 5 Jan 2020 22:04:49 -0500 Subject: [PATCH 05/10] net: add token support to rest client --- TODO | 5 ++++ src/Discord.Net/Discord.Net.csproj | 10 ++++++++ src/Discord.Net/DiscordClient.cs | 13 ++++------- src/Discord.Net/DiscordConfig.cs | 4 +++- src/Discord.Net/IDiscordClient.cs | 16 +++++++------ .../Rest/DiscordHttpClientHandler.cs | 23 +++++++++++++++++++ src/Discord.Net/Rest/DiscordRestApi.cs | 3 ++- src/Discord.Net/Rest/JsonContentSerializer.cs | 3 --- src/Discord.Net/Socket/DiscordGatewayApi.cs | 13 +++++++---- .../Socket/Providers/DefaultSocket.cs | 2 ++ 10 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 src/Discord.Net/Rest/DiscordHttpClientHandler.cs diff --git a/TODO b/TODO index 43a38094d7..06301253f0 100644 --- a/TODO +++ b/TODO @@ -9,10 +9,12 @@ - User - Voice - Webhook + - Ratelimiter with refit - Gateway - Models - Client - Socket + - use token * Receive * Compression - Voice (long) @@ -24,8 +26,11 @@ - Emoji - Guild - User + - Utilities + - Token Validation (port from @ChrisJ) - Tests - Unit test Gateway stability / deadlockability? + - Port ChrisJ's token validator tests - Extensions - Commands ? design - use finite's or quahu's diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj index 806d2361b5..cd23e7c45d 100644 --- a/src/Discord.Net/Discord.Net.csproj +++ b/src/Discord.Net/Discord.Net.csproj @@ -7,4 +7,14 @@ Discord + + + + + + + + + + diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 7fdd370457..130749d279 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -1,25 +1,20 @@ using Discord.Rest; using Discord.Socket; -using System; -using System.Collections.Generic; -using System.Text; namespace Discord { internal class DiscordClient : IDiscordClient { - public DiscordRestApi Rest => _restApi; - public DiscordGatewayApi Gateway => _gatewayApi; + public DiscordRestApi Rest { get; } + public DiscordGatewayApi Gateway { get; } private readonly DiscordConfig _config; - private readonly DiscordRestApi _restApi; - private readonly DiscordGatewayApi _gatewayApi; public DiscordClient(DiscordConfig config, DiscordRestApi restApi, DiscordGatewayApi gatewayApi) { _config = config; - _restApi = restApi; - _gatewayApi = gatewayApi; + Rest = restApi; + Gateway = gatewayApi; } } } diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index 2fb4de0902..db3b45efd1 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -27,7 +27,9 @@ public class DiscordConfig /// The URI to use when connecting to the gateway. If specified, this will override the URI Discord instructs us to use. /// public Uri? GatewayUri = null; - + /// + /// SocketFactory gets or sets how a WebSocket will be created. + /// public SocketFactory SocketFactory { get; set; } = DefaultSocketFactory.Create; } } diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index 9137266766..271db25d56 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; +using System.Net.Http.Headers; using Discord.Rest; using Discord.Socket; @@ -9,10 +6,15 @@ namespace Discord { internal interface IDiscordClient { - static IDiscordClient Create(DiscordConfig config) + static IDiscordClient Create(string token, DiscordConfig? config = default) { - var rest = new DiscordRestApi(config); - var gateway = new DiscordGatewayApi(config); + config = config ?? new DiscordConfig(); + + // todo: validate token + var tokenHeader = AuthenticationHeaderValue.Parse(token); + + var rest = new DiscordRestApi(config, tokenHeader); + var gateway = new DiscordGatewayApi(config, token); return new DiscordClient(config, rest, gateway); } diff --git a/src/Discord.Net/Rest/DiscordHttpClientHandler.cs b/src/Discord.Net/Rest/DiscordHttpClientHandler.cs new file mode 100644 index 0000000000..cf3d75f1cb --- /dev/null +++ b/src/Discord.Net/Rest/DiscordHttpClientHandler.cs @@ -0,0 +1,23 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + internal sealed class DiscordHttpClientHandler : HttpClientHandler + { + private readonly AuthenticationHeaderValue _token; + + public DiscordHttpClientHandler(AuthenticationHeaderValue token) + { + _token = token; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.Headers.Authorization = _token; + return base.SendAsync(request, cancellationToken); + } + } +} diff --git a/src/Discord.Net/Rest/DiscordRestApi.cs b/src/Discord.Net/Rest/DiscordRestApi.cs index 052d4adc42..24c682478d 100644 --- a/src/Discord.Net/Rest/DiscordRestApi.cs +++ b/src/Discord.Net/Rest/DiscordRestApi.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Refit; using Discord.Rest.Models; +using System.Net.Http.Headers; // This is essentially a reimplementation of Wumpus.Net.Rest namespace Discord.Rest @@ -10,7 +11,7 @@ public class DiscordRestApi : IDiscordRestApi { private readonly IDiscordRestApi _api; - public DiscordRestApi(DiscordConfig config) + public DiscordRestApi(DiscordConfig config, AuthenticationHeaderValue token) { var jsonOptions = new JsonSerializerOptions(); var refitSettings = new RefitSettings diff --git a/src/Discord.Net/Rest/JsonContentSerializer.cs b/src/Discord.Net/Rest/JsonContentSerializer.cs index dbbf0f0f4b..9c03ad59ee 100644 --- a/src/Discord.Net/Rest/JsonContentSerializer.cs +++ b/src/Discord.Net/Rest/JsonContentSerializer.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading.Tasks; using Refit; diff --git a/src/Discord.Net/Socket/DiscordGatewayApi.cs b/src/Discord.Net/Socket/DiscordGatewayApi.cs index ad2305d296..5f4744b90b 100644 --- a/src/Discord.Net/Socket/DiscordGatewayApi.cs +++ b/src/Discord.Net/Socket/DiscordGatewayApi.cs @@ -6,18 +6,23 @@ namespace Discord.Socket { public class DiscordGatewayApi { - static readonly Uri DefaultGatewayUri = new Uri("wss://gateway.discord.gg"); + private readonly DiscordConfig _config; + private readonly string _token; - ISocket Socket { get; set; } + public ISocket Socket { get; set; } - public DiscordGatewayApi(DiscordConfig config) + public DiscordGatewayApi(DiscordConfig config, string token) { + _config = config; + _token = token; + Socket = config.SocketFactory(OnAborted, OnPacket); } public async Task ConnectAsync(Uri? gatewayUri) { - await Socket.ConnectAsync(gatewayUri ?? DefaultGatewayUri, CancellationToken.None).ConfigureAwait(false); + var baseUri = _config.GatewayUri ?? (gatewayUri ?? DiscordConfig.DefaultGatewayUri); + await Socket.ConnectAsync(baseUri, CancellationToken.None).ConfigureAwait(false); } public void OnAborted(Exception error) diff --git a/src/Discord.Net/Socket/Providers/DefaultSocket.cs b/src/Discord.Net/Socket/Providers/DefaultSocket.cs index 66fab72ce8..e071444b00 100644 --- a/src/Discord.Net/Socket/Providers/DefaultSocket.cs +++ b/src/Discord.Net/Socket/Providers/DefaultSocket.cs @@ -88,6 +88,8 @@ public async Task ConnectAsync(Uri uri, CancellationToken connectCancelToken) } State = SocketState.Open; + _receiveTask = ReceiveAsync(); + // TODO: this should not be expected to fail _stateLock.Release(); openLock.Dispose(); From 0df54c6afad7251c9314d2e98a607e6be2974cff Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Sun, 5 Jan 2020 22:39:05 -0500 Subject: [PATCH 06/10] sample: add idn repl (Interactive Discord.Net) basic enough to test some stuff with fixed some bugs with disposables in the process --- .gitignore | 4 +- Discord.Net.sln | 17 ++++ sample/idn/Inspector.cs | 74 ++++++++++++++++++ sample/idn/Program.cs | 86 +++++++++++++++++++++ sample/idn/idn.csproj | 16 ++++ src/Discord.Net/Discord.Net.csproj | 1 + src/Discord.Net/DiscordClient.cs | 6 ++ src/Discord.Net/DiscordConfig.cs | 8 +- src/Discord.Net/IDiscordClient.cs | 5 +- src/Discord.Net/Rest/DiscordRestApi.cs | 15 +++- src/Discord.Net/Rest/IDiscordRestApi.cs | 1 + src/Discord.Net/Rest/Requests/Test.cs | 0 src/Discord.Net/Socket/DiscordGatewayApi.cs | 7 +- 13 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 sample/idn/Inspector.cs create mode 100644 sample/idn/Program.cs create mode 100644 sample/idn/idn.csproj delete mode 100644 src/Discord.Net/Rest/Requests/Test.cs diff --git a/.gitignore b/.gitignore index d72e0b5eac..a4e47d2647 100644 --- a/.gitignore +++ b/.gitignore @@ -206,4 +206,6 @@ docs/api/\.manifest \.idea/ # Codealike UID -codealike.json \ No newline at end of file +codealike.json + +*.ignore diff --git a/Discord.Net.sln b/Discord.Net.sln index 6b31e4ec4b..c52902b9b5 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -7,6 +7,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5DAC796B-0B7 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net", "src\Discord.Net\Discord.Net.csproj", "{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{4795640A-030C-4A9A-A9B0-20C56AF4DA3F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "idn", "sample\idn\idn.csproj", "{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,8 +36,21 @@ Global {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|x64.Build.0 = Release|Any CPU {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|x86.ActiveCfg = Release|Any CPU {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|x86.Build.0 = Release|Any CPU + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Debug|x64.ActiveCfg = Debug|Any CPU + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Debug|x64.Build.0 = Debug|Any CPU + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Debug|x86.ActiveCfg = Debug|Any CPU + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Debug|x86.Build.0 = Debug|Any CPU + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|Any CPU.Build.0 = Release|Any CPU + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x64.ActiveCfg = Release|Any CPU + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x64.Build.0 = Release|Any CPU + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x86.ActiveCfg = Release|Any CPU + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370} = {5DAC796B-0B77-4F84-B790-83DB78C6DFFE} + {5BE5DE89-53B7-4243-AEA8-FD8A6420908A} = {4795640A-030C-4A9A-A9B0-20C56AF4DA3F} EndGlobalSection EndGlobal diff --git a/sample/idn/Inspector.cs b/sample/idn/Inspector.cs new file mode 100644 index 0000000000..3806e0e797 --- /dev/null +++ b/sample/idn/Inspector.cs @@ -0,0 +1,74 @@ +using System.Collections; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace idn +{ + public static class Inspector + { + public static string Inspect(object value) + { + var builder = new StringBuilder(); + if (value != null) + { + var type = value.GetType().GetTypeInfo(); + builder.AppendLine($"[{type.Namespace}.{type.Name}]"); + builder.AppendLine($"{InspectProperty(value)}"); + + if (value is IEnumerable) + { + var items = (value as IEnumerable).Cast().ToArray(); + if (items.Length > 0) + { + builder.AppendLine(); + foreach (var item in items) + builder.AppendLine($"- {InspectProperty(item)}"); + } + } + else + { + var groups = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(x => x.GetIndexParameters().Length == 0) + .GroupBy(x => x.Name) + .OrderBy(x => x.Key) + .ToArray(); + if (groups.Length > 0) + { + builder.AppendLine(); + int pad = groups.Max(x => x.Key.Length) + 1; + foreach (var group in groups) + builder.AppendLine($"{group.Key.PadRight(pad, ' ')}{InspectProperty(group.First().GetValue(value))}"); + } + } + } + else + builder.AppendLine("null"); + return builder.ToString(); + } + + private static string InspectProperty(object obj) + { + if (obj == null) + return "null"; + + var type = obj.GetType(); + + var debuggerDisplay = type.GetProperty("DebuggerDisplay", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (debuggerDisplay != null) + return debuggerDisplay.GetValue(obj).ToString(); + + var toString = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.Name == "ToString" && x.DeclaringType != typeof(object)) + .FirstOrDefault(); + if (toString != null) + return obj.ToString(); + + var count = type.GetProperty("Count", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (count != null) + return $"[{count.GetValue(obj)} Items]"; + + return obj.ToString(); + } + } +} diff --git a/sample/idn/Program.cs b/sample/idn/Program.cs new file mode 100644 index 0000000000..4c1c2411d9 --- /dev/null +++ b/sample/idn/Program.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using Discord; + +namespace idn +{ + public class Program + { + public static readonly string[] Imports = + { + "System", + "System.Collections.Generic", + "System.Linq", + "System.Threading.Tasks", + "System.Diagnostics", + "System.IO", + "Discord", + "Discord.Rest", + "Discord.Socket", + "idn" + }; + + static async Task Main(string[] args) + { + var token = File.ReadAllText("token.ignore"); + var client = IDiscordClient.Create(token); + // client.start + + var options = ScriptOptions.Default + .AddReferences(GetAssemblies().ToArray()) + .AddImports(Imports); + + var globals = new ScriptGlobals + { + Client = client, + }; + + while (true) + { + Console.Write("> "); + string input = Console.ReadLine(); + + if (input == "quit") + { + break; + } + + object eval; + try + { + eval = await CSharpScript.EvaluateAsync(input, options, globals); + } + catch (Exception e) + { + eval = e; + } + Console.WriteLine(Inspector.Inspect(eval)); + } + + // client.Stop + client.Dispose(); + } + + static IEnumerable GetAssemblies() + { + var Assemblies = Assembly.GetEntryAssembly().GetReferencedAssemblies(); + foreach (var a in Assemblies) + { + var asm = Assembly.Load(a); + yield return asm; + } + yield return Assembly.GetEntryAssembly(); + } + + public class ScriptGlobals + { + public IDiscordClient Client { get; set; } + } + } +} diff --git a/sample/idn/idn.csproj b/sample/idn/idn.csproj new file mode 100644 index 0000000000..e39a15a286 --- /dev/null +++ b/sample/idn/idn.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj index cd23e7c45d..ddb15bd0a9 100644 --- a/src/Discord.Net/Discord.Net.csproj +++ b/src/Discord.Net/Discord.Net.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 130749d279..c2498985c5 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -16,5 +16,11 @@ public DiscordClient(DiscordConfig config, DiscordRestApi restApi, DiscordGatewa Rest = restApi; Gateway = gatewayApi; } + + public void Dispose() + { + Rest.Dispose(); + Gateway.Dispose(); + } } } diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index db3b45efd1..e8ba2407b2 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -20,9 +20,13 @@ public class DiscordConfig /// public static readonly Uri DefaultGatewayUri = new Uri("wss://gateway.discord.gg"); /// - /// The base URL for the Rest API. + /// The default REST URI. /// - public string RestApiUrl { get; set; } = "https://discordapp.com/api/v6/"; + public static readonly Uri DefaultRestUri = new Uri("https://discordapp.com/api/v6/"); + /// + /// The URI to use when making HTTP requests. If specified, this will override the default. + /// + public Uri? RestUri = null; /// /// The URI to use when connecting to the gateway. If specified, this will override the URI Discord instructs us to use. /// diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index 271db25d56..3a5c424fa4 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -1,12 +1,13 @@ +using System; using System.Net.Http.Headers; using Discord.Rest; using Discord.Socket; namespace Discord { - internal interface IDiscordClient + public interface IDiscordClient : IDisposable { - static IDiscordClient Create(string token, DiscordConfig? config = default) + public static IDiscordClient Create(string token, DiscordConfig? config = default) { config = config ?? new DiscordConfig(); diff --git a/src/Discord.Net/Rest/DiscordRestApi.cs b/src/Discord.Net/Rest/DiscordRestApi.cs index 24c682478d..b70c757f0b 100644 --- a/src/Discord.Net/Rest/DiscordRestApi.cs +++ b/src/Discord.Net/Rest/DiscordRestApi.cs @@ -3,6 +3,8 @@ using Refit; using Discord.Rest.Models; using System.Net.Http.Headers; +using System; +using System.Net.Http; // This is essentially a reimplementation of Wumpus.Net.Rest namespace Discord.Rest @@ -10,20 +12,31 @@ namespace Discord.Rest public class DiscordRestApi : IDiscordRestApi { private readonly IDiscordRestApi _api; + private readonly HttpClient _http; public DiscordRestApi(DiscordConfig config, AuthenticationHeaderValue token) { + _http = new HttpClient(new DiscordHttpClientHandler(token), true) + { + BaseAddress = config.RestUri ?? DiscordConfig.DefaultRestUri, + }; + var jsonOptions = new JsonSerializerOptions(); var refitSettings = new RefitSettings { ContentSerializer = new JsonContentSerializer(jsonOptions), }; - _api = RestService.For(config.RestApiUrl, refitSettings); + _api = RestService.For(_http, refitSettings); } public Task GetGatewayInfoAsync() { return _api.GetGatewayInfoAsync(); } + + public void Dispose() + { + _http.Dispose(); + } } } diff --git a/src/Discord.Net/Rest/IDiscordRestApi.cs b/src/Discord.Net/Rest/IDiscordRestApi.cs index 3e58c1ce3d..0f963351af 100644 --- a/src/Discord.Net/Rest/IDiscordRestApi.cs +++ b/src/Discord.Net/Rest/IDiscordRestApi.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Refit; using Discord.Rest.Models; diff --git a/src/Discord.Net/Rest/Requests/Test.cs b/src/Discord.Net/Rest/Requests/Test.cs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/Discord.Net/Socket/DiscordGatewayApi.cs b/src/Discord.Net/Socket/DiscordGatewayApi.cs index 5f4744b90b..705bba536e 100644 --- a/src/Discord.Net/Socket/DiscordGatewayApi.cs +++ b/src/Discord.Net/Socket/DiscordGatewayApi.cs @@ -4,7 +4,7 @@ namespace Discord.Socket { - public class DiscordGatewayApi + public class DiscordGatewayApi : IDisposable { private readonly DiscordConfig _config; private readonly string _token; @@ -33,5 +33,10 @@ public async Task OnPacket(object packet) { await Task.CompletedTask; } + + public void Dispose() + { + Socket.Dispose(); + } } } From 18438e2dda41ae3e1aeddba16c371907b27016c8 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Sun, 5 Jan 2020 23:29:48 -0500 Subject: [PATCH 07/10] util: add logging --- src/Discord.Net/DiscordClient.cs | 11 ++ src/Discord.Net/DiscordConfig.cs | 12 ++ src/Discord.Net/IDiscordClient.cs | 2 + src/Discord.Net/Rest/DiscordRestApi.cs | 4 + src/Discord.Net/Socket/DiscordGatewayApi.cs | 3 + src/Discord.Net/Utilities/Logging.cs | 159 ++++++++++++++++++++ 6 files changed, 191 insertions(+) create mode 100644 src/Discord.Net/Utilities/Logging.cs diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index c2498985c5..5c651d88d9 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -1,3 +1,4 @@ +using System; using Discord.Rest; using Discord.Socket; @@ -9,14 +10,24 @@ internal class DiscordClient : IDiscordClient public DiscordGatewayApi Gateway { get; } private readonly DiscordConfig _config; + private readonly Logger _logger; public DiscordClient(DiscordConfig config, DiscordRestApi restApi, DiscordGatewayApi gatewayApi) { _config = config; + _logger = new Logger("Client", config.MinClientSeverity); + Rest = restApi; Gateway = gatewayApi; + + Log += _ => { }; // initialize log method + Rest.Logger.Message += m => Log(m); + Gateway.Logger.Message += m => Log(m); + _logger.Message += m => Log(m); } + public event Action Log; + public void Dispose() { Rest.Dispose(); diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index e8ba2407b2..c752c27c32 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -35,5 +35,17 @@ public class DiscordConfig /// SocketFactory gets or sets how a WebSocket will be created. /// public SocketFactory SocketFactory { get; set; } = DefaultSocketFactory.Create; + /// + /// Minimum Log Severity for the Rest API. + /// + public LogSeverity MinRestSeverity { get; set; } = LogSeverity.Info; + /// + /// Minimum Log Severity for the Gateway API. + /// + public LogSeverity MinGatewaySeverity { get; set; } = LogSeverity.Info; + /// + /// Minimum Log Severity for the Client. + /// + public LogSeverity MinClientSeverity { get; set; } = LogSeverity.Info; } } diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index 3a5c424fa4..03e1224250 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -22,5 +22,7 @@ public static IDiscordClient Create(string token, DiscordConfig? config = defaul DiscordRestApi Rest { get; } DiscordGatewayApi Gateway { get; } + + event Action Log; } } diff --git a/src/Discord.Net/Rest/DiscordRestApi.cs b/src/Discord.Net/Rest/DiscordRestApi.cs index b70c757f0b..146b8025b7 100644 --- a/src/Discord.Net/Rest/DiscordRestApi.cs +++ b/src/Discord.Net/Rest/DiscordRestApi.cs @@ -14,8 +14,12 @@ public class DiscordRestApi : IDiscordRestApi private readonly IDiscordRestApi _api; private readonly HttpClient _http; + internal Logger Logger { get; private set; } + public DiscordRestApi(DiscordConfig config, AuthenticationHeaderValue token) { + Logger = new Logger("Rest", config.MinRestSeverity); + _http = new HttpClient(new DiscordHttpClientHandler(token), true) { BaseAddress = config.RestUri ?? DiscordConfig.DefaultRestUri, diff --git a/src/Discord.Net/Socket/DiscordGatewayApi.cs b/src/Discord.Net/Socket/DiscordGatewayApi.cs index 705bba536e..9e098ff5c5 100644 --- a/src/Discord.Net/Socket/DiscordGatewayApi.cs +++ b/src/Discord.Net/Socket/DiscordGatewayApi.cs @@ -9,10 +9,13 @@ public class DiscordGatewayApi : IDisposable private readonly DiscordConfig _config; private readonly string _token; + internal Logger Logger { get; private set; } public ISocket Socket { get; set; } public DiscordGatewayApi(DiscordConfig config, string token) { + Logger = new Logger("Gateway", config.MinGatewaySeverity); + _config = config; _token = token; diff --git a/src/Discord.Net/Utilities/Logging.cs b/src/Discord.Net/Utilities/Logging.cs new file mode 100644 index 0000000000..9cf6ebcff5 --- /dev/null +++ b/src/Discord.Net/Utilities/Logging.cs @@ -0,0 +1,159 @@ +using System; +using System.Text; + +namespace Discord +{ + public enum LogSeverity + { + Trace, + Debug, + Info, + Warn, + Error + } + + public struct LogMessage + { + public LogSeverity Level { get; } + public string Source { get; } + public string Message { get; } + public Exception? Exception { get; } + + public LogMessage(LogSeverity level, string source, string message, Exception? exception = null) + { + Level = level; + Source = source; + Message = message; + Exception = exception; + } + + public override string ToString() => ToString(); + public string ToString(StringBuilder? builder = null, + bool fullException = true, + bool prependTimestamp = true, + DateTimeKind timestampKind = DateTimeKind.Local, + int? padSource = 11) + { + string? exMessage = fullException ? Exception?.ToString() : Exception?.Message; + int maxLength = 1 + + (prependTimestamp ? 8 : 0) + 1 + + (padSource.HasValue ? padSource.Value : Source?.Length ?? 0) + 1 + + (Message?.Length ?? 0) + + (exMessage?.Length ?? 0) + 3; + + if (builder == null) + builder = new StringBuilder(maxLength); + else + { + builder.Clear(); + builder.EnsureCapacity(maxLength); + } + + if (prependTimestamp) + { + DateTime now; + if (timestampKind == DateTimeKind.Utc) + now = DateTime.UtcNow; + else + now = DateTime.Now; + if (now.Hour < 10) + builder.Append('0'); + builder.Append(now.Hour); + builder.Append(':'); + if (now.Minute < 10) + builder.Append('0'); + builder.Append(now.Minute); + builder.Append(':'); + if (now.Second < 10) + builder.Append('0'); + builder.Append(now.Second); + builder.Append(' '); + } + if (Source != null) + { + if (padSource.HasValue) + { + if (Source.Length < padSource.Value) + { + builder.Append(Source); + builder.Append(' ', padSource.Value - Source.Length); + } + else if (Source.Length > padSource.Value) + builder.Append(Source.Substring(0, padSource.Value)); + else + builder.Append(Source); + } + else + builder.Append(Source); + builder.Append(' '); + } + if (!string.IsNullOrEmpty(Message)) + { + char c; + for (int i = 0; i < Message.Length; i++) + { + c = Message[i]; + if (!char.IsControl(c)) + builder.Append(c); + } + } + if (exMessage != null) + { + if (!string.IsNullOrEmpty(Message)) + { + builder.Append(':'); + builder.AppendLine(); + } + builder.Append(exMessage); + } + + return builder.ToString(); + } + } + + public class Logger + { + public event Action? Message; + public string Name { get; set; } + public LogSeverity MinSeverity { get; set; } + + public Logger(string source, LogSeverity minSeverity) + { + Name = source; + MinSeverity = minSeverity; + } + + public void Log(LogMessage message) + { + if (message.Level < MinSeverity) + return; + Message?.Invoke(message); + } + + public void Log(LogSeverity severity, string message, Exception? err = null) + => Log(new LogMessage(severity, Name, message, err)); + + public void Trace(string message, Exception? err = null) + => Log(LogSeverity.Trace, message, err); + public void Debug(string message, Exception? err = null) + => Log(LogSeverity.Debug, message, err); + public void Info(string message, Exception? err = null) + => Log(LogSeverity.Info, message, err); + public void Warn(string message, Exception? err = null) + => Log(LogSeverity.Warn, message, err); + public void Error(string message, Exception? err = null) + => Log(LogSeverity.Error, message, err); + + public void Trace(Exception err) + => Log(LogSeverity.Trace, null!, err); + public void Debug(Exception err) + => Log(LogSeverity.Debug, null!, err); + public void Info(Exception err) + => Log(LogSeverity.Info, null!, err); + public void Warn(Exception err) + => Log(LogSeverity.Warn, null!, err); + public void Error(Exception err) + => Log(LogSeverity.Error, null!, err); + + } +} From 5e6f977469b143bc5ad17c630fad3481c9184861 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Mon, 6 Jan 2020 01:37:47 -0500 Subject: [PATCH 08/10] rest: write out channel model + its dependencies Optional struct needs work still, + writing the converter for it is going to be a headache --- ep.txt | 106 ++++++++++++++++++ src/Discord.Net/Entities/Snowflake.cs | 24 ++++ src/Discord.Net/Rest/DiscordRestApi.cs | 10 +- src/Discord.Net/Rest/IDiscordRestApi.cs | 23 +++- .../Rest/Models/Channel/Channel.cs | 59 ++++++++++ .../Rest/Models/Channel/ChannelType.cs | 13 +++ src/Discord.Net/Rest/Models/GatewayInfo.cs | 6 +- .../Models/Permissions/ChannelPermissions.cs | 36 ++++++ .../Models/Permissions/GuildPermissions.cs | 45 ++++++++ .../Rest/Models/Permissions/Overwrite.cs | 16 +++ .../Models/Permissions/PermissionTarget.cs | 8 ++ .../Rest/Models/Users/AccountFlags.cs | 20 ++++ .../Rest/Models/Users/PremiumType.cs | 8 ++ src/Discord.Net/Rest/Models/Users/User.cs | 33 ++++++ .../JsonContentSerializer.cs | 2 +- .../Serialization/OptionalConverter.cs | 22 ++++ src/Discord.Net/Utilities/Optional.cs | 12 ++ 17 files changed, 433 insertions(+), 10 deletions(-) create mode 100644 ep.txt create mode 100644 src/Discord.Net/Entities/Snowflake.cs create mode 100644 src/Discord.Net/Rest/Models/Channel/Channel.cs create mode 100644 src/Discord.Net/Rest/Models/Channel/ChannelType.cs create mode 100644 src/Discord.Net/Rest/Models/Permissions/ChannelPermissions.cs create mode 100644 src/Discord.Net/Rest/Models/Permissions/GuildPermissions.cs create mode 100644 src/Discord.Net/Rest/Models/Permissions/Overwrite.cs create mode 100644 src/Discord.Net/Rest/Models/Permissions/PermissionTarget.cs create mode 100644 src/Discord.Net/Rest/Models/Users/AccountFlags.cs create mode 100644 src/Discord.Net/Rest/Models/Users/PremiumType.cs create mode 100644 src/Discord.Net/Rest/Models/Users/User.cs rename src/Discord.Net/{Rest => Serialization}/JsonContentSerializer.cs (98%) create mode 100644 src/Discord.Net/Serialization/OptionalConverter.cs create mode 100644 src/Discord.Net/Utilities/Optional.cs diff --git a/ep.txt b/ep.txt new file mode 100644 index 0000000000..aad3896888 --- /dev/null +++ b/ep.txt @@ -0,0 +1,106 @@ +https://gist.github.com/SinisterRectus/9518f3e7d0d1ccb4335b2a0d389c30b0 + +Sorted By Route +-------------------------------------------------------------------------------------------------------------------- +Get Entitlements GET /applications/{application.id}/entitlements +Get Entitlement GET /applications/{application.id}/entitlements/{entitlement.id} +Delete Test Entitlement DELETE /applications/{application.id}/entitlements/{entitlement.id}/ +Consume SKU POST /applications/{application.id}/entitlements/{entitlement.id}/consume +Get SKUs GET /applications/{application.id}/skus +Delete/Close Channel DELETE /channels/{channel.id} +Get Channel GET /channels/{channel.id} +Modify Channel PUT/PATCH /channels/{channel.id} +Get Channel Invites GET /channels/{channel.id}/invites +Create Channel Invite POST /channels/{channel.id}/invites +Get Channel Messages GET /channels/{channel.id}/messages +Create Message POST /channels/{channel.id}/messages +Bulk Delete Messages POST /channels/{channel.id}/messages/bulk-delete +Bulk Delete Messages (deprecated) POST /channels/{channel.id}/messages/bulk_delete +Delete Message DELETE /channels/{channel.id}/messages/{message.id} +Get Channel Message GET /channels/{channel.id}/messages/{message.id} +Edit Message PATCH /channels/{channel.id}/messages/{message.id} +Delete All Reactions DELETE /channels/{channel.id}/messages/{message.id}/reactions +Get Reactions GET /channels/{channel.id}/messages/{message.id}/reactions/{emoji} +Delete Own Reaction DELETE /channels/{channel.id}/messages/{message.id}/reactions/{emoji}/@me +Create Reaction PUT /channels/{channel.id}/messages/{message.id}/reactions/{emoji}/@me +Delete User Reaction DELETE /channels/{channel.id}/messages/{message.id}/reactions/{emoji}/{user.id} +Delete Channel Permission DELETE /channels/{channel.id}/permissions/{overwrite.id} +Edit Channel Permissions PUT /channels/{channel.id}/permissions/{overwrite.id} +Get Pinned Messages GET /channels/{channel.id}/pins +Delete Pinned Channel Message DELETE /channels/{channel.id}/pins/{message.id} +Add Pinned Channel Message PUT /channels/{channel.id}/pins/{message.id} +Group DM Remove Recipient DELETE /channels/{channel.id}/recipients/{user.id} +Group DM Add Recipient PUT /channels/{channel.id}/recipients/{user.id} +Trigger Typing Indicator POST /channels/{channel.id}/typing +Get Channel Webhooks GET /channels/{channel.id}/webhooks +Create Webhook POST /channels/{channel.id}/webhooks +Get Gateway GET /gateway +Get Gateway Bot GET /gateway/bot +Create Guild POST /guilds +Delete Guild DELETE /guilds/{guild.id} +Get Guild GET /guilds/{guild.id} +Modify Guild PATCH /guilds/{guild.id} +Get Guild Audit Log GET /guilds/{guild.id}/audit-logs +Get Guild Bans GET /guilds/{guild.id}/bans +Remove Guild Ban DELETE /guilds/{guild.id}/bans/{user.id} +Get Guild Ban GET /guilds/{guild.id}/bans/{user.id} +Create Guild Ban PUT /guilds/{guild.id}/bans/{user.id} +Get Guild Channels GET /guilds/{guild.id}/channels +Modify Guild Channel Positions PATCH /guilds/{guild.id}/channels +Create Guild Channel POST /guilds/{guild.id}/channels +Get Guild Embed GET /guilds/{guild.id}/embed +Modify Guild Embed PATCH /guilds/{guild.id}/embed +List Guild Emojis GET /guilds/{guild.id}/emojis +Create Guild Emoji POST /guilds/{guild.id}/emojis +Delete Guild Emoji DELETE /guilds/{guild.id}/emojis/{emoji.id} +Get Guild Emoji GET /guilds/{guild.id}/emojis/{emoji.id} +Modify Guild Emoji PATCH /guilds/{guild.id}/emojis/{emoji.id} +Get Guild Integrations GET /guilds/{guild.id}/integrations +Create Guild Integration POST /guilds/{guild.id}/integrations +Delete Guild Integration DELETE /guilds/{guild.id}/integrations/{integration.id} +Modify Guild Integration PATCH /guilds/{guild.id}/integrations/{integration.id} +Sync Guild Integration POST /guilds/{guild.id}/integrations/{integration.id}/sync +Get Guild Invites GET /guilds/{guild.id}/invites +List Guild Members GET /guilds/{guild.id}/members +Modify Current User Nick PATCH /guilds/{guild.id}/members/@me/nick +Remove Guild Member DELETE /guilds/{guild.id}/members/{user.id} +Get Guild Member GET /guilds/{guild.id}/members/{user.id} +Modify Guild Member PATCH /guilds/{guild.id}/members/{user.id} +Add Guild Member PUT /guilds/{guild.id}/members/{user.id} +Remove Guild Member Role DELETE /guilds/{guild.id}/members/{user.id}/roles/{role.id} +Add Guild Member Role PUT /guilds/{guild.id}/members/{user.id}/roles/{role.id} +Get Guild Prune Count GET /guilds/{guild.id}/prune +Begin Guild Prune POST /guilds/{guild.id}/prune +Get Guild Voice Regions GET /guilds/{guild.id}/regions +Get Guild Roles GET /guilds/{guild.id}/roles +Modify Guild Role Positions PATCH /guilds/{guild.id}/roles +Create Guild Role POST /guilds/{guild.id}/roles +Delete Guild Role DELETE /guilds/{guild.id}/roles/{role.id} +Modify Guild Role PATCH /guilds/{guild.id}/roles/{role.id} +Get Guild Vanity URL GET /guilds/{guild.id}/vanity-url +Get Guild Webhooks GET /guilds/{guild.id}/webhooks +Get Guild Widget Image GET /guilds/{guild.id}/widget.png +Delete Invite DELETE /invites/{invite.code} +Get Invite GET /invites/{invite.code} +Get Current Application Information GET /oauth2/applications/@me +Delete Purchase Discount DELETE /store/skus/{sku.id}/discounts/{user.id}/ +Create Purchase Discount PUT /store/skus/{sku.id}/discounts/{user.id}/ +Get Current User GET /users/@me +Modify Current User PATCH /users/@me +Get User DMs GET /users/@me/channels +Create DM POST /users/@me/channels +Create Group DM POST /users/@me/channels +Get User Connections GET /users/@me/connections +Get Current User Guilds GET /users/@me/guilds +Leave Guild DELETE /users/@me/guilds/{guild.id} +Get User GET /users/{user.id} +List Voice Regions GET /voice/regions +Delete Webhook DELETE /webhooks/{webhook.id} +Get Webhook GET /webhooks/{webhook.id} +Modify Webhook PATCH /webhooks/{webhook.id} +Delete Webhook with Token DELETE /webhooks/{webhook.id}/{webhook.token} +Get Webhook with Token GET /webhooks/{webhook.id}/{webhook.token} +Modify Webhook with Token PATCH /webhooks/{webhook.id}/{webhook.token} +Execute Webhook POST /webhooks/{webhook.id}/{webhook.token} +Execute GitHub-Compatible Webhook POST /webhooks/{webhook.id}/{webhook.token}/github +Execute Slack-Compatible Webhook POST /webhooks/{webhook.id}/{webhook.token}/slack \ No newline at end of file diff --git a/src/Discord.Net/Entities/Snowflake.cs b/src/Discord.Net/Entities/Snowflake.cs new file mode 100644 index 0000000000..5dc8dad0ff --- /dev/null +++ b/src/Discord.Net/Entities/Snowflake.cs @@ -0,0 +1,24 @@ +namespace Discord +{ + /// + /// A Snowflake represents a unique, 64-bit identifier. + /// + public struct Snowflake + { + private readonly ulong _value; + + private Snowflake(ulong value) + { + _value = value; + } + + public static implicit operator ulong(Snowflake snowflake) + { + return snowflake._value; + } + public static implicit operator Snowflake(ulong value) + { + return new Snowflake(value); + } + } +} diff --git a/src/Discord.Net/Rest/DiscordRestApi.cs b/src/Discord.Net/Rest/DiscordRestApi.cs index 146b8025b7..94395fb296 100644 --- a/src/Discord.Net/Rest/DiscordRestApi.cs +++ b/src/Discord.Net/Rest/DiscordRestApi.cs @@ -1,7 +1,7 @@ using System.Text.Json; using System.Threading.Tasks; using Refit; -using Discord.Rest.Models; +using Discord.Models; using System.Net.Http.Headers; using System; using System.Net.Http; @@ -32,11 +32,11 @@ public DiscordRestApi(DiscordConfig config, AuthenticationHeaderValue token) }; _api = RestService.For(_http, refitSettings); } - + public Task GetGatewayInfoAsync() - { - return _api.GetGatewayInfoAsync(); - } + => _api.GetGatewayInfoAsync(); + public Task GetBotGatewayInfoAsync() + => _api.GetBotGatewayInfoAsync(); public void Dispose() { diff --git a/src/Discord.Net/Rest/IDiscordRestApi.cs b/src/Discord.Net/Rest/IDiscordRestApi.cs index 0f963351af..83b5103290 100644 --- a/src/Discord.Net/Rest/IDiscordRestApi.cs +++ b/src/Discord.Net/Rest/IDiscordRestApi.cs @@ -1,13 +1,34 @@ using System; using System.Threading.Tasks; using Refit; -using Discord.Rest.Models; +using Discord.Models; namespace Discord.Rest { public interface IDiscordRestApi { + // --- /applications + + // --- /channels + + // --- /gateway [Get("/gateway/bot")] Task GetGatewayInfoAsync(); + [Get("/gateway/bot")] + Task GetBotGatewayInfoAsync(); + + // --- /guilds + + // --- /invites + + // --- /oauth2 + + // --- /store + + // --- /users + + // --- /voice + + // --- /webhooks } } diff --git a/src/Discord.Net/Rest/Models/Channel/Channel.cs b/src/Discord.Net/Rest/Models/Channel/Channel.cs new file mode 100644 index 0000000000..463daecf56 --- /dev/null +++ b/src/Discord.Net/Rest/Models/Channel/Channel.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace Discord.Models +{ + public class Channel + { + public const int MinChannelNameLength = 2; + public const int MaxChannelNameLength = 100; + + public const int MinChannelTopicLength = 0; + public const int MaxChannelTopicLength = 1024; + + public const int MinUserLimit = 0; + public const int MaxUserLimit = 100; + + public const int MinBitrate = 8000; + public const int MaxBitrate = 384000; + + public const int MinRateLimitPerUser = 0; + public const int MaxRateLimitPerUser = 21600; + + [JsonPropertyName("id")] + public Snowflake Id { get; set; } + [JsonPropertyName("type")] + public ChannelType Type { get; set; } + [JsonPropertyName("guild_id")] + public Optional GuildId { get; set; } + [JsonPropertyName("position")] + public Optional Position { get; set; } + [JsonPropertyName("permission_overwrites")] + public Optional Overwrites { get; set; } + [JsonPropertyName("name")] + public Optional Name { get; set; } + [JsonPropertyName("topic")] + public Optional Topic { get; set; } + [JsonPropertyName("nsfw")] + public Optional Nsfw { get; set; } + [JsonPropertyName("user_limit")] + public Optional Bitrate { get; set; } + [JsonPropertyName("rate_limit_per_user")] + public Optional RateLimitPerUser { get; set; } + [JsonPropertyName("recipients")] + public Optional Recipients { get; set; } + [JsonPropertyName("icon")] + public Optional IconId { get; set; } + [JsonPropertyName("owner_id")] + public Optional OwnerId { get; set; } + [JsonPropertyName("application_id")] + public Optional ApplicationId { get; set; } + [JsonPropertyName("parent_id")] + public Optional ParentId { get; set; } + [JsonPropertyName("last_pin_timestamp")] + public Optional LastPinTimestamp { get; set; } + // omitted: last_message_id + } +} diff --git a/src/Discord.Net/Rest/Models/Channel/ChannelType.cs b/src/Discord.Net/Rest/Models/Channel/ChannelType.cs new file mode 100644 index 0000000000..47d8bb0556 --- /dev/null +++ b/src/Discord.Net/Rest/Models/Channel/ChannelType.cs @@ -0,0 +1,13 @@ +namespace Discord.Models +{ + public enum ChannelType : byte + { + Text = 0, + Direct = 1, + Voice = 2, + Group = 3, + Category = 4, + News = 5, + Store = 6 + } +} diff --git a/src/Discord.Net/Rest/Models/GatewayInfo.cs b/src/Discord.Net/Rest/Models/GatewayInfo.cs index e1497be06d..59331bddf1 100644 --- a/src/Discord.Net/Rest/Models/GatewayInfo.cs +++ b/src/Discord.Net/Rest/Models/GatewayInfo.cs @@ -1,16 +1,16 @@ #pragma warning disable CS8618 // Uninitialized NRT expected in models using System.Text.Json.Serialization; -namespace Discord.Rest.Models +namespace Discord.Models { public class GatewayInfo { [JsonPropertyName("url")] public string Url { get; set; } [JsonPropertyName("shards")] - public int Shards { get; set; } + public int? Shards { get; set; } [JsonPropertyName("session_start_limit")] - public GatewaySessionStartInfo SessionStartInfo { get; set; } + public GatewaySessionStartInfo? SessionStartInfo { get; set; } } public class GatewaySessionStartInfo diff --git a/src/Discord.Net/Rest/Models/Permissions/ChannelPermissions.cs b/src/Discord.Net/Rest/Models/Permissions/ChannelPermissions.cs new file mode 100644 index 0000000000..d3b018ab40 --- /dev/null +++ b/src/Discord.Net/Rest/Models/Permissions/ChannelPermissions.cs @@ -0,0 +1,36 @@ +using System; + +namespace Discord.Models +{ + [Flags] + public enum ChannelPermissions : ulong + { + // General + CreateInstantInvite = 0x0000_0001, + ManageChannel = 0x0000_0010, + AddReactions = 0x0000_0040, + ViewChannel = 0x0000_0400, + ManagePermissions = 0x1000_0000, + ManageWebhooks = 0x2000_0000, + + // Messages + SendMessages = 0x0000_0800, + SendTtsMessages = 0x0000_0100, + ManageMessages = 0x0000_02000, + EmbedLinks = 0x0000_4000, + AttachFiles = 0x0000_8000, + ReadMessageHistory = 0x0001_0000, + MentionEveryone = 0x0002_0000, + UseExternalEmoji = 0x0004_0000, + + // Voice + Connect = 0x0010_0000, + Speak = 0x0020_0000, + MuteMembers = 0x0040_0000, + DeafenMembers = 0x0080_0000, + MoveMembers = 0x0100_0000, + UseVoiceActivity = 0x0200_0000, + PrioritySpeaker = 0x0000_0100, + Stream = 0x0000_0200, + } +} diff --git a/src/Discord.Net/Rest/Models/Permissions/GuildPermissions.cs b/src/Discord.Net/Rest/Models/Permissions/GuildPermissions.cs new file mode 100644 index 0000000000..5ce197a48b --- /dev/null +++ b/src/Discord.Net/Rest/Models/Permissions/GuildPermissions.cs @@ -0,0 +1,45 @@ +using System; + +namespace Discord.Models +{ + // todo: doc these when other models exist + [Flags] + public enum GuildPermissions : ulong + { + // General + CreateInstantInvite = 0x0000_0001, + KickMembers = 0x0000_0002, + BanMembers = 0x0000_0004, + Administrator = 0x0000_0008, + ManageChannels = 0x0000_0010, + ManageGuild = 0x0000_0020, + AddReactions = 0x0000_0040, + ViewAuditLog = 0x0000_0080, + ViewChannel = 0x0000_0400, + ChangeNickname = 0x0400_0000, + ManageNicknames = 0x0800_0000, + ManageRoles = 0x1000_0000, + ManageWebhooks = 0x2000_0000, + ManageEmoji = 0x4000_0000, + + // Messages + SendMessages = 0x0000_0800, + SendTtsMessages = 0x0000_0100, + ManageMessages = 0x0000_02000, + EmbedLinks = 0x0000_4000, + AttachFiles = 0x0000_8000, + ReadMessageHistory = 0x0001_0000, + MentionEveryone = 0x0002_0000, + UseExternalEmoji = 0x0004_0000, + + // Voice + Connect = 0x0010_0000, + Speak = 0x0020_0000, + MuteMembers = 0x0040_0000, + DeafenMembers = 0x0080_0000, + MoveMembers = 0x0100_0000, + UseVoiceActivity = 0x0200_0000, + PrioritySpeaker = 0x0000_0100, + Stream = 0x0000_0200, + } +} diff --git a/src/Discord.Net/Rest/Models/Permissions/Overwrite.cs b/src/Discord.Net/Rest/Models/Permissions/Overwrite.cs new file mode 100644 index 0000000000..4b947eab4f --- /dev/null +++ b/src/Discord.Net/Rest/Models/Permissions/Overwrite.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace Discord.Models +{ + public class Overwrite + { + [JsonPropertyName("id")] + public Snowflake Id { get; set; } + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public PermissionTarget TargetType { get; set; } + } +} diff --git a/src/Discord.Net/Rest/Models/Permissions/PermissionTarget.cs b/src/Discord.Net/Rest/Models/Permissions/PermissionTarget.cs new file mode 100644 index 0000000000..980aac4537 --- /dev/null +++ b/src/Discord.Net/Rest/Models/Permissions/PermissionTarget.cs @@ -0,0 +1,8 @@ +namespace Discord.Models +{ + public enum PermissionTarget + { + Member, + Role + } +} diff --git a/src/Discord.Net/Rest/Models/Users/AccountFlags.cs b/src/Discord.Net/Rest/Models/Users/AccountFlags.cs new file mode 100644 index 0000000000..4ce70def6b --- /dev/null +++ b/src/Discord.Net/Rest/Models/Users/AccountFlags.cs @@ -0,0 +1,20 @@ +using System; + +namespace Discord.Models +{ + [Flags] + public enum AccountFlags : short + { + None = 0, + Employee = 1<<0, + Partner = 1<<1, + HypesquadEvents = 1<<2, + BugHunter = 1<<3, + HypesquadBravery = 1<<6, + HypesquadBrilliance = 1<<7, + HypesquadBalance = 1<<8, + EarlySupporter = 1<<9, + TeamUser = 1<<10, + System = 1<<12, + } +} diff --git a/src/Discord.Net/Rest/Models/Users/PremiumType.cs b/src/Discord.Net/Rest/Models/Users/PremiumType.cs new file mode 100644 index 0000000000..492586ffc4 --- /dev/null +++ b/src/Discord.Net/Rest/Models/Users/PremiumType.cs @@ -0,0 +1,8 @@ +namespace Discord.Models +{ + public enum PremiumType : byte + { + Classic = 1, + Nitro = 2 + } +} diff --git a/src/Discord.Net/Rest/Models/Users/User.cs b/src/Discord.Net/Rest/Models/Users/User.cs new file mode 100644 index 0000000000..e6d643e47d --- /dev/null +++ b/src/Discord.Net/Rest/Models/Users/User.cs @@ -0,0 +1,33 @@ +#pragma warning disable CS8618 // Uninitialized NRT expected in models +using System.Text.Json.Serialization; + +namespace Discord.Models +{ + public class User + { + [JsonPropertyName("id")] + public Snowflake Id { get; set; } + [JsonPropertyName("username")] + public string Username { get; set; } + [JsonPropertyName("discriminator")] + public ushort Discriminator { get; set; } + [JsonPropertyName("avatar")] + public string? AvatarId { get; set; } + [JsonPropertyName("bot")] + public Optional Bot { get; set; } + [JsonPropertyName("system")] + public Optional System { get; set; } + [JsonPropertyName("mfa_enabled")] + public Optional MfaEnabled { get; set; } + [JsonPropertyName("locale")] + public Optional Locale { get; set; } + [JsonPropertyName("verified")] + public Optional Verified { get; set; } + [JsonPropertyName("email")] + public Optional Email { get; set; } + [JsonPropertyName("flags")] + public Optional Flags { get; set; } + [JsonPropertyName("premium_type")] + public Optional PremiumType { get; set; } + } +} diff --git a/src/Discord.Net/Rest/JsonContentSerializer.cs b/src/Discord.Net/Serialization/JsonContentSerializer.cs similarity index 98% rename from src/Discord.Net/Rest/JsonContentSerializer.cs rename to src/Discord.Net/Serialization/JsonContentSerializer.cs index 9c03ad59ee..4fd4c19715 100644 --- a/src/Discord.Net/Rest/JsonContentSerializer.cs +++ b/src/Discord.Net/Serialization/JsonContentSerializer.cs @@ -8,7 +8,7 @@ // https://blog.martincostello.com/refit-and-system-text-json/ -namespace Discord.Rest +namespace Discord { public class JsonContentSerializer : IContentSerializer { diff --git a/src/Discord.Net/Serialization/OptionalConverter.cs b/src/Discord.Net/Serialization/OptionalConverter.cs new file mode 100644 index 0000000000..55b988ea17 --- /dev/null +++ b/src/Discord.Net/Serialization/OptionalConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Discord.Serialization +{ + // 😅 + public class OptionalConverter : JsonConverter> + { + public override Optional Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, Optional value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Discord.Net/Utilities/Optional.cs b/src/Discord.Net/Utilities/Optional.cs new file mode 100644 index 0000000000..a87cc2f7ab --- /dev/null +++ b/src/Discord.Net/Utilities/Optional.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; +// todo: impl +namespace Discord +{ + public struct Optional + { + public bool IsSpecified { get; private set; } + public T Value { get; set; } + } +} From 68f89bf2fce814bc28c516d0f504c0ce89e1289e Mon Sep 17 00:00:00 2001 From: Patrick Michallet Date: Mon, 6 Jan 2020 16:39:55 +0000 Subject: [PATCH 09/10] rest: add webhook model (#1435) --- .../Rest/Models/Webhook/Webhook.cs | 33 +++++++++++++++++++ .../Rest/Models/Webhook/WebhookType.cs | 8 +++++ 2 files changed, 41 insertions(+) create mode 100644 src/Discord.Net/Rest/Models/Webhook/Webhook.cs create mode 100644 src/Discord.Net/Rest/Models/Webhook/WebhookType.cs diff --git a/src/Discord.Net/Rest/Models/Webhook/Webhook.cs b/src/Discord.Net/Rest/Models/Webhook/Webhook.cs new file mode 100644 index 0000000000..08ea61f47d --- /dev/null +++ b/src/Discord.Net/Rest/Models/Webhook/Webhook.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace Discord.Models +{ + public class Webhook + { + public const int MinWebhookNameLength = 2; + public const int MaxWebhookNameLength = 32; + + public const int MinMessageContentLength = 0; + public const int MaxMessageContentLength = 2000; + + public const int MinEmbedLimit = 0; + public const int MaxEmbedLimit = 10; + + [JsonPropertyName("id")] + public Snowflake Id { get; set; } + [JsonPropertyName("type")] + public WebhookType Type { get; set; } + [JsonPropertyName("guild_id")] + public Optional GuildId { get; set; } + [JsonPropertyName("channel_id")] + public Snowflake ChannelId { get; set; } + [JsonPropertyName("user")] + public Optional Creator { get; set; } + [JsonPropertyName("name")] + public string? Name { get; set; } + [JsonPropertyName("avatar")] + public string? AvatarId { get; set; } + [JsonPropertyName("token")] + public Optional Token { get; set; } + } +} diff --git a/src/Discord.Net/Rest/Models/Webhook/WebhookType.cs b/src/Discord.Net/Rest/Models/Webhook/WebhookType.cs new file mode 100644 index 0000000000..574d5c9ca8 --- /dev/null +++ b/src/Discord.Net/Rest/Models/Webhook/WebhookType.cs @@ -0,0 +1,8 @@ +namespace Discord.Models +{ + public enum WebhookType : byte + { + Incoming = 1, + ChannelFollower = 2 + } +} From 07f74b70b4487e9b3c20cda162b7c799cfd57468 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Sun, 12 Jan 2020 19:12:09 -0500 Subject: [PATCH 10/10] net: add barebones optional converter does not support property omission at this time, will need to be added later using a separate converter and base marker class. -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 "git failing to recognize gpg key, this identity is still valid" -----BEGIN PGP SIGNATURE----- iQIzBAEBCgAdFiEErbDRxgZ77MPT+ajAOrLKmA3cYakFAl4bthEACgkQOrLKmA3c YamuWw/7Bn/Ks0mTRN3tg3Z/voETJ/8JQZXJEiW7wwv8c7nSOemxRNB/Tmzo3kzC N6T5fH7Gep4o4iA7CfJ5CZtx+OY92OpyBwsJgkNvANVpjXWCeDaww0Ci5dyVwFUk fFq21l6p2sbM6PB9sEOCvryeIOrgkqBl915MkAlj+/UtnAQ9qFhomIGNLPPFeYOS eaHWjZF6ArbF5NMaOhboDDCIl2nCf+RGEetDoBP2BRaIf+eOyl0lGyQqiY1mNqkD DX8nmcaY5/Lnxhf3pwmYZbqKBPQt5R2FxmqWTg5ey0R4//izE4TJ54nlhdSnTZpH 7ZligmR9rQFdQ5jbSq6cIclo9i988ELHKBgt8mG3SiC4AT0+SBXRpPRBitkA0CPb O4W8J0HrbSFmILx9Zvuy72KC/Zzo+SOS8257S35ihosrlyupcR4zladVcIviAPWk Ovpy85W4uxPdWc6zkMOZSx9OiYFYkNlK/QdNJBXGg7LLcaLf8p33lj+T8UXa7dyC Sw/pW5RL1FYalh7iXF55ylJrKo+oySBejods+ATnmYG4JMywO+GNCE+XLCcDpoBx 9H2z0qJNb5Dgkc4cRulKwYEoT+LQKUhLFdj4wNEqE8mBw0ZoxUiBBqOD1TiZr2mf 1AFQVS/AeOc03t25OfmhNz026OAGy01bjeHr09deT20dsssEpQY= =n76m -----END PGP SIGNATURE----- --- Discord.Net.sln | 17 +++ src/Discord.Net/Rest/DiscordRestApi.cs | 7 +- .../Serialization/OptionalConverter.cs | 35 ++++-- src/Discord.Net/Utilities/Optional.cs | 54 +++++++- .../Discord.Tests.Unit.csproj | 20 +++ .../Serialization/OptionalConverterTests.cs | 118 ++++++++++++++++++ test/Discord.Tests.Unit/UnitTest1.cs | 13 ++ 7 files changed, 251 insertions(+), 13 deletions(-) create mode 100644 test/Discord.Tests.Unit/Discord.Tests.Unit.csproj create mode 100644 test/Discord.Tests.Unit/Serialization/OptionalConverterTests.cs create mode 100644 test/Discord.Tests.Unit/UnitTest1.cs diff --git a/Discord.Net.sln b/Discord.Net.sln index c52902b9b5..ea814bbc97 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -11,6 +11,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{479564 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "idn", "sample\idn\idn.csproj", "{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{68EE1EAC-F487-4BAC-917B-233370B3AEA1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Tests.Unit", "test\Discord.Tests.Unit\Discord.Tests.Unit.csproj", "{6AD4FF67-D45E-4E7E-8853-990390D35C9F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,9 +52,22 @@ Global {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x64.Build.0 = Release|Any CPU {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x86.ActiveCfg = Release|Any CPU {5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x86.Build.0 = Release|Any CPU + {6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|x64.ActiveCfg = Debug|Any CPU + {6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|x64.Build.0 = Debug|Any CPU + {6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|x86.ActiveCfg = Debug|Any CPU + {6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|x86.Build.0 = Debug|Any CPU + {6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|Any CPU.Build.0 = Release|Any CPU + {6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|x64.ActiveCfg = Release|Any CPU + {6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|x64.Build.0 = Release|Any CPU + {6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|x86.ActiveCfg = Release|Any CPU + {6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3194F5DC-C0AF-4459-AAA3-91CB8FB8C370} = {5DAC796B-0B77-4F84-B790-83DB78C6DFFE} {5BE5DE89-53B7-4243-AEA8-FD8A6420908A} = {4795640A-030C-4A9A-A9B0-20C56AF4DA3F} + {6AD4FF67-D45E-4E7E-8853-990390D35C9F} = {68EE1EAC-F487-4BAC-917B-233370B3AEA1} EndGlobalSection EndGlobal diff --git a/src/Discord.Net/Rest/DiscordRestApi.cs b/src/Discord.Net/Rest/DiscordRestApi.cs index 94395fb296..3681a70142 100644 --- a/src/Discord.Net/Rest/DiscordRestApi.cs +++ b/src/Discord.Net/Rest/DiscordRestApi.cs @@ -1,10 +1,10 @@ using System.Text.Json; using System.Threading.Tasks; -using Refit; -using Discord.Models; using System.Net.Http.Headers; -using System; using System.Net.Http; +using Refit; +using Discord.Models; +using Discord.Serialization; // This is essentially a reimplementation of Wumpus.Net.Rest namespace Discord.Rest @@ -26,6 +26,7 @@ public DiscordRestApi(DiscordConfig config, AuthenticationHeaderValue token) }; var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new OptionalConverter()); var refitSettings = new RefitSettings { ContentSerializer = new JsonContentSerializer(jsonOptions), diff --git a/src/Discord.Net/Serialization/OptionalConverter.cs b/src/Discord.Net/Serialization/OptionalConverter.cs index 55b988ea17..375dd60d59 100644 --- a/src/Discord.Net/Serialization/OptionalConverter.cs +++ b/src/Discord.Net/Serialization/OptionalConverter.cs @@ -1,22 +1,41 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; namespace Discord.Serialization { - // 😅 - public class OptionalConverter : JsonConverter> + // TODO: This does not allow us to omit properties at runtime + // Need to evaluate which cases need us to omit properties and write a separate converter + // for those. At this time I can only think of the outgoing REST PATCH requests. Incoming + // omitted properties will be correctly treated as Optional.Unspecified (the default) + public class OptionalConverter : JsonConverterFactory { - public override Optional Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + private class OptionalTypeConverter : JsonConverter> { - throw new NotImplementedException(); + public override Optional Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return Optional.Unspecified; + else + return new Optional(JsonSerializer.Deserialize(ref reader, options)); + } + + public override void Write(Utf8JsonWriter writer, Optional value, JsonSerializerOptions options) + { + if (!value.IsSpecified) + writer.WriteNullValue(); + else + JsonSerializer.Serialize(writer, value.Value, options); + } } - public override void Write(Utf8JsonWriter writer, Optional value, JsonSerializerOptions options) + public override bool CanConvert(Type typeToConvert) + => typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(Optional<>); + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + var innerType = typeToConvert.GetGenericArguments()[0]; + var converterType = typeof(OptionalTypeConverter<>).MakeGenericType(innerType); + return (JsonConverter)Activator.CreateInstance(converterType); } } } diff --git a/src/Discord.Net/Utilities/Optional.cs b/src/Discord.Net/Utilities/Optional.cs index a87cc2f7ab..f61560764e 100644 --- a/src/Discord.Net/Utilities/Optional.cs +++ b/src/Discord.Net/Utilities/Optional.cs @@ -6,7 +6,57 @@ namespace Discord { public struct Optional { - public bool IsSpecified { get; private set; } - public T Value { get; set; } + public static Optional Unspecified => default; + + public bool IsSpecified { get; } + private readonly T _innerValue; + + public T Value + { + get + { + if (!IsSpecified) + throw new UnspecifiedOptionalException(); + return _innerValue; + } + } + + public Optional(T value) + { + IsSpecified = true; + _innerValue = value; + } + + public override string ToString() + { + return $""; + } + + public override bool Equals(object obj) + { + if (obj is Optional opt) + { + if (IsSpecified && opt.IsSpecified) + return Value?.Equals(opt.Value) ?? opt.Value == null; + return IsSpecified == opt.IsSpecified; + } + return base.Equals(obj); + } + + public override int GetHashCode() + => IsSpecified ? Value?.GetHashCode() ?? 0 : 0; + + public static bool operator ==(Optional a, Optional b) + => a.Equals(b); + public static bool operator !=(Optional a, Optional b) + => !a.Equals(b); + + // todo: implement comparing, GetValueOrDefault, hash codes etc + } + + + public class UnspecifiedOptionalException : Exception + { + public UnspecifiedOptionalException() : base("An attempt was made to access an unspecified optional value") { } } } diff --git a/test/Discord.Tests.Unit/Discord.Tests.Unit.csproj b/test/Discord.Tests.Unit/Discord.Tests.Unit.csproj new file mode 100644 index 0000000000..0a4bb8e5e4 --- /dev/null +++ b/test/Discord.Tests.Unit/Discord.Tests.Unit.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + diff --git a/test/Discord.Tests.Unit/Serialization/OptionalConverterTests.cs b/test/Discord.Tests.Unit/Serialization/OptionalConverterTests.cs new file mode 100644 index 0000000000..f606bc94b3 --- /dev/null +++ b/test/Discord.Tests.Unit/Serialization/OptionalConverterTests.cs @@ -0,0 +1,118 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Xunit; +using Discord.Serialization; + +namespace Discord.Tests.Unit.Serialization +{ + public class OptionalConverterTests + { + private readonly JsonSerializerOptions _jsonOptions; + + public OptionalConverterTests() + { + _jsonOptions = new JsonSerializerOptions(); + _jsonOptions.Converters.Add(new OptionalConverter()); + } + + public class SampleOptionalClass + { + [JsonPropertyName("optional_number")] + public Optional OptionalNumber { get; set; } + [JsonPropertyName("required_number")] + public int RequiredNumber { get; set; } + + public override bool Equals(object obj) + => (obj is SampleOptionalClass other) && (other.OptionalNumber == OptionalNumber && other.RequiredNumber == RequiredNumber); + public override int GetHashCode() + => OptionalNumber.GetHashCode() ^ RequiredNumber.GetHashCode(); + } + + private string expectedOptionalUnset = "{\"optional_number\":null,\"required_number\":10}"; + private SampleOptionalClass withOptionalUnset = new SampleOptionalClass + { + OptionalNumber = Optional.Unspecified, + RequiredNumber = 10, + }; + private string expectedOptionalSet = "{\"optional_number\":11,\"required_number\":10}"; + private SampleOptionalClass withOptionalSet = new SampleOptionalClass + { + OptionalNumber = new Optional(11), + RequiredNumber = 10, + }; + + [Fact] + public void OptionalConverter_Can_Write() + { + // todo: is STJ deterministic in writing order? want to make sure this test doesn't fail because of cosmic rays + var unsetString = JsonSerializer.Serialize(withOptionalUnset, _jsonOptions); + Assert.Equal(expectedOptionalUnset, unsetString); + + var setString = JsonSerializer.Serialize(withOptionalSet, _jsonOptions); + Assert.Equal(expectedOptionalSet, setString); + } + + [Fact] + public void OptionalConverter_Can_Read() + { + var unset = JsonSerializer.Deserialize(expectedOptionalUnset, _jsonOptions); + Assert.Equal(withOptionalUnset, unset); + + var set = JsonSerializer.Deserialize(expectedOptionalSet, _jsonOptions); + Assert.Equal(withOptionalSet, set); + } + + public class NestedPoco + { + [JsonPropertyName("name")] + public string Name { get; set; } + [JsonPropertyName("age")] + public int Age { get; set; } + + public override bool Equals(object obj) + => (obj is NestedPoco other) && (Name == other.Name && Age == other.Age); + public override int GetHashCode() + => Name.GetHashCode() ^ Age.GetHashCode(); + + } + public class NestedSampleClass + { + [JsonPropertyName("nested")] + public Optional Nested { get; set; } + } + + private string expectedNestedWithUnset = "{\"nested\":null}"; + private NestedSampleClass nestedWithUnset = new NestedSampleClass + { + Nested = Optional.Unspecified + }; + private string expectedNestedWithSet = "{\"nested\":{\"name\":\"Ashley\",\"age\":23}}"; + private NestedSampleClass nestedWithSet = new NestedSampleClass + { + Nested = new Optional(new NestedPoco + { + Name = "Ashley", + Age = 23 + }), + }; + + [Fact] + public void OptionalConverter_Can_Write_Nested_Poco() + { + var unset = JsonSerializer.Serialize(nestedWithUnset, _jsonOptions); + Assert.Equal(expectedNestedWithUnset, unset); + + var set = JsonSerializer.Serialize(nestedWithSet, _jsonOptions); + Assert.Equal(expectedNestedWithSet, set); + } + [Fact] + public void OptionalConverter_Can_Read_Nested_Poco() + { + var unset = JsonSerializer.Deserialize(expectedNestedWithUnset, _jsonOptions); + Assert.Equal(nestedWithUnset.Nested, unset.Nested); + + var set = JsonSerializer.Deserialize(expectedNestedWithSet, _jsonOptions); + Assert.Equal(nestedWithSet.Nested, set.Nested); + } + } +} diff --git a/test/Discord.Tests.Unit/UnitTest1.cs b/test/Discord.Tests.Unit/UnitTest1.cs new file mode 100644 index 0000000000..547a0b053a --- /dev/null +++ b/test/Discord.Tests.Unit/UnitTest1.cs @@ -0,0 +1,13 @@ +using Xunit; + +namespace Discord.Tests.Unit +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + Assert.True(true); + } + } +}