Browse Source

Prep for WebSocket client, Several MessageQueue fixes

tags/1.0-rc
RogueException 9 years ago
parent
commit
c01e3c069d
45 changed files with 1994 additions and 160 deletions
  1. +38
    -29
      src/Discord.Net/API/DiscordRawClient.cs
  2. +9
    -0
      src/Discord.Net/API/IWebSocketMessage.cs
  3. +23
    -0
      src/Discord.Net/API/WebSocketMessage.cs
  4. +1
    -1
      src/Discord.Net/Common/Entities/Guilds/IGuildIntegration.cs
  5. +0
    -7
      src/Discord.Net/Common/Entities/Guilds/IIntegrationAccount.cs
  6. +2
    -2
      src/Discord.Net/Common/Entities/Guilds/IntegrationAccount.cs
  7. +0
    -0
      src/Discord.Net/Common/Entities/Guilds/VoiceRegion.cs
  8. +2
    -2
      src/Discord.Net/Common/Entities/Invites/Invite.cs
  9. +2
    -2
      src/Discord.Net/Common/Entities/Invites/PublicInvite.cs
  10. +1
    -1
      src/Discord.Net/Common/Entities/Permissions/ChannelPermissions.cs
  11. +5
    -6
      src/Discord.Net/Common/Entities/Users/Connection.cs
  12. +1
    -1
      src/Discord.Net/Common/Entities/Users/IConnection.cs
  13. +24
    -6
      src/Discord.Net/Discord.Net.csproj
  14. +3
    -1
      src/Discord.Net/DiscordConfig.cs
  15. +1
    -1
      src/Discord.Net/Net/Converters/UInt64ArrayConverter.cs
  16. +5
    -7
      src/Discord.Net/Net/Rest/DefaultRestClient.cs
  17. +3
    -2
      src/Discord.Net/Net/Rest/IRestClient.cs
  18. +36
    -5
      src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs
  19. +12
    -45
      src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs
  20. +3
    -1
      src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs
  21. +1
    -1
      src/Discord.Net/Net/Rest/RestClientProvider.cs
  22. +23
    -21
      src/Discord.Net/Rest/DiscordClient.cs
  23. +2
    -5
      src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs
  24. +1
    -1
      src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs
  25. +3
    -7
      src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs
  26. +1
    -1
      src/Discord.Net/Rest/Entities/Invites/GuildInvite.cs
  27. +0
    -1
      src/Discord.Net/Rest/Entities/Message.cs
  28. +1
    -4
      src/Discord.Net/Rest/Entities/Users/User.cs
  29. +71
    -0
      src/Discord.Net/WebSocket/Caches/ChannelPermissionsCache.cs
  30. +98
    -0
      src/Discord.Net/WebSocket/Caches/MessageCache.cs
  31. +139
    -0
      src/Discord.Net/WebSocket/DiscordClient.cs
  32. +141
    -0
      src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs
  33. +171
    -0
      src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs
  34. +122
    -0
      src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs
  35. +45
    -0
      src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs
  36. +374
    -0
      src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs
  37. +87
    -0
      src/Discord.Net/WebSocket/Entities/Guilds/GuildIntegration.cs
  38. +52
    -0
      src/Discord.Net/WebSocket/Entities/Invites/GuildInvite.cs
  39. +146
    -0
      src/Discord.Net/WebSocket/Entities/Message.cs
  40. +80
    -0
      src/Discord.Net/WebSocket/Entities/Role.cs
  41. +20
    -0
      src/Discord.Net/WebSocket/Entities/Users/DMUser.cs
  42. +117
    -0
      src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs
  43. +15
    -0
      src/Discord.Net/WebSocket/Entities/Users/PublicUser.cs
  44. +48
    -0
      src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs
  45. +65
    -0
      src/Discord.Net/WebSocket/Entities/Users/User.cs

+ 38
- 29
src/Discord.Net/API/DiscordRawClient.cs View File

@@ -20,23 +20,22 @@ namespace Discord.API
public class DiscordRawClient
{
internal event EventHandler<SentRequestEventArgs> SentRequest;
private readonly RequestQueue _requestQueue;
private readonly IRestClient _restClient;
private readonly CancellationToken _cancelToken;
private readonly JsonSerializer _serializer;
private IRestClient _restClient;
private CancellationToken _cancelToken;

public TokenType AuthTokenType { get; private set; }
public IRestClient RestClient { get; private set; }
public IRequestQueue RequestQueue { get; private set; }
internal DiscordRawClient(RestClientProvider restClientProvider, CancellationToken cancelToken)
internal DiscordRawClient(RestClientProvider restClientProvider)
{
_cancelToken = cancelToken;

_restClient = restClientProvider(DiscordConfig.ClientAPIUrl, cancelToken);
_restClient = restClientProvider(DiscordConfig.ClientAPIUrl);
_restClient.SetHeader("accept", "*/*");
_restClient.SetHeader("user-agent", DiscordConfig.UserAgent);

_requestQueue = new RequestQueue(_restClient);

_serializer = new JsonSerializer();
@@ -53,29 +52,40 @@ namespace Discord.API
_serializer.ContractResolver = new OptionalContractResolver();
}

public void SetToken(TokenType tokenType, string token)
public async Task Login(TokenType tokenType, string token, CancellationToken cancelToken)
{
AuthTokenType = tokenType;

if (token != null)
_cancelToken = cancelToken;
await _requestQueue.SetCancelToken(cancelToken).ConfigureAwait(false);
switch (tokenType)
{
switch (tokenType)
{
case TokenType.Bot:
token = $"Bot {token}";
break;
case TokenType.Bearer:
token = $"Bearer {token}";
break;
case TokenType.User:
break;
default:
throw new ArgumentException("Unknown oauth token type", nameof(tokenType));
}
case TokenType.Bot:
token = $"Bot {token}";
break;
case TokenType.Bearer:
token = $"Bearer {token}";
break;
case TokenType.User:
break;
default:
throw new ArgumentException("Unknown oauth token type", nameof(tokenType));
}

_restClient.SetHeader("authorization", token);
}
public async Task Login(LoginParams args, CancellationToken cancelToken)
{
var response = await Send<LoginResponse>("POST", "auth/login", args).ConfigureAwait(false);

AuthTokenType = TokenType.User;
_restClient.SetHeader("authorization", response.Token);
}
public async Task Logout()
{
await _requestQueue.Clear().ConfigureAwait(false);
_restClient = null;
}

//Core
public Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General)
@@ -121,6 +131,8 @@ namespace Discord.API

private async Task<Stream> SendInternal(string method, string endpoint, object payload, bool headerOnly, BucketGroup group, int bucketId, ulong guildId)
{
_cancelToken.ThrowIfCancellationRequested();

var stopwatch = Stopwatch.StartNew();
string json = null;
if (payload != null)
@@ -136,6 +148,8 @@ namespace Discord.API
}
private async Task<Stream> SendInternal(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, bool headerOnly, BucketGroup group, int bucketId, ulong guildId)
{
_cancelToken.ThrowIfCancellationRequested();

var stopwatch = Stopwatch.StartNew();
var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, multipartArgs, headerOnly), group, bucketId, guildId).ConfigureAwait(false);
int bytes = headerOnly ? 0 : (int)responseStream.Length;
@@ -149,11 +163,6 @@ namespace Discord.API


//Auth
public async Task Login(LoginParams args)
{
var response = await Send<LoginResponse>("POST", "auth/login", args).ConfigureAwait(false);
SetToken(TokenType.User, response.Token);
}
public async Task ValidateToken()
{
await Send("GET", "auth/login").ConfigureAwait(false);


+ 9
- 0
src/Discord.Net/API/IWebSocketMessage.cs View File

@@ -0,0 +1,9 @@
namespace Discord.API
{
public interface IWebSocketMessage
{
int OpCode { get; }
object Payload { get; }
bool IsPrivate { get; }
}
}

+ 23
- 0
src/Discord.Net/API/WebSocketMessage.cs View File

@@ -0,0 +1,23 @@
using Newtonsoft.Json;

namespace Discord.API
{
public class WebSocketMessage
{
[JsonProperty("op")]
public int? Operation { get; set; }
[JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)]
public string Type { get; set; }
[JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)]
public uint? Sequence { get; set; }
[JsonProperty("d")]
public object Payload { get; set; }

public WebSocketMessage() { }
public WebSocketMessage(IWebSocketMessage msg)
{
Operation = msg.OpCode;
Payload = msg.Payload;
}
}
}

+ 1
- 1
src/Discord.Net/Common/Entities/Guilds/IGuildIntegration.cs View File

@@ -12,10 +12,10 @@ namespace Discord
ulong ExpireBehavior { get; }
ulong ExpireGracePeriod { get; }
DateTime SyncedAt { get; }
IntegrationAccount Account { get; }

IGuild Guild { get; }
IUser User { get; }
IRole Role { get; }
IIntegrationAccount Account { get; }
}
}

+ 0
- 7
src/Discord.Net/Common/Entities/Guilds/IIntegrationAccount.cs View File

@@ -1,7 +0,0 @@
namespace Discord
{
public interface IIntegrationAccount : IEntity<string>
{
string Name { get; }
}
}

src/Discord.Net/Rest/Entities/Guilds/IntegrationAccount.cs → src/Discord.Net/Common/Entities/Guilds/IntegrationAccount.cs View File

