| @@ -1,4 +1,4 @@ | |||||
| | |||||
| Microsoft Visual Studio Solution File, Format Version 12.00 | Microsoft Visual Studio Solution File, Format Version 12.00 | ||||
| # Visual Studio 15 | # Visual Studio 15 | ||||
| VisualStudioVersion = 15.0.26014.0 | VisualStudioVersion = 15.0.26014.0 | ||||
| @@ -25,6 +25,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", | |||||
| EndProject | EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\Discord.Net.Tests\Discord.Net.Tests.csproj", "{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}" | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\Discord.Net.Tests\Discord.Net.Tests.csproj", "{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}" | ||||
| EndProject | EndProject | ||||
| Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F66D75C0-E304-46E0-9C3A-294F340DB37D}" | |||||
| EndProject | |||||
| Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Discord.Net.Relay", "src\Discord.Net.Relay\Discord.Net.Relay.csproj", "{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}" | |||||
| EndProject | |||||
| Global | Global | ||||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| Debug|Any CPU = Debug|Any CPU | Debug|Any CPU = Debug|Any CPU | ||||
| @@ -143,6 +147,18 @@ Global | |||||
| {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.Build.0 = Release|x64 | {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.Build.0 = Release|x64 | ||||
| {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.ActiveCfg = Release|x86 | {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.ActiveCfg = Release|x86 | ||||
| {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.Build.0 = Release|x86 | {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.Build.0 = Release|x86 | ||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.ActiveCfg = Debug|x64 | |||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.Build.0 = Debug|x64 | |||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.ActiveCfg = Debug|x86 | |||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.Build.0 = Debug|x86 | |||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.ActiveCfg = Release|x64 | |||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.Build.0 = Release|x64 | |||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.ActiveCfg = Release|x86 | |||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.Build.0 = Release|x86 | |||||
| EndGlobalSection | EndGlobalSection | ||||
| GlobalSection(SolutionProperties) = preSolution | GlobalSection(SolutionProperties) = preSolution | ||||
| HideSolutionNode = FALSE | HideSolutionNode = FALSE | ||||
| @@ -154,5 +170,6 @@ Global | |||||
| {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} | {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} | ||||
| {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} | {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} | ||||
| {ABC9F4B9-2452-4725-B522-754E0A02E282} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} | {ABC9F4B9-2452-4725-B522-754E0A02E282} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} | ||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B} = {F66D75C0-E304-46E0-9C3A-294F340DB37D} | |||||
| EndGlobalSection | EndGlobalSection | ||||
| EndGlobal | EndGlobal | ||||
| @@ -1,5 +1,6 @@ | |||||
| using System.Runtime.CompilerServices; | using System.Runtime.CompilerServices; | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Relay")] | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Rest")] | [assembly: InternalsVisibleTo("Discord.Net.Rest")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Rpc")] | [assembly: InternalsVisibleTo("Discord.Net.Rpc")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] | [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] | ||||
| @@ -0,0 +1,20 @@ | |||||
| using Microsoft.AspNetCore.Builder; | |||||
| using System; | |||||
| namespace Discord.Relay | |||||
| { | |||||
| public static class ApplicationBuilderExtensions | |||||
| { | |||||
| public static void UseDiscordRelay(this IApplicationBuilder app, Action<RelayServer> configAction = null) | |||||
| { | |||||
| var server = new RelayServer(configAction); | |||||
| server.StartAsync(); | |||||
| app.Use(async (context, next) => | |||||
| { | |||||
| if (context.WebSockets.IsWebSocketRequest) | |||||
| await server.AcceptAsync(context); | |||||
| await next(); | |||||
| }); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,3 @@ | |||||
| using System.Runtime.CompilerServices; | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||||
| @@ -0,0 +1,32 @@ | |||||
| <Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||||
| <PropertyGroup> | |||||
| <VersionPrefix>1.0.0</VersionPrefix> | |||||
| <VersionSuffix Condition="'$(BuildNumber)' == ''">rc-dev</VersionSuffix> | |||||
| <VersionSuffix Condition="'$(BuildNumber)' != ''">rc-$(BuildNumber)</VersionSuffix> | |||||
| <TargetFramework>netstandard1.3</TargetFramework> | |||||
| <AssemblyName>Discord.Net.Relay</AssemblyName> | |||||
| <Authors>RogueException</Authors> | |||||
| <Description>A core Discord.Net library containing the Relay server.</Description> | |||||
| <PackageTags>discord;discordapp</PackageTags> | |||||
| <PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl> | |||||
| <PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl> | |||||
| <RepositoryType>git</RepositoryType> | |||||
| <RepositoryUrl>git://github.com/RogueException/Discord.Net</RepositoryUrl> | |||||
| <RootNamespace>Discord.Relay</RootNamespace> | |||||
| <DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | |||||
| <ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" /> | |||||
| <ProjectReference Include="..\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <PackageReference Include="Microsoft.AspNetCore" Version="1.1.0" /> | |||||
| <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="1.0.0" /> | |||||
| </ItemGroup> | |||||
| <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> | |||||
| <NoWarn>$(NoWarn);CS1573;CS1591</NoWarn> | |||||
| <WarningsAsErrors>true</WarningsAsErrors> | |||||
| <GenerateDocumentationFile>true</GenerateDocumentationFile> | |||||
| </PropertyGroup> | |||||
| </Project> | |||||
| @@ -0,0 +1,79 @@ | |||||
| using Discord.API; | |||||
| using Discord.API.Gateway; | |||||
| using Discord.Logging; | |||||
| using System; | |||||
| using System.Net.WebSockets; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| using WebSocketClient = System.Net.WebSockets.WebSocket; | |||||
| namespace Discord.Relay | |||||
| { | |||||
| public class RelayConnection | |||||
| { | |||||
| private readonly RelayServer _server; | |||||
| private readonly WebSocketClient _socket; | |||||
| private readonly CancellationTokenSource _cancelToken; | |||||
| private readonly byte[] _inBuffer, _outBuffer; | |||||
| private readonly Logger _logger; | |||||
| internal RelayConnection(RelayServer server, WebSocketClient socket, int id) | |||||
| { | |||||
| _server = server; | |||||
| _socket = socket; | |||||
| _cancelToken = new CancellationTokenSource(); | |||||
| _inBuffer = new byte[4000]; | |||||
| _outBuffer = new byte[4000]; | |||||
| _logger = server.LogManager.CreateLogger($"Client #{id}"); | |||||
| } | |||||
| internal async Task RunAsync() | |||||
| { | |||||
| await _logger.InfoAsync($"Connected"); | |||||
| var token = _cancelToken.Token; | |||||
| try | |||||
| { | |||||
| var segment = new ArraySegment<byte>(_inBuffer); | |||||
| //Send HELLO | |||||
| await SendAsync(GatewayOpCode.Hello, new HelloEvent { HeartbeatInterval = 15000 }).ConfigureAwait(false); | |||||
| while (_socket.State == WebSocketState.Open) | |||||
| { | |||||
| var result = await _socket.ReceiveAsync(segment, token).ConfigureAwait(false); | |||||
| if (result.MessageType == WebSocketMessageType.Close) | |||||
| await _logger.WarningAsync($"Received Close {result.CloseStatus} ({result.CloseStatusDescription ?? "No Reason"})").ConfigureAwait(false); | |||||
| else | |||||
| await _logger.InfoAsync($"Received {result.Count} bytes"); | |||||
| } | |||||
| } | |||||
| catch (OperationCanceledException) | |||||
| { | |||||
| try { await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).ConfigureAwait(false); } | |||||
| catch { } | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| try { await _socket.CloseAsync(WebSocketCloseStatus.InternalServerError, ex.Message, CancellationToken.None).ConfigureAwait(false); } | |||||
| catch { } | |||||
| } | |||||
| finally | |||||
| { | |||||
| await _logger.InfoAsync($"Disconnected"); | |||||
| } | |||||
| } | |||||
| internal void Stop() | |||||
| { | |||||
| _cancelToken.Cancel(); | |||||
| } | |||||
| private async Task SendAsync(GatewayOpCode opCode, object payload) | |||||
| { | |||||
| var frame = new SocketFrame { Operation = (int)opCode, Payload = payload }; | |||||
| var bytes = _server.Serialize(frame, _outBuffer); | |||||
| var segment = new ArraySegment<byte>(_outBuffer, 0, bytes); | |||||
| await _socket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,103 @@ | |||||
| using Discord.API; | |||||
| using Discord.Logging; | |||||
| using Discord.Net.Rest; | |||||
| using Discord.Net.WebSockets; | |||||
| using Discord.Rest; | |||||
| using Microsoft.AspNetCore.Http; | |||||
| using Newtonsoft.Json; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.IO; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| using WebSocketClient = System.Net.WebSockets.WebSocket; | |||||
| namespace Discord.Relay | |||||
| { | |||||
| public class RelayServer | |||||
| { | |||||
| public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } | |||||
| internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>(); | |||||
| private readonly HashSet<RelayConnection> _connections; | |||||
| private readonly SemaphoreSlim _lock; | |||||
| private readonly JsonSerializer _serializer; | |||||
| private readonly DiscordSocketApiClient _discord; | |||||
| private int _nextId; | |||||
| internal LogManager LogManager { get; } | |||||
| internal RelayServer(Action<RelayServer> configAction) | |||||
| { | |||||
| _connections = new HashSet<RelayConnection>(); | |||||
| _lock = new SemaphoreSlim(1, 1); | |||||
| _serializer = new JsonSerializer(); | |||||
| _discord = new DiscordSocketApiClient( | |||||
| DefaultRestClientProvider.Instance, | |||||
| DefaultWebSocketProvider.Instance, | |||||
| DiscordRestConfig.UserAgent); | |||||
| configAction?.Invoke(this); | |||||
| LogManager = new LogManager(LogSeverity.Debug); | |||||
| LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); | |||||
| } | |||||
| internal async Task AcceptAsync(HttpContext context) | |||||
| { | |||||
| WebSocketClient socket; | |||||
| try | |||||
| { | |||||
| socket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); | |||||
| } | |||||
| catch { return; } | |||||
| var _ = Task.Run(async () => | |||||
| { | |||||
| var conn = new RelayConnection(this, socket, Interlocked.Increment(ref _nextId)); | |||||
| await AddConnection(conn).ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| await conn.RunAsync().ConfigureAwait(false); | |||||
| } | |||||
| finally { await RemoveConnection(conn).ConfigureAwait(false); } | |||||
| }); | |||||
| } | |||||
| internal void StartAsync() | |||||
| { | |||||
| Task.Run(async () => | |||||
| { | |||||
| await _discord.ConnectAsync().ConfigureAwait(false); | |||||
| }); | |||||
| } | |||||
| internal async Task AddConnection(RelayConnection conn) | |||||
| { | |||||
| await _lock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| _connections.Add(conn); | |||||
| } | |||||
| finally { _lock.Release(); } | |||||
| } | |||||
| internal async Task RemoveConnection(RelayConnection conn) | |||||
| { | |||||
| await _lock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| _connections.Remove(conn); | |||||
| } | |||||
| finally { _lock.Release(); } | |||||
| } | |||||
| internal int Serialize(object obj, byte[] buffer) | |||||
| { | |||||
| using (var stream = new MemoryStream(buffer)) | |||||
| using (var writer = new StreamWriter(stream)) | |||||
| { | |||||
| _serializer.Serialize(writer, obj); | |||||
| return (int)stream.Position; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,3 +1,4 @@ | |||||
| using System.Runtime.CompilerServices; | using System.Runtime.CompilerServices; | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Relay")] | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | [assembly: InternalsVisibleTo("Discord.Net.Tests")] | ||||
| @@ -33,10 +33,11 @@ namespace Discord.API | |||||
| public ConnectionState ConnectionState { get; private set; } | public ConnectionState ConnectionState { get; private set; } | ||||
| public DiscordSocketApiClient(RestClientProvider restClientProvider, string userAgent, WebSocketProvider webSocketProvider, | |||||
| RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null) | |||||
| public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent, | |||||
| string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null) | |||||
| : base(restClientProvider, userAgent, defaultRetryMode, serializer) | : base(restClientProvider, userAgent, defaultRetryMode, serializer) | ||||
| { | |||||
| { | |||||
| _gatewayUrl = url; | |||||
| WebSocketClient = webSocketProvider(); | WebSocketClient = webSocketProvider(); | ||||
| //WebSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+) | //WebSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+) | ||||
| WebSocketClient.BinaryMessage += async (data, index, count) => | WebSocketClient.BinaryMessage += async (data, index, count) => | ||||
| @@ -115,9 +116,9 @@ namespace Discord.API | |||||
| ConnectionState = ConnectionState.Connected; | ConnectionState = ConnectionState.Connected; | ||||
| } | } | ||||
| catch (Exception) | |||||
| catch | |||||
| { | { | ||||
| _gatewayUrl = null; //Uncache in case the gateway url changed | |||||
| _gatewayUrl = null; //Uncache in case the gateway url changed | |||||
| await DisconnectInternalAsync().ConfigureAwait(false); | await DisconnectInternalAsync().ConfigureAwait(false); | ||||
| throw; | throw; | ||||
| } | } | ||||
| @@ -142,7 +142,7 @@ namespace Discord.WebSocket | |||||
| _largeGuilds = new ConcurrentQueue<ulong>(); | _largeGuilds = new ConcurrentQueue<ulong>(); | ||||
| } | } | ||||
| private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) | private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) | ||||
| => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent); | |||||
| => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost); | |||||
| protected override async Task OnLoginAsync(TokenType tokenType, string token) | protected override async Task OnLoginAsync(TokenType tokenType, string token) | ||||
| { | { | ||||
| @@ -2,7 +2,6 @@ | |||||
| using Discord.Net.Udp; | using Discord.Net.Udp; | ||||
| using Discord.Net.WebSockets; | using Discord.Net.WebSockets; | ||||
| using Discord.Rest; | using Discord.Rest; | ||||
| using System; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -10,6 +9,9 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| public const string GatewayEncoding = "json"; | public const string GatewayEncoding = "json"; | ||||
| /// <summary> Gets or sets the websocket host to connect to. If null, the client will use the /gateway endpoint. | |||||
| public string GatewayHost { get; set; } = null; | |||||
| /// <summary> Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. </summary> | /// <summary> Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. </summary> | ||||
| public int ConnectionTimeout { get; set; } = 30000; | public int ConnectionTimeout { get; set; } = 30000; | ||||
| @@ -38,41 +40,8 @@ namespace Discord.WebSocket | |||||
| public DiscordSocketConfig() | public DiscordSocketConfig() | ||||
| { | { | ||||
| #if NETSTANDARD1_3 | |||||
| WebSocketProvider = () => | |||||
| { | |||||
| try | |||||
| { | |||||
| return new DefaultWebSocketClient(); | |||||
| } | |||||
| catch (PlatformNotSupportedException ex) | |||||
| { | |||||
| throw new PlatformNotSupportedException("The default websocket provider is not supported on this platform.", ex); | |||||
| } | |||||
| }; | |||||
| UdpSocketProvider = () => | |||||
| { | |||||
| try | |||||
| { | |||||
| return new DefaultUdpSocket(); | |||||
| } | |||||
| catch (PlatformNotSupportedException ex) | |||||
| { | |||||
| throw new PlatformNotSupportedException("The default UDP provider is not supported on this platform.", ex); | |||||
| } | |||||
| }; | |||||
| #else | |||||
| WebSocketProvider = () => | |||||
| { | |||||
| throw new PlatformNotSupportedException("The default websocket provider is not supported on this platform.\n" + | |||||
| "You must specify a WebSocketProvider or target a runtime supporting .NET Standard 1.3, such as .NET Framework 4.6+."); | |||||
| }; | |||||
| UdpSocketProvider = () => | |||||
| { | |||||
| throw new PlatformNotSupportedException("The default UDP provider is not supported on this platform.\n" + | |||||
| "You must specify a UdpSocketProvider or target a runtime supporting .NET Standard 1.3, such as .NET Framework 4.6+."); | |||||
| }; | |||||
| #endif | |||||
| WebSocketProvider = DefaultWebSocketProvider.Instance; | |||||
| UdpSocketProvider = DefaultUdpSocketProvider.Instance; | |||||
| } | } | ||||
| internal DiscordSocketConfig Clone() => MemberwiseClone() as DiscordSocketConfig; | internal DiscordSocketConfig Clone() => MemberwiseClone() as DiscordSocketConfig; | ||||