Browse Source

Merge branch 'dev' of https://github.com/RogueException/Discord.Net into unit-testing

pull/62/head
Christopher F 9 years ago
parent
commit
d47bae2863
61 changed files with 1082 additions and 305 deletions
  1. +155
    -130
      src/Discord.Net/API/DiscordRawClient.cs
  2. +8
    -0
      src/Discord.Net/API/IOptional.cs
  3. +48
    -0
      src/Discord.Net/API/Optional.cs
  4. +4
    -4
      src/Discord.Net/API/Rest/CreateChannelInviteParams.cs
  5. +1
    -1
      src/Discord.Net/API/Rest/CreateGuildBanParams.cs
  6. +2
    -1
      src/Discord.Net/API/Rest/CreateGuildChannelParams.cs
  7. +2
    -1
      src/Discord.Net/API/Rest/CreateGuildParams.cs
  8. +3
    -2
      src/Discord.Net/API/Rest/CreateMessageParams.cs
  9. +2
    -1
      src/Discord.Net/API/Rest/GetChannelMessagesParams.cs
  10. +2
    -2
      src/Discord.Net/API/Rest/GetGuildMembersParams.cs
  11. +12
    -0
      src/Discord.Net/API/Rest/LoginParams.cs
  12. +10
    -0
      src/Discord.Net/API/Rest/LoginResponse.cs
  13. +2
    -2
      src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs
  14. +10
    -0
      src/Discord.Net/API/Rest/ModifyCurrentUserNickParams.cs
  15. +5
    -5
      src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs
  16. +2
    -2
      src/Discord.Net/API/Rest/ModifyGuildChannelParams.cs
  17. +2
    -2
      src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs
  18. +3
    -3
      src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs
  19. +3
    -3
      src/Discord.Net/API/Rest/ModifyGuildIntegrationParams.cs
  20. +8
    -7
      src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs
  21. +9
    -9
      src/Discord.Net/API/Rest/ModifyGuildParams.cs
  22. +5
    -5
      src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs
  23. +1
    -1
      src/Discord.Net/API/Rest/ModifyGuildRolesParams.cs
  24. +1
    -1
      src/Discord.Net/API/Rest/ModifyMessageParams.cs
  25. +1
    -1
      src/Discord.Net/API/Rest/ModifyTextChannelParams.cs
  26. +1
    -1
      src/Discord.Net/API/Rest/ModifyVoiceChannelParams.cs
  27. +12
    -11
      src/Discord.Net/API/Rest/UploadFileParams.cs
  28. +3
    -1
      src/Discord.Net/Common/Entities/Guilds/IGuild.cs
  29. +0
    -2
      src/Discord.Net/Common/Entities/Messages/IMessage.cs
  30. +15
    -1
      src/Discord.Net/Discord.Net.csproj
  31. +7
    -3
      src/Discord.Net/IDiscordClient.cs
  32. +1
    -0
      src/Discord.Net/Logging/ILogger.cs
  33. +25
    -15
      src/Discord.Net/Logging/LogManager.cs
  34. +5
    -2
      src/Discord.Net/Net/Converters/ImageConverter.cs
  35. +34
    -0
      src/Discord.Net/Net/Converters/OptionalContractResolver.cs
  36. +23
    -0
      src/Discord.Net/Net/Converters/OptionalConverter.cs
  37. +15
    -0
      src/Discord.Net/Net/RateLimitException.cs
  38. +19
    -9
      src/Discord.Net/Net/Rest/DefaultRestClient.cs
  39. +0
    -7
      src/Discord.Net/Net/Rest/IMessageQueue.cs
  40. +3
    -2
      src/Discord.Net/Net/Rest/IRestClient.cs
  41. +8
    -0
      src/Discord.Net/Net/Rest/RequestQueue/BucketGroup.cs
  42. +8
    -0
      src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs
  43. +10
    -0
      src/Discord.Net/Net/Rest/RequestQueue/GuildBucket.cs
  44. +11
    -0
      src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs
  45. +163
    -0
      src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs
  46. +225
    -0
      src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs
  47. +40
    -0
      src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs
  48. +70
    -32
      src/Discord.Net/Rest/DiscordClient.cs
  49. +13
    -12
      src/Discord.Net/Rest/Entities/Channels/DMChannel.cs
  50. +0
    -3
      src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs
  51. +4
    -4
      src/Discord.Net/Rest/Entities/Channels/TextChannel.cs
  52. +12
    -4
      src/Discord.Net/Rest/Entities/Guilds/Guild.cs
  53. +2
    -0
      src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs
  54. +2
    -0
      src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs
  55. +2
    -0
      src/Discord.Net/Rest/Entities/Guilds/IntegrationAccount.cs
  56. +4
    -2
      src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs
  57. +2
    -0
      src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs
  58. +14
    -7
      src/Discord.Net/Rest/Entities/Message.cs
  59. +2
    -0
      src/Discord.Net/Rest/Entities/Users/Connection.cs
  60. +24
    -4
      src/Discord.Net/Rest/Entities/Users/GuildUser.cs
  61. +2
    -0
      src/Discord.Net/Rest/Entities/Users/User.cs

+ 155
- 130
src/Discord.Net/API/DiscordRawClient.cs View File

@@ -21,33 +21,26 @@ namespace Discord.API
{
internal event EventHandler<SentRequestEventArgs> SentRequest;

private readonly RequestQueue _requestQueue;
private readonly IRestClient _restClient;
private readonly CancellationToken _cancelToken;
private readonly JsonSerializer _serializer;

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

switch (authTokenType)
{
case TokenType.Bot:
authToken = $"Bot {authToken}";
break;
case TokenType.Bearer:
authToken = $"Bearer {authToken}";
break;
case TokenType.User:
break;
default:
throw new ArgumentException("Unknown oauth token type", nameof(authTokenType));
}

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

_serializer = new JsonSerializer();
_serializer.Converters.Add(new OptionalConverter());
_serializer.Converters.Add(new ChannelTypeConverter());
_serializer.Converters.Add(new ImageConverter());
_serializer.Converters.Add(new NullableUInt64Converter());
@@ -57,116 +50,113 @@ namespace Discord.API
_serializer.Converters.Add(new UInt64Converter());
_serializer.Converters.Add(new UInt64EntityConverter());
_serializer.Converters.Add(new UserStatusConverter());
_serializer.ContractResolver = new OptionalContractResolver();
}

//Core
public async Task<TResponse> Send<TResponse>(string method, string endpoint)
where TResponse : class
public void SetToken(TokenType tokenType, string token)
{
var stopwatch = Stopwatch.StartNew();
Stream responseStream;
try
{
responseStream = await _restClient.Send(method, endpoint, (string)null).ConfigureAwait(false);
}
catch (HttpException ex)
{
if (!HandleException(ex))
throw;
return null;
}
int bytes = (int)responseStream.Length;
stopwatch.Stop();
var response = Deserialize<TResponse>(responseStream);
AuthTokenType = tokenType;

double milliseconds = ToMilliseconds(stopwatch);
SentRequest(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds));

return response;
}
public async Task Send(string method, string endpoint)
{
var stopwatch = Stopwatch.StartNew();
try
{
await _restClient.Send(method, endpoint, (string)null).ConfigureAwait(false);
}
catch (HttpException ex)
if (token != null)
{
if (!HandleException(ex))
throw;
return;
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));
}
}
stopwatch.Stop();

double milliseconds = ToMilliseconds(stopwatch);
SentRequest(this, new SentRequestEventArgs(method, endpoint, 0, milliseconds));
_restClient.SetHeader("authorization", token);
}
public async Task<TResponse> Send<TResponse>(string method, string endpoint, object payload)

//Core
public Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General)
=> SendInternal(method, endpoint, null, true, bucket);
public Task Send(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.General)
=> SendInternal(method, endpoint, payload, true, bucket);
public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary<string, string> multipartArgs, GlobalBucket bucket = GlobalBucket.General)
=> SendInternal(method, endpoint, multipartArgs, true, bucket);
public async Task<TResponse> Send<TResponse>(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General)
where TResponse : class
=> Deserialize<TResponse>(await SendInternal(method, endpoint, null, false, bucket).ConfigureAwait(false));
public async Task<TResponse> Send<TResponse>(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.General)
where TResponse : class
=> Deserialize<TResponse>(await SendInternal(method, endpoint, payload, false, bucket).ConfigureAwait(false));
public async Task<TResponse> Send<TResponse>(string method, string endpoint, Stream file, IReadOnlyDictionary<string, string> multipartArgs, GlobalBucket bucket = GlobalBucket.General)
where TResponse : class
=> Deserialize<TResponse>(await SendInternal(method, endpoint, multipartArgs, false, bucket).ConfigureAwait(false));