@@ -1,6 +1,6 @@
namespace Discord.Rest
namespace Discord
{
public class IntegrationAccount : IIntegrationAccount
public struct IntegrationAccount
{
/// <inheritdoc />
public string Id { get; }

src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs → src/Discord.Net/Common/Entities/Guilds/VoiceRegion.cs View File


src/Discord.Net/Rest/Entities/Invites/Invite.cs → src/Discord.Net/Common/Entities/Invites/Invite.cs View File

@@ -1,7 +1,7 @@
using System.Threading.Tasks;
using Model = Discord.API.Invite;

namespace Discord.Rest
namespace Discord
{
public abstract class Invite : IInvite
{
@@ -17,7 +17,7 @@ namespace Discord.Rest
/// <inheritdoc />
public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null;

internal abstract DiscordClient Discord { get; }
internal abstract IDiscordClient Discord { get; }

internal Invite(Model model)
{

src/Discord.Net/Rest/Entities/Invites/PublicInvite.cs → src/Discord.Net/Common/Entities/Invites/PublicInvite.cs View File

@@ -15,9 +15,9 @@ namespace Discord.Rest
/// <inheritdoc />
public ulong ChannelId => _channelId;

internal override DiscordClient Discord { get; }
internal override IDiscordClient Discord { get; }

internal PublicInvite(DiscordClient discord, Model model)
internal PublicInvite(IDiscordClient discord, Model model)
: base(model)
{
Discord = discord;

+ 1
- 1
src/Discord.Net/Common/Entities/Permissions/ChannelPermissions.cs View File

@@ -18,7 +18,7 @@ namespace Discord
{
case ITextChannel _: return _allText;
case IVoiceChannel _: return _allVoice;
case IDMChannel _: return _allDM;
case IGuildChannel _: return _allDM;
default:
throw new ArgumentException("Unknown channel type", nameof(channel));
}


src/Discord.Net/Rest/Entities/Users/Connection.cs → src/Discord.Net/Common/Entities/Users/Connection.cs View File

@@ -6,12 +6,11 @@ namespace Discord.Rest
public class Connection : IConnection
{
public string Id { get; }
public string Type { get; }
public string Name { get; }
public bool IsRevoked { get; }

public string Type { get; private set; }
public string Name { get; private set; }
public bool IsRevoked { get; private set; }

public IEnumerable<ulong> Integrations { get; private set; }
public IEnumerable<ulong> IntegrationIds { get; }

public Connection(Model model)
{
@@ -21,7 +20,7 @@ namespace Discord.Rest
Name = model.Name;
IsRevoked = model.Revoked;

Integrations = model.Integrations;
IntegrationIds = model.Integrations;
}

public override string ToString() => $"{Name ?? Id.ToString()} ({Type})";

+ 1
- 1
src/Discord.Net/Common/Entities/Users/IConnection.cs View File

@@ -9,6 +9,6 @@ namespace Discord
string Name { get; }
bool IsRevoked { get; }

IEnumerable<ulong> Integrations { get; }
IEnumerable<ulong> IntegrationIds { get; }
}
}

+ 24
- 6
src/Discord.Net/Discord.Net.csproj View File

@@ -67,6 +67,7 @@
<Compile Include="API\Common\VoiceRegion.cs" />
<Compile Include="API\Common\VoiceState.cs" />
<Compile Include="API\IOptional.cs" />
<Compile Include="API\IWebSocketMessage.cs" />
<Compile Include="API\Optional.cs" />
<Compile Include="API\Rest\DeleteMessagesParam.cs" />
<Compile Include="API\Rest\GetGuildMembersParams.cs" />
@@ -98,6 +99,7 @@
<Compile Include="API\Rest\ModifyMessageParams.cs" />
<Compile Include="API\Rest\ModifyTextChannelParams.cs" />
<Compile Include="API\Rest\ModifyVoiceChannelParams.cs" />
<Compile Include="API\WebSocketMessage.cs" />
<Compile Include="DiscordConfig.cs" />
<Compile Include="API\DiscordRawClient.cs" />
<Compile Include="Net\Converters\OptionalContractResolver.cs" />
@@ -111,7 +113,6 @@
<Compile Include="Net\Rest\RequestQueue\RestRequest.cs" />
<Compile Include="Rest\DiscordClient.cs" />
<Compile Include="Common\Entities\Guilds\IGuildEmbed.cs" />
<Compile Include="Common\Entities\Guilds\IIntegrationAccount.cs" />
<Compile Include="Common\Entities\Users\IConnection.cs" />
<Compile Include="Common\Entities\Guilds\IGuildIntegration.cs" />
<Compile Include="Common\Entities\Invites\IPublicInvite.cs" />
@@ -161,19 +162,19 @@
<Compile Include="Rest\Entities\Channels\VoiceChannel.cs" />
<Compile Include="Rest\Entities\Guilds\GuildEmbed.cs" />
<Compile Include="Rest\Entities\Guilds\GuildIntegration.cs" />
<Compile Include="Rest\Entities\Guilds\IntegrationAccount.cs" />
<Compile Include="Rest\Entities\Users\Connection.cs" />
<Compile Include="Common\Entities\Guilds\IntegrationAccount.cs" />
<Compile Include="Common\Entities\Users\Connection.cs" />
<Compile Include="Common\Helpers\PermissionHelper.cs" />
<Compile Include="Rest\Entities\Invites\GuildInvite.cs" />
<Compile Include="Rest\Entities\Invites\Invite.cs" />
<Compile Include="Rest\Entities\Invites\PublicInvite.cs" />
<Compile Include="Common\Entities\Invites\Invite.cs" />
<Compile Include="Common\Entities\Invites\PublicInvite.cs" />
<Compile Include="Rest\Entities\Message.cs" />
<Compile Include="Rest\Entities\Role.cs" />
<Compile Include="Rest\Entities\Guilds\UserGuild.cs" />
<Compile Include="Rest\Entities\Users\DMUser.cs" />
<Compile Include="Rest\Entities\Users\GuildUser.cs" />
<Compile Include="Rest\Entities\Users\PublicUser.cs" />
<Compile Include="Rest\Entities\Guilds\VoiceRegion.cs" />
<Compile Include="Common\Entities\Guilds\VoiceRegion.cs" />
<Compile Include="Common\Events\LogMessageEventArgs.cs" />
<Compile Include="Common\Events\SentRequestEventArgs.cs" />
<Compile Include="Common\Helpers\DateTimeHelper.cs" />
@@ -204,6 +205,23 @@
<Compile Include="Rest\Entities\Users\SelfUser.cs" />
<Compile Include="Rest\Entities\Users\User.cs" />
<Compile Include="TokenType.cs" />
<Compile Include="WebSocket\Caches\MessageCache.cs" />
<Compile Include="WebSocket\Caches\ChannelPermissionsCache.cs" />
<Compile Include="WebSocket\DiscordClient.cs" />
<Compile Include="WebSocket\Entities\Channels\DMChannel.cs" />
<Compile Include="WebSocket\Entities\Channels\GuildChannel.cs" />
<Compile Include="WebSocket\Entities\Channels\TextChannel.cs" />
<Compile Include="WebSocket\Entities\Channels\VoiceChannel.cs" />
<Compile Include="WebSocket\Entities\Guilds\Guild.cs" />
<Compile Include="WebSocket\Entities\Guilds\GuildIntegration.cs" />
<Compile Include="WebSocket\Entities\Invites\GuildInvite.cs" />
<Compile Include="WebSocket\Entities\Users\DMUser.cs" />
<Compile Include="WebSocket\Entities\Users\GuildUser.cs" />
<Compile Include="WebSocket\Entities\Users\PublicUser.cs" />
<Compile Include="WebSocket\Entities\Users\SelfUser.cs" />
<Compile Include="WebSocket\Entities\Users\User.cs" />
<Compile Include="WebSocket\Entities\Message.cs" />
<Compile Include="WebSocket\Entities\Role.cs" />
</ItemGroup>
<ItemGroup>
<None Include="Common\Entities\Users\IVoiceState.cs.old" />


+ 3
- 1
src/Discord.Net/DiscordConfig.cs View File

@@ -3,6 +3,8 @@ using System.Reflection;

namespace Discord
{
//TODO: Add socket config items in their own class

public class DiscordConfig
{
public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown";
@@ -26,6 +28,6 @@ namespace Discord
public LogSeverity LogLevel { get; set; } = LogSeverity.Info;

/// <summary> Gets or sets the provider used to generate new REST connections. </summary>
public RestClientProvider RestClientProvider { get; set; } = (url, ct) => new DefaultRestClient(url, ct);
public RestClientProvider RestClientProvider { get; set; } = url => new DefaultRestClient(url);
}
}

+ 1
- 1
src/Discord.Net/Net/Converters/UInt64ArrayConverter.cs View File

@@ -5,7 +5,7 @@ using System.Globalization;

namespace Discord.Net.Converters
{
internal class UInt64ArrayConverter : JsonConverter
public class UInt64ArrayConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable<ulong[]>);
public override bool CanRead => true;


+ 5
- 7
src/Discord.Net/Net/Rest/DefaultRestClient.cs View File

@@ -17,13 +17,11 @@ namespace Discord.Net.Rest

protected readonly HttpClient _client;
protected readonly string _baseUrl;
protected readonly CancellationToken _cancelToken;
protected bool _isDisposed;

public DefaultRestClient(string baseUrl, CancellationToken cancelToken)
public DefaultRestClient(string baseUrl)
{
_baseUrl = baseUrl;
_cancelToken = cancelToken;

_client = new HttpClient(new HttpClientHandler
{
@@ -56,18 +54,18 @@ namespace Discord.Net.Rest
_client.DefaultRequestHeaders.Add(key, value);
}

public async Task<Stream> Send(string method, string endpoint, string json = null, bool headerOnly = false)
public async Task<Stream> Send(string method, string endpoint, CancellationToken cancelToken, string json = null, bool headerOnly = false)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
if (json != null)
restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json");
return await SendInternal(restRequest, _cancelToken, headerOnly).ConfigureAwait(false);
return await SendInternal(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
}

public async Task<Stream> Send(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, bool headerOnly = false)
public async Task<Stream> Send(string method, string endpoint, CancellationToken cancelToken, IReadOnlyDictionary<string, object> multipartParams, bool headerOnly = false)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
@@ -97,7 +95,7 @@ namespace Discord.Net.Rest
}
}
restRequest.Content = content;
return await SendInternal(restRequest, _cancelToken, headerOnly).ConfigureAwait(false);
return await SendInternal(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
}



+ 3
- 2
src/Discord.Net/Net/Rest/IRestClient.cs View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Net.Rest
@@ -9,7 +10,7 @@ namespace Discord.Net.Rest
{
void SetHeader(string key, string value);

Task<Stream> Send(string method, string endpoint, string json = null, bool headerOnly = false);
Task<Stream> Send(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, bool headerOnly = false);
Task<Stream> Send(string method, string endpoint, CancellationToken cancelToken, string json = null, bool headerOnly = false);
Task<Stream> Send(string method, string endpoint, CancellationToken cancelToken, IReadOnlyDictionary<string, object> multipartParams, bool headerOnly = false);
}
}

+ 36
- 5
src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs View File

@@ -8,9 +8,12 @@ namespace Discord.Net.Rest
{
public class RequestQueue : IRequestQueue
{
private SemaphoreSlim _lock;
private RequestQueueBucket[] _globalBuckets;
private Dictionary<ulong, RequestQueueBucket>[] _guildBuckets;
private readonly SemaphoreSlim _lock;
private readonly RequestQueueBucket[] _globalBuckets;
private readonly Dictionary<ulong, RequestQueueBucket>[] _guildBuckets;
private CancellationTokenSource _clearToken;
private CancellationToken? _parentToken;
private CancellationToken _cancelToken;

public IRestClient RestClient { get; }

@@ -21,12 +24,26 @@ namespace Discord.Net.Rest
_lock = new SemaphoreSlim(1, 1);
_globalBuckets = new RequestQueueBucket[Enum.GetValues(typeof(GlobalBucket)).Length];
_guildBuckets = new Dictionary<ulong, RequestQueueBucket>[Enum.GetValues(typeof(GuildBucket)).Length];
_clearToken = new CancellationTokenSource();
_cancelToken = _clearToken.Token;
}
internal async Task SetCancelToken(CancellationToken cancelToken)
{
await Lock().ConfigureAwait(false);
try
{
_parentToken = cancelToken;
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token).Token;
}
finally { Unlock(); }
}
internal async Task<Stream> Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId)
{
RequestQueueBucket bucket;

request.CancelToken = _cancelToken;

await Lock().ConfigureAwait(false);
try
{
@@ -129,6 +146,20 @@ namespace Discord.Net.Rest
_lock.Release();
}

public async Task Clear()
{
await Lock().ConfigureAwait(false);
try
{
_clearToken?.Cancel();
_clearToken = new CancellationTokenSource();
if (_parentToken != null)
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken.Value).Token;
else
_cancelToken = _clearToken.Token;
}
finally { Unlock(); }
}
public async Task Clear(GlobalBucket type)
{
var bucket = _globalBuckets[(int)type];
@@ -136,7 +167,7 @@ namespace Discord.Net.Rest
{
try
{
await bucket.Lock();
await bucket.Lock().ConfigureAwait(false);
bucket.Clear();
}
finally { bucket.Unlock(); }
@@ -152,7 +183,7 @@ namespace Discord.Net.Rest
{
try
{
await bucket.Lock();
await bucket.Lock().ConfigureAwait(false);
bucket.Clear();
}
finally { bucket.Unlock(); }


+ 12
- 45
src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs View File

@@ -16,7 +16,7 @@ namespace Discord.Net.Rest
private readonly ConcurrentQueue<RestRequest> _queue;
private readonly SemaphoreSlim _lock;
private Task _resetTask;
private bool _waitingToProcess, _destroyed; //TODO: Remove _destroyed
private bool _waitingToProcess;
private int _id;

public int WindowMaxCount { get; }
@@ -49,11 +49,7 @@ namespace Discord.Net.Rest

public void Queue(RestRequest request)
{
if (_destroyed) throw new Exception();
//Assume this obj's parent is under lock

_queue.Enqueue(request);
Debug($"Request queued ({WindowCount}/{WindowMaxCount} + {_queue.Count})");
}
public async Task ProcessQueue(bool acquireLock = false)
{
@@ -81,12 +77,17 @@ namespace Discord.Net.Rest

try
{
Stream stream;
if (request.IsMultipart)
stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.MultipartParams, request.HeaderOnly).ConfigureAwait(false);
if (request.CancelToken.IsCancellationRequested)
request.Promise.SetException(new OperationCanceledException(request.CancelToken));
else
stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.Json, request.HeaderOnly).ConfigureAwait(false);
request.Promise.SetResult(stream);
{
Stream stream;
if (request.IsMultipart)
stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.CancelToken, request.MultipartParams, request.HeaderOnly).ConfigureAwait(false);
else
stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.CancelToken, request.Json, request.HeaderOnly).ConfigureAwait(false);
request.Promise.SetResult(stream);
}
}
catch (HttpRateLimitException ex) //Preemptive check failed, use Discord's time instead of our own
{
@@ -94,17 +95,13 @@ namespace Discord.Net.Rest
var task = _resetTask;
if (task != null)
{
Debug($"External rate limit: Extended to {ex.RetryAfterMilliseconds} ms");
var retryAfter = DateTime.UtcNow.AddMilliseconds(ex.RetryAfterMilliseconds);
await task.ConfigureAwait(false);
int millis = (int)Math.Ceiling((DateTime.UtcNow - retryAfter).TotalMilliseconds);
_resetTask = ResetAfter(millis);
}
else
{
Debug($"External rate limit: Reset in {ex.RetryAfterMilliseconds} ms");
_resetTask = ResetAfter(ex.RetryAfterMilliseconds);
}
return;
}
catch (HttpException ex)
@@ -128,13 +125,11 @@ namespace Discord.Net.Rest
_queue.TryDequeue(out request);
WindowCount++;
nextRetry = 1000;
Debug($"Request succeeded ({WindowCount}/{WindowMaxCount} + {_queue.Count})");

if (WindowCount == 1 && WindowSeconds > 0)
{
//First request for this window, schedule a reset
_resetTask = ResetAfter(WindowSeconds * 1000);
Debug($"Internal rate limit: Reset in {WindowSeconds * 1000} ms");
}
}

@@ -145,11 +140,7 @@ namespace Discord.Net.Rest
{
await _parent.Lock().ConfigureAwait(false);
if (_queue.IsEmpty) //Double check, in case a request was queued before we got both locks
{
Debug($"Destroy");
_parent.DestroyGuildBucket((GuildBucket)_bucketId, _guildId);
_destroyed = true;
}
}
finally
{
@@ -179,8 +170,6 @@ namespace Discord.Net.Rest
{
await Lock().ConfigureAwait(false);

Debug($"Reset");

//Reset the current window count and set our state back to normal
WindowCount = 0;
_resetTask = null;
@@ -188,10 +177,7 @@ namespace Discord.Net.Rest
//Wait is over, work through the current queue
await ProcessQueue().ConfigureAwait(false);
}
finally
{
Unlock();
}
finally { Unlock(); }
}

public async Task Lock()
@@ -202,24 +188,5 @@ namespace Discord.Net.Rest
{
_lock.Release();
}

//TODO: Remove
private void Debug(string text)
{
string name;
switch (_bucketGroup)
{
case BucketGroup.Global:
name = ((GlobalBucket)_bucketId).ToString();
break;
case BucketGroup.Guild:
name = ((GuildBucket)_bucketId).ToString();
break;
default:
name = "Unknown";
break;
}
System.Diagnostics.Debug.WriteLine($"[{name} {_id}] {text}");
}
}
}

