@@ -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; | ||||