public Task Send(string method, string endpoint, GuildBucket bucket, ulong guildId)
=> SendInternal(method, endpoint, null, true, bucket, guildId);
public Task Send(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId)
=> SendInternal(method, endpoint, payload, true, bucket, guildId);
public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary<string, string> multipartArgs, GuildBucket bucket, ulong guildId)
=> SendInternal(method, endpoint, multipartArgs, true, bucket, guildId);
public async Task<TResponse> Send<TResponse>(string method, string endpoint, GuildBucket bucket, ulong guildId)
where TResponse : class
=> Deserialize<TResponse>(await SendInternal(method, endpoint, null, false, bucket, guildId).ConfigureAwait(false));
public async Task<TResponse> Send<TResponse>(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId)
where TResponse : class
=> Deserialize<TResponse>(await SendInternal(method, endpoint, payload, false, bucket, guildId).ConfigureAwait(false));
public async Task<TResponse> Send<TResponse>(string method, string endpoint, Stream file, IReadOnlyDictionary<string, string> multipartArgs, GuildBucket bucket, ulong guildId)
where TResponse : class
=> Deserialize<TResponse>(await SendInternal(method, endpoint, multipartArgs, false, bucket, guildId).ConfigureAwait(false));

private Task<Stream> SendInternal(string method, string endpoint, object payload, bool headerOnly, GlobalBucket bucket)
=> SendInternal(method, endpoint, payload, headerOnly, BucketGroup.Global, (int)bucket, 0);
private Task<Stream> SendInternal(string method, string endpoint, object payload, bool headerOnly, GuildBucket bucket, ulong guildId)
=> SendInternal(method, endpoint, payload, headerOnly, BucketGroup.Guild, (int)bucket, guildId);
private Task<Stream> SendInternal(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, bool headerOnly, GlobalBucket bucket)
=> SendInternal(method, endpoint, multipartArgs, headerOnly, BucketGroup.Global, (int)bucket, 0);
private Task<Stream> SendInternal(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, bool headerOnly, GuildBucket bucket, ulong guildId)
=> SendInternal(method, endpoint, multipartArgs, headerOnly, BucketGroup.Guild, (int)bucket, guildId);

private async Task<Stream> SendInternal(string method, string endpoint, object payload, bool headerOnly, BucketGroup group, int bucketId, ulong guildId)
{
string requestStream = Serialize(payload);
var stopwatch = Stopwatch.StartNew();
Stream responseStream;
try
{
responseStream = await _restClient.Send(method, endpoint, requestStream).ConfigureAwait(false);
}
catch (HttpException ex)
{
if (!HandleException(ex))
throw;
return null;
}
int bytes = (int)responseStream.Length;
string json = null;
if (payload != null)
json = Serialize(payload);
var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, json, headerOnly), group, bucketId, guildId).ConfigureAwait(false);
int bytes = headerOnly ? 0 : (int)responseStream.Length;
stopwatch.Stop();
var response = Deserialize<TResponse>(responseStream);

double milliseconds = ToMilliseconds(stopwatch);
SentRequest(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds));
SentRequest?.Invoke(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds));

return response;
return responseStream;
}
public async Task Send(string method, string endpoint, object payload)
private async Task<Stream> SendInternal(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, bool headerOnly, BucketGroup group, int bucketId, ulong guildId)
{
string requestStream = Serialize(payload);
var stopwatch = Stopwatch.StartNew();
try
{
await _restClient.Send(method, endpoint, requestStream).ConfigureAwait(false);
}
catch (HttpException ex)
{
if (!HandleException(ex))
throw;
return;
}
var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, multipartArgs, headerOnly), group, bucketId, guildId).ConfigureAwait(false);
int bytes = headerOnly ? 0 : (int)responseStream.Length;
stopwatch.Stop();

double milliseconds = ToMilliseconds(stopwatch);
SentRequest(this, new SentRequestEventArgs(method, endpoint, 0, milliseconds));
SentRequest?.Invoke(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds));

return responseStream;
}
public async Task<TResponse> Send<TResponse>(string method, string endpoint, Stream file, IReadOnlyDictionary<string, string> multipartArgs)
where TResponse : class
{
var stopwatch = Stopwatch.StartNew();
var responseStream = await _restClient.Send(method, endpoint).ConfigureAwait(false);
stopwatch.Stop();
var response = Deserialize<TResponse>(responseStream);

double milliseconds = ToMilliseconds(stopwatch);
SentRequest(this, new SentRequestEventArgs(method, endpoint, (int)responseStream.Length, milliseconds));

return response;
//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 Send(string method, string endpoint, Stream file, IReadOnlyDictionary<string, string> multipartArgs)
public async Task ValidateToken()
{
var stopwatch = Stopwatch.StartNew();
await _restClient.Send(method, endpoint).ConfigureAwait(false);
stopwatch.Stop();

double milliseconds = ToMilliseconds(stopwatch);
SentRequest(this, new SentRequestEventArgs(method, endpoint, 0, milliseconds));
await Send("GET", "auth/login").ConfigureAwait(false);
}

//Gateway
@@ -257,7 +247,7 @@ namespace Discord.API
switch (channels.Length)
{
case 0:
throw new ArgumentOutOfRangeException(nameof(args));
return;
case 1:
await ModifyGuildChannel(channels[0].Id, new ModifyGuildChannelParams { Position = channels[0].Position }).ConfigureAwait(false);
break;
@@ -486,11 +476,11 @@ namespace Discord.API
if (args.Limit <= 0) throw new ArgumentOutOfRangeException(nameof(args.Limit));
if (args.Offset < 0) throw new ArgumentOutOfRangeException(nameof(args.Offset));

int limit = args.Limit ?? int.MaxValue;
int limit = args.Limit.GetValueOrDefault(int.MaxValue);
int offset = args.Offset;

List<GuildMember[]> result;
if (args.Limit != null)
if (args.Limit.IsSpecified)
result = new List<GuildMember[]>((limit + DiscordConfig.MaxUsersPerBatch - 1) / DiscordConfig.MaxUsersPerBatch);
else
result = new List<GuildMember[]>();
@@ -498,7 +488,7 @@ namespace Discord.API
while (true)
{
int runLimit = (limit >= DiscordConfig.MaxUsersPerBatch) ? DiscordConfig.MaxUsersPerBatch : limit;
string endpoint = $"guild/{guildId}/members?limit={limit}&offset={offset}";
string endpoint = $"guilds/{guildId}/members?limit={runLimit}&offset={offset}";
var models = await Send<GuildMember[]>("GET", endpoint).ConfigureAwait(false);

//Was this an empty batch?
@@ -514,8 +504,10 @@ namespace Discord.API

if (result.Count > 1)
return result.SelectMany(x => x);
else
else if (result.Count == 1)
return result[0];
else
return Array.Empty<GuildMember>();
}
public async Task RemoveGuildMember(ulong guildId, ulong userId)
{
@@ -524,13 +516,13 @@ namespace Discord.API

await Send("DELETE", $"guilds/{guildId}/members/{userId}").ConfigureAwait(false);
}
public async Task<GuildMember> ModifyGuildMember(ulong guildId, ulong userId, ModifyGuildMemberParams args)
public async Task ModifyGuildMember(ulong guildId, ulong userId, ModifyGuildMemberParams args)
{
if (args == null) throw new ArgumentNullException(nameof(args));
if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId));
if (userId == 0) throw new ArgumentOutOfRangeException(nameof(userId));
return await Send<GuildMember>("PATCH", $"guilds/{guildId}/members/{userId}", args).ConfigureAwait(false);
await Send("PATCH", $"guilds/{guildId}/members/{userId}", args).ConfigureAwait(false);
}

//Guild Roles
@@ -573,7 +565,7 @@ namespace Discord.API
switch (roles.Length)
{
case 0:
throw new ArgumentOutOfRangeException(nameof(args));
return Array.Empty<Role>();
case 1:
return ImmutableArray.Create(await ModifyGuildRole(guildId, roles[0].Id, roles[0]).ConfigureAwait(false));
default:
@@ -618,34 +610,57 @@ namespace Discord.API
if (models.Length != DiscordConfig.MaxMessagesPerBatch) { i++; break; }
}

if (runs > 1)
return result.Take(runs).SelectMany(x => x);
else
if (i > 1)
return result.Take(i).SelectMany(x => x);
else if (i == 1)
return result[0];
else
return Array.Empty<Message>();
}
public async Task<Message> CreateMessage(ulong channelId, CreateMessageParams args)
public Task<Message> CreateMessage(ulong channelId, CreateMessageParams args)
=> CreateMessage(0, channelId, args);
public async Task<Message> CreateMessage(ulong guildId, ulong channelId, CreateMessageParams args)
{
if (args == null) throw new ArgumentNullException(nameof(args));
if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId));

return await Send<Message>("POST", $"channels/{channelId}/messages", args).ConfigureAwait(false);
if (guildId != 0)
return await Send<Message>("POST", $"channels/{channelId}/messages", args, GuildBucket.SendEditMessage, guildId).ConfigureAwait(false);
else
return await Send<Message>("POST", $"channels/{channelId}/messages", args, GlobalBucket.DirectMessage).ConfigureAwait(false);
}
public async Task<Message> UploadFile(ulong channelId, Stream file, UploadFileParams args)
public Task<Message> UploadFile(ulong channelId, Stream file, UploadFileParams args)
=> UploadFile(0, channelId, file, args);
public async Task<Message> UploadFile(ulong guildId, ulong channelId, Stream file, UploadFileParams args)
{
if (args == null) throw new ArgumentNullException(nameof(args));
//if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId));
if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId));