+ 3
- 1
src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs View File

@@ -1,15 +1,17 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Net.Rest
{
internal struct RestRequest
internal class RestRequest
{
public string Method { get; }
public string Endpoint { get; }
public string Json { get; }
public bool HeaderOnly { get; }
public CancellationToken CancelToken { get; internal set; }
public IReadOnlyDictionary<string, object> MultipartParams { get; }
public TaskCompletionSource<Stream> Promise { get; }



+ 1
- 1
src/Discord.Net/Net/Rest/RestClientProvider.cs View File

@@ -2,5 +2,5 @@

namespace Discord.Net.Rest
{
public delegate IRestClient RestClientProvider(string baseUrl, CancellationToken cancelToken);
public delegate IRestClient RestClientProvider(string baseUrl);
}

+ 23
- 21
src/Discord.Net/Rest/DiscordClient.cs View File

@@ -1,13 +1,10 @@
using Discord.API.Rest;
using Discord.Logging;
using Discord.Net;
using Discord.Net.Rest;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

@@ -43,7 +40,7 @@ namespace Discord.Rest
_connectionLock = new SemaphoreSlim(1, 1);
_log = new LogManager(config.LogLevel);
_userAgent = DiscordConfig.UserAgent;
BaseClient = new API.DiscordRawClient(_restClientProvider, _cancelTokenSource.Token);
BaseClient = new API.DiscordRawClient(_restClientProvider);

_log.Message += (s,e) => Log.Raise(this, e);
}
@@ -69,38 +66,43 @@ namespace Discord.Rest
private async Task LoginInternal(string email, string password)
{
if (IsLoggedIn)
LogoutInternal();
await LogoutInternal().ConfigureAwait(false);
try
{
var cancelTokenSource = new CancellationTokenSource();
_cancelTokenSource = new CancellationTokenSource();

var args = new LoginParams { Email = email, Password = password };
await BaseClient.Login(args).ConfigureAwait(false);
await CompleteLogin(cancelTokenSource, false).ConfigureAwait(false);
await BaseClient.Login(args, _cancelTokenSource.Token).ConfigureAwait(false);
await CompleteLogin(false).ConfigureAwait(false);
}
catch { LogoutInternal(); throw; }
catch { await LogoutInternal().ConfigureAwait(false); throw; }
}
private async Task LoginInternal(TokenType tokenType, string token, bool validateToken)
{
if (IsLoggedIn)
LogoutInternal();
await LogoutInternal().ConfigureAwait(false);
try
{
var cancelTokenSource = new CancellationTokenSource();
_cancelTokenSource = new CancellationTokenSource();

BaseClient.SetToken(tokenType, token);
await CompleteLogin(cancelTokenSource, validateToken).ConfigureAwait(false);
await BaseClient.Login(tokenType, token, _cancelTokenSource.Token).ConfigureAwait(false);
await CompleteLogin(validateToken).ConfigureAwait(false);
}
catch { LogoutInternal(); throw; }
catch { await LogoutInternal().ConfigureAwait(false); throw; }
}
private async Task CompleteLogin(CancellationTokenSource cancelTokenSource, bool validateToken)
private async Task CompleteLogin(bool validateToken)
{
BaseClient.SentRequest += (s, e) => _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms");

if (validateToken)
await BaseClient.ValidateToken().ConfigureAwait(false);

_cancelTokenSource = cancelTokenSource;
{
try
{
await BaseClient.ValidateToken().ConfigureAwait(false);
}
catch { await BaseClient.Logout().ConfigureAwait(false); }
}
IsLoggedIn = true;
LoggedIn.Raise(this);
}
@@ -111,11 +113,11 @@ namespace Discord.Rest
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
LogoutInternal();
await LogoutInternal().ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private void LogoutInternal()
private async Task LogoutInternal()
{
bool wasLoggedIn = IsLoggedIn;

@@ -125,7 +127,7 @@ namespace Discord.Rest
catch { }
}

BaseClient.SetToken(TokenType.User, null);
await BaseClient.Logout().ConfigureAwait(false);
_currentUser = null;

if (wasLoggedIn)


+ 2
- 5
src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs View File

@@ -1,7 +1,7 @@
using System;
using Model = Discord.API.GuildEmbed;

namespace Discord.Rest
namespace Discord
{
public class GuildEmbed : IGuildEmbed
{
@@ -12,14 +12,11 @@ namespace Discord.Rest
/// <inheritdoc />
public ulong? ChannelId { get; private set; }

internal DiscordClient Discord { get; }

/// <inheritdoc />
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);

internal GuildEmbed(DiscordClient discord, Model model)
internal GuildEmbed(Model model)
{
Discord = discord;
Update(model);
}



+ 1
- 1
src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs View File

@@ -82,6 +82,6 @@ namespace Discord.Rest
IGuild IGuildIntegration.Guild => Guild;
IRole IGuildIntegration.Role => Role;
IUser IGuildIntegration.User => User;
IIntegrationAccount IGuildIntegration.Account => Account;
IntegrationAccount IGuildIntegration.Account => Account;
}
}

+ 3
- 7
src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs View File

@@ -2,7 +2,7 @@
using System.Threading.Tasks;
using Model = Discord.API.UserGuild;

