| @@ -1,4 +1,4 @@ | |||
| | |||
| Microsoft Visual Studio Solution File, Format Version 12.00 | |||
| # Visual Studio 15 | |||
| VisualStudioVersion = 15.0.26014.0 | |||
| @@ -25,6 +25,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", | |||
| EndProject | |||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\Discord.Net.Tests\Discord.Net.Tests.csproj", "{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}" | |||
| 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 | |||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||
| 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|x86.ActiveCfg = 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 | |||
| GlobalSection(SolutionProperties) = preSolution | |||
| HideSolutionNode = FALSE | |||
| @@ -154,5 +170,6 @@ Global | |||
| {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} | |||
| {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {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 | |||
| EndGlobal | |||
| @@ -1,5 +1,6 @@ | |||
| using System.Runtime.CompilerServices; | |||
| [assembly: InternalsVisibleTo("Discord.Net.Relay")] | |||
| [assembly: InternalsVisibleTo("Discord.Net.Rest")] | |||
| [assembly: InternalsVisibleTo("Discord.Net.Rpc")] | |||
| [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; | |||
| [assembly: InternalsVisibleTo("Discord.Net.Relay")] | |||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||
| @@ -33,10 +33,11 @@ namespace Discord.API | |||
| 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) | |||
| { | |||
| { | |||
| _gatewayUrl = url; | |||
| WebSocketClient = webSocketProvider(); | |||
| //WebSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+) | |||
| WebSocketClient.BinaryMessage += async (data, index, count) => | |||
| @@ -115,9 +116,9 @@ namespace Discord.API | |||
| 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); | |||
| throw; | |||
| } | |||
| @@ -142,7 +142,7 @@ namespace Discord.WebSocket | |||
| _largeGuilds = new ConcurrentQueue<ulong>(); | |||
| } | |||
| 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) | |||
| { | |||
| @@ -2,7 +2,6 @@ | |||
| using Discord.Net.Udp; | |||
| using Discord.Net.WebSockets; | |||
| using Discord.Rest; | |||
| using System; | |||
| namespace Discord.WebSocket | |||
| { | |||
| @@ -10,6 +9,9 @@ namespace Discord.WebSocket | |||
| { | |||
| 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> | |||
| public int ConnectionTimeout { get; set; } = 30000; | |||
| @@ -38,41 +40,8 @@ namespace Discord.WebSocket | |||
| 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; | |||