return await Send<Message>("POST", $"channels/{channelId}/messages", file, args.ToDictionary()).ConfigureAwait(false);
if (guildId != 0)
return await Send<Message>("POST", $"channels/{channelId}/messages", file, args.ToDictionary(), GuildBucket.SendEditMessage, guildId).ConfigureAwait(false);
else
return await Send<Message>("POST", $"channels/{channelId}/messages", file, args.ToDictionary()).ConfigureAwait(false);
}
public async Task DeleteMessage(ulong channelId, ulong messageId)
public Task DeleteMessage(ulong channelId, ulong messageId)
=> DeleteMessage(0, channelId, messageId);
public async Task DeleteMessage(ulong guildId, ulong channelId, ulong messageId)
{
//if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId));
if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId));
if (messageId == 0) throw new ArgumentOutOfRangeException(nameof(messageId));

await Send("DELETE", $"channels/{channelId}/messages/{messageId}").ConfigureAwait(false);
if (guildId != 0)
await Send("DELETE", $"channels/{channelId}/messages/{messageId}", GuildBucket.DeleteMessage, guildId).ConfigureAwait(false);
else
await Send("DELETE", $"channels/{channelId}/messages/{messageId}").ConfigureAwait(false);
}
public async Task DeleteMessages(ulong channelId, DeleteMessagesParam args)
public Task DeleteMessages(ulong channelId, DeleteMessagesParam args)
=> DeleteMessages(0, channelId, args);
public async Task DeleteMessages(ulong guildId, ulong channelId, DeleteMessagesParam args)
{
//if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId));
if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId));
if (args == null) throw new ArgumentNullException(nameof(args));
if (args.MessageIds == null) throw new ArgumentNullException(nameof(args.MessageIds));

@@ -653,22 +668,31 @@ namespace Discord.API
switch (messageIds.Length)
{
case 0:
throw new ArgumentOutOfRangeException(nameof(args.MessageIds));
return;
case 1:
await DeleteMessage(channelId, messageIds[0]).ConfigureAwait(false);
await DeleteMessage(guildId, channelId, messageIds[0]).ConfigureAwait(false);
break;
default:
await Send("POST", $"channels/{channelId}/messages/bulk_delete", args).ConfigureAwait(false);
if (guildId != 0)
await Send("POST", $"channels/{channelId}/messages/bulk_delete", args, GuildBucket.DeleteMessages, guildId).ConfigureAwait(false);
else
await Send("POST", $"channels/{channelId}/messages/bulk_delete", args).ConfigureAwait(false);
break;
}
}
public async Task<Message> ModifyMessage(ulong channelId, ulong messageId, ModifyMessageParams args)
public Task<Message> ModifyMessage(ulong channelId, ulong messageId, ModifyMessageParams args)
=> ModifyMessage(0, channelId, messageId, args);
public async Task<Message> ModifyMessage(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args)
{
if (args == null) throw new ArgumentNullException(nameof(args));
//if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId));
if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId));
if (messageId == 0) throw new ArgumentOutOfRangeException(nameof(messageId));

return await Send<Message>("PATCH", $"channels/{channelId}/messages/{messageId}", args).ConfigureAwait(false);
if (guildId != 0)
return await Send<Message>("PATCH", $"channels/{channelId}/messages/{messageId}", args, GuildBucket.SendEditMessage, guildId).ConfigureAwait(false);
else
return await Send<Message>("PATCH", $"channels/{channelId}/messages/{messageId}", args).ConfigureAwait(false);
}
public async Task AckMessage(ulong channelId, ulong messageId)
{
@@ -739,6 +763,13 @@ namespace Discord.API

return await Send<User>("PATCH", "users/@me", args).ConfigureAwait(false);
}
public async Task ModifyCurrentUserNick(ulong guildId, ModifyCurrentUserNickParams args)
{
if (args == null) throw new ArgumentNullException(nameof(args));
if (args.Nickname == "") throw new ArgumentOutOfRangeException(nameof(args.Nickname));

await Send("PATCH", $"guilds/{guildId}/members/@me/nick", args).ConfigureAwait(false);
}
public async Task<Channel> CreateDMChannel(CreateDMChannelParams args)
{
if (args == null) throw new ArgumentNullException(nameof(args));
@@ -775,11 +806,5 @@ namespace Discord.API
using (JsonReader reader = new JsonTextReader(text))
return _serializer.Deserialize<T>(reader);
}

private bool HandleException(Exception ex)
{
//TODO: Implement... maybe via SentRequest? Need to bubble this up to DiscordClient or a MessageQueue
return false;
}
}
}

+ 8
- 0
src/Discord.Net/API/IOptional.cs View File

@@ -0,0 +1,8 @@
namespace Discord.API
{
public interface IOptional
{
object Value { get; }
bool IsSpecified { get; }
}
}

+ 48
- 0
src/Discord.Net/API/Optional.cs View File

@@ -0,0 +1,48 @@
using System;

namespace Discord.API
{
//Based on https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Nullable.cs
public struct Optional<T> : IOptional
{
private readonly T _value;

/// <summary> Gets the value for this paramter, or default(T) if unspecified. </summary>
public T Value
{
get
{
if (!IsSpecified)
throw new InvalidOperationException("This property has no value set.");
return _value;
}
}
/// <summary> Returns true if this value has been specified. </summary>
public bool IsSpecified { get; }

object IOptional.Value => _value;

/// <summary> Creates a new Parameter with the provided value. </summary>
public Optional(T value)
{
_value = value;
IsSpecified = true;
}
public T GetValueOrDefault() => _value;
public T GetValueOrDefault(T defaultValue) => IsSpecified ? _value : default(T);

public override bool Equals(object other)
{
if (!IsSpecified) return other == null;
if (other == null) return false;
return _value.Equals(other);
}

public override int GetHashCode() => IsSpecified ? _value.GetHashCode() : 0;
public override string ToString() => IsSpecified ? _value.ToString() : "";

public static implicit operator Optional<T>(T value) => new Optional<T>(value);
public static implicit operator T(Optional<T> value) => value.Value;
}
}

+ 4
- 4
src/Discord.Net/API/Rest/CreateChannelInviteParams.cs View File

@@ -5,12 +5,12 @@ namespace Discord.API.Rest
public class CreateChannelInviteParams
{
[JsonProperty("max_age")]
public int MaxAge { get; set; } = 86400; //24 Hours
public Optional<int> MaxAge { get; set; }
[JsonProperty("max_uses")]
public int MaxUses { get; set; } = 0;
public Optional<int> MaxUses { get; set; }
[JsonProperty("temporary")]
public bool Temporary { get; set; } = false;
public Optional<bool> Temporary { get; set; }
[JsonProperty("xkcdpass")]
public bool XkcdPass { get; set; } = false;
public Optional<bool> XkcdPass { get; set; }
}
}

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

@@ -5,6 +5,6 @@ namespace Discord.API.Rest
public class CreateGuildBanParams
{
[JsonProperty("delete-message-days")]
public int PruneDays { get; set; } = 0;
public Optional<int> PruneDays { get; set; }
}
}

+ 2
- 1
src/Discord.Net/API/Rest/CreateGuildChannelParams.cs View File

@@ -8,7 +8,8 @@ namespace Discord.API.Rest
public string Name { get; set; }
[JsonProperty("type")]
public ChannelType Type { get; set; }

[JsonProperty("bitrate")]
public int Bitrate { get; set; }
public Optional<int> Bitrate { get; set; }
}
}

+ 2
- 1
src/Discord.Net/API/Rest/CreateGuildParams.cs View File

@@ -10,7 +10,8 @@ namespace Discord.API.Rest
public string Name { get; set; }
[JsonProperty("region")]
public string Region { get; set; }

[JsonProperty("icon"), JsonConverter(typeof(ImageConverter))]
public Stream Icon { get; set; }
public Optional<Stream> Icon { get; set; }
}
}

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

@@ -6,9 +6,10 @@ namespace Discord.API.Rest
{
[JsonProperty("content")]
public string Content { get; set; } = "";

[JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)]
public string Nonce { get; set; } = null;
public Optional<string> Nonce { get; set; }
[JsonProperty("tts", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool IsTTS { get; set; } = false;
public Optional<bool> IsTTS { get; set; }
}
}

+ 2
- 1
src/Discord.Net/API/Rest/GetChannelMessagesParams.cs View File

@@ -4,6 +4,7 @@
{
public int Limit { get; set; } = DiscordConfig.MaxMessagesPerBatch;
public Direction RelativeDirection { get; set; } = Direction.Before;
public ulong? RelativeMessageId { get; set; } = null;

public Optional<ulong> RelativeMessageId { get; set; }
}
}

+ 2
- 2
src/Discord.Net/API/Rest/GetGuildMembersParams.cs View File

@@ -2,7 +2,7 @@
{
public class GetGuildMembersParams
{
public int? Limit { get; set; } = null;
public int Offset { get; set; } = 0;
public Optional<int> Limit { get; set; }
public Optional<int> Offset { get; set; }
}
}

+ 12
- 0
src/Discord.Net/API/Rest/LoginParams.cs View File

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

namespace Discord.API.Rest
{
public class LoginParams
{
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("password")]
public string Password { get; set; }
}
}