namespace Discord.Rest
namespace Discord
{
public class UserGuild : IUserGuild
{
@@ -10,7 +10,7 @@ namespace Discord.Rest

/// <inheritdoc />
public ulong Id { get; }
internal DiscordClient Discord { get; }
internal IDiscordClient Discord { get; }

/// <inheritdoc />
public string Name { get; private set; }
@@ -22,7 +22,7 @@ namespace Discord.Rest
/// <inheritdoc />
public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId);

internal UserGuild(DiscordClient discord, Model model)
internal UserGuild(IDiscordClient discord, Model model)
{
Discord = discord;
Id = model.Id;
@@ -40,15 +40,11 @@ namespace Discord.Rest
/// <inheritdoc />
public async Task Leave()
{
if (IsOwner)
throw new InvalidOperationException("Unable to leave a guild the current user owns.");
await Discord.BaseClient.LeaveGuild(Id).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task Delete()
{
if (!IsOwner)
throw new InvalidOperationException("Unable to delete a guild the current user does not own.");
await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false);
}



+ 1
- 1
src/Discord.Net/Rest/Entities/Invites/GuildInvite.cs View File

@@ -21,7 +21,7 @@ namespace Discord.Rest
/// <inheritdoc />
public int Uses { get; private set; }

internal override DiscordClient Discord => Guild.Discord;
internal override IDiscordClient Discord => Guild.Discord;

internal GuildInvite(Guild guild, Model model)
: base(model)


+ 0
- 1
src/Discord.Net/Rest/Entities/Message.cs View File

@@ -135,7 +135,6 @@ namespace Discord.Rest
await Discord.BaseClient.DeleteMessage(Channel.Id, Id).ConfigureAwait(false);
}


public override string ToString() => $"{Author.ToString()}: {Text}";

IUser IMessage.Author => Author;


+ 1
- 4
src/Discord.Net/Rest/Entities/Users/User.cs View File

@@ -45,10 +45,7 @@ namespace Discord.Rest

public async Task<DMChannel> CreateDMChannel()
{
var args = new CreateDMChannelParams
{
RecipientId = Id
};
var args = new CreateDMChannelParams { RecipientId = Id };
var model = await Discord.BaseClient.CreateDMChannel(args).ConfigureAwait(false);

return new DMChannel(Discord, model);


+ 71
- 0
src/Discord.Net/WebSocket/Caches/ChannelPermissionsCache.cs View File

@@ -0,0 +1,71 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Discord.WebSocket
{
internal struct ChannelMember
{
public GuildUser User { get; }
public ChannelPermissions Permissions { get; }

public ChannelMember(GuildUser user, ChannelPermissions permissions)
{
User = user;
Permissions = permissions;
}
}

internal class ChannelPermissionsCache
{
private readonly GuildChannel _channel;
private readonly ConcurrentDictionary<ulong, ChannelMember> _users;

public IEnumerable<ChannelMember> Members => _users.Select(x => x.Value);

public ChannelPermissionsCache(GuildChannel channel)
{
_channel = channel;
_users = new ConcurrentDictionary<ulong, ChannelMember>(1, (int)(_channel.Guild.UserCount * 1.05));
}
public ChannelMember? Get(ulong id)
{
ChannelMember member;
if (_users.TryGetValue(id, out member))
return member;
return null;
}
public void Add(GuildUser user)
{
_users[user.Id] = new ChannelMember(user, new ChannelPermissions(PermissionHelper.Resolve(user, _channel)));
}
public void Remove(GuildUser user)
{
ChannelMember member;
_users.TryRemove(user.Id, out member);
}

public void UpdateAll()
{
foreach (var pair in _users)
{
var member = pair.Value;
var newPerms = PermissionHelper.Resolve(member.User, _channel);
if (newPerms != member.Permissions.RawValue)
_users[pair.Key] = new ChannelMember(member.User, new ChannelPermissions(newPerms));
}
}
public void Update(GuildUser user)
{
ChannelMember member;
if (_users.TryGetValue(user.Id, out member))
{
var newPerms = PermissionHelper.Resolve(user, _channel);
if (newPerms != member.Permissions.RawValue)
_users[user.Id] = new ChannelMember(user, new ChannelPermissions(newPerms));
}
}
}
}

+ 98
- 0
src/Discord.Net/WebSocket/Caches/MessageCache.cs View File

@@ -0,0 +1,98 @@
using Discord.API.Rest;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.WebSocket
{
internal class MessageCache
{
private readonly DiscordClient _discord;
private readonly IMessageChannel _channel;
private readonly ConcurrentDictionary<ulong, Message> _messages;
private readonly ConcurrentQueue<ulong> _orderedMessages;
private readonly int _size;

public MessageCache(DiscordClient discord, IMessageChannel channel)
{
_discord = discord;
_channel = channel;
_size = discord.MessageCacheSize;
_messages = new ConcurrentDictionary<ulong, Message>(1, (int)(_size * 1.05));
_orderedMessages = new ConcurrentQueue<ulong>();
}

internal void Add(Message message)
{
if (_messages.TryAdd(message.Id, message))
{
_orderedMessages.Enqueue(message.Id);

ulong msgId;
Message msg;
while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msgId))
_messages.TryRemove(msgId, out msg);
}
}

internal void Remove(ulong id)
{
Message msg;
_messages.TryRemove(id, out msg);
}

public Message Get(ulong id)
{
Message result;
if (_messages.TryGetValue(id, out result))
return result;
return null;
}
public async Task<IEnumerable<Message>> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
{
//TODO: Test heavily

if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit));
if (limit == 0) return ImmutableArray<Message>.Empty;
IEnumerable<ulong> cachedMessageIds;
if (fromMessageId == null)
cachedMessageIds = _orderedMessages;
else if (dir == Direction.Before)
cachedMessageIds = _orderedMessages.Where(x => x < fromMessageId.Value);
else
cachedMessageIds = _orderedMessages.Where(x => x > fromMessageId.Value);

var cachedMessages = cachedMessageIds
.Take(limit)
.Select(x =>
{
Message msg;
if (_messages.TryGetValue(x, out msg))
return msg;
return null;
})
.Where(x => x != null)
.ToArray();

if (cachedMessages.Length == limit)
return cachedMessages;
else if (cachedMessages.Length > limit)
return cachedMessages.Skip(cachedMessages.Length - limit);
else
{
var args = new GetChannelMessagesParams
{
Limit = limit - cachedMessages.Length,
RelativeDirection = dir,
RelativeMessageId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Length - 1].Id
};
var downloadedMessages = await _discord.BaseClient.GetChannelMessages(_channel.Id, args).ConfigureAwait(false);
return cachedMessages.AsEnumerable().Concat(downloadedMessages.Select(x => new Message(_channel, x))).ToImmutableArray();
}
}
}
}

+ 139
- 0
src/Discord.Net/WebSocket/DiscordClient.cs View File

@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Discord.API;
using Discord.Net.Rest;

namespace Discord.WebSocket
{
public class DiscordClient : IDiscordClient
{
internal int MessageCacheSize { get; } = 100;

public SelfUser CurrentUser
{
get
{
throw new NotImplementedException();
}
}

public TokenType AuthTokenType
{
get
{
throw new NotImplementedException();
}
}

public DiscordRawClient BaseClient
{
get
{
throw new NotImplementedException();
}
}

public IRequestQueue RequestQueue
{
get
{
throw new NotImplementedException();
}
}

public IRestClient RestClient
{
get
{
throw new NotImplementedException();
}
}

public Task<IGuild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null)
{
throw new NotImplementedException();
}

public Task<IChannel> GetChannel(ulong id)
{
throw new NotImplementedException();
}

public Task<IEnumerable<IConnection>> GetConnections()
{
throw new NotImplementedException();
}

public Task<ISelfUser> GetCurrentUser()
{
throw new NotImplementedException();
}

public Task<IEnumerable<IDMChannel>> GetDMChannels()
{
throw new NotImplementedException();
}

public Task<IGuild> GetGuild(ulong id)
{
throw new NotImplementedException();
}

public Task<IEnumerable<IUserGuild>> GetGuilds()
{
throw new NotImplementedException();
}

public Task<IPublicInvite> GetInvite(string inviteIdOrXkcd)
{
throw new NotImplementedException();
}

public Task<IVoiceRegion> GetOptimalVoiceRegion()
{
throw new NotImplementedException();
}

public Task<IUser> GetUser(ulong id)
{
throw new NotImplementedException();
}

public Task<IUser> GetUser(string username, ushort discriminator)
{
throw new NotImplementedException();
}

public Task<IVoiceRegion> GetVoiceRegion(string id)
{
throw new NotImplementedException();
}

public Task<IEnumerable<IVoiceRegion>> GetVoiceRegions()
{
throw new NotImplementedException();
}

public Task Login(string email, string password)
{
throw new NotImplementedException();
}

public Task Login(TokenType tokenType, string token, bool validateToken = true)
{
throw new NotImplementedException();
}

public Task Logout()
{
throw new NotImplementedException();
}

public Task<IEnumerable<IUser>> QueryUsers(string query, int limit)
{
throw new NotImplementedException();
}
}
}

+ 141
- 0
src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs View File

@@ -0,0 +1,141 @@
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;

namespace Discord.WebSocket
{
public class DMChannel : IDMChannel
{
private readonly MessageCache _messages;

/// <inheritdoc />
public ulong Id { get; }
internal DiscordClient Discord { get; }

/// <inheritdoc />
public DMUser Recipient { get; private set; }

/// <inheritdoc />
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
/// <inheritdoc />
public IEnumerable<IUser> Users => ImmutableArray.Create<IUser>(Discord.CurrentUser, Recipient);

internal DMChannel(DiscordClient discord, Model model)
{
Id = model.Id;
Discord = discord;
_messages = new MessageCache(Discord, this);

Update(model);
}
private void Update(Model model)
{
if (Recipient == null)
Recipient = new DMUser(this, model.Recipient);
else
Recipient.Update(model.Recipient);
}

/// <inheritdoc />
public IUser GetUser(ulong id)
{
if (id == Recipient.Id)
return Recipient;
else if (id == Discord.CurrentUser.Id)
return Discord.CurrentUser;
else
return null;
}

/// <inheritdoc />
public async Task<IEnumerable<Message>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch)
{
return await _messages.GetMany(null, Direction.Before, limit).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IEnumerable<Message>> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
{
return await _messages.GetMany(fromMessageId, dir, limit).ConfigureAwait(false);
}

/// <inheritdoc />
public async Task<Message> SendMessage(string text, bool isTTS = false)
{
var args = new CreateMessageParams { Content = text, IsTTS = isTTS };
var model = await Discord.BaseClient.CreateMessage(Id, args).ConfigureAwait(false);
return new Message(this, model);
}
/// <inheritdoc />
public async Task<Message> SendFile(string filePath, string text = null, bool isTTS = false)
{
string filename = Path.GetFileName(filePath);
using (var file = File.OpenRead(filePath))
{
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS };
var model = await Discord.BaseClient.UploadFile(Id, file, args).ConfigureAwait(false);
return new Message(this, model);
}
}
/// <inheritdoc />
public async Task<Message> SendFile(Stream stream, string filename, string text = null, bool isTTS = false)
{
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS };
var model = await Discord.BaseClient.UploadFile(Id, stream, args).ConfigureAwait(false);
return new Message(this, model);
}

