@@ -21,33 +21,26 @@ namespace Discord.API | |||||
{ | { | ||||
internal event EventHandler<SentRequestEventArgs> SentRequest; | internal event EventHandler<SentRequestEventArgs> SentRequest; | ||||
private readonly RequestQueue _requestQueue; | |||||
private readonly IRestClient _restClient; | private readonly IRestClient _restClient; | ||||
private readonly CancellationToken _cancelToken; | private readonly CancellationToken _cancelToken; | ||||
private readonly JsonSerializer _serializer; | 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; | _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 = restClientProvider(DiscordConfig.ClientAPIUrl, cancelToken); | ||||
_restClient.SetHeader("authorization", authToken); | |||||
_restClient.SetHeader("accept", "*/*"); | |||||
_restClient.SetHeader("user-agent", DiscordConfig.UserAgent); | _restClient.SetHeader("user-agent", DiscordConfig.UserAgent); | ||||
_requestQueue = new RequestQueue(_restClient); | |||||
_serializer = new JsonSerializer(); | _serializer = new JsonSerializer(); | ||||
_serializer.Converters.Add(new OptionalConverter()); | |||||
_serializer.Converters.Add(new ChannelTypeConverter()); | _serializer.Converters.Add(new ChannelTypeConverter()); | ||||
_serializer.Converters.Add(new ImageConverter()); | _serializer.Converters.Add(new ImageConverter()); | ||||
_serializer.Converters.Add(new NullableUInt64Converter()); | _serializer.Converters.Add(new NullableUInt64Converter()); | ||||
@@ -57,116 +50,113 @@ namespace Discord.API | |||||
_serializer.Converters.Add(new UInt64Converter()); | _serializer.Converters.Add(new UInt64Converter()); | ||||
_serializer.Converters.Add(new UInt64EntityConverter()); | _serializer.Converters.Add(new UInt64EntityConverter()); | ||||
_serializer.Converters.Add(new UserStatusConverter()); | _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 | 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(); | 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(); | stopwatch.Stop(); | ||||
var response = Deserialize<TResponse>(responseStream); | |||||
double milliseconds = ToMilliseconds(stopwatch); | 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(); | 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(); | stopwatch.Stop(); | ||||
double milliseconds = ToMilliseconds(stopwatch); | 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 | //Gateway | ||||
@@ -257,7 +247,7 @@ namespace Discord.API | |||||
switch (channels.Length) | switch (channels.Length) | ||||
{ | { | ||||
case 0: | case 0: | ||||
throw new ArgumentOutOfRangeException(nameof(args)); | |||||
return; | |||||
case 1: | case 1: | ||||
await ModifyGuildChannel(channels[0].Id, new ModifyGuildChannelParams { Position = channels[0].Position }).ConfigureAwait(false); | await ModifyGuildChannel(channels[0].Id, new ModifyGuildChannelParams { Position = channels[0].Position }).ConfigureAwait(false); | ||||
break; | break; | ||||
@@ -486,11 +476,11 @@ namespace Discord.API | |||||
if (args.Limit <= 0) throw new ArgumentOutOfRangeException(nameof(args.Limit)); | if (args.Limit <= 0) throw new ArgumentOutOfRangeException(nameof(args.Limit)); | ||||
if (args.Offset < 0) throw new ArgumentOutOfRangeException(nameof(args.Offset)); | 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; | int offset = args.Offset; | ||||
List<GuildMember[]> result; | List<GuildMember[]> result; | ||||
if (args.Limit != null) | |||||
if (args.Limit.IsSpecified) | |||||
result = new List<GuildMember[]>((limit + DiscordConfig.MaxUsersPerBatch - 1) / DiscordConfig.MaxUsersPerBatch); | result = new List<GuildMember[]>((limit + DiscordConfig.MaxUsersPerBatch - 1) / DiscordConfig.MaxUsersPerBatch); | ||||
else | else | ||||
result = new List<GuildMember[]>(); | result = new List<GuildMember[]>(); | ||||
@@ -498,7 +488,7 @@ namespace Discord.API | |||||
while (true) | while (true) | ||||
{ | { | ||||
int runLimit = (limit >= DiscordConfig.MaxUsersPerBatch) ? DiscordConfig.MaxUsersPerBatch : limit; | 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); | var models = await Send<GuildMember[]>("GET", endpoint).ConfigureAwait(false); | ||||
//Was this an empty batch? | //Was this an empty batch? | ||||
@@ -514,8 +504,10 @@ namespace Discord.API | |||||
if (result.Count > 1) | if (result.Count > 1) | ||||
return result.SelectMany(x => x); | return result.SelectMany(x => x); | ||||
else | |||||
else if (result.Count == 1) | |||||
return result[0]; | return result[0]; | ||||
else | |||||
return Array.Empty<GuildMember>(); | |||||
} | } | ||||
public async Task RemoveGuildMember(ulong guildId, ulong userId) | public async Task RemoveGuildMember(ulong guildId, ulong userId) | ||||
{ | { | ||||
@@ -524,13 +516,13 @@ namespace Discord.API | |||||
await Send("DELETE", $"guilds/{guildId}/members/{userId}").ConfigureAwait(false); | 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 (args == null) throw new ArgumentNullException(nameof(args)); | ||||
if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); | if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); | ||||
if (userId == 0) throw new ArgumentOutOfRangeException(nameof(userId)); | 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 | //Guild Roles | ||||
@@ -573,7 +565,7 @@ namespace Discord.API | |||||
switch (roles.Length) | switch (roles.Length) | ||||
{ | { | ||||
case 0: | case 0: | ||||
throw new ArgumentOutOfRangeException(nameof(args)); | |||||
return Array.Empty<Role>(); | |||||
case 1: | case 1: | ||||
return ImmutableArray.Create(await ModifyGuildRole(guildId, roles[0].Id, roles[0]).ConfigureAwait(false)); | return ImmutableArray.Create(await ModifyGuildRole(guildId, roles[0].Id, roles[0]).ConfigureAwait(false)); | ||||
default: | default: | ||||
@@ -618,34 +610,57 @@ namespace Discord.API | |||||
if (models.Length != DiscordConfig.MaxMessagesPerBatch) { i++; break; } | 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]; | 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 (args == null) throw new ArgumentNullException(nameof(args)); | ||||
if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); | 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 (args == null) throw new ArgumentNullException(nameof(args)); | ||||
//if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); | |||||
if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); | 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 (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); | ||||
if (messageId == 0) throw new ArgumentOutOfRangeException(nameof(messageId)); | 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 == null) throw new ArgumentNullException(nameof(args)); | ||||
if (args.MessageIds == null) throw new ArgumentNullException(nameof(args.MessageIds)); | if (args.MessageIds == null) throw new ArgumentNullException(nameof(args.MessageIds)); | ||||
@@ -653,22 +668,31 @@ namespace Discord.API | |||||
switch (messageIds.Length) | switch (messageIds.Length) | ||||
{ | { | ||||
case 0: | case 0: | ||||
throw new ArgumentOutOfRangeException(nameof(args.MessageIds)); | |||||
return; | |||||
case 1: | case 1: | ||||
await DeleteMessage(channelId, messageIds[0]).ConfigureAwait(false); | |||||
await DeleteMessage(guildId, channelId, messageIds[0]).ConfigureAwait(false); | |||||
break; | break; | ||||
default: | 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; | 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 (args == null) throw new ArgumentNullException(nameof(args)); | ||||
//if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); | |||||
if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); | if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); | ||||
if (messageId == 0) throw new ArgumentOutOfRangeException(nameof(messageId)); | 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) | 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); | 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) | public async Task<Channel> CreateDMChannel(CreateDMChannelParams args) | ||||
{ | { | ||||
if (args == null) throw new ArgumentNullException(nameof(args)); | if (args == null) throw new ArgumentNullException(nameof(args)); | ||||
@@ -775,11 +806,5 @@ namespace Discord.API | |||||
using (JsonReader reader = new JsonTextReader(text)) | using (JsonReader reader = new JsonTextReader(text)) | ||||
return _serializer.Deserialize<T>(reader); | 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; | |||||
} | |||||
} | } | ||||
} | } |
@@ -0,0 +1,8 @@ | |||||
namespace Discord.API | |||||
{ | |||||
public interface IOptional | |||||
{ | |||||
object Value { get; } | |||||
bool IsSpecified { get; } | |||||
} | |||||
} |
@@ -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; | |||||
} | |||||
} |
@@ -5,12 +5,12 @@ namespace Discord.API.Rest | |||||
public class CreateChannelInviteParams | public class CreateChannelInviteParams | ||||
{ | { | ||||
[JsonProperty("max_age")] | [JsonProperty("max_age")] | ||||
public int MaxAge { get; set; } = 86400; //24 Hours | |||||
public Optional<int> MaxAge { get; set; } | |||||
[JsonProperty("max_uses")] | [JsonProperty("max_uses")] | ||||
public int MaxUses { get; set; } = 0; | |||||
public Optional<int> MaxUses { get; set; } | |||||
[JsonProperty("temporary")] | [JsonProperty("temporary")] | ||||
public bool Temporary { get; set; } = false; | |||||
public Optional<bool> Temporary { get; set; } | |||||
[JsonProperty("xkcdpass")] | [JsonProperty("xkcdpass")] | ||||
public bool XkcdPass { get; set; } = false; | |||||
public Optional<bool> XkcdPass { get; set; } | |||||
} | } | ||||
} | } |
@@ -5,6 +5,6 @@ namespace Discord.API.Rest | |||||
public class CreateGuildBanParams | public class CreateGuildBanParams | ||||
{ | { | ||||
[JsonProperty("delete-message-days")] | [JsonProperty("delete-message-days")] | ||||
public int PruneDays { get; set; } = 0; | |||||
public Optional<int> PruneDays { get; set; } | |||||
} | } | ||||
} | } |
@@ -8,7 +8,8 @@ namespace Discord.API.Rest | |||||
public string Name { get; set; } | public string Name { get; set; } | ||||
[JsonProperty("type")] | [JsonProperty("type")] | ||||
public ChannelType Type { get; set; } | public ChannelType Type { get; set; } | ||||
[JsonProperty("bitrate")] | [JsonProperty("bitrate")] | ||||
public int Bitrate { get; set; } | |||||
public Optional<int> Bitrate { get; set; } | |||||
} | } | ||||
} | } |
@@ -10,7 +10,8 @@ namespace Discord.API.Rest | |||||
public string Name { get; set; } | public string Name { get; set; } | ||||
[JsonProperty("region")] | [JsonProperty("region")] | ||||
public string Region { get; set; } | public string Region { get; set; } | ||||
[JsonProperty("icon"), JsonConverter(typeof(ImageConverter))] | [JsonProperty("icon"), JsonConverter(typeof(ImageConverter))] | ||||
public Stream Icon { get; set; } | |||||
public Optional<Stream> Icon { get; set; } | |||||
} | } | ||||
} | } |
@@ -6,9 +6,10 @@ namespace Discord.API.Rest | |||||
{ | { | ||||
[JsonProperty("content")] | [JsonProperty("content")] | ||||
public string Content { get; set; } = ""; | public string Content { get; set; } = ""; | ||||
[JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] | [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] | ||||
public string Nonce { get; set; } = null; | |||||
public Optional<string> Nonce { get; set; } | |||||
[JsonProperty("tts", DefaultValueHandling = DefaultValueHandling.Ignore)] | [JsonProperty("tts", DefaultValueHandling = DefaultValueHandling.Ignore)] | ||||
public bool IsTTS { get; set; } = false; | |||||
public Optional<bool> IsTTS { get; set; } | |||||
} | } | ||||
} | } |
@@ -4,6 +4,7 @@ | |||||
{ | { | ||||
public int Limit { get; set; } = DiscordConfig.MaxMessagesPerBatch; | public int Limit { get; set; } = DiscordConfig.MaxMessagesPerBatch; | ||||
public Direction RelativeDirection { get; set; } = Direction.Before; | public Direction RelativeDirection { get; set; } = Direction.Before; | ||||
public ulong? RelativeMessageId { get; set; } = null; | |||||
public Optional<ulong> RelativeMessageId { get; set; } | |||||
} | } | ||||
} | } |
@@ -2,7 +2,7 @@ | |||||
{ | { | ||||
public class GetGuildMembersParams | 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; } | |||||
} | } | ||||
} | } |
@@ -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; } | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rest | |||||
{ | |||||
public class LoginResponse | |||||
{ | |||||
[JsonProperty("token")] | |||||
public string Token { get; set; } | |||||
} | |||||
} |
@@ -5,8 +5,8 @@ namespace Discord.API.Rest | |||||
public class ModifyChannelPermissionsParams | public class ModifyChannelPermissionsParams | ||||
{ | { | ||||
[JsonProperty("allow")] | [JsonProperty("allow")] | ||||
public uint Allow { get; set; } | |||||
public Optional<uint> Allow { get; set; } | |||||
[JsonProperty("deny")] | [JsonProperty("deny")] | ||||
public uint Deny { get; set; } | |||||
public Optional<uint> Deny { get; set; } | |||||
} | } | ||||
} | } |
@@ -0,0 +1,10 @@ | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rest | |||||
{ | |||||
public class ModifyCurrentUserNickParams | |||||
{ | |||||
[JsonProperty("nick")] | |||||
public string Nickname { get; set; } | |||||
} | |||||
} |
@@ -7,14 +7,14 @@ namespace Discord.API.Rest | |||||
public class ModifyCurrentUserParams | public class ModifyCurrentUserParams | ||||
{ | { | ||||
[JsonProperty("username")] | [JsonProperty("username")] | ||||
public string Username { get; set; } | |||||
public Optional<string> Username { get; set; } | |||||
[JsonProperty("email")] | [JsonProperty("email")] | ||||
public string Email { get; set; } | |||||
public Optional<string> Email { get; set; } | |||||
[JsonProperty("password")] | [JsonProperty("password")] | ||||
public string Password { get; set; } | |||||
public Optional<string> Password { get; set; } | |||||
[JsonProperty("new_password")] | [JsonProperty("new_password")] | ||||
public string NewPassword { get; set; } | |||||
public Optional<string> NewPassword { get; set; } | |||||
[JsonProperty("avatar"), JsonConverter(typeof(ImageConverter))] | [JsonProperty("avatar"), JsonConverter(typeof(ImageConverter))] | ||||
public Stream Avatar { get; set; } | |||||
public Optional<Stream> Avatar { get; set; } | |||||
} | } | ||||
} | } |
@@ -5,8 +5,8 @@ namespace Discord.API.Rest | |||||
public class ModifyGuildChannelParams | public class ModifyGuildChannelParams | ||||
{ | { | ||||
[JsonProperty("name")] | [JsonProperty("name")] | ||||
public string Name { get; set; } | |||||
public Optional<string> Name { get; set; } | |||||
[JsonProperty("position")] | [JsonProperty("position")] | ||||
public int Position { get; set; } | |||||
public Optional<int> Position { get; set; } | |||||
} | } | ||||
} | } |
@@ -5,8 +5,8 @@ namespace Discord.API.Rest | |||||
public class ModifyGuildChannelsParams | public class ModifyGuildChannelsParams | ||||
{ | { | ||||
[JsonProperty("id")] | [JsonProperty("id")] | ||||
public ulong Id { get; set; } | |||||
public Optional<ulong> Id { get; set; } | |||||
[JsonProperty("position")] | [JsonProperty("position")] | ||||
public int Position { get; set; } | |||||
public Optional<int> Position { get; set; } | |||||
} | } | ||||
} | } |
@@ -6,8 +6,8 @@ namespace Discord.API.Rest | |||||
public class ModifyGuildEmbedParams | public class ModifyGuildEmbedParams | ||||
{ | { | ||||
[JsonProperty("enabled")] | [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; } | |||||
} | } | ||||
} | } |
@@ -5,10 +5,10 @@ namespace Discord.API.Rest | |||||
public class ModifyGuildIntegrationParams | public class ModifyGuildIntegrationParams | ||||
{ | { | ||||
[JsonProperty("expire_behavior")] | [JsonProperty("expire_behavior")] | ||||
public int ExpireBehavior { get; set; } | |||||
public Optional<int> ExpireBehavior { get; set; } | |||||
[JsonProperty("expire_grace_period")] | [JsonProperty("expire_grace_period")] | ||||
public int ExpireGracePeriod { get; set; } | |||||
public Optional<int> ExpireGracePeriod { get; set; } | |||||
[JsonProperty("enable_emoticons")] | [JsonProperty("enable_emoticons")] | ||||
public bool EnableEmoticons { get; set; } | |||||
public Optional<bool> EnableEmoticons { get; set; } | |||||
} | } | ||||
} | } |
@@ -1,17 +1,18 @@ | |||||
using Discord.Net.Converters; | |||||
using Newtonsoft.Json; | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API.Rest | namespace Discord.API.Rest | ||||
{ | { | ||||
public class ModifyGuildMemberParams | public class ModifyGuildMemberParams | ||||
{ | { | ||||
[JsonProperty("roles")] | [JsonProperty("roles")] | ||||
public ulong[] Roles { get; set; } | |||||
public Optional<ulong[]> Roles { get; set; } | |||||
[JsonProperty("mute")] | [JsonProperty("mute")] | ||||
public bool Mute { get; set; } | |||||
public Optional<bool> Mute { get; set; } | |||||
[JsonProperty("deaf")] | [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; } | |||||
} | } | ||||
} | } |
@@ -7,20 +7,20 @@ namespace Discord.API.Rest | |||||
public class ModifyGuildParams | public class ModifyGuildParams | ||||
{ | { | ||||
[JsonProperty("name")] | [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")] | [JsonProperty("verification_level")] | ||||
public int VerificationLevel { get; set; } | |||||
public Optional<int> VerificationLevel { get; set; } | |||||
[JsonProperty("afk_channel_id")] | [JsonProperty("afk_channel_id")] | ||||
public ulong? AFKChannelId { get; set; } | |||||
public Optional<ulong?> AFKChannelId { get; set; } | |||||
[JsonProperty("afk_timeout")] | [JsonProperty("afk_timeout")] | ||||
public int AFKTimeout { get; set; } | |||||
public Optional<int> AFKTimeout { get; set; } | |||||
[JsonProperty("icon"), JsonConverter(typeof(ImageConverter))] | [JsonProperty("icon"), JsonConverter(typeof(ImageConverter))] | ||||
public Stream Icon { get; set; } | |||||
public Optional<Stream> Icon { get; set; } | |||||
[JsonProperty("owner_id")] | [JsonProperty("owner_id")] | ||||
public GuildMember Owner { get; set; } | |||||
public Optional<GuildMember> Owner { get; set; } | |||||
[JsonProperty("splash"), JsonConverter(typeof(ImageConverter))] | [JsonProperty("splash"), JsonConverter(typeof(ImageConverter))] | ||||
public Stream Splash { get; set; } | |||||
public Optional<Stream> Splash { get; set; } | |||||
} | } | ||||
} | } |
@@ -5,14 +5,14 @@ namespace Discord.API.Rest | |||||
public class ModifyGuildRoleParams | public class ModifyGuildRoleParams | ||||
{ | { | ||||
[JsonProperty("name")] | [JsonProperty("name")] | ||||
public string Name { get; set; } | |||||
public Optional<string> Name { get; set; } | |||||
[JsonProperty("permissions")] | [JsonProperty("permissions")] | ||||
public uint Permissions { get; set; } | |||||
public Optional<uint> Permissions { get; set; } | |||||
[JsonProperty("position")] | [JsonProperty("position")] | ||||
public int Position { get; set; } | |||||
public Optional<int> Position { get; set; } | |||||
[JsonProperty("color")] | [JsonProperty("color")] | ||||
public uint Color { get; set; } | |||||
public Optional<uint> Color { get; set; } | |||||
[JsonProperty("hoist")] | [JsonProperty("hoist")] | ||||
public bool Hoist { get; set; } | |||||
public Optional<bool> Hoist { get; set; } | |||||
} | } | ||||
} | } |
@@ -5,6 +5,6 @@ namespace Discord.API.Rest | |||||
public class ModifyGuildRolesParams : ModifyGuildRoleParams | public class ModifyGuildRolesParams : ModifyGuildRoleParams | ||||
{ | { | ||||
[JsonProperty("id")] | [JsonProperty("id")] | ||||
public ulong Id { get; set; } | |||||
public Optional<ulong> Id { get; set; } | |||||
} | } | ||||
} | } |
@@ -5,6 +5,6 @@ namespace Discord.API.Rest | |||||
public class ModifyMessageParams | public class ModifyMessageParams | ||||
{ | { | ||||
[JsonProperty("content")] | [JsonProperty("content")] | ||||
public string Content { get; set; } = ""; | |||||
public Optional<string> Content { get; set; } = ""; | |||||
} | } | ||||
} | } |
@@ -5,6 +5,6 @@ namespace Discord.API.Rest | |||||
public class ModifyTextChannelParams : ModifyGuildChannelParams | public class ModifyTextChannelParams : ModifyGuildChannelParams | ||||
{ | { | ||||
[JsonProperty("topic")] | [JsonProperty("topic")] | ||||
public string Topic { get; set; } | |||||
public Optional<string> Topic { get; set; } | |||||
} | } | ||||
} | } |
@@ -5,6 +5,6 @@ namespace Discord.API.Rest | |||||
public class ModifyVoiceChannelParams : ModifyGuildChannelParams | public class ModifyVoiceChannelParams : ModifyGuildChannelParams | ||||
{ | { | ||||
[JsonProperty("bitrate")] | [JsonProperty("bitrate")] | ||||
public int Bitrate { get; set; } | |||||
public Optional<int> Bitrate { get; set; } | |||||
} | } | ||||
} | } |
@@ -4,21 +4,22 @@ namespace Discord.API.Rest | |||||
{ | { | ||||
public class UploadFileParams | 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 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() | 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; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -88,7 +88,9 @@ namespace Discord | |||||
/// <summary> Gets a collection of all users in this guild. </summary> | /// <summary> Gets a collection of all users in this guild. </summary> | ||||
Task<IEnumerable<IGuildUser>> GetUsers(); | Task<IEnumerable<IGuildUser>> GetUsers(); | ||||
/// <summary> Gets the user in this guild with the provided id, or null if not found. </summary> | /// <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); | Task<int> PruneUsers(int days = 30, bool simulate = false); | ||||
} | } | ||||
} | } |
@@ -9,8 +9,6 @@ namespace Discord | |||||
{ | { | ||||
/// <summary> Gets the time of this message's last edit, if any. </summary> | /// <summary> Gets the time of this message's last edit, if any. </summary> | ||||
DateTime? EditedTimestamp { get; } | 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> | /// <summary> Returns true if this message was sent as a text-to-speech message. </summary> | ||||
bool IsTTS { get; } | bool IsTTS { get; } | ||||
/// <summary> Returns the original, unprocessed text for this message. </summary> | /// <summary> Returns the original, unprocessed text for this message. </summary> | ||||
@@ -66,8 +66,13 @@ | |||||
<Compile Include="API\Common\UserGuild.cs" /> | <Compile Include="API\Common\UserGuild.cs" /> | ||||
<Compile Include="API\Common\VoiceRegion.cs" /> | <Compile Include="API\Common\VoiceRegion.cs" /> | ||||
<Compile Include="API\Common\VoiceState.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\DeleteMessagesParam.cs" /> | ||||
<Compile Include="API\Rest\GetGuildMembersParams.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\UploadFileParams.cs" /> | ||||
<Compile Include="API\Rest\GuildPruneParams.cs" /> | <Compile Include="API\Rest\GuildPruneParams.cs" /> | ||||
<Compile Include="API\Rest\CreateChannelInviteParams.cs" /> | <Compile Include="API\Rest\CreateChannelInviteParams.cs" /> | ||||
@@ -95,6 +100,15 @@ | |||||
<Compile Include="API\Rest\ModifyVoiceChannelParams.cs" /> | <Compile Include="API\Rest\ModifyVoiceChannelParams.cs" /> | ||||
<Compile Include="DiscordConfig.cs" /> | <Compile Include="DiscordConfig.cs" /> | ||||
<Compile Include="API\DiscordRawClient.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="Rest\DiscordClient.cs" /> | ||||
<Compile Include="Common\Entities\Guilds\IGuildEmbed.cs" /> | <Compile Include="Common\Entities\Guilds\IGuildEmbed.cs" /> | ||||
<Compile Include="Common\Entities\Guilds\IIntegrationAccount.cs" /> | <Compile Include="Common\Entities\Guilds\IIntegrationAccount.cs" /> | ||||
@@ -181,7 +195,7 @@ | |||||
<Compile Include="Net\Converters\UserStatusConverter.cs" /> | <Compile Include="Net\Converters\UserStatusConverter.cs" /> | ||||
<Compile Include="Net\HttpException.cs" /> | <Compile Include="Net\HttpException.cs" /> | ||||
<Compile Include="Net\Rest\DefaultRestClient.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\IRestClient.cs" /> | ||||
<Compile Include="Net\Rest\MultipartFile.cs" /> | <Compile Include="Net\Rest\MultipartFile.cs" /> | ||||
<Compile Include="Net\Rest\RestClientProvider.cs" /> | <Compile Include="Net\Rest\RestClientProvider.cs" /> | ||||
@@ -1,17 +1,21 @@ | |||||
using Discord.API; | using Discord.API; | ||||
using Discord.Net.Rest; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.IO; | using System.IO; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
//TODO: Add docstrings | |||||
public interface IDiscordClient | public interface IDiscordClient | ||||
{ | { | ||||
ISelfUser CurrentUser { get; } | |||||
TokenType AuthTokenType { get; } | |||||
DiscordRawClient BaseClient { 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 Logout(); | ||||
Task<IChannel> GetChannel(ulong id); | Task<IChannel> GetChannel(ulong id); | ||||
@@ -8,6 +8,7 @@ namespace Discord.Logging | |||||
void Log(LogSeverity severity, string message, Exception exception = null); | void Log(LogSeverity severity, string message, Exception exception = null); | ||||
void Log(LogSeverity severity, FormattableString 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(string message, Exception exception = null); | ||||
void Error(FormattableString message, Exception exception = null); | void Error(FormattableString message, Exception exception = null); | ||||
@@ -23,6 +23,11 @@ namespace Discord.Logging | |||||
if (severity <= Level) | if (severity <= Level) | ||||
Message(this, new LogMessageEventArgs(severity, source, message.ToString(), ex)); | 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) | void ILogger.Log(LogSeverity severity, string message, Exception ex) | ||||
{ | { | ||||
if (severity <= Level) | if (severity <= Level) | ||||
@@ -33,71 +38,76 @@ namespace Discord.Logging | |||||
if (severity <= Level) | if (severity <= Level) | ||||
Message(this, new LogMessageEventArgs(severity, "Discord", message.ToString(), ex)); | 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) | public void Error(string source, string message, Exception ex = null) | ||||
=> Log(LogSeverity.Error, source, message, ex); | => Log(LogSeverity.Error, source, message, ex); | ||||
public void Error(string source, FormattableString message, Exception ex = null) | public void Error(string source, FormattableString message, Exception ex = null) | ||||
=> Log(LogSeverity.Error, source, message, ex); | => 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) | void ILogger.Error(string message, Exception ex) | ||||
=> Log(LogSeverity.Error, "Discord", message, ex); | => Log(LogSeverity.Error, "Discord", message, ex); | ||||
void ILogger.Error(FormattableString message, Exception ex) | void ILogger.Error(FormattableString message, Exception ex) | ||||
=> Log(LogSeverity.Error, "Discord", message, ex); | => Log(LogSeverity.Error, "Discord", message, ex); | ||||
void ILogger.Error(Exception 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) | public void Warning(string source, string message, Exception ex = null) | ||||
=> Log(LogSeverity.Warning, source, message, ex); | => Log(LogSeverity.Warning, source, message, ex); | ||||
public void Warning(string source, FormattableString message, Exception ex = null) | public void Warning(string source, FormattableString message, Exception ex = null) | ||||
=> Log(LogSeverity.Warning, source, message, ex); | => 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) | void ILogger.Warning(string message, Exception ex) | ||||
=> Log(LogSeverity.Warning, "Discord", message, ex); | => Log(LogSeverity.Warning, "Discord", message, ex); | ||||
void ILogger.Warning(FormattableString message, Exception ex) | void ILogger.Warning(FormattableString message, Exception ex) | ||||
=> Log(LogSeverity.Warning, "Discord", message, ex); | => Log(LogSeverity.Warning, "Discord", message, ex); | ||||
void ILogger.Warning(Exception 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) | public void Info(string source, string message, Exception ex = null) | ||||
=> Log(LogSeverity.Info, source, message, ex); | => Log(LogSeverity.Info, source, message, ex); | ||||
public void Info(string source, FormattableString message, Exception ex = null) | public void Info(string source, FormattableString message, Exception ex = null) | ||||
=> Log(LogSeverity.Info, source, message, ex); | => 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) | void ILogger.Info(string message, Exception ex) | ||||
=> Log(LogSeverity.Info, "Discord", message, ex); | => Log(LogSeverity.Info, "Discord", message, ex); | ||||
void ILogger.Info(FormattableString message, Exception ex) | void ILogger.Info(FormattableString message, Exception ex) | ||||
=> Log(LogSeverity.Info, "Discord", message, ex); | => Log(LogSeverity.Info, "Discord", message, ex); | ||||
void ILogger.Info(Exception 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) | public void Verbose(string source, string message, Exception ex = null) | ||||
=> Log(LogSeverity.Verbose, source, message, ex); | => Log(LogSeverity.Verbose, source, message, ex); | ||||
public void Verbose(string source, FormattableString message, Exception ex = null) | public void Verbose(string source, FormattableString message, Exception ex = null) | ||||
=> Log(LogSeverity.Verbose, source, message, ex); | => 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) | void ILogger.Verbose(string message, Exception ex) | ||||
=> Log(LogSeverity.Verbose, "Discord", message, ex); | => Log(LogSeverity.Verbose, "Discord", message, ex); | ||||
void ILogger.Verbose(FormattableString message, Exception ex) | void ILogger.Verbose(FormattableString message, Exception ex) | ||||
=> Log(LogSeverity.Verbose, "Discord", message, ex); | => Log(LogSeverity.Verbose, "Discord", message, ex); | ||||
void ILogger.Verbose(Exception 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) | public void Debug(string source, string message, Exception ex = null) | ||||
=> Log(LogSeverity.Debug, source, message, ex); | => Log(LogSeverity.Debug, source, message, ex); | ||||
public void Debug(string source, FormattableString message, Exception ex = null) | public void Debug(string source, FormattableString message, Exception ex = null) | ||||
=> Log(LogSeverity.Debug, source, message, ex); | => 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) | void ILogger.Debug(string message, Exception ex) | ||||
=> Log(LogSeverity.Debug, "Discord", message, ex); | => Log(LogSeverity.Debug, "Discord", message, ex); | ||||
void ILogger.Debug(FormattableString message, Exception ex) | void ILogger.Debug(FormattableString message, Exception ex) | ||||
=> Log(LogSeverity.Debug, "Discord", message, ex); | => Log(LogSeverity.Debug, "Discord", message, ex); | ||||
void ILogger.Debug(Exception 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); | internal Logger CreateLogger(string name) => new Logger(this, name); | ||||
} | } | ||||
@@ -1,4 +1,5 @@ | |||||
using Newtonsoft.Json; | |||||
using Discord.API; | |||||
using Newtonsoft.Json; | |||||
using System; | using System; | ||||
using System.IO; | using System.IO; | ||||
@@ -6,7 +7,7 @@ namespace Discord.Net.Converters | |||||
{ | { | ||||
public class ImageConverter : JsonConverter | 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 CanRead => true; | ||||
public override bool CanWrite => true; | public override bool CanWrite => true; | ||||
@@ -17,6 +18,8 @@ namespace Discord.Net.Converters | |||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | ||||
{ | { | ||||
if (value is Optional<Stream>) | |||||
value = (Optional<Stream>)value; | |||||
var stream = value as Stream; | var stream = value as Stream; | ||||
byte[] bytes = new byte[stream.Length - stream.Position]; | byte[] bytes = new byte[stream.Length - stream.Position]; | ||||
@@ -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; | |||||
} | |||||
} | |||||
} |
@@ -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); | |||||
} | |||||
} | |||||
} |
@@ -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; | |||||
} | |||||
} | |||||
} |
@@ -1,8 +1,8 @@ | |||||
using Newtonsoft.Json; | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Globalization; | using System.Globalization; | ||||
using System.IO; | using System.IO; | ||||
using System.Linq; | |||||
using System.Net; | using System.Net; | ||||
using System.Net.Http; | using System.Net.Http; | ||||
using System.Text; | using System.Text; | ||||
@@ -32,6 +32,8 @@ namespace Discord.Net.Rest | |||||
UseProxy = false, | UseProxy = false, | ||||
PreAuthenticate = false | PreAuthenticate = false | ||||
}); | }); | ||||
SetHeader("accept-encoding", "gzip, deflate"); | |||||
} | } | ||||
protected virtual void Dispose(bool disposing) | protected virtual void Dispose(bool disposing) | ||||
{ | { | ||||
@@ -50,21 +52,22 @@ namespace Discord.Net.Rest | |||||
public void SetHeader(string key, string value) | public void SetHeader(string key, string value) | ||||
{ | { | ||||
_client.DefaultRequestHeaders.Remove(key); | _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); | string uri = Path.Combine(_baseUrl, endpoint); | ||||
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | ||||
{ | { | ||||
if (json != null) | if (json != null) | ||||
restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); | 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); | string uri = Path.Combine(_baseUrl, endpoint); | ||||
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | ||||
@@ -94,11 +97,11 @@ namespace Discord.Net.Rest | |||||
} | } | ||||
} | } | ||||
restRequest.Content = content; | 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; | int retryCount = 0; | ||||
while (true) | while (true) | ||||
@@ -118,9 +121,16 @@ namespace Discord.Net.Rest | |||||
int statusCode = (int)response.StatusCode; | int statusCode = (int)response.StatusCode; | ||||
if (statusCode < 200 || statusCode >= 300) //2xx = Success | 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); | throw new HttpException(response.StatusCode); | ||||
} | |||||
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); | |||||
if (headerOnly) | |||||
return null; | |||||
else | |||||
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); | |||||
} | } | ||||
} | } | ||||
@@ -1,7 +0,0 @@ | |||||
namespace Discord.Net.Rest | |||||
{ | |||||
public interface IMessageQueue | |||||
{ | |||||
int Count { get; } | |||||
} | |||||
} |
@@ -4,11 +4,12 @@ using System.Threading.Tasks; | |||||
namespace Discord.Net.Rest | namespace Discord.Net.Rest | ||||
{ | { | ||||
//TODO: Add docstrings | |||||
public interface IRestClient | public interface IRestClient | ||||
{ | { | ||||
void SetHeader(string key, string value); | 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); | |||||
} | } | ||||
} | } |
@@ -0,0 +1,8 @@ | |||||
namespace Discord.Net.Rest | |||||
{ | |||||
internal enum BucketGroup | |||||
{ | |||||
Global, | |||||
Guild | |||||
} | |||||
} |
@@ -0,0 +1,8 @@ | |||||
namespace Discord.Net.Rest | |||||
{ | |||||
public enum GlobalBucket | |||||
{ | |||||
General, | |||||
DirectMessage | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
namespace Discord.Net.Rest | |||||
{ | |||||
public enum GuildBucket | |||||
{ | |||||
SendEditMessage, | |||||
DeleteMessage, | |||||
DeleteMessages, | |||||
Nickname | |||||
} | |||||
} |
@@ -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); | |||||
} | |||||
} |
@@ -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(); } | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -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}"); | |||||
} | |||||
} | |||||
} |
@@ -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>(); | |||||
} | |||||
} | |||||
} |
@@ -1,11 +1,13 @@ | |||||
using Discord.API.Rest; | using Discord.API.Rest; | ||||
using Discord.Logging; | using Discord.Logging; | ||||
using Discord.Net; | |||||
using Discord.Net.Rest; | using Discord.Net.Rest; | ||||
using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.IO; | using System.IO; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Net; | |||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
@@ -22,10 +24,14 @@ namespace Discord.Rest | |||||
private CancellationTokenSource _cancelTokenSource; | private CancellationTokenSource _cancelTokenSource; | ||||
private bool _isDisposed; | private bool _isDisposed; | ||||
private string _userAgent; | private string _userAgent; | ||||
private SelfUser _currentUser; | |||||
public bool IsLoggedIn { get; private set; } | 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) | public DiscordClient(DiscordConfig config = null) | ||||
{ | { | ||||
@@ -37,43 +43,67 @@ namespace Discord.Rest | |||||
_connectionLock = new SemaphoreSlim(1, 1); | _connectionLock = new SemaphoreSlim(1, 1); | ||||
_log = new LogManager(config.LogLevel); | _log = new LogManager(config.LogLevel); | ||||
_userAgent = DiscordConfig.UserAgent; | _userAgent = DiscordConfig.UserAgent; | ||||
BaseClient = new API.DiscordRawClient(_restClientProvider, _cancelTokenSource.Token); | |||||
_log.Message += (s,e) => Log.Raise(this, e); | _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); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
try | try | ||||
{ | { | ||||
await LoginInternal(tokenType, token).ConfigureAwait(false); | |||||
await LoginInternal(email, password).ConfigureAwait(false); | |||||
} | } | ||||
finally { _connectionLock.Release(); } | 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) | if (IsLoggedIn) | ||||
LogoutInternal(); | LogoutInternal(); | ||||
try | try | ||||
{ | { | ||||
var cancelTokenSource = new CancellationTokenSource(); | 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; } | 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() | public async Task Logout() | ||||
{ | { | ||||
@@ -89,9 +119,14 @@ namespace Discord.Rest | |||||
{ | { | ||||
bool wasLoggedIn = IsLoggedIn; | 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) | if (wasLoggedIn) | ||||
{ | { | ||||
@@ -150,7 +185,7 @@ namespace Discord.Rest | |||||
{ | { | ||||
var model = await BaseClient.GetGuildEmbed(id).ConfigureAwait(false); | var model = await BaseClient.GetGuildEmbed(id).ConfigureAwait(false); | ||||
if (model != null) | if (model != null) | ||||
return new GuildEmbed(this, model); | |||||
return new GuildEmbed(model); | |||||
return null; | return null; | ||||
} | } | ||||
public async Task<IEnumerable<UserGuild>> GetGuilds() | public async Task<IEnumerable<UserGuild>> GetGuilds() | ||||
@@ -173,25 +208,25 @@ namespace Discord.Rest | |||||
return new PublicUser(this, model); | return new PublicUser(this, model); | ||||
return null; | 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); | var model = await BaseClient.GetUser(username, discriminator).ConfigureAwait(false); | ||||
if (model != null) | if (model != null) | ||||
return new PublicUser(this, model); | return new PublicUser(this, model); | ||||
return null; | 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); | 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); | var models = await BaseClient.QueryUsers(query, limit).ConfigureAwait(false); | ||||
return models.Select(x => new PublicUser(this, x)); | return models.Select(x => new PublicUser(this, x)); | ||||
@@ -225,7 +260,6 @@ namespace Discord.Rest | |||||
public void Dispose() => Dispose(true); | public void Dispose() => Dispose(true); | ||||
API.DiscordRawClient IDiscordClient.BaseClient => BaseClient; | API.DiscordRawClient IDiscordClient.BaseClient => BaseClient; | ||||
ISelfUser IDiscordClient.CurrentUser => CurrentUser; | |||||
async Task<IChannel> IDiscordClient.GetChannel(ulong id) | async Task<IChannel> IDiscordClient.GetChannel(ulong id) | ||||
=> await GetChannel(id).ConfigureAwait(false); | => await GetChannel(id).ConfigureAwait(false); | ||||
@@ -243,6 +277,10 @@ namespace Discord.Rest | |||||
=> await CreateGuild(name, region, jpegIcon).ConfigureAwait(false); | => await CreateGuild(name, region, jpegIcon).ConfigureAwait(false); | ||||
async Task<IUser> IDiscordClient.GetUser(ulong id) | async Task<IUser> IDiscordClient.GetUser(ulong id) | ||||
=> await GetUser(id).ConfigureAwait(false); | => 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) | async Task<IEnumerable<IUser>> IDiscordClient.QueryUsers(string query, int limit) | ||||
=> await QueryUsers(query, limit).ConfigureAwait(false); | => await QueryUsers(query, limit).ConfigureAwait(false); | ||||
async Task<IEnumerable<IVoiceRegion>> IDiscordClient.GetVoiceRegions() | async Task<IEnumerable<IVoiceRegion>> IDiscordClient.GetVoiceRegions() | ||||
@@ -20,8 +20,6 @@ namespace Discord.Rest | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); | public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); | ||||
/// <inheritdoc /> | |||||
public IEnumerable<IUser> Users => ImmutableArray.Create<IUser>(Discord.CurrentUser, Recipient); | |||||
internal DMChannel(DiscordClient discord, Model model) | internal DMChannel(DiscordClient discord, Model model) | ||||
{ | { | ||||
@@ -39,20 +37,23 @@ namespace Discord.Rest | |||||
} | } | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public IUser GetUser(ulong id) | |||||
public async Task<IUser> GetUser(ulong id) | |||||
{ | { | ||||
var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); | |||||
if (id == Recipient.Id) | if (id == Recipient.Id) | ||||
return Recipient; | return Recipient; | ||||
else if (id == Discord.CurrentUser.Id) | |||||
return Discord.CurrentUser; | |||||
else if (id == currentUser.Id) | |||||
return currentUser; | |||||
else | else | ||||
return null; | 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 /> | /// <inheritdoc /> | ||||
public async Task<IEnumerable<Message>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) | public async Task<IEnumerable<Message>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) | ||||
{ | { | ||||
@@ -124,10 +125,10 @@ namespace Discord.Rest | |||||
IDMUser IDMChannel.Recipient => Recipient; | 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) | Task<IMessage> IMessageChannel.GetMessage(ulong id) | ||||
=> throw new NotSupportedException(); | => throw new NotSupportedException(); | ||||
async Task<IEnumerable<IMessage>> IMessageChannel.GetMessages(int limit) | async Task<IEnumerable<IMessage>> IMessageChannel.GetMessages(int limit) | ||||
@@ -153,9 +153,6 @@ namespace Discord.Rest | |||||
Update(model); | Update(model); | ||||
} | } | ||||
/// <inheritdoc /> | |||||
public override string ToString() => Name ?? Id.ToString(); | |||||
IGuild IGuildChannel.Guild => Guild; | IGuild IGuildChannel.Guild => Guild; | ||||
async Task<IGuildInvite> IGuildChannel.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) | async Task<IGuildInvite> IGuildChannel.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) | ||||
=> await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false); | => await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false); | ||||
@@ -64,7 +64,7 @@ namespace Discord.Rest | |||||
public async Task<Message> SendMessage(string text, bool isTTS = false) | public async Task<Message> SendMessage(string text, bool isTTS = false) | ||||
{ | { | ||||
var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; | 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); | return new Message(this, model); | ||||
} | } | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
@@ -74,7 +74,7 @@ namespace Discord.Rest | |||||
using (var file = File.OpenRead(filePath)) | using (var file = File.OpenRead(filePath)) | ||||
{ | { | ||||
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; | 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); | 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) | 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 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); | return new Message(this, model); | ||||
} | } | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public async Task DeleteMessages(IEnumerable<IMessage> messages) | 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 /> | /// <inheritdoc /> | ||||
@@ -81,11 +81,11 @@ namespace Discord.Rest | |||||
{ | { | ||||
var emojis = ImmutableArray.CreateBuilder<Emoji>(model.Emojis.Length); | var emojis = ImmutableArray.CreateBuilder<Emoji>(model.Emojis.Length); | ||||
for (int i = 0; i < model.Emojis.Length; i++) | 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(); | Emojis = emojis.ToArray(); | ||||
} | } | ||||
else | else | ||||
Emojis = ImmutableArray<Emoji>.Empty; | |||||
Emojis = Array.Empty<Emoji>(); | |||||
var roles = new ConcurrentDictionary<ulong, Role>(1, model.Roles?.Length ?? 0); | var roles = new ConcurrentDictionary<ulong, Role>(1, model.Roles?.Length ?? 0); | ||||
if (model.Roles != null) | if (model.Roles != null) | ||||
@@ -300,7 +300,6 @@ namespace Discord.Rest | |||||
var models = await Discord.BaseClient.GetGuildMembers(Id, args).ConfigureAwait(false); | var models = await Discord.BaseClient.GetGuildMembers(Id, args).ConfigureAwait(false); | ||||
return models.Select(x => new GuildUser(this, x)); | 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> | /// <summary> Gets the user in this guild with the provided id, or null if not found. </summary> | ||||
public async Task<GuildUser> GetUser(ulong id) | public async Task<GuildUser> GetUser(ulong id) | ||||
{ | { | ||||
@@ -309,7 +308,12 @@ namespace Discord.Rest | |||||
return new GuildUser(this, model); | return new GuildUser(this, model); | ||||
return null; | 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) | public async Task<int> PruneUsers(int days = 30, bool simulate = false) | ||||
{ | { | ||||
var args = new GuildPruneParams() { Days = days }; | 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; | IEnumerable<Emoji> IGuild.Emojis => Emojis; | ||||
ulong IGuild.EveryoneRoleId => EveryoneRole.Id; | ulong IGuild.EveryoneRoleId => EveryoneRole.Id; | ||||
IEnumerable<string> IGuild.Features => Features; | IEnumerable<string> IGuild.Features => Features; | ||||
@@ -359,6 +365,8 @@ namespace Discord.Rest | |||||
=> Task.FromResult<IEnumerable<IRole>>(Roles); | => Task.FromResult<IEnumerable<IRole>>(Roles); | ||||
async Task<IGuildUser> IGuild.GetUser(ulong id) | async Task<IGuildUser> IGuild.GetUser(ulong id) | ||||
=> await GetUser(id).ConfigureAwait(false); | => await GetUser(id).ConfigureAwait(false); | ||||
async Task<IGuildUser> IGuild.GetCurrentUser() | |||||
=> await GetCurrentUser().ConfigureAwait(false); | |||||
async Task<IEnumerable<IGuildUser>> IGuild.GetUsers() | async Task<IEnumerable<IGuildUser>> IGuild.GetUsers() | ||||
=> await GetUsers().ConfigureAwait(false); | => await GetUsers().ConfigureAwait(false); | ||||
} | } | ||||
@@ -28,5 +28,7 @@ namespace Discord.Rest | |||||
ChannelId = model.ChannelId; | ChannelId = model.ChannelId; | ||||
IsEnabled = model.Enabled; | IsEnabled = model.Enabled; | ||||
} | } | ||||
public override string ToString() => $"{Id} ({(IsEnabled ? "Enabled" : "Disabled")})"; | |||||
} | } | ||||
} | } |
@@ -77,6 +77,8 @@ namespace Discord.Rest | |||||
await Discord.BaseClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); | await Discord.BaseClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); | ||||
} | } | ||||
public override string ToString() => $"{Name ?? Id.ToString()} ({(IsEnabled ? "Enabled" : "Disabled")})"; | |||||
IGuild IGuildIntegration.Guild => Guild; | IGuild IGuildIntegration.Guild => Guild; | ||||
IRole IGuildIntegration.Role => Role; | IRole IGuildIntegration.Role => Role; | ||||
IUser IGuildIntegration.User => User; | IUser IGuildIntegration.User => User; | ||||
@@ -7,5 +7,7 @@ | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public string Name { get; private set; } | public string Name { get; private set; } | ||||
public override string ToString() => Name ?? Id.ToString(); | |||||
} | } | ||||
} | } |
@@ -41,15 +41,17 @@ namespace Discord.Rest | |||||
public async Task Leave() | public async Task Leave() | ||||
{ | { | ||||
if (IsOwner) | 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); | await Discord.BaseClient.LeaveGuild(Id).ConfigureAwait(false); | ||||
} | } | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public async Task Delete() | public async Task Delete() | ||||
{ | { | ||||
if (!IsOwner) | 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); | await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false); | ||||
} | } | ||||
public override string ToString() => Name ?? Id.ToString(); | |||||
} | } | ||||
} | } |
@@ -26,5 +26,7 @@ namespace Discord.Rest | |||||
SampleHostname = model.SampleHostname; | SampleHostname = model.SampleHostname; | ||||
SamplePort = model.SamplePort; | SamplePort = model.SamplePort; | ||||
} | } | ||||
public override string ToString() => $"{Name ?? Id.ToString()}"; | |||||
} | } | ||||
} | } |
@@ -41,8 +41,6 @@ namespace Discord.Rest | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); | 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 DiscordClient Discord => (Channel as TextChannel)?.Discord ?? (Channel as DMChannel).Discord; | ||||
internal Message(IMessageChannel channel, Model model) | internal Message(IMessageChannel channel, Model model) | ||||
@@ -68,7 +66,7 @@ namespace Discord.Rest | |||||
Attachments = ImmutableArray.Create(attachments); | Attachments = ImmutableArray.Create(attachments); | ||||
} | } | ||||
else | else | ||||
Attachments = ImmutableArray<Attachment>.Empty; | |||||
Attachments = Array.Empty<Attachment>(); | |||||
if (model.Embeds.Length > 0) | if (model.Embeds.Length > 0) | ||||
{ | { | ||||
@@ -78,18 +76,18 @@ namespace Discord.Rest | |||||
Embeds = ImmutableArray.Create(embeds); | Embeds = ImmutableArray.Create(embeds); | ||||
} | } | ||||
else | else | ||||
Embeds = ImmutableArray<Embed>.Empty; | |||||
Embeds = Array.Empty<Embed>(); | |||||
if (model.Mentions.Length > 0) | if (model.Mentions.Length > 0) | ||||
{ | { | ||||
var discord = Discord; | var discord = Discord; | ||||
var builder = ImmutableArray.CreateBuilder<PublicUser>(model.Mentions.Length); | var builder = ImmutableArray.CreateBuilder<PublicUser>(model.Mentions.Length); | ||||
for (int i = 0; i < model.Mentions.Length; i++) | 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(); | MentionedUsers = builder.ToArray(); | ||||
} | } | ||||
else | else | ||||
MentionedUsers = ImmutableArray<PublicUser>.Empty; | |||||
MentionedUsers = Array.Empty<PublicUser>(); | |||||
MentionedChannelIds = MentionHelper.GetChannelMentions(model.Content); | MentionedChannelIds = MentionHelper.GetChannelMentions(model.Content); | ||||
MentionedRoleIds = MentionHelper.GetRoleMentions(model.Content); | MentionedRoleIds = MentionHelper.GetRoleMentions(model.Content); | ||||
if (model.IsMentioningEveryone) | if (model.IsMentioningEveryone) | ||||
@@ -121,7 +119,13 @@ namespace Discord.Rest | |||||
var args = new ModifyMessageParams(); | var args = new ModifyMessageParams(); | ||||
func(args); | 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); | Update(model); | ||||
} | } | ||||
@@ -131,6 +135,9 @@ namespace Discord.Rest | |||||
await Discord.BaseClient.DeleteMessage(Channel.Id, Id).ConfigureAwait(false); | await Discord.BaseClient.DeleteMessage(Channel.Id, Id).ConfigureAwait(false); | ||||
} | } | ||||
public override string ToString() => $"{Author.ToString()}: {Text}"; | |||||
IUser IMessage.Author => Author; | IUser IMessage.Author => Author; | ||||
IReadOnlyList<Attachment> IMessage.Attachments => Attachments; | IReadOnlyList<Attachment> IMessage.Attachments => Attachments; | ||||
IReadOnlyList<Embed> IMessage.Embeds => Embeds; | IReadOnlyList<Embed> IMessage.Embeds => Embeds; | ||||
@@ -23,5 +23,7 @@ namespace Discord.Rest | |||||
Integrations = model.Integrations; | Integrations = model.Integrations; | ||||
} | } | ||||
public override string ToString() => $"{Name ?? Id.ToString()} ({Type})"; | |||||
} | } | ||||
} | } |
@@ -2,6 +2,7 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
using System.Linq; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Model = Discord.API.GuildMember; | using Model = Discord.API.GuildMember; | ||||
@@ -39,9 +40,9 @@ namespace Discord.Rest | |||||
Nickname = model.Nick; | Nickname = model.Nick; | ||||
var roles = ImmutableArray.CreateBuilder<Role>(model.Roles.Length + 1); | 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++) | 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(); | _roles = roles.ToImmutable(); | ||||
} | } | ||||
@@ -82,8 +83,27 @@ namespace Discord.Rest | |||||
var args = new ModifyGuildMemberParams(); | var args = new ModifyGuildMemberParams(); | ||||
func(args); | 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(); | |||||
} | |||||
} | } | ||||
@@ -54,6 +54,8 @@ namespace Discord.Rest | |||||
return new DMChannel(Discord, model); | return new DMChannel(Discord, model); | ||||
} | } | ||||
public override string ToString() => $"{Username ?? Id.ToString()}"; | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
string IUser.CurrentGame => null; | string IUser.CurrentGame => null; | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||