+ 10
- 0
src/Discord.Net/API/Rest/LoginResponse.cs View File

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

namespace Discord.API.Rest
{
public class LoginResponse
{
[JsonProperty("token")]
public string Token { get; set; }
}
}

+ 2
- 2
src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs View File

@@ -5,8 +5,8 @@ namespace Discord.API.Rest
public class ModifyChannelPermissionsParams
{
[JsonProperty("allow")]
public uint Allow { get; set; }
public Optional<uint> Allow { get; set; }
[JsonProperty("deny")]
public uint Deny { get; set; }
public Optional<uint> Deny { get; set; }
}
}

+ 10
- 0
src/Discord.Net/API/Rest/ModifyCurrentUserNickParams.cs View File

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

namespace Discord.API.Rest
{
public class ModifyCurrentUserNickParams
{
[JsonProperty("nick")]
public string Nickname { get; set; }
}
}

+ 5
- 5
src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs View File

@@ -7,14 +7,14 @@ namespace Discord.API.Rest
public class ModifyCurrentUserParams
{
[JsonProperty("username")]
public string Username { get; set; }
public Optional<string> Username { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
public Optional<string> Email { get; set; }
[JsonProperty("password")]
public string Password { get; set; }
public Optional<string> Password { get; set; }
[JsonProperty("new_password")]
public string NewPassword { get; set; }
public Optional<string> NewPassword { get; set; }
[JsonProperty("avatar"), JsonConverter(typeof(ImageConverter))]
public Stream Avatar { get; set; }
public Optional<Stream> Avatar { get; set; }
}
}

+ 2
- 2
src/Discord.Net/API/Rest/ModifyGuildChannelParams.cs View File

@@ -5,8 +5,8 @@ namespace Discord.API.Rest
public class ModifyGuildChannelParams
{
[JsonProperty("name")]
public string Name { get; set; }
public Optional<string> Name { get; set; }
[JsonProperty("position")]
public int Position { get; set; }
public Optional<int> Position { get; set; }
}
}

+ 2
- 2
src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs View File

@@ -5,8 +5,8 @@ namespace Discord.API.Rest
public class ModifyGuildChannelsParams
{
[JsonProperty("id")]
public ulong Id { get; set; }
public Optional<ulong> Id { get; set; }
[JsonProperty("position")]
public int Position { get; set; }
public Optional<int> Position { get; set; }
}
}

+ 3
- 3
src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs View File

@@ -6,8 +6,8 @@ namespace Discord.API.Rest
public class ModifyGuildEmbedParams
{
[JsonProperty("enabled")]
public bool Enabled { get; set; }
[JsonProperty("channel"), JsonConverter(typeof(UInt64EntityConverter))]
public IVoiceChannel Channel { get; set; }
public Optional<bool> Enabled { get; set; }
[JsonProperty("channel")]
public Optional<IVoiceChannel> Channel { get; set; }
}
}

+ 3
- 3
src/Discord.Net/API/Rest/ModifyGuildIntegrationParams.cs View File

@@ -5,10 +5,10 @@ namespace Discord.API.Rest
public class ModifyGuildIntegrationParams
{
[JsonProperty("expire_behavior")]
public int ExpireBehavior { get; set; }
public Optional<int> ExpireBehavior { get; set; }
[JsonProperty("expire_grace_period")]
public int ExpireGracePeriod { get; set; }
public Optional<int> ExpireGracePeriod { get; set; }
[JsonProperty("enable_emoticons")]
public bool EnableEmoticons { get; set; }
public Optional<bool> EnableEmoticons { get; set; }
}
}

+ 8
- 7
src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs View File

@@ -1,17 +1,18 @@
using Discord.Net.Converters;
using Newtonsoft.Json;
using Newtonsoft.Json;

namespace Discord.API.Rest
{
public class ModifyGuildMemberParams
{
[JsonProperty("roles")]
public ulong[] Roles { get; set; }
public Optional<ulong[]> Roles { get; set; }
[JsonProperty("mute")]
public bool Mute { get; set; }
public Optional<bool> Mute { get; set; }
[JsonProperty("deaf")]
public bool Deaf { get; set; }
[JsonProperty("channel_id"), JsonConverter(typeof(UInt64EntityConverter))]
public IVoiceChannel VoiceChannel { get; set; }
public Optional<bool> Deaf { get; set; }
[JsonProperty("nick")]
public Optional<string> Nickname { get; set; }
[JsonProperty("channel_id")]
public Optional<IVoiceChannel> VoiceChannel { get; set; }
}
}

+ 9
- 9
src/Discord.Net/API/Rest/ModifyGuildParams.cs View File

@@ -7,20 +7,20 @@ namespace Discord.API.Rest
public class ModifyGuildParams
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("region"), JsonConverterAttribute(typeof(StringEntityConverter))]
public IVoiceRegion Region { get; set; }
public Optional<string> Name { get; set; }
[JsonProperty("region")]
public Optional<IVoiceRegion> Region { get; set; }
[JsonProperty("verification_level")]
public int VerificationLevel { get; set; }
public Optional<int> VerificationLevel { get; set; }
[JsonProperty("afk_channel_id")]
public ulong? AFKChannelId { get; set; }
public Optional<ulong?> AFKChannelId { get; set; }
[JsonProperty("afk_timeout")]
public int AFKTimeout { get; set; }
public Optional<int> AFKTimeout { get; set; }
[JsonProperty("icon"), JsonConverter(typeof(ImageConverter))]
public Stream Icon { get; set; }
public Optional<Stream> Icon { get; set; }
[JsonProperty("owner_id")]
public GuildMember Owner { get; set; }
public Optional<GuildMember> Owner { get; set; }
[JsonProperty("splash"), JsonConverter(typeof(ImageConverter))]
public Stream Splash { get; set; }
public Optional<Stream> Splash { get; set; }
}
}

+ 5
- 5
src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs View File

@@ -5,14 +5,14 @@ namespace Discord.API.Rest
public class ModifyGuildRoleParams
{
[JsonProperty("name")]
public string Name { get; set; }
public Optional<string> Name { get; set; }
[JsonProperty("permissions")]
public uint Permissions { get; set; }
public Optional<uint> Permissions { get; set; }
[JsonProperty("position")]
public int Position { get; set; }
public Optional<int> Position { get; set; }
[JsonProperty("color")]
public uint Color { get; set; }
public Optional<uint> Color { get; set; }
[JsonProperty("hoist")]
public bool Hoist { get; set; }
public Optional<bool> Hoist { get; set; }
}
}

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

@@ -5,6 +5,6 @@ namespace Discord.API.Rest
public class ModifyGuildRolesParams : ModifyGuildRoleParams
{
[JsonProperty("id")]
public ulong Id { get; set; }
public Optional<ulong> Id { get; set; }
}
}

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

@@ -5,6 +5,6 @@ namespace Discord.API.Rest
public class ModifyMessageParams
{
[JsonProperty("content")]
public string Content { get; set; } = "";
public Optional<string> Content { get; set; } = "";
}
}

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

@@ -5,6 +5,6 @@ namespace Discord.API.Rest
public class ModifyTextChannelParams : ModifyGuildChannelParams
{
[JsonProperty("topic")]
public string Topic { get; set; }
public Optional<string> Topic { get; set; }
}
}

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

@@ -5,6 +5,6 @@ namespace Discord.API.Rest
public class ModifyVoiceChannelParams : ModifyGuildChannelParams
{
[JsonProperty("bitrate")]
public int Bitrate { get; set; }
public Optional<int> Bitrate { get; set; }
}
}

+ 12
- 11
src/Discord.Net/API/Rest/UploadFileParams.cs View File

@@ -4,21 +4,22 @@ namespace Discord.API.Rest
{
public class UploadFileParams
{
public string Content { get; set; } = "";
public string Nonce { get; set; } = null;
public bool IsTTS { get; set; } = false;
public string Filename { get; set; } = "unknown.dat";

public Optional<string> Content { get; set; }
public Optional<string> Nonce { get; set; }
public Optional<bool> IsTTS { get; set; }

public IReadOnlyDictionary<string, string> ToDictionary()
{
var dic = new Dictionary<string, string>
{
["content"] = Content,
["tts"] = IsTTS.ToString()
};
if (Nonce != null)
dic.Add("nonce", Nonce);
return dic;
var d = new Dictionary<string, string>();
if (Content.IsSpecified)
d["content"] = Content.Value;
if (IsTTS.IsSpecified)
d["tts"] = IsTTS.Value.ToString();
if (Nonce.IsSpecified)
d["nonce"] = Nonce.Value;
return d;
}
}
}

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

@@ -88,7 +88,9 @@ namespace Discord
/// <summary> Gets a collection of all users in this guild. </summary>
Task<IEnumerable<IGuildUser>> GetUsers();
/// <summary> Gets the user in this guild with the provided id, or null if not found. </summary>
Task<IGuildUser> GetUser(ulong id);
Task<IGuildUser> GetUser(ulong id);
/// <summary> Gets the current user for this guild. </summary>
Task<IGuildUser> GetCurrentUser();
Task<int> PruneUsers(int days = 30, bool simulate = false);
}
}

+ 0
- 2
src/Discord.Net/Common/Entities/Messages/IMessage.cs View File