/// <inheritdoc />
public async Task DeleteMessages(IEnumerable<IMessage> messages)
{
await Discord.BaseClient.DeleteMessages(Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false);
}

/// <inheritdoc />
public async Task TriggerTyping()
{
await Discord.BaseClient.TriggerTypingIndicator(Id).ConfigureAwait(false);
}

/// <inheritdoc />
public async Task Close()
{
await Discord.BaseClient.DeleteChannel(Id).ConfigureAwait(false);
}

/// <inheritdoc />
public async Task Update()
{
var model = await Discord.BaseClient.GetChannel(Id).ConfigureAwait(false);
Update(model);
}

/// <inheritdoc />
public override string ToString() => $"@{Recipient} [DM]";
IDMUser IDMChannel.Recipient => Recipient;

Task<IEnumerable<IUser>> IChannel.GetUsers()
=> Task.FromResult(Users);
Task<IUser> IChannel.GetUser(ulong id)
=> Task.FromResult(GetUser(id));
Task<IMessage> IMessageChannel.GetMessage(ulong id)
=> throw new NotSupportedException();
async Task<IEnumerable<IMessage>> IMessageChannel.GetMessages(int limit)
=> await GetMessages(limit).ConfigureAwait(false);
async Task<IEnumerable<IMessage>> IMessageChannel.GetMessages(ulong fromMessageId, Direction dir, int limit)
=> await GetMessages(fromMessageId, dir, limit).ConfigureAwait(false);
async Task<IMessage> IMessageChannel.SendMessage(string text, bool isTTS)
=> await SendMessage(text, isTTS).ConfigureAwait(false);
async Task<IMessage> IMessageChannel.SendFile(string filePath, string text, bool isTTS)
=> await SendFile(filePath, text, isTTS).ConfigureAwait(false);
async Task<IMessage> IMessageChannel.SendFile(Stream stream, string filename, string text, bool isTTS)
=> await SendFile(stream, filename, text, isTTS).ConfigureAwait(false);
async Task IMessageChannel.TriggerTyping()
=> await TriggerTyping().ConfigureAwait(false);
}
}

+ 171
- 0
src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs View File

@@ -0,0 +1,171 @@
using Discord.API.Rest;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;

namespace Discord.WebSocket
{
public abstract class GuildChannel : IGuildChannel
{
private ConcurrentDictionary<ulong, Overwrite> _overwrites;
private ChannelPermissionsCache _permissions;

/// <inheritdoc />
public ulong Id { get; }
/// <summary> Gets the guild this channel is a member of. </summary>
public Guild Guild { get; }

/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public int Position { get; private set; }

/// <inheritdoc />
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
/// <inheritdoc />
public IReadOnlyDictionary<ulong, Overwrite> PermissionOverwrites => _overwrites;
internal DiscordClient Discord => Guild.Discord;

internal GuildChannel(Guild guild, Model model)
{
Id = model.Id;
Guild = guild;

Update(model);
}
internal virtual void Update(Model model)
{
Name = model.Name;
Position = model.Position;

var newOverwrites = new ConcurrentDictionary<ulong, Overwrite>();
for (int i = 0; i < model.PermissionOverwrites.Length; i++)
{
var overwrite = model.PermissionOverwrites[i];
newOverwrites[overwrite.TargetId] = new Overwrite(overwrite);
}
_overwrites = newOverwrites;
}

public async Task Modify(Action<ModifyGuildChannelParams> func)
{
if (func != null) throw new NullReferenceException(nameof(func));

var args = new ModifyGuildChannelParams();
func(args);
var model = await Discord.BaseClient.ModifyGuildChannel(Id, args).ConfigureAwait(false);
Update(model);
}

/// <summary> Gets a user in this channel with the given id. </summary>
public async Task<GuildUser> GetUser(ulong id)
{
var model = await Discord.BaseClient.GetGuildMember(Guild.Id, id).ConfigureAwait(false);
if (model != null)
return new GuildUser(Guild, model);
return null;
}
protected abstract Task<IEnumerable<GuildUser>> GetUsers();

/// <summary> Gets the permission overwrite for a specific user, or null if one does not exist. </summary>
public OverwritePermissions? GetPermissionOverwrite(IUser user)
{
Overwrite value;
if (_overwrites.TryGetValue(Id, out value))
return value.Permissions;
return null;
}
/// <summary> Gets the permission overwrite for a specific role, or null if one does not exist. </summary>
public OverwritePermissions? GetPermissionOverwrite(IRole role)
{
Overwrite value;
if (_overwrites.TryGetValue(Id, out value))
return value.Permissions;
return null;
}
/// <summary> Downloads a collection of all invites to this channel. </summary>
public async Task<IEnumerable<GuildInvite>> GetInvites()
{
var models = await Discord.BaseClient.GetChannelInvites(Id).ConfigureAwait(false);
return models.Select(x => new GuildInvite(Guild, x));
}

/// <summary> Adds or updates the permission overwrite for the given user. </summary>
public async Task AddPermissionOverwrite(IUser user, OverwritePermissions perms)
{
var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue };
await Discord.BaseClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false);
_overwrites[user.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User });
}
/// <summary> Adds or updates the permission overwrite for the given role. </summary>
public async Task AddPermissionOverwrite(IRole role, OverwritePermissions perms)
{
var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue };
await Discord.BaseClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false);
_overwrites[role.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role });
}
/// <summary> Removes the permission overwrite for the given user, if one exists. </summary>
public async Task RemovePermissionOverwrite(IUser user)
{
await Discord.BaseClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false);

Overwrite value;
_overwrites.TryRemove(user.Id, out value);
}
/// <summary> Removes the permission overwrite for the given role, if one exists. </summary>
public async Task RemovePermissionOverwrite(IRole role)
{
await Discord.BaseClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false);

Overwrite value;
_overwrites.TryRemove(role.Id, out value);
}

/// <summary> Creates a new invite to this channel. </summary>
/// <param name="maxAge"> Time (in seconds) until the invite expires. Set to null to never expire. </param>
/// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param>
/// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the guild after closing their client. </param>
/// <param name="withXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to null. </param>
public async Task<GuildInvite> CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false)
{
var args = new CreateChannelInviteParams
{
MaxAge = maxAge ?? 0,
MaxUses = maxUses ?? 0,
Temporary = isTemporary,
XkcdPass = withXkcd
};
var model = await Discord.BaseClient.CreateChannelInvite(Id, args).ConfigureAwait(false);
return new GuildInvite(Guild, model);
}

/// <inheritdoc />
public async Task Delete()
{
await Discord.BaseClient.DeleteChannel(Id).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task Update()
{
var model = await Discord.BaseClient.GetChannel(Id).ConfigureAwait(false);
Update(model);
}

IGuild IGuildChannel.Guild => Guild;
async Task<IGuildInvite> IGuildChannel.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd)
=> await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false);
async Task<IEnumerable<IGuildInvite>> IGuildChannel.GetInvites()
=> await GetInvites().ConfigureAwait(false);
async Task<IEnumerable<IGuildUser>> IGuildChannel.GetUsers()
=> await GetUsers().ConfigureAwait(false);
async Task<IGuildUser> IGuildChannel.GetUser(ulong id)
=> await GetUser(id).ConfigureAwait(false);
async Task<IEnumerable<IUser>> IChannel.GetUsers()
=> await GetUsers().ConfigureAwait(false);
async Task<IUser> IChannel.GetUser(ulong id)
=> await GetUser(id).ConfigureAwait(false);
}
}

+ 122
- 0
src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs View File

@@ -0,0 +1,122 @@
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;

namespace Discord.WebSocket
{
public class TextChannel : GuildChannel, ITextChannel
{
private readonly MessageCache _messages;

/// <inheritdoc />
public string Topic { get; private set; }

/// <inheritdoc />
public string Mention => MentionHelper.Mention(this);

internal TextChannel(Guild guild, Model model)
: base(guild, model)
{
_messages = new MessageCache(Discord, this);
}

internal override void Update(Model model)
{
Topic = model.Topic;
base.Update(model);
}

public async Task Modify(Action<ModifyTextChannelParams> func)
{
if (func != null) throw new NullReferenceException(nameof(func));

var args = new ModifyTextChannelParams();
func(args);
var model = await Discord.BaseClient.ModifyGuildChannel(Id, args).ConfigureAwait(false);
Update(model);
}

protected override async Task<IEnumerable<GuildUser>> GetUsers()
{
var users = await Guild.GetUsers().ConfigureAwait(false);
return users.Where(x => PermissionUtilities.GetValue(PermissionHelper.Resolve(x, this), ChannelPermission.ReadMessages));
}

/// <inheritdoc />
public Task<Message> GetMessage(ulong id) { throw new NotSupportedException(); } //Not implemented
/// <inheritdoc />
public async Task<IEnumerable<Message>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch)
{
var args = new GetChannelMessagesParams { Limit = limit };
var models = await Discord.BaseClient.GetChannelMessages(Id, args).ConfigureAwait(false);
return models.Select(x => new Message(this, x));
}
/// <inheritdoc />
public async Task<IEnumerable<Message>> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
{
var args = new GetChannelMessagesParams { Limit = limit };
var models = await Discord.BaseClient.GetChannelMessages(Id, args).ConfigureAwait(false);
return models.Select(x => new Message(this, x));
}

/// <inheritdoc />
public async Task<Message> SendMessage(string text, bool isTTS = false)
{
var args = new CreateMessageParams { Content = text, IsTTS = isTTS };
var model = await Discord.BaseClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false);
return new Message(this, model);
}
/// <inheritdoc />
public async Task<Message> SendFile(string filePath, string text = null, bool isTTS = false)
{
string filename = Path.GetFileName(filePath);
using (var file = File.OpenRead(filePath))
{
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS };
var model = await Discord.BaseClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false);
return new Message(this, model);
}
}
/// <inheritdoc />
public async Task<Message> SendFile(Stream stream, string filename, string text = null, bool isTTS = false)
{
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS };
var model = await Discord.BaseClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false);
return new Message(this, model);
}

/// <inheritdoc />
public async Task DeleteMessages(IEnumerable<IMessage> messages)
{
await Discord.BaseClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false);
}

/// <inheritdoc />
public async Task TriggerTyping()
{
await Discord.BaseClient.TriggerTypingIndicator(Id).ConfigureAwait(false);
}

/// <inheritdoc />
public override string ToString() => $"{base.ToString()} [Text]";

async Task<IMessage> IMessageChannel.GetMessage(ulong id)
=> await GetMessage(id).ConfigureAwait(false);
async Task<IEnumerable<IMessage>> IMessageChannel.GetMessages(int limit)
=> await GetMessages(limit).ConfigureAwait(false);
async Task<IEnumerable<IMessage>> IMessageChannel.GetMessages(ulong fromMessageId, Direction dir, int limit)
=> await GetMessages(fromMessageId, dir, limit).ConfigureAwait(false);
async Task<IMessage> IMessageChannel.SendMessage(string text, bool isTTS)
=> await SendMessage(text, isTTS).ConfigureAwait(false);
async Task<IMessage> IMessageChannel.SendFile(string filePath, string text, bool isTTS)
=> await SendFile(filePath, text, isTTS).ConfigureAwait(false);
async Task<IMessage> IMessageChannel.SendFile(Stream stream, string filename, string text, bool isTTS)
=> await SendFile(stream, filename, text, isTTS).ConfigureAwait(false);
async Task IMessageChannel.TriggerTyping()
=> await TriggerTyping().ConfigureAwait(false);
}
}

+ 45
- 0
src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs View File

@@ -0,0 +1,45 @@
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;

namespace Discord.WebSocket
{
public class VoiceChannel : GuildChannel, IVoiceChannel
{
/// <inheritdoc />
public int Bitrate { get; private set; }

internal VoiceChannel(Guild guild, Model model)
: base(guild, model)
{
}
internal override void Update(Model model)
{
base.Update(model);
Bitrate = model.Bitrate;
}

/// <inheritdoc />
public async Task Modify(Action<ModifyVoiceChannelParams> func)
{
if (func != null) throw new NullReferenceException(nameof(func));

var args = new ModifyVoiceChannelParams();
func(args);
var model = await Discord.BaseClient.ModifyGuildChannel(Id, args).ConfigureAwait(false);
Update(model);
}

protected override async Task<IEnumerable<GuildUser>> GetUsers()
{
var users = await Guild.GetUsers().ConfigureAwait(false);
return users.Where(x => PermissionUtilities.GetValue(PermissionHelper.Resolve(x, this), ChannelPermission.Connect));
}

/// <inheritdoc />
public override string ToString() => $"{base.ToString()} [Voice]";
}
}

+ 374
- 0
src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs View File

@@ -0,0 +1,374 @@
using Discord.API.Rest;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Guild;
using EmbedModel = Discord.API.GuildEmbed;
using RoleModel = Discord.API.Role;

namespace Discord.WebSocket
{
/// <summary> Represents a Discord guild (called a server in the official client). </summary>
public class Guild : IGuild
{
private ConcurrentDictionary<ulong, Role> _roles;
private string _iconId, _splashId;

/// <inheritdoc />
public ulong Id { get; }
internal DiscordClient Discord { get; }

/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public int AFKTimeout { get; private set; }
/// <inheritdoc />
public bool IsEmbeddable { get; private set; }
/// <inheritdoc />
public int VerificationLevel { get; private set; }
public int UserCount { get; private set; }

/// <inheritdoc />
public ulong? AFKChannelId { get; private set; }
/// <inheritdoc />
public ulong? EmbedChannelId { get; private set; }
/// <inheritdoc />
public ulong OwnerId { get; private set; }
/// <inheritdoc />
public string VoiceRegionId { get; private set; }
/// <inheritdoc />
public IReadOnlyList<Emoji> Emojis { get; private set; }
/// <inheritdoc />
public IReadOnlyList<string> Features { get; private set; }

/// <inheritdoc />
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
/// <inheritdoc />
public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId);
/// <inheritdoc />
public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, _splashId);
/// <inheritdoc />
public ulong DefaultChannelId => Id;
/// <inheritdoc />
public Role EveryoneRole => GetRole(Id);
/// <summary> Gets a collection of all roles in this guild. </summary>
public IEnumerable<Role> Roles => _roles?.Select(x => x.Value) ?? Enumerable.Empty<Role>();

internal Guild(DiscordClient discord, Model model)
{
Id = model.Id;
Discord = discord;

Update(model);
}
private void Update(Model model)
{
AFKChannelId = model.AFKChannelId;
AFKTimeout = model.AFKTimeout;
EmbedChannelId = model.EmbedChannelId;
IsEmbeddable = model.EmbedEnabled;
Features = model.Features;
_iconId = model.Icon;
Name = model.Name;
OwnerId = model.OwnerId;
VoiceRegionId = model.Region;
_splashId = model.Splash;
VerificationLevel = model.VerificationLevel;

if (model.Emojis != null)
{
var emojis = ImmutableArray.CreateBuilder<Emoji>(model.Emojis.Length);
for (int i = 0; i < model.Emojis.Length; i++)
emojis.Add(new Emoji(model.Emojis[i]));
Emojis = emojis.ToArray();
}
else
Emojis = Array.Empty<Emoji>();

var roles = new ConcurrentDictionary<ulong, Role>(1, model.Roles?.Length ?? 0);
if (model.Roles != null)
{
for (int i = 0; i < model.Roles.Length; i++)
roles[model.Roles[i].Id] = new Role(this, model.Roles[i]);
}
_roles = roles;
}
private void Update(EmbedModel model)
{
IsEmbeddable = model.Enabled;
EmbedChannelId = model.ChannelId;
}
private void Update(IEnumerable<RoleModel> models)
{
Role role;
foreach (var model in models)
{
if (_roles.TryGetValue(model.Id, out role))
role.Update(model);
}
}

/// <inheritdoc />
public async Task Update()
{
var response = await Discord.BaseClient.GetGuild(Id).ConfigureAwait(false);
Update(response);
}
/// <inheritdoc />
public async Task Modify(Action<ModifyGuildParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));

var args = new ModifyGuildParams();
func(args);
var model = await Discord.BaseClient.ModifyGuild(Id, args).ConfigureAwait(false);
Update(model);
}
/// <inheritdoc />
public async Task ModifyEmbed(Action<ModifyGuildEmbedParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));

var args = new ModifyGuildEmbedParams();
func(args);
var model = await Discord.BaseClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false);

Update(model);
}
/// <inheritdoc />
public async Task ModifyChannels(IEnumerable<ModifyGuildChannelsParams> args)
{
if (args == null) throw new NullReferenceException(nameof(args));

await Discord.BaseClient.ModifyGuildChannels(Id, args).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task ModifyRoles(IEnumerable<ModifyGuildRolesParams> args)
{
if (args == null) throw new NullReferenceException(nameof(args));
var models = await Discord.BaseClient.ModifyGuildRoles(Id, args).ConfigureAwait(false);
Update(models);
}
/// <inheritdoc />
public async Task Leave()
{
await Discord.BaseClient.LeaveGuild(Id).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task Delete()
{
await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false);
}

/// <inheritdoc />
public async Task<IEnumerable<User>> GetBans()
{
var models = await Discord.BaseClient.GetGuildBans(Id).ConfigureAwait(false);
return models.Select(x => new PublicUser(Discord, x));
}
/// <inheritdoc />
public Task AddBan(IUser user, int pruneDays = 0) => AddBan(user, pruneDays);
/// <inheritdoc />
public async Task AddBan(ulong userId, int pruneDays = 0)
{
var args = new CreateGuildBanParams()
{
PruneDays = pruneDays
};
await Discord.BaseClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false);
}
/// <inheritdoc />
public Task RemoveBan(IUser user) => RemoveBan(user.Id);
/// <inheritdoc />
public async Task RemoveBan(ulong userId)
{
await Discord.BaseClient.RemoveGuildBan(Id, userId).ConfigureAwait(false);
}

/// <summary> Gets the channel in this guild with the provided id, or null if not found. </summary>
public async Task<GuildChannel> GetChannel(ulong id)
{
var model = await Discord.BaseClient.GetChannel(Id, id).ConfigureAwait(false);
if (model != null)
return ToChannel(model);
return null;
}
/// <summary> Gets a collection of all channels in this guild. </summary>
public async Task<IEnumerable<GuildChannel>> GetChannels()
{
var models = await Discord.BaseClient.GetGuildChannels(Id).ConfigureAwait(false);
return models.Select(x => ToChannel(x));
}
/// <summary> Creates a new text channel. </summary>
public async Task<TextChannel> CreateTextChannel(string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));

var args = new CreateGuildChannelParams() { Name = name, Type = ChannelType.Text };
var model = await Discord.BaseClient.CreateGuildChannel(Id, args).ConfigureAwait(false);
return new TextChannel(this, model);
}
/// <summary> Creates a new voice channel. </summary>
public async Task<VoiceChannel> CreateVoiceChannel(string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));

var args = new CreateGuildChannelParams { Name = name, Type = ChannelType.Voice };
var model = await Discord.BaseClient.CreateGuildChannel(Id, args).ConfigureAwait(false);
return new VoiceChannel(this, model);
}

/// <summary> Gets a collection of all integrations attached to this guild. </summary>
public async Task<IEnumerable<GuildIntegration>> GetIntegrations()
{
var models = await Discord.BaseClient.GetGuildIntegrations(Id).ConfigureAwait(false);
return models.Select(x => new GuildIntegration(this, x));
}
/// <summary> Creates a new integration for this guild. </summary>
public async Task<GuildIntegration> CreateIntegration(ulong id, string type)
{
var args = new CreateGuildIntegrationParams { Id = id, Type = type };
var model = await Discord.BaseClient.CreateGuildIntegration(Id, args).ConfigureAwait(false);
return new GuildIntegration(this, model);
}

/// <summary> Gets a collection of all invites to this guild. </summary>
public async Task<IEnumerable<GuildInvite>> GetInvites()
{
var models = await Discord.BaseClient.GetGuildInvites(Id).ConfigureAwait(false);
return models.Select(x => new GuildInvite(this, x));
}
/// <summary> Creates a new invite to this guild. </summary>
public async Task<GuildInvite> CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false)
{
if (maxAge <= 0) throw new ArgumentOutOfRangeException(nameof(maxAge));
if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses));

var args = new CreateChannelInviteParams()
{
MaxAge = maxAge ?? 0,
MaxUses = maxUses ?? 0,
Temporary = isTemporary,
XkcdPass = withXkcd
};
var model = await Discord.BaseClient.CreateChannelInvite(DefaultChannelId, args).ConfigureAwait(false);
return new GuildInvite(this, model);
}

/// <summary> Gets the role in this guild with the provided id, or null if not found. </summary>
public Role GetRole(ulong id)
{
Role result = null;
if (_roles?.TryGetValue(id, out result) == true)
return result;
return null;
}