@@ -9,8 +9,6 @@ namespace Discord
{
/// <summary> Gets the time of this message's last edit, if any. </summary>
DateTime? EditedTimestamp { get; }
/// <summary> Returns true if this message originated from the logged-in account. </summary>
bool IsAuthor { get; }
/// <summary> Returns true if this message was sent as a text-to-speech message. </summary>
bool IsTTS { get; }
/// <summary> Returns the original, unprocessed text for this message. </summary>


+ 15
- 1
src/Discord.Net/Discord.Net.csproj View File

@@ -66,8 +66,13 @@
<Compile Include="API\Common\UserGuild.cs" />
<Compile Include="API\Common\VoiceRegion.cs" />
<Compile Include="API\Common\VoiceState.cs" />
<Compile Include="API\IOptional.cs" />
<Compile Include="API\Optional.cs" />
<Compile Include="API\Rest\DeleteMessagesParam.cs" />
<Compile Include="API\Rest\GetGuildMembersParams.cs" />
<Compile Include="API\Rest\LoginParams.cs" />
<Compile Include="API\Rest\LoginResponse.cs" />
<Compile Include="API\Rest\ModifyCurrentUserNickParams.cs" />
<Compile Include="API\Rest\UploadFileParams.cs" />
<Compile Include="API\Rest\GuildPruneParams.cs" />
<Compile Include="API\Rest\CreateChannelInviteParams.cs" />
@@ -95,6 +100,15 @@
<Compile Include="API\Rest\ModifyVoiceChannelParams.cs" />
<Compile Include="DiscordConfig.cs" />
<Compile Include="API\DiscordRawClient.cs" />
<Compile Include="Net\Converters\OptionalContractResolver.cs" />
<Compile Include="Net\Converters\OptionalConverter.cs" />
<Compile Include="Net\RateLimitException.cs" />
<Compile Include="Net\Rest\RequestQueue\BucketGroup.cs" />
<Compile Include="Net\Rest\RequestQueue\GlobalBucket.cs" />
<Compile Include="Net\Rest\RequestQueue\GuildBucket.cs" />
<Compile Include="Net\Rest\RequestQueue\RequestQueue.cs" />
<Compile Include="Net\Rest\RequestQueue\RequestQueueBucket.cs" />
<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" />
@@ -181,7 +195,7 @@
<Compile Include="Net\Converters\UserStatusConverter.cs" />
<Compile Include="Net\HttpException.cs" />
<Compile Include="Net\Rest\DefaultRestClient.cs" />
<Compile Include="Net\Rest\IMessageQueue.cs" />
<Compile Include="Net\Rest\RequestQueue\IRequestQueue.cs" />
<Compile Include="Net\Rest\IRestClient.cs" />
<Compile Include="Net\Rest\MultipartFile.cs" />
<Compile Include="Net\Rest\RestClientProvider.cs" />


+ 7
- 3
src/Discord.Net/IDiscordClient.cs View File

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

namespace Discord
{
//TODO: Add docstrings
public interface IDiscordClient
{
ISelfUser CurrentUser { get; }
TokenType AuthTokenType { get; }
DiscordRawClient BaseClient { get; }
//IMessageQueue MessageQueue { get; }
IRestClient RestClient { get; }
IRequestQueue RequestQueue { get; }

Task Login(TokenType tokenType, string token);
Task Login(string email, string password);
Task Login(TokenType tokenType, string token, bool validateToken = true);
Task Logout();

Task<IChannel> GetChannel(ulong id);


+ 1
- 0
src/Discord.Net/Logging/ILogger.cs View File

@@ -8,6 +8,7 @@ namespace Discord.Logging

void Log(LogSeverity severity, string message, Exception exception = null);
void Log(LogSeverity severity, FormattableString message, Exception exception = null);
void Log(LogSeverity severity, Exception exception);

void Error(string message, Exception exception = null);
void Error(FormattableString message, Exception exception = null);


+ 25
- 15
src/Discord.Net/Logging/LogManager.cs View File

@@ -23,6 +23,11 @@ namespace Discord.Logging
if (severity <= Level)
Message(this, new LogMessageEventArgs(severity, source, message.ToString(), ex));
}
public void Log(LogSeverity severity, string source, Exception ex)
{
if (severity <= Level)
Message(this, new LogMessageEventArgs(severity, source, null, ex));
}
void ILogger.Log(LogSeverity severity, string message, Exception ex)
{
if (severity <= Level)
@@ -33,71 +38,76 @@ namespace Discord.Logging
if (severity <= Level)
Message(this, new LogMessageEventArgs(severity, "Discord", message.ToString(), ex));
}
void ILogger.Log(LogSeverity severity, Exception ex)
{
if (severity <= Level)
Message(this, new LogMessageEventArgs(severity, "Discord", null, ex));
}

public void Error(string source, string message, Exception ex = null)
=> Log(LogSeverity.Error, source, message, ex);
public void Error(string source, FormattableString message, Exception ex = null)
=> Log(LogSeverity.Error, source, message, ex);
public void Error(string source, Exception ex = null)
=> Log(LogSeverity.Error, source, (string)null, ex);
public void Error(string source, Exception ex)
=> Log(LogSeverity.Error, source, ex);
void ILogger.Error(string message, Exception ex)
=> Log(LogSeverity.Error, "Discord", message, ex);
void ILogger.Error(FormattableString message, Exception ex)
=> Log(LogSeverity.Error, "Discord", message, ex);
void ILogger.Error(Exception ex)
=> Log(LogSeverity.Error, "Discord", (string)null, ex);
=> Log(LogSeverity.Error, "Discord", ex);

public void Warning(string source, string message, Exception ex = null)
=> Log(LogSeverity.Warning, source, message, ex);
public void Warning(string source, FormattableString message, Exception ex = null)
=> Log(LogSeverity.Warning, source, message, ex);
public void Warning(string source, Exception ex = null)
=> Log(LogSeverity.Warning, source, (string)null, ex);
public void Warning(string source, Exception ex)
=> Log(LogSeverity.Warning, source, ex);
void ILogger.Warning(string message, Exception ex)
=> Log(LogSeverity.Warning, "Discord", message, ex);
void ILogger.Warning(FormattableString message, Exception ex)
=> Log(LogSeverity.Warning, "Discord", message, ex);
void ILogger.Warning(Exception ex)
=> Log(LogSeverity.Warning, "Discord", (string)null, ex);
=> Log(LogSeverity.Warning, "Discord", ex);

public void Info(string source, string message, Exception ex = null)
=> Log(LogSeverity.Info, source, message, ex);
public void Info(string source, FormattableString message, Exception ex = null)
=> Log(LogSeverity.Info, source, message, ex);
public void Info(string source, Exception ex = null)
=> Log(LogSeverity.Info, source, (string)null, ex);
public void Info(string source, Exception ex)
=> Log(LogSeverity.Info, source, ex);
void ILogger.Info(string message, Exception ex)
=> Log(LogSeverity.Info, "Discord", message, ex);
void ILogger.Info(FormattableString message, Exception ex)
=> Log(LogSeverity.Info, "Discord", message, ex);
void ILogger.Info(Exception ex)
=> Log(LogSeverity.Info, "Discord", (string)null, ex);
=> Log(LogSeverity.Info, "Discord", ex);

public void Verbose(string source, string message, Exception ex = null)
=> Log(LogSeverity.Verbose, source, message, ex);
public void Verbose(string source, FormattableString message, Exception ex = null)
=> Log(LogSeverity.Verbose, source, message, ex);
public void Verbose(string source, Exception ex = null)
=> Log(LogSeverity.Verbose, source, (string)null, ex);
public void Verbose(string source, Exception ex)
=> Log(LogSeverity.Verbose, source, ex);
void ILogger.Verbose(string message, Exception ex)
=> Log(LogSeverity.Verbose, "Discord", message, ex);
void ILogger.Verbose(FormattableString message, Exception ex)
=> Log(LogSeverity.Verbose, "Discord", message, ex);
void ILogger.Verbose(Exception ex)
=> Log(LogSeverity.Verbose, "Discord", (string)null, ex);
=> Log(LogSeverity.Verbose, "Discord", ex);

public void Debug(string source, string message, Exception ex = null)
=> Log(LogSeverity.Debug, source, message, ex);
public void Debug(string source, FormattableString message, Exception ex = null)
=> Log(LogSeverity.Debug, source, message, ex);
public void Debug(string source, Exception ex = null)
=> Log(LogSeverity.Debug, source, (string)null, ex);
public void Debug(string source, Exception ex)
=> Log(LogSeverity.Debug, source, ex);
void ILogger.Debug(string message, Exception ex)
=> Log(LogSeverity.Debug, "Discord", message, ex);
void ILogger.Debug(FormattableString message, Exception ex)
=> Log(LogSeverity.Debug, "Discord", message, ex);
void ILogger.Debug(Exception ex)
=> Log(LogSeverity.Debug, "Discord", (string)null, ex);
=> Log(LogSeverity.Debug, "Discord", ex);

internal Logger CreateLogger(string name) => new Logger(this, name);
}


+ 5
- 2
src/Discord.Net/Net/Converters/ImageConverter.cs View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using Discord.API;
using Newtonsoft.Json;
using System;
using System.IO;

@@ -6,7 +7,7 @@ namespace Discord.Net.Converters
{
public class ImageConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(Stream);
public override bool CanConvert(Type objectType) => objectType == typeof(Stream) || objectType == typeof(Optional<Stream>);
public override bool CanRead => true;
public override bool CanWrite => true;

@@ -17,6 +18,8 @@ namespace Discord.Net.Converters

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is Optional<Stream>)
value = (Optional<Stream>)value;
var stream = value as Stream;

byte[] bytes = new byte[stream.Length - stream.Position];


+ 34
- 0
src/Discord.Net/Net/Converters/OptionalContractResolver.cs View File

@@ -0,0 +1,34 @@
using Discord.API;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Linq.Expressions;
using System.Reflection;

namespace Discord.Net.Converters
{
public class OptionalContractResolver : DefaultContractResolver
{
private static readonly PropertyInfo _isSpecified = typeof(IOptional).GetProperty(nameof(IOptional.IsSpecified));

protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
var type = property.PropertyType;

if (member.MemberType == MemberTypes.Property)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>))
{
var parentArg = Expression.Parameter(typeof(object));
var optional = Expression.Property(Expression.Convert(parentArg, property.DeclaringType), member as PropertyInfo);
var isSpecified = Expression.Property(optional, _isSpecified);
var lambda = Expression.Lambda<Func<object, bool>>(isSpecified, parentArg).Compile();
property.ShouldSerialize = x => lambda(x);
}
}

return property;
}
}
}

+ 23
- 0
src/Discord.Net/Net/Converters/OptionalConverter.cs View File

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

namespace Discord.Net.Converters
{
public class OptionalConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Optional<>);
public override bool CanRead => false;
public override bool CanWrite => true;

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new InvalidOperationException();
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, (value as IOptional).Value);
}
}
}

+ 15
- 0
src/Discord.Net/Net/RateLimitException.cs View File

@@ -0,0 +1,15 @@
using System.Net;

namespace Discord.Net
{
public class HttpRateLimitException : HttpException
{
public int RetryAfterMilliseconds { get; }

public HttpRateLimitException(int retryAfterMilliseconds)
: base((HttpStatusCode)429)
{
RetryAfterMilliseconds = retryAfterMilliseconds;
}
}
}

+ 19
- 9
src/Discord.Net/Net/Rest/DefaultRestClient.cs View File

@@ -1,8 +1,8 @@
using Newtonsoft.Json;
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
@@ -32,6 +32,8 @@ namespace Discord.Net.Rest
UseProxy = false,
PreAuthenticate = false
});

SetHeader("accept-encoding", "gzip, deflate");
}
protected virtual void Dispose(bool disposing)
{
@@ -50,21 +52,22 @@ namespace Discord.Net.Rest
public void SetHeader(string key, string value)
{
_client.DefaultRequestHeaders.Remove(key);
_client.DefaultRequestHeaders.Add(key, value);
if (value != null)
_client.DefaultRequestHeaders.Add(key, value);
}

public async Task<Stream> Send(string method, string endpoint, string json = null)
public async Task<Stream> Send(string method, string endpoint, 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).ConfigureAwait(false);
return await SendInternal(restRequest, _cancelToken, headerOnly).ConfigureAwait(false);
}
}

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

private async Task<Stream> SendInternal(HttpRequestMessage request, CancellationToken cancelToken)
private async Task<Stream> SendInternal(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly)
{
int retryCount = 0;
while (true)
@@ -118,9 +121,16 @@ namespace Discord.Net.Rest

int statusCode = (int)response.StatusCode;
if (statusCode < 200 || statusCode >= 300) //2xx = Success
{
if (statusCode == 429)
throw new HttpRateLimitException(int.Parse(response.Headers.GetValues("retry-after").First()));
throw new HttpException(response.StatusCode);
}

return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
if (headerOnly)
return null;
else
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
}
}



+ 0
- 7
src/Discord.Net/Net/Rest/IMessageQueue.cs View File

@@ -1,7 +0,0 @@
namespace Discord.Net.Rest
{
public interface IMessageQueue
{
int Count { get; }
}
}

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

@@ -4,11 +4,12 @@ using System.Threading.Tasks;

namespace Discord.Net.Rest
{
//TODO: Add docstrings
public interface IRestClient
{
void SetHeader(string key, string value);

Task<Stream> Send(string method, string endpoint, string json = null);
Task<Stream> Send(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams);
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);
}
}

+ 8
- 0
src/Discord.Net/Net/Rest/RequestQueue/BucketGroup.cs View File

@@ -0,0 +1,8 @@
namespace Discord.Net.Rest
{
internal enum BucketGroup
{
Global,
Guild
}
}

+ 8
- 0
src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs View File

@@ -0,0 +1,8 @@
namespace Discord.Net.Rest
{
public enum GlobalBucket
{
General,
DirectMessage
}
}

+ 10
- 0
src/Discord.Net/Net/Rest/RequestQueue/GuildBucket.cs View File

@@ -0,0 +1,10 @@
namespace Discord.Net.Rest
{
public enum GuildBucket
{
SendEditMessage,
DeleteMessage,
DeleteMessages,
Nickname
}
}

+ 11
- 0
src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs View File

@@ -0,0 +1,11 @@
using System.Threading.Tasks;

namespace Discord.Net.Rest
{
//TODO: Add docstrings
public interface IRequestQueue
{
Task Clear(GlobalBucket type);
Task Clear(GuildBucket type, ulong guildId);
}
}

+ 163
- 0
src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Net.Rest
{
public class RequestQueue : IRequestQueue
{
private SemaphoreSlim _lock;
private RequestQueueBucket[] _globalBuckets;
private Dictionary<ulong, RequestQueueBucket>[] _guildBuckets;

public IRestClient RestClient { get; }

public RequestQueue(IRestClient restClient)
{
RestClient = restClient;

_lock = new SemaphoreSlim(1, 1);
_globalBuckets = new RequestQueueBucket[Enum.GetValues(typeof(GlobalBucket)).Length];
_guildBuckets = new Dictionary<ulong, RequestQueueBucket>[Enum.GetValues(typeof(GuildBucket)).Length];
}
internal async Task<Stream> Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId)
{
RequestQueueBucket bucket;

await Lock().ConfigureAwait(false);
try
{
bucket = GetBucket(group, bucketId, guildId);
bucket.Queue(request);
}
finally { Unlock(); }

//There is a chance the bucket will send this request on its own, but this will simply become a noop then.
var _ = bucket.ProcessQueue(acquireLock: true).ConfigureAwait(false);

return await request.Promise.Task.ConfigureAwait(false);
}

private RequestQueueBucket CreateBucket(GlobalBucket bucket)
{
switch (bucket)
{
//Globals
case GlobalBucket.General: return new RequestQueueBucket(this, bucket, int.MaxValue, 0); //Catch-all
case GlobalBucket.DirectMessage: return new RequestQueueBucket(this, bucket, 5, 5);

default: throw new ArgumentException($"Unknown global bucket: {bucket}", nameof(bucket));
}
}
private RequestQueueBucket CreateBucket(GuildBucket bucket, ulong guildId)
{
switch (bucket)
{
//Per Guild
case GuildBucket.SendEditMessage: return new RequestQueueBucket(this, bucket, guildId, 5, 5);
case GuildBucket.DeleteMessage: return new RequestQueueBucket(this, bucket, guildId, 5, 1);
case GuildBucket.DeleteMessages: return new RequestQueueBucket(this, bucket, guildId, 1, 1);
case GuildBucket.Nickname: return new RequestQueueBucket(this, bucket, guildId, 1, 1);

default: throw new ArgumentException($"Unknown guild bucket: {bucket}", nameof(bucket));
}
}

private RequestQueueBucket GetBucket(BucketGroup group, int bucketId, ulong guildId)
{
switch (group)
{
case BucketGroup.Global:
return GetGlobalBucket((GlobalBucket)bucketId);
case BucketGroup.Guild:
return GetGuildBucket((GuildBucket)bucketId, guildId);
default:
throw new ArgumentException($"Unknown bucket group: {group}", nameof(group));
}
}
private RequestQueueBucket GetGlobalBucket(GlobalBucket type)
{
var bucket = _globalBuckets[(int)type];
if (bucket == null)
{
bucket = CreateBucket(type);
_globalBuckets[(int)type] = bucket;
}
return bucket;
}
private RequestQueueBucket GetGuildBucket(GuildBucket type, ulong guildId)
{
var bucketGroup = _guildBuckets[(int)type];
if (bucketGroup == null)
{
bucketGroup = new Dictionary<ulong, RequestQueueBucket>();
_guildBuckets[(int)type] = bucketGroup;
}
RequestQueueBucket bucket;
if (!bucketGroup.TryGetValue(guildId, out bucket))
{
bucket = CreateBucket(type, guildId);
bucketGroup[guildId] = bucket;
}
return bucket;
}

internal void DestroyGlobalBucket(GlobalBucket type)
{
//Assume this object is locked

_globalBuckets[(int)type] = null;
}
internal void DestroyGuildBucket(GuildBucket type, ulong guildId)
{
//Assume this object is locked

var bucketGroup = _guildBuckets[(int)type];
if (bucketGroup != null)
bucketGroup.Remove(guildId);
}

public async Task Lock()
{
await _lock.WaitAsync();
}
public void Unlock()
{
_lock.Release();
}

public async Task Clear(GlobalBucket type)
{
var bucket = _globalBuckets[(int)type];
if (bucket != null)
{
try
{
await bucket.Lock();
bucket.Clear();
}
finally { bucket.Unlock(); }
}
}
public async Task Clear(GuildBucket type, ulong guildId)
{
var bucketGroup = _guildBuckets[(int)type];
if (bucketGroup != null)
{
RequestQueueBucket bucket;
if (bucketGroup.TryGetValue(guildId, out bucket))
{
try
{
await bucket.Lock();
bucket.Clear();
}
finally { bucket.Unlock(); }
}
}
}
}
}