/// <summary> Creates a new role. </summary>
public async Task<Role> CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false)
{
if (name == null) throw new ArgumentNullException(nameof(name));
var model = await Discord.BaseClient.CreateGuildRole(Id).ConfigureAwait(false);
var role = new Role(this, model);

await role.Modify(x =>
{
x.Name = name;
x.Permissions = (permissions ?? role.Permissions).RawValue;
x.Color = (color ?? Color.Default).RawValue;
x.Hoist = isHoisted;
}).ConfigureAwait(false);

return role;
}

/// <summary> Gets a collection of all users in this guild. </summary>
public async Task<IEnumerable<GuildUser>> GetUsers()
{
var args = new GetGuildMembersParams();
var models = await Discord.BaseClient.GetGuildMembers(Id, args).ConfigureAwait(false);
return models.Select(x => new GuildUser(this, x));
}
/// <summary> Gets a paged collection of all users in this guild. </summary>
public async Task<IEnumerable<GuildUser>> GetUsers(int limit, int offset)
{
var args = new GetGuildMembersParams { Limit = limit, Offset = offset };
var models = await Discord.BaseClient.GetGuildMembers(Id, args).ConfigureAwait(false);
return models.Select(x => new GuildUser(this, x));
}
/// <summary> Gets the user in this guild with the provided id, or null if not found. </summary>
public async Task<GuildUser> GetUser(ulong id)
{
var model = await Discord.BaseClient.GetGuildMember(Id, id).ConfigureAwait(false);
if (model != null)
return new GuildUser(this, model);
return null;
}
/// <summary> Gets a the current user. </summary>
public async Task<GuildUser> GetCurrentUser()
{
var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false);
return await GetUser(currentUser.Id).ConfigureAwait(false);
}
public async Task<int> PruneUsers(int days = 30, bool simulate = false)
{
var args = new GuildPruneParams() { Days = days };
GetGuildPruneCountResponse model;
if (simulate)
model = await Discord.BaseClient.GetGuildPruneCount(Id, args).ConfigureAwait(false);
else
model = await Discord.BaseClient.BeginGuildPrune(Id, args).ConfigureAwait(false);
return model.Pruned;
}

internal GuildChannel ToChannel(API.Channel model)
{
switch (model.Type)
{
case ChannelType.Text:
default:
return new TextChannel(this, model);
case ChannelType.Voice:
return new VoiceChannel(this, model);
}
}

public override string ToString() => Name ?? Id.ToString();

IEnumerable<Emoji> IGuild.Emojis => Emojis;
ulong IGuild.EveryoneRoleId => EveryoneRole.Id;
IEnumerable<string> IGuild.Features => Features;

async Task<IEnumerable<IUser>> IGuild.GetBans()
=> await GetBans().ConfigureAwait(false);
async Task<IGuildChannel> IGuild.GetChannel(ulong id)
=> await GetChannel(id).ConfigureAwait(false);
async Task<IEnumerable<IGuildChannel>> IGuild.GetChannels()
=> await GetChannels().ConfigureAwait(false);
async Task<IGuildInvite> IGuild.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd)
=> await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false);
async Task<IRole> IGuild.CreateRole(string name, GuildPermissions? permissions, Color? color, bool isHoisted)
=> await CreateRole(name, permissions, color, isHoisted).ConfigureAwait(false);
async Task<ITextChannel> IGuild.CreateTextChannel(string name)
=> await CreateTextChannel(name).ConfigureAwait(false);
async Task<IVoiceChannel> IGuild.CreateVoiceChannel(string name)
=> await CreateVoiceChannel(name).ConfigureAwait(false);
async Task<IEnumerable<IGuildInvite>> IGuild.GetInvites()
=> await GetInvites().ConfigureAwait(false);
Task<IRole> IGuild.GetRole(ulong id)
=> Task.FromResult<IRole>(GetRole(id));
Task<IEnumerable<IRole>> IGuild.GetRoles()
=> Task.FromResult<IEnumerable<IRole>>(Roles);
async Task<IGuildUser> IGuild.GetUser(ulong id)
=> await GetUser(id).ConfigureAwait(false);
async Task<IGuildUser> IGuild.GetCurrentUser()
=> await GetCurrentUser().ConfigureAwait(false);
async Task<IEnumerable<IGuildUser>> IGuild.GetUsers()
=> await GetUsers().ConfigureAwait(false);
}
}

+ 87
- 0
src/Discord.Net/WebSocket/Entities/Guilds/GuildIntegration.cs View File

@@ -0,0 +1,87 @@
using Discord.API.Rest;
using System;
using System.Threading.Tasks;
using Model = Discord.API.Integration;

namespace Discord.WebSocket
{
public class GuildIntegration : IGuildIntegration
{
/// <inheritdoc />
public ulong Id { get; private set; }
/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public string Type { get; private set; }
/// <inheritdoc />
public bool IsEnabled { get; private set; }
/// <inheritdoc />
public bool IsSyncing { get; private set; }
/// <inheritdoc />
public ulong ExpireBehavior { get; private set; }
/// <inheritdoc />
public ulong ExpireGracePeriod { get; private set; }
/// <inheritdoc />
public DateTime SyncedAt { get; private set; }

/// <inheritdoc />
public Guild Guild { get; private set; }
/// <inheritdoc />
public Role Role { get; private set; }
/// <inheritdoc />
public User User { get; private set; }
/// <inheritdoc />
public IntegrationAccount Account { get; private set; }
internal DiscordClient Discord => Guild.Discord;
internal GuildIntegration(Guild guild, Model model)
{
Guild = guild;
Update(model);
}

private void Update(Model model)
{
Id = model.Id;
Name = model.Name;
Type = model.Type;
IsEnabled = model.Enabled;
IsSyncing = model.Syncing;
ExpireBehavior = model.ExpireBehavior;
ExpireGracePeriod = model.ExpireGracePeriod;
SyncedAt = model.SyncedAt;

Role = Guild.GetRole(model.RoleId);
User = new PublicUser(Discord, model.User);
}

/// <summary> </summary>
public async Task Delete()
{
await Discord.BaseClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false);
}
/// <summary> </summary>
public async Task Modify(Action<ModifyGuildIntegrationParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));

var args = new ModifyGuildIntegrationParams();
func(args);
var model = await Discord.BaseClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false);

Update(model);
}
/// <summary> </summary>
public async Task Sync()
{
await Discord.BaseClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false);
}

public override string ToString() => $"{Name ?? Id.ToString()} ({(IsEnabled ? "Enabled" : "Disabled")})";

IGuild IGuildIntegration.Guild => Guild;
IRole IGuildIntegration.Role => Role;
IUser IGuildIntegration.User => User;
IntegrationAccount IGuildIntegration.Account => Account;
}
}

+ 52
- 0
src/Discord.Net/WebSocket/Entities/Invites/GuildInvite.cs View File

@@ -0,0 +1,52 @@
using System.Threading.Tasks;
using Model = Discord.API.InviteMetadata;

namespace Discord.WebSocket
{
public class GuildInvite : Invite, IGuildInvite
{
/// <summary> Gets the guild this invite is linked to. </summary>
public Guild Guild { get; private set; }
/// <inheritdoc />
public ulong ChannelId { get; private set; }

/// <inheritdoc />
public bool IsRevoked { get; private set; }
/// <inheritdoc />
public bool IsTemporary { get; private set; }
/// <inheritdoc />
public int? MaxAge { get; private set; }
/// <inheritdoc />
public int? MaxUses { get; private set; }
/// <inheritdoc />
public int Uses { get; private set; }

internal override IDiscordClient Discord => Guild.Discord;

internal GuildInvite(Guild guild, Model model)
: base(model)
{
Guild = guild;

Update(model); //Causes base.Update(Model) to be run twice, but that's fine.
}
private void Update(Model model)
{
base.Update(model);
IsRevoked = model.Revoked;
IsTemporary = model.Temporary;
MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null;
MaxUses = model.MaxUses;
Uses = model.Uses;
}

/// <inheritdoc />
public async Task Delete()
{
await Discord.BaseClient.DeleteInvite(Code).ConfigureAwait(false);
}

IGuild IGuildInvite.Guild => Guild;
ulong IInvite.GuildId => Guild.Id;
}
}

+ 146
- 0
src/Discord.Net/WebSocket/Entities/Message.cs View File

@@ -0,0 +1,146 @@
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Model = Discord.API.Message;

namespace Discord.WebSocket
{
public class Message : IMessage
{
/// <inheritdoc />
public ulong Id { get; }

/// <inheritdoc />
public DateTime? EditedTimestamp { get; private set; }
/// <inheritdoc />
public bool IsTTS { get; private set; }
/// <inheritdoc />
public string RawText { get; private set; }
/// <inheritdoc />
public string Text { get; private set; }
/// <inheritdoc />
public DateTime Timestamp { get; private set; }

/// <inheritdoc />
public IMessageChannel Channel { get; }
/// <inheritdoc />
public User Author { get; }

/// <inheritdoc />
public IReadOnlyList<Attachment> Attachments { get; private set; }
/// <inheritdoc />
public IReadOnlyList<Embed> Embeds { get; private set; }
/// <inheritdoc />
public IReadOnlyList<PublicUser> MentionedUsers { get; private set; }
/// <inheritdoc />
public IReadOnlyList<ulong> MentionedChannelIds { get; private set; }
/// <inheritdoc />
public IReadOnlyList<ulong> MentionedRoleIds { get; private set; }

/// <inheritdoc />
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
internal DiscordClient Discord => (Channel as TextChannel)?.Discord ?? (Channel as DMChannel).Discord;

internal Message(IMessageChannel channel, Model model)
{
Id = model.Id;
Channel = channel;
Author = new PublicUser(Discord, model.Author);

Update(model);
}
private void Update(Model model)
{
IsTTS = model.IsTextToSpeech;
Timestamp = model.Timestamp;
EditedTimestamp = model.EditedTimestamp;
RawText = model.Content;

if (model.Attachments.Length > 0)
{
var attachments = new Attachment[model.Attachments.Length];
for (int i = 0; i < attachments.Length; i++)
attachments[i] = new Attachment(model.Attachments[i]);
Attachments = ImmutableArray.Create(attachments);
}
else
Attachments = Array.Empty<Attachment>();

if (model.Embeds.Length > 0)
{
var embeds = new Embed[model.Attachments.Length];
for (int i = 0; i < embeds.Length; i++)
embeds[i] = new Embed(model.Embeds[i]);
Embeds = ImmutableArray.Create(embeds);
}
else
Embeds = Array.Empty<Embed>();

if (model.Mentions.Length > 0)
{
var discord = Discord;
var builder = ImmutableArray.CreateBuilder<PublicUser>(model.Mentions.Length);
for (int i = 0; i < model.Mentions.Length; i++)
builder.Add(new PublicUser(discord, model.Mentions[i]));
MentionedUsers = builder.ToArray();
}
else
MentionedUsers = Array.Empty<PublicUser>();
MentionedChannelIds = MentionHelper.GetChannelMentions(model.Content);
MentionedRoleIds = MentionHelper.GetRoleMentions(model.Content);
if (model.IsMentioningEveryone)
{
ulong? guildId = (Channel as IGuildChannel)?.Guild.Id;
if (guildId != null)
{
if (MentionedRoleIds.Count == 0)
MentionedRoleIds = ImmutableArray.Create(guildId.Value);
else
{
var builder = ImmutableArray.CreateBuilder<ulong>(MentionedRoleIds.Count + 1);
builder.AddRange(MentionedRoleIds);
builder.Add(guildId.Value);
MentionedRoleIds = builder.ToImmutable();
}
}
}
Text = MentionHelper.CleanUserMentions(model.Content, model.Mentions);

Author.Update(model.Author);
}

/// <inheritdoc />
public async Task Modify(Action<ModifyMessageParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));

var args = new ModifyMessageParams();
func(args);
var guildChannel = Channel as GuildChannel;

Model model;
if (guildChannel != null)
model = await Discord.BaseClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false);
else
model = await Discord.BaseClient.ModifyMessage(Channel.Id, Id, args).ConfigureAwait(false);
Update(model);
}