+ 225
- 0
src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs View File

@@ -0,0 +1,225 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Net.Rest
{
internal class RequestQueueBucket
{
private readonly RequestQueue _parent;
private readonly BucketGroup _bucketGroup;
private readonly int _bucketId;
private readonly ulong _guildId;
private readonly ConcurrentQueue<RestRequest> _queue;
private readonly SemaphoreSlim _lock;
private Task _resetTask;
private bool _waitingToProcess, _destroyed; //TODO: Remove _destroyed
private int _id;

public int WindowMaxCount { get; }
public int WindowSeconds { get; }
public int WindowCount { get; private set; }

public RequestQueueBucket(RequestQueue parent, GlobalBucket bucket, int windowMaxCount, int windowSeconds)
: this(parent, windowMaxCount, windowSeconds)
{
_bucketGroup = BucketGroup.Global;
_bucketId = (int)bucket;
_guildId = 0;
}
public RequestQueueBucket(RequestQueue parent, GuildBucket bucket, ulong guildId, int windowMaxCount, int windowSeconds)
: this(parent, windowMaxCount, windowSeconds)
{
_bucketGroup = BucketGroup.Guild;
_bucketId = (int)bucket;
_guildId = guildId;
}
private RequestQueueBucket(RequestQueue parent, int windowMaxCount, int windowSeconds)
{
_parent = parent;
WindowMaxCount = windowMaxCount;
WindowSeconds = windowSeconds;
_queue = new ConcurrentQueue<RestRequest>();
_lock = new SemaphoreSlim(1, 1);
_id = new System.Random().Next(0, int.MaxValue);
}

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)
{
//Assume this obj is under lock

int nextRetry = 1000;

//If we have another ProcessQueue waiting to run, dont bother with this one
if (_waitingToProcess) return;
_waitingToProcess = true;

if (acquireLock)
await Lock().ConfigureAwait(false);
try
{
_waitingToProcess = false;
while (true)
{
RestRequest request;

//If we're waiting to reset (due to a rate limit exception, or preemptive check), abort
if (WindowCount == WindowMaxCount) return;
//Get next request, return if queue is empty
if (!_queue.TryPeek(out request)) break;

try
{
Stream stream;
if (request.IsMultipart)
stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.MultipartParams, request.HeaderOnly).ConfigureAwait(false);
else
stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.Json, request.HeaderOnly).ConfigureAwait(false);
request.Promise.SetResult(stream);
}
catch (HttpRateLimitException ex) //Preemptive check failed, use Discord's time instead of our own
{
WindowCount = WindowMaxCount;
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)
{
if (ex.StatusCode == HttpStatusCode.BadGateway) //Gateway unavailable, retry
{
await Task.Delay(nextRetry).ConfigureAwait(false);
nextRetry *= 2;
if (nextRetry > 30000)
nextRetry = 30000;
continue;
}
else
{
//We dont need to throw this here, pass the exception via the promise
request.Promise.SetException(ex);
}
}

//Request completed or had an error other than 429
_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");
}
}

//If queue is empty, non-global, and there is no active rate limit, remove this bucket
if (_resetTask == null && _bucketGroup == BucketGroup.Guild)
{
try
{
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
{
_parent.Unlock();
}
}
}
finally
{
if (acquireLock)
Unlock();
}
}
public void Clear()
{
//Assume this obj is under lock
RestRequest request;

while (_queue.TryDequeue(out request)) { }
}

private async Task ResetAfter(int milliseconds)
{
if (milliseconds > 0)
await Task.Delay(milliseconds).ConfigureAwait(false);
try
{
await Lock().ConfigureAwait(false);

Debug($"Reset");

//Reset the current window count and set our state back to normal
WindowCount = 0;
_resetTask = null;

//Wait is over, work through the current queue
await ProcessQueue().ConfigureAwait(false);
}
finally
{
Unlock();
}
}

public async Task Lock()
{
await _lock.WaitAsync();
}
public void Unlock()
{
_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}");
}
}
}

+ 40
- 0
src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

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

public bool IsMultipart => MultipartParams != null;

public RestRequest(string method, string endpoint, string json, bool headerOnly)
: this(method, endpoint, headerOnly)
{
Json = json;
}

public RestRequest(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, bool headerOnly)
: this(method, endpoint, headerOnly)
{
MultipartParams = multipartParams;
}

private RestRequest(string method, string endpoint, bool headerOnly)
{
Method = method;
Endpoint = endpoint;
Json = null;
MultipartParams = null;
HeaderOnly = headerOnly;
Promise = new TaskCompletionSource<Stream>();
}
}
}

+ 70
- 32
src/Discord.Net/Rest/DiscordClient.cs View File

@@ -1,11 +1,13 @@
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;

@@ -22,10 +24,14 @@ namespace Discord.Rest
private CancellationTokenSource _cancelTokenSource;
private bool _isDisposed;
private string _userAgent;
private SelfUser _currentUser;

public bool IsLoggedIn { get; private set; }
internal API.DiscordRawClient BaseClient { get; private set; }
internal SelfUser CurrentUser { get; private set; }
public API.DiscordRawClient BaseClient { get; private set; }

public TokenType AuthTokenType => BaseClient.AuthTokenType;
public IRestClient RestClient => BaseClient.RestClient;
public IRequestQueue RequestQueue => BaseClient.RequestQueue;

public DiscordClient(DiscordConfig config = null)
{
@@ -37,43 +43,67 @@ namespace Discord.Rest
_connectionLock = new SemaphoreSlim(1, 1);
_log = new LogManager(config.LogLevel);
_userAgent = DiscordConfig.UserAgent;
BaseClient = new API.DiscordRawClient(_restClientProvider, _cancelTokenSource.Token);

_log.Message += (s,e) => Log.Raise(this, e);
}

public async Task Login(TokenType tokenType, string token)
public async Task Login(string email, string password)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await LoginInternal(tokenType, token).ConfigureAwait(false);
await LoginInternal(email, password).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task LoginInternal(TokenType tokenType, string token)
public async Task Login(TokenType tokenType, string token, bool validateToken = true)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await LoginInternal(tokenType, token, validateToken).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task LoginInternal(string email, string password)
{
if (IsLoggedIn)
LogoutInternal();

try
{
var cancelTokenSource = new CancellationTokenSource();
BaseClient = new API.DiscordRawClient(_restClientProvider, cancelTokenSource.Token, tokenType, token);
BaseClient.SentRequest += (s, e) => _log.Verbose($"{e.Method} {e.Endpoint}: {e.Milliseconds} ms");

//MessageQueue = new MessageQueue(RestClient, _restLogger);
//await MessageQueue.Start(_cancelTokenSource.Token).ConfigureAwait(false);
var currentUser = await BaseClient.GetCurrentUser().ConfigureAwait(false);
CurrentUser = new SelfUser(this, currentUser);
_cancelTokenSource = cancelTokenSource;
IsLoggedIn = true;
LoggedIn.Raise(this);

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

BaseClient.SetToken(tokenType, token);
await CompleteLogin(cancelTokenSource, validateToken).ConfigureAwait(false);
}
catch { LogoutInternal(); throw; }
}
private async Task CompleteLogin(CancellationTokenSource cancelTokenSource, 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;
IsLoggedIn = true;
LoggedIn.Raise(this);
}

public async Task Logout()
{
@@ -89,9 +119,14 @@ namespace Discord.Rest
{
bool wasLoggedIn = IsLoggedIn;

try { _cancelTokenSource.Cancel(false); } catch { }
if (_cancelTokenSource != null)
{
try { _cancelTokenSource.Cancel(false); }
catch { }
}

BaseClient = null;
BaseClient.SetToken(TokenType.User, null);
_currentUser = null;

if (wasLoggedIn)
{
@@ -150,7 +185,7 @@ namespace Discord.Rest
{
var model = await BaseClient.GetGuildEmbed(id).ConfigureAwait(false);
if (model != null)
return new GuildEmbed(this, model);
return new GuildEmbed(model);
return null;
}
public async Task<IEnumerable<UserGuild>> GetGuilds()
@@ -173,25 +208,25 @@ namespace Discord.Rest
return new PublicUser(this, model);
return null;
}
public async Task<IUser> GetUser(string username, ushort discriminator)
public async Task<User> GetUser(string username, ushort discriminator)
{
var model = await BaseClient.GetUser(username, discriminator).ConfigureAwait(false);
if (model != null)
return new PublicUser(this, model);
return null;
}
public async Task<ISelfUser> GetCurrentUser()
public async Task<SelfUser> GetCurrentUser()
{
var currentUser = CurrentUser;
if (currentUser == null)
var user = _currentUser;
if (user == null)
{
var model = await BaseClient.GetCurrentUser().ConfigureAwait(false);
currentUser = new SelfUser(this, model);
CurrentUser = currentUser;
user = new SelfUser(this, model);
_currentUser = user;
}
return currentUser;
return user;
}
public async Task<IEnumerable<IUser>> QueryUsers(string query, int limit)
public async Task<IEnumerable<User>> QueryUsers(string query, int limit)
{
var models = await BaseClient.QueryUsers(query, limit).ConfigureAwait(false);
return models.Select(x => new PublicUser(this, x));
@@ -225,7 +260,6 @@ namespace Discord.Rest
public void Dispose() => Dispose(true);

API.DiscordRawClient IDiscordClient.BaseClient => BaseClient;
ISelfUser IDiscordClient.CurrentUser => CurrentUser;

async Task<IChannel> IDiscordClient.GetChannel(ulong id)
=> await GetChannel(id).ConfigureAwait(false);
@@ -243,6 +277,10 @@ namespace Discord.Rest
=> await CreateGuild(name, region, jpegIcon).ConfigureAwait(false);
async Task<IUser> IDiscordClient.GetUser(ulong id)
=> await GetUser(id).ConfigureAwait(false);
async Task<IUser> IDiscordClient.GetUser(string username, ushort discriminator)
=> await GetUser(username, discriminator).ConfigureAwait(false);
async Task<ISelfUser> IDiscordClient.GetCurrentUser()
=> await GetCurrentUser().ConfigureAwait(false);
async Task<IEnumerable<IUser>> IDiscordClient.QueryUsers(string query, int limit)
=> await QueryUsers(query, limit).ConfigureAwait(false);
async Task<IEnumerable<IVoiceRegion>> IDiscordClient.GetVoiceRegions()


+ 13
- 12
src/Discord.Net/Rest/Entities/Channels/DMChannel.cs View File

@@ -20,8 +20,6 @@ namespace Discord.Rest

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

internal DMChannel(DiscordClient discord, Model model)
{
@@ -39,20 +37,23 @@ namespace Discord.Rest
}

/// <inheritdoc />
public IUser GetUser(ulong id)
public async Task<IUser> GetUser(ulong id)
{
var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false);
if (id == Recipient.Id)
return Recipient;
else if (id == Discord.CurrentUser.Id)
return Discord.CurrentUser;
else if (id == currentUser.Id)
return currentUser;
else
return null;
}
public IEnumerable<IUser> GetUsers()
/// <inheritdoc />
public async Task<IEnumerable<IUser>> GetUsers()
{
return ImmutableArray.Create<IUser>(Discord.CurrentUser, Recipient);
var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false);
return ImmutableArray.Create<IUser>(currentUser, Recipient);
}
/// <inheritdoc />
public async Task<IEnumerable<Message>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch)
{
@@ -124,10 +125,10 @@ namespace Discord.Rest
IDMUser IDMChannel.Recipient => Recipient;

Task<IEnumerable<IUser>> IChannel.GetUsers()
=> Task.FromResult(GetUsers());
Task<IUser> IChannel.GetUser(ulong id)
=> Task.FromResult(GetUser(id));
async Task<IEnumerable<IUser>> IChannel.GetUsers()
=> await GetUsers().ConfigureAwait(false);
async Task<IUser> IChannel.GetUser(ulong id)
=> await GetUser(id).ConfigureAwait(false);
Task<IMessage> IMessageChannel.GetMessage(ulong id)
=> throw new NotSupportedException();
async Task<IEnumerable<IMessage>> IMessageChannel.GetMessages(int limit)


+ 0
- 3
src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs View File

@@ -153,9 +153,6 @@ namespace Discord.Rest
Update(model);
}

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

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


+ 4
- 4
src/Discord.Net/Rest/Entities/Channels/TextChannel.cs View File

@@ -64,7 +64,7 @@ namespace Discord.Rest
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);
var model = await Discord.BaseClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false);
return new Message(this, model);
}
/// <inheritdoc />
@@ -74,7 +74,7 @@ namespace Discord.Rest
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);
var model = await Discord.BaseClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false);
return new Message(this, model);
}
}
@@ -82,14 +82,14 @@ namespace Discord.Rest
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);
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(Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false);
await Discord.BaseClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false);
}

/// <inheritdoc />


+ 12
- 4
src/Discord.Net/Rest/Entities/Guilds/Guild.cs View File

@@ -81,11 +81,11 @@ namespace Discord.Rest
{
var emojis = ImmutableArray.CreateBuilder<Emoji>(model.Emojis.Length);
for (int i = 0; i < model.Emojis.Length; i++)
emojis[i] = new Emoji(model.Emojis[i]);
emojis.Add(new Emoji(model.Emojis[i]));
Emojis = emojis.ToArray();
}
else
Emojis = ImmutableArray<Emoji>.Empty;
Emojis = Array.Empty<Emoji>();

var roles = new ConcurrentDictionary<ulong, Role>(1, model.Roles?.Length ?? 0);
if (model.Roles != null)
@@ -300,7 +300,6 @@ namespace Discord.Rest
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)
{
@@ -309,7 +308,12 @@ namespace Discord.Rest
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 };
@@ -333,6 +337,8 @@ namespace Discord.Rest
}
}

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

IEnumerable<Emoji> IGuild.Emojis => Emojis;
ulong IGuild.EveryoneRoleId => EveryoneRole.Id;
IEnumerable<string> IGuild.Features => Features;
@@ -359,6 +365,8 @@ namespace Discord.Rest
=> 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);
}


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

@@ -28,5 +28,7 @@ namespace Discord.Rest
ChannelId = model.ChannelId;
IsEnabled = model.Enabled;
}

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

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

@@ -77,6 +77,8 @@ namespace Discord.Rest
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;


+ 2
- 0
src/Discord.Net/Rest/Entities/Guilds/IntegrationAccount.cs View File

@@ -7,5 +7,7 @@

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

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

src/Discord.Net/Rest/Entities/Guilds/UserGUild.cs → src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs View File

@@ -41,15 +41,17 @@ namespace Discord.Rest
public async Task Leave()
{
if (IsOwner)
throw new InvalidOperationException("Unable to leave a guild the current user owns, use Delete() instead.");
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 leave a guild the current user owns, use Delete() instead.");
throw new InvalidOperationException("Unable to delete a guild the current user does not own.");
await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false);
}

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

+ 2
- 0
src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs View File

@@ -26,5 +26,7 @@ namespace Discord.Rest
SampleHostname = model.SampleHostname;
SamplePort = model.SamplePort;
}

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

+ 14
- 7
src/Discord.Net/Rest/Entities/Message.cs View File

@@ -41,8 +41,6 @@ namespace Discord.Rest

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

internal Message(IMessageChannel channel, Model model)
@@ -68,7 +66,7 @@ namespace Discord.Rest
Attachments = ImmutableArray.Create(attachments);
}
else
Attachments = ImmutableArray<Attachment>.Empty;
Attachments = Array.Empty<Attachment>();

if (model.Embeds.Length > 0)
{
@@ -78,18 +76,18 @@ namespace Discord.Rest
Embeds = ImmutableArray.Create(embeds);
}
else
Embeds = ImmutableArray<Embed>.Empty;
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[i] = new PublicUser(discord, model.Mentions[i]);
builder.Add(new PublicUser(discord, model.Mentions[i]));
MentionedUsers = builder.ToArray();
}
else
MentionedUsers = ImmutableArray<PublicUser>.Empty;
MentionedUsers = Array.Empty<PublicUser>();
MentionedChannelIds = MentionHelper.GetChannelMentions(model.Content);
MentionedRoleIds = MentionHelper.GetRoleMentions(model.Content);
if (model.IsMentioningEveryone)
@@ -121,7 +119,13 @@ namespace Discord.Rest

var args = new ModifyMessageParams();
func(args);
var model = await Discord.BaseClient.ModifyMessage(Channel.Id, Id, args).ConfigureAwait(false);
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);
}

@@ -131,6 +135,9 @@ namespace Discord.Rest
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;


+ 2
- 0
src/Discord.Net/Rest/Entities/Users/Connection.cs View File

@@ -23,5 +23,7 @@ namespace Discord.Rest

Integrations = model.Integrations;
}

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

+ 24
- 4
src/Discord.Net/Rest/Entities/Users/GuildUser.cs View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.GuildMember;

@@ -39,9 +40,9 @@ namespace Discord.Rest
Nickname = model.Nick;

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

@@ -82,8 +83,27 @@ namespace Discord.Rest

var args = new ModifyGuildMemberParams();
func(args);
var model = await Discord.BaseClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false);
Update(model);

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();
}
}




+ 2
- 0
src/Discord.Net/Rest/Entities/Users/User.cs View File

@@ -54,6 +54,8 @@ namespace Discord.Rest
return new DMChannel(Discord, model);
}

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

/// <inheritdoc />
string IUser.CurrentGame => null;
/// <inheritdoc />


Loading…
Cancel
Save