/// <inheritdoc />
public async Task Delete()
{
await Discord.BaseClient.DeleteMessage(Channel.Id, Id).ConfigureAwait(false);
}

public override string ToString() => $"{Author.ToString()}: {Text}";

IUser IMessage.Author => Author;
IReadOnlyList<Attachment> IMessage.Attachments => Attachments;
IReadOnlyList<Embed> IMessage.Embeds => Embeds;
IReadOnlyList<ulong> IMessage.MentionedChannelIds => MentionedChannelIds;
IReadOnlyList<IUser> IMessage.MentionedUsers => MentionedUsers;
}
}

+ 80
- 0
src/Discord.Net/WebSocket/Entities/Role.cs View File

@@ -0,0 +1,80 @@
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Role;

namespace Discord.WebSocket
{
public class Role : IRole, IMentionable
{
/// <inheritdoc />
public ulong Id { get; }
/// <summary> Returns the guild this role belongs to. </summary>
public Guild Guild { get; }

/// <inheritdoc />
public Color Color { get; private set; }
/// <inheritdoc />
public bool IsHoisted { get; private set; }
/// <inheritdoc />
public bool IsManaged { get; private set; }
/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public GuildPermissions Permissions { get; private set; }
/// <inheritdoc />
public int Position { get; private set; }

/// <inheritdoc />
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
/// <inheritdoc />
public bool IsEveryone => Id == Guild.Id;
/// <inheritdoc />
public string Mention => MentionHelper.Mention(this);
internal DiscordClient Discord => Guild.Discord;

internal Role(Guild guild, Model model)
{
Id = model.Id;
Guild = guild;

Update(model);
}
internal void Update(Model model)
{
Name = model.Name;
IsHoisted = model.Hoist.Value;
IsManaged = model.Managed.Value;
Position = model.Position.Value;
Color = new Color(model.Color.Value);
Permissions = new GuildPermissions(model.Permissions.Value);
}
/// <summary> Modifies the properties of this role. </summary>
public async Task Modify(Action<ModifyGuildRoleParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));

var args = new ModifyGuildRoleParams();
func(args);
var response = await Discord.BaseClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false);
Update(response);
}
/// <summary> Deletes this message. </summary>
public async Task Delete()
=> await Discord.BaseClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false);

/// <inheritdoc />
public override string ToString() => Name ?? Id.ToString();
ulong IRole.GuildId => Guild.Id;
async Task<IEnumerable<IGuildUser>> IRole.GetUsers()
{
//A tad hacky, but it works
var models = await Discord.BaseClient.GetGuildMembers(Guild.Id, new GetGuildMembersParams()).ConfigureAwait(false);
return models.Where(x => x.Roles.Contains(Id)).Select(x => new GuildUser(Guild, x));
}
}
}

+ 20
- 0
src/Discord.Net/WebSocket/Entities/Users/DMUser.cs View File

@@ -0,0 +1,20 @@
using Model = Discord.API.User;

namespace Discord.WebSocket
{
public class DMUser : User, IDMUser
{
/// <inheritdoc />
public DMChannel Channel { get; }

internal override DiscordClient Discord => Channel.Discord;

internal DMUser(DMChannel channel, Model model)
: base(model)
{
Channel = channel;
}

IDMChannel IDMUser.Channel => Channel;
}
}

+ 117
- 0
src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs View File

@@ -0,0 +1,117 @@
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.GuildMember;

namespace Discord.WebSocket
{
public class GuildUser : User, IGuildUser
{
private ImmutableArray<Role> _roles;

public Guild Guild { get; }

/// <inheritdoc />
public bool IsDeaf { get; private set; }
/// <inheritdoc />
public bool IsMute { get; private set; }
/// <inheritdoc />
public DateTime JoinedAt { get; private set; }
/// <inheritdoc />
public string Nickname { get; private set; }

/// <inheritdoc />
public IReadOnlyList<Role> Roles => _roles;
internal override DiscordClient Discord => Guild.Discord;

internal GuildUser(Guild guild, Model model)
: base(model.User)
{
Guild = guild;
}
internal void Update(Model model)
{
IsDeaf = model.Deaf;
IsMute = model.Mute;
JoinedAt = model.JoinedAt.Value;
Nickname = model.Nick;

var roles = ImmutableArray.CreateBuilder<Role>(model.Roles.Length + 1);
roles.Add(Guild.EveryoneRole);
for (int i = 0; i < model.Roles.Length; i++)
roles.Add(Guild.GetRole(model.Roles[i]));
_roles = roles.ToImmutable();
}

public async Task Update()
{
var model = await Discord.BaseClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false);
Update(model);
}

public bool HasRole(IRole role)
{
for (int i = 0; i < _roles.Length; i++)
{
if (_roles[i].Id == role.Id)
return true;
}
return false;
}

public async Task Kick()
{
await Discord.BaseClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false);
}

public GuildPermissions GetGuildPermissions()
{
return new GuildPermissions(PermissionHelper.Resolve(this));
}
public ChannelPermissions GetPermissions(IGuildChannel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
return new ChannelPermissions(PermissionHelper.Resolve(this, channel));
}

public async Task Modify(Action<ModifyGuildMemberParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));

var args = new ModifyGuildMemberParams();
func(args);

bool isCurrentUser = (await Discord.GetCurrentUser().ConfigureAwait(false)).Id == Id;
if (isCurrentUser && args.Nickname.IsSpecified)
{
var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value };
await Discord.BaseClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false);
args.Nickname = new API.Optional<string>(); //Remove
}

if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified)
{
await Discord.BaseClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false);
if (args.Deaf.IsSpecified)
IsDeaf = args.Deaf;
if (args.Mute.IsSpecified)
IsMute = args.Mute;
if (args.Nickname.IsSpecified)
Nickname = args.Nickname;
if (args.Roles.IsSpecified)
_roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray();
}
}


IGuild IGuildUser.Guild => Guild;
IReadOnlyList<IRole> IGuildUser.Roles => Roles;
ulong? IGuildUser.VoiceChannelId => null;

ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel)
=> GetPermissions(channel);
}
}

+ 15
- 0
src/Discord.Net/WebSocket/Entities/Users/PublicUser.cs View File

@@ -0,0 +1,15 @@
using Model = Discord.API.User;

namespace Discord.WebSocket
{
public class PublicUser : User
{
internal override DiscordClient Discord { get; }

internal PublicUser(DiscordClient discord, Model model)
: base(model)
{
Discord = discord;
}
}
}

+ 48
- 0
src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs View File

@@ -0,0 +1,48 @@
using Discord.API.Rest;
using System;
using System.Threading.Tasks;
using Model = Discord.API.User;

namespace Discord.WebSocket
{
public class SelfUser : User, ISelfUser
{
internal override DiscordClient Discord { get; }

/// <inheritdoc />
public string Email { get; private set; }
/// <inheritdoc />
public bool IsVerified { get; private set; }

internal SelfUser(DiscordClient discord, Model model)
: base(model)
{
Discord = discord;
}
internal override void Update(Model model)
{
base.Update(model);

Email = model.Email;
IsVerified = model.IsVerified;
}

/// <inheritdoc />
public async Task Update()
{
var model = await Discord.BaseClient.GetCurrentUser().ConfigureAwait(false);
Update(model);
}

/// <inheritdoc />
public async Task Modify(Action<ModifyCurrentUserParams> func)
{
if (func != null) throw new NullReferenceException(nameof(func));

var args = new ModifyCurrentUserParams();
func(args);
var model = await Discord.BaseClient.ModifyCurrentUser(args).ConfigureAwait(false);
Update(model);
}
}
}

+ 65
- 0
src/Discord.Net/WebSocket/Entities/Users/User.cs View File

@@ -0,0 +1,65 @@
using Discord.API.Rest;
using System;
using System.Threading.Tasks;
using Model = Discord.API.User;

namespace Discord.WebSocket
{
public abstract class User : IUser
{
private string _avatarId;

/// <inheritdoc />
public ulong Id { get; }
internal abstract DiscordClient Discord { get; }

/// <inheritdoc />
public ushort Discriminator { get; private set; }
/// <inheritdoc />
public bool IsBot { get; private set; }
/// <inheritdoc />
public string Username { get; private set; }

/// <inheritdoc />
public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId);
/// <inheritdoc />
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
/// <inheritdoc />
public string Mention => MentionHelper.Mention(this, false);
/// <inheritdoc />
public string NicknameMention => MentionHelper.Mention(this, true);

internal User(Model model)
{
Id = model.Id;

Update(model);
}
internal virtual void Update(Model model)
{
_avatarId = model.Avatar;
Discriminator = model.Discriminator;
IsBot = model.Bot;
Username = model.Username;
}

public async Task<DMChannel> CreateDMChannel()
{
var args = new CreateDMChannelParams { RecipientId = Id };
var model = await Discord.BaseClient.CreateDMChannel(args).ConfigureAwait(false);

return new DMChannel(Discord, model);
}

public override string ToString() => $"{Username ?? Id.ToString()}";

/// <inheritdoc />
string IUser.CurrentGame => null;
/// <inheritdoc />
UserStatus IUser.Status => UserStatus.Unknown;

/// <inheritdoc />
async Task<IDMChannel> IUser.CreateDMChannel()
=> await CreateDMChannel().ConfigureAwait(false);
}
}

Loading…
Cancel
Save