Prep for WebSocket client, Several MessageQueue fixes
This commit is contained in:
@@ -20,23 +20,22 @@ namespace Discord.API
|
||||
public class DiscordRawClient
|
||||
{
|
||||
internal event EventHandler<SentRequestEventArgs> SentRequest;
|
||||
|
||||
|
||||
private readonly RequestQueue _requestQueue;
|
||||
private readonly IRestClient _restClient;
|
||||
private readonly CancellationToken _cancelToken;
|
||||
private readonly JsonSerializer _serializer;
|
||||
|
||||
private IRestClient _restClient;
|
||||
private CancellationToken _cancelToken;
|
||||
|
||||
public TokenType AuthTokenType { get; private set; }
|
||||
public IRestClient RestClient { get; private set; }
|
||||
public IRequestQueue RequestQueue { get; private set; }
|
||||
|
||||
internal DiscordRawClient(RestClientProvider restClientProvider, CancellationToken cancelToken)
|
||||
internal DiscordRawClient(RestClientProvider restClientProvider)
|
||||
{
|
||||
_cancelToken = cancelToken;
|
||||
|
||||
_restClient = restClientProvider(DiscordConfig.ClientAPIUrl, cancelToken);
|
||||
_restClient = restClientProvider(DiscordConfig.ClientAPIUrl);
|
||||
_restClient.SetHeader("accept", "*/*");
|
||||
_restClient.SetHeader("user-agent", DiscordConfig.UserAgent);
|
||||
|
||||
_requestQueue = new RequestQueue(_restClient);
|
||||
|
||||
_serializer = new JsonSerializer();
|
||||
@@ -53,29 +52,40 @@ namespace Discord.API
|
||||
_serializer.ContractResolver = new OptionalContractResolver();
|
||||
}
|
||||
|
||||
public void SetToken(TokenType tokenType, string token)
|
||||
public async Task Login(TokenType tokenType, string token, CancellationToken cancelToken)
|
||||
{
|
||||
AuthTokenType = tokenType;
|
||||
|
||||
if (token != null)
|
||||
_cancelToken = cancelToken;
|
||||
await _requestQueue.SetCancelToken(cancelToken).ConfigureAwait(false);
|
||||
|
||||
switch (tokenType)
|
||||
{
|
||||
switch (tokenType)
|
||||
{
|
||||
case TokenType.Bot:
|
||||
token = $"Bot {token}";
|
||||
break;
|
||||
case TokenType.Bearer:
|
||||
token = $"Bearer {token}";
|
||||
break;
|
||||
case TokenType.User:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown oauth token type", nameof(tokenType));
|
||||
}
|
||||
case TokenType.Bot:
|
||||
token = $"Bot {token}";
|
||||
break;
|
||||
case TokenType.Bearer:
|
||||
token = $"Bearer {token}";
|
||||
break;
|
||||
case TokenType.User:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown oauth token type", nameof(tokenType));
|
||||
}
|
||||
|
||||
_restClient.SetHeader("authorization", token);
|
||||
}
|
||||
public async Task Login(LoginParams args, CancellationToken cancelToken)
|
||||
{
|
||||
var response = await Send<LoginResponse>("POST", "auth/login", args).ConfigureAwait(false);
|
||||
|
||||
AuthTokenType = TokenType.User;
|
||||
_restClient.SetHeader("authorization", response.Token);
|
||||
}
|
||||
public async Task Logout()
|
||||
{
|
||||
await _requestQueue.Clear().ConfigureAwait(false);
|
||||
_restClient = null;
|
||||
}
|
||||
|
||||
//Core
|
||||
public Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General)
|
||||
@@ -121,6 +131,8 @@ namespace Discord.API
|
||||
|
||||
private async Task<Stream> SendInternal(string method, string endpoint, object payload, bool headerOnly, BucketGroup group, int bucketId, ulong guildId)
|
||||
{
|
||||
_cancelToken.ThrowIfCancellationRequested();
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
string json = null;
|
||||
if (payload != null)
|
||||
@@ -136,6 +148,8 @@ namespace Discord.API
|
||||
}
|
||||
private async Task<Stream> SendInternal(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, bool headerOnly, BucketGroup group, int bucketId, ulong guildId)
|
||||
{
|
||||
_cancelToken.ThrowIfCancellationRequested();
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, multipartArgs, headerOnly), group, bucketId, guildId).ConfigureAwait(false);
|
||||
int bytes = headerOnly ? 0 : (int)responseStream.Length;
|
||||
@@ -149,11 +163,6 @@ namespace Discord.API
|
||||
|
||||
|
||||
//Auth
|
||||
public async Task Login(LoginParams args)
|
||||
{
|
||||
var response = await Send<LoginResponse>("POST", "auth/login", args).ConfigureAwait(false);
|
||||
SetToken(TokenType.User, response.Token);
|
||||
}
|
||||
public async Task ValidateToken()
|
||||
{
|
||||
await Send("GET", "auth/login").ConfigureAwait(false);
|
||||
|
||||
9
src/Discord.Net/API/IWebSocketMessage.cs
Normal file
9
src/Discord.Net/API/IWebSocketMessage.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Discord.API
|
||||
{
|
||||
public interface IWebSocketMessage
|
||||
{
|
||||
int OpCode { get; }
|
||||
object Payload { get; }
|
||||
bool IsPrivate { get; }
|
||||
}
|
||||
}
|
||||
23
src/Discord.Net/API/WebSocketMessage.cs
Normal file
23
src/Discord.Net/API/WebSocketMessage.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Discord.API
|
||||
{
|
||||
public class WebSocketMessage
|
||||
{
|
||||
[JsonProperty("op")]
|
||||
public int? Operation { get; set; }
|
||||
[JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Type { get; set; }
|
||||
[JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public uint? Sequence { get; set; }
|
||||
[JsonProperty("d")]
|
||||
public object Payload { get; set; }
|
||||
|
||||
public WebSocketMessage() { }
|
||||
public WebSocketMessage(IWebSocketMessage msg)
|
||||
{
|
||||
Operation = msg.OpCode;
|
||||
Payload = msg.Payload;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@ namespace Discord
|
||||
ulong ExpireBehavior { get; }
|
||||
ulong ExpireGracePeriod { get; }
|
||||
DateTime SyncedAt { get; }
|
||||
IntegrationAccount Account { get; }
|
||||
|
||||
IGuild Guild { get; }
|
||||
IUser User { get; }
|
||||
IRole Role { get; }
|
||||
IIntegrationAccount Account { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Discord
|
||||
{
|
||||
public interface IIntegrationAccount : IEntity<string>
|
||||
{
|
||||
string Name { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Discord.Rest
|
||||
namespace Discord
|
||||
{
|
||||
public class IntegrationAccount : IIntegrationAccount
|
||||
public struct IntegrationAccount
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Id { get; }
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.Invite;
|
||||
|
||||
namespace Discord.Rest
|
||||
namespace Discord
|
||||
{
|
||||
public abstract class Invite : IInvite
|
||||
{
|
||||
@@ -17,7 +17,7 @@ namespace Discord.Rest
|
||||
/// <inheritdoc />
|
||||
public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null;
|
||||
|
||||
internal abstract DiscordClient Discord { get; }
|
||||
internal abstract IDiscordClient Discord { get; }
|
||||
|
||||
internal Invite(Model model)
|
||||
{
|
||||
@@ -15,9 +15,9 @@ namespace Discord.Rest
|
||||
/// <inheritdoc />
|
||||
public ulong ChannelId => _channelId;
|
||||
|
||||
internal override DiscordClient Discord { get; }
|
||||
internal override IDiscordClient Discord { get; }
|
||||
|
||||
internal PublicInvite(DiscordClient discord, Model model)
|
||||
internal PublicInvite(IDiscordClient discord, Model model)
|
||||
: base(model)
|
||||
{
|
||||
Discord = discord;
|
||||
@@ -18,7 +18,7 @@ namespace Discord
|
||||
{
|
||||
case ITextChannel _: return _allText;
|
||||
case IVoiceChannel _: return _allVoice;
|
||||
case IDMChannel _: return _allDM;
|
||||
case IGuildChannel _: return _allDM;
|
||||
default:
|
||||
throw new ArgumentException("Unknown channel type", nameof(channel));
|
||||
}
|
||||
|
||||
@@ -6,12 +6,11 @@ namespace Discord.Rest
|
||||
public class Connection : IConnection
|
||||
{
|
||||
public string Id { get; }
|
||||
public string Type { get; }
|
||||
public string Name { get; }
|
||||
public bool IsRevoked { get; }
|
||||
|
||||
public string Type { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public bool IsRevoked { get; private set; }
|
||||
|
||||
public IEnumerable<ulong> Integrations { get; private set; }
|
||||
public IEnumerable<ulong> IntegrationIds { get; }
|
||||
|
||||
public Connection(Model model)
|
||||
{
|
||||
@@ -21,7 +20,7 @@ namespace Discord.Rest
|
||||
Name = model.Name;
|
||||
IsRevoked = model.Revoked;
|
||||
|
||||
Integrations = model.Integrations;
|
||||
IntegrationIds = model.Integrations;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Name ?? Id.ToString()} ({Type})";
|
||||
@@ -9,6 +9,6 @@ namespace Discord
|
||||
string Name { get; }
|
||||
bool IsRevoked { get; }
|
||||
|
||||
IEnumerable<ulong> Integrations { get; }
|
||||
IEnumerable<ulong> IntegrationIds { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
<Compile Include="API\Common\VoiceRegion.cs" />
|
||||
<Compile Include="API\Common\VoiceState.cs" />
|
||||
<Compile Include="API\IOptional.cs" />
|
||||
<Compile Include="API\IWebSocketMessage.cs" />
|
||||
<Compile Include="API\Optional.cs" />
|
||||
<Compile Include="API\Rest\DeleteMessagesParam.cs" />
|
||||
<Compile Include="API\Rest\GetGuildMembersParams.cs" />
|
||||
@@ -98,6 +99,7 @@
|
||||
<Compile Include="API\Rest\ModifyMessageParams.cs" />
|
||||
<Compile Include="API\Rest\ModifyTextChannelParams.cs" />
|
||||
<Compile Include="API\Rest\ModifyVoiceChannelParams.cs" />
|
||||
<Compile Include="API\WebSocketMessage.cs" />
|
||||
<Compile Include="DiscordConfig.cs" />
|
||||
<Compile Include="API\DiscordRawClient.cs" />
|
||||
<Compile Include="Net\Converters\OptionalContractResolver.cs" />
|
||||
@@ -111,7 +113,6 @@
|
||||
<Compile Include="Net\Rest\RequestQueue\RestRequest.cs" />
|
||||
<Compile Include="Rest\DiscordClient.cs" />
|
||||
<Compile Include="Common\Entities\Guilds\IGuildEmbed.cs" />
|
||||
<Compile Include="Common\Entities\Guilds\IIntegrationAccount.cs" />
|
||||
<Compile Include="Common\Entities\Users\IConnection.cs" />
|
||||
<Compile Include="Common\Entities\Guilds\IGuildIntegration.cs" />
|
||||
<Compile Include="Common\Entities\Invites\IPublicInvite.cs" />
|
||||
@@ -161,19 +162,19 @@
|
||||
<Compile Include="Rest\Entities\Channels\VoiceChannel.cs" />
|
||||
<Compile Include="Rest\Entities\Guilds\GuildEmbed.cs" />
|
||||
<Compile Include="Rest\Entities\Guilds\GuildIntegration.cs" />
|
||||
<Compile Include="Rest\Entities\Guilds\IntegrationAccount.cs" />
|
||||
<Compile Include="Rest\Entities\Users\Connection.cs" />
|
||||
<Compile Include="Common\Entities\Guilds\IntegrationAccount.cs" />
|
||||
<Compile Include="Common\Entities\Users\Connection.cs" />
|
||||
<Compile Include="Common\Helpers\PermissionHelper.cs" />
|
||||
<Compile Include="Rest\Entities\Invites\GuildInvite.cs" />
|
||||
<Compile Include="Rest\Entities\Invites\Invite.cs" />
|
||||
<Compile Include="Rest\Entities\Invites\PublicInvite.cs" />
|
||||
<Compile Include="Common\Entities\Invites\Invite.cs" />
|
||||
<Compile Include="Common\Entities\Invites\PublicInvite.cs" />
|
||||
<Compile Include="Rest\Entities\Message.cs" />
|
||||
<Compile Include="Rest\Entities\Role.cs" />
|
||||
<Compile Include="Rest\Entities\Guilds\UserGuild.cs" />
|
||||
<Compile Include="Rest\Entities\Users\DMUser.cs" />
|
||||
<Compile Include="Rest\Entities\Users\GuildUser.cs" />
|
||||
<Compile Include="Rest\Entities\Users\PublicUser.cs" />
|
||||
<Compile Include="Rest\Entities\Guilds\VoiceRegion.cs" />
|
||||
<Compile Include="Common\Entities\Guilds\VoiceRegion.cs" />
|
||||
<Compile Include="Common\Events\LogMessageEventArgs.cs" />
|
||||
<Compile Include="Common\Events\SentRequestEventArgs.cs" />
|
||||
<Compile Include="Common\Helpers\DateTimeHelper.cs" />
|
||||
@@ -204,6 +205,23 @@
|
||||
<Compile Include="Rest\Entities\Users\SelfUser.cs" />
|
||||
<Compile Include="Rest\Entities\Users\User.cs" />
|
||||
<Compile Include="TokenType.cs" />
|
||||
<Compile Include="WebSocket\Caches\MessageCache.cs" />
|
||||
<Compile Include="WebSocket\Caches\ChannelPermissionsCache.cs" />
|
||||
<Compile Include="WebSocket\DiscordClient.cs" />
|
||||
<Compile Include="WebSocket\Entities\Channels\DMChannel.cs" />
|
||||
<Compile Include="WebSocket\Entities\Channels\GuildChannel.cs" />
|
||||
<Compile Include="WebSocket\Entities\Channels\TextChannel.cs" />
|
||||
<Compile Include="WebSocket\Entities\Channels\VoiceChannel.cs" />
|
||||
<Compile Include="WebSocket\Entities\Guilds\Guild.cs" />
|
||||
<Compile Include="WebSocket\Entities\Guilds\GuildIntegration.cs" />
|
||||
<Compile Include="WebSocket\Entities\Invites\GuildInvite.cs" />
|
||||
<Compile Include="WebSocket\Entities\Users\DMUser.cs" />
|
||||
<Compile Include="WebSocket\Entities\Users\GuildUser.cs" />
|
||||
<Compile Include="WebSocket\Entities\Users\PublicUser.cs" />
|
||||
<Compile Include="WebSocket\Entities\Users\SelfUser.cs" />
|
||||
<Compile Include="WebSocket\Entities\Users\User.cs" />
|
||||
<Compile Include="WebSocket\Entities\Message.cs" />
|
||||
<Compile Include="WebSocket\Entities\Role.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Common\Entities\Users\IVoiceState.cs.old" />
|
||||
|
||||
@@ -3,6 +3,8 @@ using System.Reflection;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
//TODO: Add socket config items in their own class
|
||||
|
||||
public class DiscordConfig
|
||||
{
|
||||
public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown";
|
||||
@@ -26,6 +28,6 @@ namespace Discord
|
||||
public LogSeverity LogLevel { get; set; } = LogSeverity.Info;
|
||||
|
||||
/// <summary> Gets or sets the provider used to generate new REST connections. </summary>
|
||||
public RestClientProvider RestClientProvider { get; set; } = (url, ct) => new DefaultRestClient(url, ct);
|
||||
public RestClientProvider RestClientProvider { get; set; } = url => new DefaultRestClient(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Globalization;
|
||||
|
||||
namespace Discord.Net.Converters
|
||||
{
|
||||
internal class UInt64ArrayConverter : JsonConverter
|
||||
public class UInt64ArrayConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable<ulong[]>);
|
||||
public override bool CanRead => true;
|
||||
|
||||
@@ -17,13 +17,11 @@ namespace Discord.Net.Rest
|
||||
|
||||
protected readonly HttpClient _client;
|
||||
protected readonly string _baseUrl;
|
||||
protected readonly CancellationToken _cancelToken;
|
||||
protected bool _isDisposed;
|
||||
|
||||
public DefaultRestClient(string baseUrl, CancellationToken cancelToken)
|
||||
public DefaultRestClient(string baseUrl)
|
||||
{
|
||||
_baseUrl = baseUrl;
|
||||
_cancelToken = cancelToken;
|
||||
|
||||
_client = new HttpClient(new HttpClientHandler
|
||||
{
|
||||
@@ -56,18 +54,18 @@ namespace Discord.Net.Rest
|
||||
_client.DefaultRequestHeaders.Add(key, value);
|
||||
}
|
||||
|
||||
public async Task<Stream> Send(string method, string endpoint, string json = null, bool headerOnly = false)
|
||||
public async Task<Stream> Send(string method, string endpoint, CancellationToken cancelToken, string json = null, bool headerOnly = false)
|
||||
{
|
||||
string uri = Path.Combine(_baseUrl, endpoint);
|
||||
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
|
||||
{
|
||||
if (json != null)
|
||||
restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
return await SendInternal(restRequest, _cancelToken, headerOnly).ConfigureAwait(false);
|
||||
return await SendInternal(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Stream> Send(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, bool headerOnly = false)
|
||||
public async Task<Stream> Send(string method, string endpoint, CancellationToken cancelToken, IReadOnlyDictionary<string, object> multipartParams, bool headerOnly = false)
|
||||
{
|
||||
string uri = Path.Combine(_baseUrl, endpoint);
|
||||
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
|
||||
@@ -97,7 +95,7 @@ namespace Discord.Net.Rest
|
||||
}
|
||||
}
|
||||
restRequest.Content = content;
|
||||
return await SendInternal(restRequest, _cancelToken, headerOnly).ConfigureAwait(false);
|
||||
return await SendInternal(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Net.Rest
|
||||
@@ -9,7 +10,7 @@ namespace Discord.Net.Rest
|
||||
{
|
||||
void SetHeader(string key, string value);
|
||||
|
||||
Task<Stream> Send(string method, string endpoint, string json = null, bool headerOnly = false);
|
||||
Task<Stream> Send(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, bool headerOnly = false);
|
||||
Task<Stream> Send(string method, string endpoint, CancellationToken cancelToken, string json = null, bool headerOnly = false);
|
||||
Task<Stream> Send(string method, string endpoint, CancellationToken cancelToken, IReadOnlyDictionary<string, object> multipartParams, bool headerOnly = false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ namespace Discord.Net.Rest
|
||||
{
|
||||
public class RequestQueue : IRequestQueue
|
||||
{
|
||||
private SemaphoreSlim _lock;
|
||||
private RequestQueueBucket[] _globalBuckets;
|
||||
private Dictionary<ulong, RequestQueueBucket>[] _guildBuckets;
|
||||
private readonly SemaphoreSlim _lock;
|
||||
private readonly RequestQueueBucket[] _globalBuckets;
|
||||
private readonly Dictionary<ulong, RequestQueueBucket>[] _guildBuckets;
|
||||
private CancellationTokenSource _clearToken;
|
||||
private CancellationToken? _parentToken;
|
||||
private CancellationToken _cancelToken;
|
||||
|
||||
public IRestClient RestClient { get; }
|
||||
|
||||
@@ -21,12 +24,26 @@ namespace Discord.Net.Rest
|
||||
_lock = new SemaphoreSlim(1, 1);
|
||||
_globalBuckets = new RequestQueueBucket[Enum.GetValues(typeof(GlobalBucket)).Length];
|
||||
_guildBuckets = new Dictionary<ulong, RequestQueueBucket>[Enum.GetValues(typeof(GuildBucket)).Length];
|
||||
_clearToken = new CancellationTokenSource();
|
||||
_cancelToken = _clearToken.Token;
|
||||
}
|
||||
internal async Task SetCancelToken(CancellationToken cancelToken)
|
||||
{
|
||||
await Lock().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
_parentToken = cancelToken;
|
||||
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token).Token;
|
||||
}
|
||||
finally { Unlock(); }
|
||||
}
|
||||
|
||||
internal async Task<Stream> Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId)
|
||||
{
|
||||
RequestQueueBucket bucket;
|
||||
|
||||
request.CancelToken = _cancelToken;
|
||||
|
||||
await Lock().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
@@ -129,6 +146,20 @@ namespace Discord.Net.Rest
|
||||
_lock.Release();
|
||||
}
|
||||
|
||||
public async Task Clear()
|
||||
{
|
||||
await Lock().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
_clearToken?.Cancel();
|
||||
_clearToken = new CancellationTokenSource();
|
||||
if (_parentToken != null)
|
||||
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken.Value).Token;
|
||||
else
|
||||
_cancelToken = _clearToken.Token;
|
||||
}
|
||||
finally { Unlock(); }
|
||||
}
|
||||
public async Task Clear(GlobalBucket type)
|
||||
{
|
||||
var bucket = _globalBuckets[(int)type];
|
||||
@@ -136,7 +167,7 @@ namespace Discord.Net.Rest
|
||||
{
|
||||
try
|
||||
{
|
||||
await bucket.Lock();
|
||||
await bucket.Lock().ConfigureAwait(false);
|
||||
bucket.Clear();
|
||||
}
|
||||
finally { bucket.Unlock(); }
|
||||
@@ -152,7 +183,7 @@ namespace Discord.Net.Rest
|
||||
{
|
||||
try
|
||||
{
|
||||
await bucket.Lock();
|
||||
await bucket.Lock().ConfigureAwait(false);
|
||||
bucket.Clear();
|
||||
}
|
||||
finally { bucket.Unlock(); }
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Discord.Net.Rest
|
||||
private readonly ConcurrentQueue<RestRequest> _queue;
|
||||
private readonly SemaphoreSlim _lock;
|
||||
private Task _resetTask;
|
||||
private bool _waitingToProcess, _destroyed; //TODO: Remove _destroyed
|
||||
private bool _waitingToProcess;
|
||||
private int _id;
|
||||
|
||||
public int WindowMaxCount { get; }
|
||||
@@ -49,11 +49,7 @@ namespace Discord.Net.Rest
|
||||
|
||||
public void Queue(RestRequest request)
|
||||
{
|
||||
if (_destroyed) throw new Exception();
|
||||
//Assume this obj's parent is under lock
|
||||
|
||||
_queue.Enqueue(request);
|
||||
Debug($"Request queued ({WindowCount}/{WindowMaxCount} + {_queue.Count})");
|
||||
}
|
||||
public async Task ProcessQueue(bool acquireLock = false)
|
||||
{
|
||||
@@ -81,12 +77,17 @@ namespace Discord.Net.Rest
|
||||
|
||||
try
|
||||
{
|
||||
Stream stream;
|
||||
if (request.IsMultipart)
|
||||
stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.MultipartParams, request.HeaderOnly).ConfigureAwait(false);
|
||||
if (request.CancelToken.IsCancellationRequested)
|
||||
request.Promise.SetException(new OperationCanceledException(request.CancelToken));
|
||||
else
|
||||
stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.Json, request.HeaderOnly).ConfigureAwait(false);
|
||||
request.Promise.SetResult(stream);
|
||||
{
|
||||
Stream stream;
|
||||
if (request.IsMultipart)
|
||||
stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.CancelToken, request.MultipartParams, request.HeaderOnly).ConfigureAwait(false);
|
||||
else
|
||||
stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.CancelToken, request.Json, request.HeaderOnly).ConfigureAwait(false);
|
||||
request.Promise.SetResult(stream);
|
||||
}
|
||||
}
|
||||
catch (HttpRateLimitException ex) //Preemptive check failed, use Discord's time instead of our own
|
||||
{
|
||||
@@ -94,17 +95,13 @@ namespace Discord.Net.Rest
|
||||
var task = _resetTask;
|
||||
if (task != null)
|
||||
{
|
||||
Debug($"External rate limit: Extended to {ex.RetryAfterMilliseconds} ms");
|
||||
var retryAfter = DateTime.UtcNow.AddMilliseconds(ex.RetryAfterMilliseconds);
|
||||
await task.ConfigureAwait(false);
|
||||
int millis = (int)Math.Ceiling((DateTime.UtcNow - retryAfter).TotalMilliseconds);
|
||||
_resetTask = ResetAfter(millis);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug($"External rate limit: Reset in {ex.RetryAfterMilliseconds} ms");
|
||||
_resetTask = ResetAfter(ex.RetryAfterMilliseconds);
|
||||
}
|
||||
return;
|
||||
}
|
||||
catch (HttpException ex)
|
||||
@@ -128,13 +125,11 @@ namespace Discord.Net.Rest
|
||||
_queue.TryDequeue(out request);
|
||||
WindowCount++;
|
||||
nextRetry = 1000;
|
||||
Debug($"Request succeeded ({WindowCount}/{WindowMaxCount} + {_queue.Count})");
|
||||
|
||||
if (WindowCount == 1 && WindowSeconds > 0)
|
||||
{
|
||||
//First request for this window, schedule a reset
|
||||
_resetTask = ResetAfter(WindowSeconds * 1000);
|
||||
Debug($"Internal rate limit: Reset in {WindowSeconds * 1000} ms");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,11 +140,7 @@ namespace Discord.Net.Rest
|
||||
{
|
||||
await _parent.Lock().ConfigureAwait(false);
|
||||
if (_queue.IsEmpty) //Double check, in case a request was queued before we got both locks
|
||||
{
|
||||
Debug($"Destroy");
|
||||
_parent.DestroyGuildBucket((GuildBucket)_bucketId, _guildId);
|
||||
_destroyed = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -179,8 +170,6 @@ namespace Discord.Net.Rest
|
||||
{
|
||||
await Lock().ConfigureAwait(false);
|
||||
|
||||
Debug($"Reset");
|
||||
|
||||
//Reset the current window count and set our state back to normal
|
||||
WindowCount = 0;
|
||||
_resetTask = null;
|
||||
@@ -188,10 +177,7 @@ namespace Discord.Net.Rest
|
||||
//Wait is over, work through the current queue
|
||||
await ProcessQueue().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Unlock();
|
||||
}
|
||||
finally { Unlock(); }
|
||||
}
|
||||
|
||||
public async Task Lock()
|
||||
@@ -202,24 +188,5 @@ namespace Discord.Net.Rest
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
|
||||
//TODO: Remove
|
||||
private void Debug(string text)
|
||||
{
|
||||
string name;
|
||||
switch (_bucketGroup)
|
||||
{
|
||||
case BucketGroup.Global:
|
||||
name = ((GlobalBucket)_bucketId).ToString();
|
||||
break;
|
||||
case BucketGroup.Guild:
|
||||
name = ((GuildBucket)_bucketId).ToString();
|
||||
break;
|
||||
default:
|
||||
name = "Unknown";
|
||||
break;
|
||||
}
|
||||
System.Diagnostics.Debug.WriteLine($"[{name} {_id}] {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Net.Rest
|
||||
{
|
||||
internal struct RestRequest
|
||||
internal class RestRequest
|
||||
{
|
||||
public string Method { get; }
|
||||
public string Endpoint { get; }
|
||||
public string Json { get; }
|
||||
public bool HeaderOnly { get; }
|
||||
public CancellationToken CancelToken { get; internal set; }
|
||||
public IReadOnlyDictionary<string, object> MultipartParams { get; }
|
||||
public TaskCompletionSource<Stream> Promise { get; }
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
namespace Discord.Net.Rest
|
||||
{
|
||||
public delegate IRestClient RestClientProvider(string baseUrl, CancellationToken cancelToken);
|
||||
public delegate IRestClient RestClientProvider(string baseUrl);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
using Discord.API.Rest;
|
||||
using Discord.Logging;
|
||||
using Discord.Net;
|
||||
using Discord.Net.Rest;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -43,7 +40,7 @@ namespace Discord.Rest
|
||||
_connectionLock = new SemaphoreSlim(1, 1);
|
||||
_log = new LogManager(config.LogLevel);
|
||||
_userAgent = DiscordConfig.UserAgent;
|
||||
BaseClient = new API.DiscordRawClient(_restClientProvider, _cancelTokenSource.Token);
|
||||
BaseClient = new API.DiscordRawClient(_restClientProvider);
|
||||
|
||||
_log.Message += (s,e) => Log.Raise(this, e);
|
||||
}
|
||||
@@ -69,38 +66,43 @@ namespace Discord.Rest
|
||||
private async Task LoginInternal(string email, string password)
|
||||
{
|
||||
if (IsLoggedIn)
|
||||
LogoutInternal();
|
||||
await LogoutInternal().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var cancelTokenSource = new CancellationTokenSource();
|
||||
_cancelTokenSource = new CancellationTokenSource();
|
||||
|
||||
var args = new LoginParams { Email = email, Password = password };
|
||||
await BaseClient.Login(args).ConfigureAwait(false);
|
||||
await CompleteLogin(cancelTokenSource, false).ConfigureAwait(false);
|
||||
await BaseClient.Login(args, _cancelTokenSource.Token).ConfigureAwait(false);
|
||||
await CompleteLogin(false).ConfigureAwait(false);
|
||||
}
|
||||
catch { LogoutInternal(); throw; }
|
||||
catch { await LogoutInternal().ConfigureAwait(false); throw; }
|
||||
}
|
||||
private async Task LoginInternal(TokenType tokenType, string token, bool validateToken)
|
||||
{
|
||||
if (IsLoggedIn)
|
||||
LogoutInternal();
|
||||
await LogoutInternal().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var cancelTokenSource = new CancellationTokenSource();
|
||||
_cancelTokenSource = new CancellationTokenSource();
|
||||
|
||||
BaseClient.SetToken(tokenType, token);
|
||||
await CompleteLogin(cancelTokenSource, validateToken).ConfigureAwait(false);
|
||||
await BaseClient.Login(tokenType, token, _cancelTokenSource.Token).ConfigureAwait(false);
|
||||
await CompleteLogin(validateToken).ConfigureAwait(false);
|
||||
}
|
||||
catch { LogoutInternal(); throw; }
|
||||
catch { await LogoutInternal().ConfigureAwait(false); throw; }
|
||||
}
|
||||
private async Task CompleteLogin(CancellationTokenSource cancelTokenSource, bool validateToken)
|
||||
private async Task CompleteLogin(bool validateToken)
|
||||
{
|
||||
BaseClient.SentRequest += (s, e) => _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms");
|
||||
|
||||
if (validateToken)
|
||||
await BaseClient.ValidateToken().ConfigureAwait(false);
|
||||
|
||||
_cancelTokenSource = cancelTokenSource;
|
||||
{
|
||||
try
|
||||
{
|
||||
await BaseClient.ValidateToken().ConfigureAwait(false);
|
||||
}
|
||||
catch { await BaseClient.Logout().ConfigureAwait(false); }
|
||||
}
|
||||
|
||||
IsLoggedIn = true;
|
||||
LoggedIn.Raise(this);
|
||||
}
|
||||
@@ -111,11 +113,11 @@ namespace Discord.Rest
|
||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
LogoutInternal();
|
||||
await LogoutInternal().ConfigureAwait(false);
|
||||
}
|
||||
finally { _connectionLock.Release(); }
|
||||
}
|
||||
private void LogoutInternal()
|
||||
private async Task LogoutInternal()
|
||||
{
|
||||
bool wasLoggedIn = IsLoggedIn;
|
||||
|
||||
@@ -125,7 +127,7 @@ namespace Discord.Rest
|
||||
catch { }
|
||||
}
|
||||
|
||||
BaseClient.SetToken(TokenType.User, null);
|
||||
await BaseClient.Logout().ConfigureAwait(false);
|
||||
_currentUser = null;
|
||||
|
||||
if (wasLoggedIn)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using Model = Discord.API.GuildEmbed;
|
||||
|
||||
namespace Discord.Rest
|
||||
namespace Discord
|
||||
{
|
||||
public class GuildEmbed : IGuildEmbed
|
||||
{
|
||||
@@ -12,14 +12,11 @@ namespace Discord.Rest
|
||||
/// <inheritdoc />
|
||||
public ulong? ChannelId { get; private set; }
|
||||
|
||||
internal DiscordClient Discord { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
|
||||
|
||||
internal GuildEmbed(DiscordClient discord, Model model)
|
||||
internal GuildEmbed(Model model)
|
||||
{
|
||||
Discord = discord;
|
||||
Update(model);
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,6 @@ namespace Discord.Rest
|
||||
IGuild IGuildIntegration.Guild => Guild;
|
||||
IRole IGuildIntegration.Role => Role;
|
||||
IUser IGuildIntegration.User => User;
|
||||
IIntegrationAccount IGuildIntegration.Account => Account;
|
||||
IntegrationAccount IGuildIntegration.Account => Account;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.UserGuild;
|
||||
|
||||
namespace Discord.Rest
|
||||
namespace Discord
|
||||
{
|
||||
public class UserGuild : IUserGuild
|
||||
{
|
||||
@@ -10,7 +10,7 @@ namespace Discord.Rest
|
||||
|
||||
/// <inheritdoc />
|
||||
public ulong Id { get; }
|
||||
internal DiscordClient Discord { get; }
|
||||
internal IDiscordClient Discord { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; private set; }
|
||||
@@ -22,7 +22,7 @@ namespace Discord.Rest
|
||||
/// <inheritdoc />
|
||||
public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId);
|
||||
|
||||
internal UserGuild(DiscordClient discord, Model model)
|
||||
internal UserGuild(IDiscordClient discord, Model model)
|
||||
{
|
||||
Discord = discord;
|
||||
Id = model.Id;
|
||||
@@ -40,15 +40,11 @@ namespace Discord.Rest
|
||||
/// <inheritdoc />
|
||||
public async Task Leave()
|
||||
{
|
||||
if (IsOwner)
|
||||
throw new InvalidOperationException("Unable to leave a guild the current user owns.");
|
||||
await Discord.BaseClient.LeaveGuild(Id).ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task Delete()
|
||||
{
|
||||
if (!IsOwner)
|
||||
throw new InvalidOperationException("Unable to delete a guild the current user does not own.");
|
||||
await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Discord.Rest
|
||||
/// <inheritdoc />
|
||||
public int Uses { get; private set; }
|
||||
|
||||
internal override DiscordClient Discord => Guild.Discord;
|
||||
internal override IDiscordClient Discord => Guild.Discord;
|
||||
|
||||
internal GuildInvite(Guild guild, Model model)
|
||||
: base(model)
|
||||
|
||||
@@ -135,7 +135,6 @@ namespace Discord.Rest
|
||||
await Discord.BaseClient.DeleteMessage(Channel.Id, Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
public override string ToString() => $"{Author.ToString()}: {Text}";
|
||||
|
||||
IUser IMessage.Author => Author;
|
||||
|
||||
@@ -45,10 +45,7 @@ namespace Discord.Rest
|
||||
|
||||
public async Task<DMChannel> CreateDMChannel()
|
||||
{
|
||||
var args = new CreateDMChannelParams
|
||||
{
|
||||
RecipientId = Id
|
||||
};
|
||||
var args = new CreateDMChannelParams { RecipientId = Id };
|
||||
var model = await Discord.BaseClient.CreateDMChannel(args).ConfigureAwait(false);
|
||||
|
||||
return new DMChannel(Discord, model);
|
||||
|
||||
71
src/Discord.Net/WebSocket/Caches/ChannelPermissionsCache.cs
Normal file
71
src/Discord.Net/WebSocket/Caches/ChannelPermissionsCache.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
internal struct ChannelMember
|
||||
{
|
||||
public GuildUser User { get; }
|
||||
public ChannelPermissions Permissions { get; }
|
||||
|
||||
public ChannelMember(GuildUser user, ChannelPermissions permissions)
|
||||
{
|
||||
User = user;
|
||||
Permissions = permissions;
|
||||
}
|
||||
}
|
||||
|
||||
internal class ChannelPermissionsCache
|
||||
{
|
||||
private readonly GuildChannel _channel;
|
||||
private readonly ConcurrentDictionary<ulong, ChannelMember> _users;
|
||||
|
||||
public IEnumerable<ChannelMember> Members => _users.Select(x => x.Value);
|
||||
|
||||
public ChannelPermissionsCache(GuildChannel channel)
|
||||
{
|
||||
_channel = channel;
|
||||
_users = new ConcurrentDictionary<ulong, ChannelMember>(1, (int)(_channel.Guild.UserCount * 1.05));
|
||||
}
|
||||
|
||||
public ChannelMember? Get(ulong id)
|
||||
{
|
||||
ChannelMember member;
|
||||
if (_users.TryGetValue(id, out member))
|
||||
return member;
|
||||
return null;
|
||||
}
|
||||
public void Add(GuildUser user)
|
||||
{
|
||||
_users[user.Id] = new ChannelMember(user, new ChannelPermissions(PermissionHelper.Resolve(user, _channel)));
|
||||
}
|
||||
public void Remove(GuildUser user)
|
||||
{
|
||||
ChannelMember member;
|
||||
_users.TryRemove(user.Id, out member);
|
||||
}
|
||||
|
||||
public void UpdateAll()
|
||||
{
|
||||
foreach (var pair in _users)
|
||||
{
|
||||
var member = pair.Value;
|
||||
var newPerms = PermissionHelper.Resolve(member.User, _channel);
|
||||
if (newPerms != member.Permissions.RawValue)
|
||||
_users[pair.Key] = new ChannelMember(member.User, new ChannelPermissions(newPerms));
|
||||
}
|
||||
}
|
||||
public void Update(GuildUser user)
|
||||
{
|
||||
ChannelMember member;
|
||||
if (_users.TryGetValue(user.Id, out member))
|
||||
{
|
||||
var newPerms = PermissionHelper.Resolve(user, _channel);
|
||||
if (newPerms != member.Permissions.RawValue)
|
||||
_users[user.Id] = new ChannelMember(user, new ChannelPermissions(newPerms));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/Discord.Net/WebSocket/Caches/MessageCache.cs
Normal file
98
src/Discord.Net/WebSocket/Caches/MessageCache.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using Discord.API.Rest;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
internal class MessageCache
|
||||
{
|
||||
private readonly DiscordClient _discord;
|
||||
private readonly IMessageChannel _channel;
|
||||
private readonly ConcurrentDictionary<ulong, Message> _messages;
|
||||
private readonly ConcurrentQueue<ulong> _orderedMessages;
|
||||
private readonly int _size;
|
||||
|
||||
public MessageCache(DiscordClient discord, IMessageChannel channel)
|
||||
{
|
||||
_discord = discord;
|
||||
_channel = channel;
|
||||
_size = discord.MessageCacheSize;
|
||||
_messages = new ConcurrentDictionary<ulong, Message>(1, (int)(_size * 1.05));
|
||||
_orderedMessages = new ConcurrentQueue<ulong>();
|
||||
}
|
||||
|
||||
internal void Add(Message message)
|
||||
{
|
||||
if (_messages.TryAdd(message.Id, message))
|
||||
{
|
||||
_orderedMessages.Enqueue(message.Id);
|
||||
|
||||
ulong msgId;
|
||||
Message msg;
|
||||
while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msgId))
|
||||
_messages.TryRemove(msgId, out msg);
|
||||
}
|
||||
}
|
||||
|
||||
internal void Remove(ulong id)
|
||||
{
|
||||
Message msg;
|
||||
_messages.TryRemove(id, out msg);
|
||||
}
|
||||
|
||||
public Message Get(ulong id)
|
||||
{
|
||||
Message result;
|
||||
if (_messages.TryGetValue(id, out result))
|
||||
return result;
|
||||
return null;
|
||||
}
|
||||
public async Task<IEnumerable<Message>> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
|
||||
{
|
||||
//TODO: Test heavily
|
||||
|
||||
if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit));
|
||||
if (limit == 0) return ImmutableArray<Message>.Empty;
|
||||
|
||||
IEnumerable<ulong> cachedMessageIds;
|
||||
if (fromMessageId == null)
|
||||
cachedMessageIds = _orderedMessages;
|
||||
else if (dir == Direction.Before)
|
||||
cachedMessageIds = _orderedMessages.Where(x => x < fromMessageId.Value);
|
||||
else
|
||||
cachedMessageIds = _orderedMessages.Where(x => x > fromMessageId.Value);
|
||||
|
||||
var cachedMessages = cachedMessageIds
|
||||
.Take(limit)
|
||||
.Select(x =>
|
||||
{
|
||||
Message msg;
|
||||
if (_messages.TryGetValue(x, out msg))
|
||||
return msg;
|
||||
return null;
|
||||
})
|
||||
.Where(x => x != null)
|
||||
.ToArray();
|
||||
|
||||
if (cachedMessages.Length == limit)
|
||||
return cachedMessages;
|
||||
else if (cachedMessages.Length > limit)
|
||||
return cachedMessages.Skip(cachedMessages.Length - limit);
|
||||
else
|
||||
{
|
||||
var args = new GetChannelMessagesParams
|
||||
{
|
||||
Limit = limit - cachedMessages.Length,
|
||||
RelativeDirection = dir,
|
||||
RelativeMessageId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Length - 1].Id
|
||||
};
|
||||
var downloadedMessages = await _discord.BaseClient.GetChannelMessages(_channel.Id, args).ConfigureAwait(false);
|
||||
return cachedMessages.AsEnumerable().Concat(downloadedMessages.Select(x => new Message(_channel, x))).ToImmutableArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/Discord.Net/WebSocket/DiscordClient.cs
Normal file
139
src/Discord.Net/WebSocket/DiscordClient.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Discord.API;
|
||||
using Discord.Net.Rest;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public class DiscordClient : IDiscordClient
|
||||
{
|
||||
internal int MessageCacheSize { get; } = 100;
|
||||
|
||||
public SelfUser CurrentUser
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public TokenType AuthTokenType
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public DiscordRawClient BaseClient
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public IRequestQueue RequestQueue
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public IRestClient RestClient
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IGuild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IChannel> GetChannel(ulong id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<IConnection>> GetConnections()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<ISelfUser> GetCurrentUser()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<IDMChannel>> GetDMChannels()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IGuild> GetGuild(ulong id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<IUserGuild>> GetGuilds()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IPublicInvite> GetInvite(string inviteIdOrXkcd)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IVoiceRegion> GetOptimalVoiceRegion()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IUser> GetUser(ulong id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IUser> GetUser(string username, ushort discriminator)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IVoiceRegion> GetVoiceRegion(string id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<IVoiceRegion>> GetVoiceRegions()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task Login(string email, string password)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task Login(TokenType tokenType, string token, bool validateToken = true)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task Logout()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<IUser>> QueryUsers(string query, int limit)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
141
src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs
Normal file
141
src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using Discord.API.Rest;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.Channel;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public class DMChannel : IDMChannel
|
||||
{
|
||||
private readonly MessageCache _messages;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ulong Id { get; }
|
||||
internal DiscordClient Discord { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DMUser Recipient { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IUser> Users => ImmutableArray.Create<IUser>(Discord.CurrentUser, Recipient);
|
||||
|
||||
internal DMChannel(DiscordClient discord, Model model)
|
||||
{
|
||||
Id = model.Id;
|
||||
Discord = discord;
|
||||
_messages = new MessageCache(Discord, this);
|
||||
|
||||
Update(model);
|
||||
}
|
||||
private void Update(Model model)
|
||||
{
|
||||
if (Recipient == null)
|
||||
Recipient = new DMUser(this, model.Recipient);
|
||||
else
|
||||
Recipient.Update(model.Recipient);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IUser GetUser(ulong id)
|
||||
{
|
||||
if (id == Recipient.Id)
|
||||
return Recipient;
|
||||
else if (id == Discord.CurrentUser.Id)
|
||||
return Discord.CurrentUser;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<Message>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch)
|
||||
{
|
||||
return await _messages.GetMany(null, Direction.Before, limit).ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<Message>> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
|
||||
{
|
||||
return await _messages.GetMany(fromMessageId, dir, limit).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Message> SendMessage(string text, bool isTTS = false)
|
||||
{
|
||||
var args = new CreateMessageParams { Content = text, IsTTS = isTTS };
|
||||
var model = await Discord.BaseClient.CreateMessage(Id, args).ConfigureAwait(false);
|
||||
return new Message(this, model);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task<Message> SendFile(string filePath, string text = null, bool isTTS = false)
|
||||
{
|
||||
string filename = Path.GetFileName(filePath);
|
||||
using (var file = File.OpenRead(filePath))
|
||||
{
|
||||
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS };
|
||||
var model = await Discord.BaseClient.UploadFile(Id, file, args).ConfigureAwait(false);
|
||||
return new Message(this, model);
|
||||
}
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task<Message> SendFile(Stream stream, string filename, string text = null, bool isTTS = false)
|
||||
{
|
||||
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS };
|
||||
var model = await Discord.BaseClient.UploadFile(Id, stream, args).ConfigureAwait(false);
|
||||
return new Message(this, model);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteMessages(IEnumerable<IMessage> messages)
|
||||
{
|
||||
await Discord.BaseClient.DeleteMessages(Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task TriggerTyping()
|
||||
{
|
||||
await Discord.BaseClient.TriggerTypingIndicator(Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Close()
|
||||
{
|
||||
await Discord.BaseClient.DeleteChannel(Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Update()
|
||||
{
|
||||
var model = await Discord.BaseClient.GetChannel(Id).ConfigureAwait(false);
|
||||
Update(model);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => $"@{Recipient} [DM]";
|
||||
|
||||
IDMUser IDMChannel.Recipient => Recipient;
|
||||
|
||||
Task<IEnumerable<IUser>> IChannel.GetUsers()
|
||||
=> Task.FromResult(Users);
|
||||
Task<IUser> IChannel.GetUser(ulong id)
|
||||
=> Task.FromResult(GetUser(id));
|
||||
Task<IMessage> IMessageChannel.GetMessage(ulong id)
|
||||
=> throw new NotSupportedException();
|
||||
async Task<IEnumerable<IMessage>> IMessageChannel.GetMessages(int limit)
|
||||
=> await GetMessages(limit).ConfigureAwait(false);
|
||||
async Task<IEnumerable<IMessage>> IMessageChannel.GetMessages(ulong fromMessageId, Direction dir, int limit)
|
||||
=> await GetMessages(fromMessageId, dir, limit).ConfigureAwait(false);
|
||||
async Task<IMessage> IMessageChannel.SendMessage(string text, bool isTTS)
|
||||
=> await SendMessage(text, isTTS).ConfigureAwait(false);
|
||||
async Task<IMessage> IMessageChannel.SendFile(string filePath, string text, bool isTTS)
|
||||
=> await SendFile(filePath, text, isTTS).ConfigureAwait(false);
|
||||
async Task<IMessage> IMessageChannel.SendFile(Stream stream, string filename, string text, bool isTTS)
|
||||
=> await SendFile(stream, filename, text, isTTS).ConfigureAwait(false);
|
||||
async Task IMessageChannel.TriggerTyping()
|
||||
=> await TriggerTyping().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
171
src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs
Normal file
171
src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using Discord.API.Rest;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.Channel;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public abstract class GuildChannel : IGuildChannel
|
||||
{
|
||||
private ConcurrentDictionary<ulong, Overwrite> _overwrites;
|
||||
private ChannelPermissionsCache _permissions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ulong Id { get; }
|
||||
/// <summary> Gets the guild this channel is a member of. </summary>
|
||||
public Guild Guild { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public int Position { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<ulong, Overwrite> PermissionOverwrites => _overwrites;
|
||||
internal DiscordClient Discord => Guild.Discord;
|
||||
|
||||
internal GuildChannel(Guild guild, Model model)
|
||||
{
|
||||
Id = model.Id;
|
||||
Guild = guild;
|
||||
|
||||
Update(model);
|
||||
}
|
||||
internal virtual void Update(Model model)
|
||||
{
|
||||
Name = model.Name;
|
||||
Position = model.Position;
|
||||
|
||||
var newOverwrites = new ConcurrentDictionary<ulong, Overwrite>();
|
||||
for (int i = 0; i < model.PermissionOverwrites.Length; i++)
|
||||
{
|
||||
var overwrite = model.PermissionOverwrites[i];
|
||||
newOverwrites[overwrite.TargetId] = new Overwrite(overwrite);
|
||||
}
|
||||
_overwrites = newOverwrites;
|
||||
}
|
||||
|
||||
public async Task Modify(Action<ModifyGuildChannelParams> func)
|
||||
{
|
||||
if (func != null) throw new NullReferenceException(nameof(func));
|
||||
|
||||
var args = new ModifyGuildChannelParams();
|
||||
func(args);
|
||||
var model = await Discord.BaseClient.ModifyGuildChannel(Id, args).ConfigureAwait(false);
|
||||
Update(model);
|
||||
}
|
||||
|
||||
/// <summary> Gets a user in this channel with the given id. </summary>
|
||||
public async Task<GuildUser> GetUser(ulong id)
|
||||
{
|
||||
var model = await Discord.BaseClient.GetGuildMember(Guild.Id, id).ConfigureAwait(false);
|
||||
if (model != null)
|
||||
return new GuildUser(Guild, model);
|
||||
return null;
|
||||
}
|
||||
protected abstract Task<IEnumerable<GuildUser>> GetUsers();
|
||||
|
||||
/// <summary> Gets the permission overwrite for a specific user, or null if one does not exist. </summary>
|
||||
public OverwritePermissions? GetPermissionOverwrite(IUser user)
|
||||
{
|
||||
Overwrite value;
|
||||
if (_overwrites.TryGetValue(Id, out value))
|
||||
return value.Permissions;
|
||||
return null;
|
||||
}
|
||||
/// <summary> Gets the permission overwrite for a specific role, or null if one does not exist. </summary>
|
||||
public OverwritePermissions? GetPermissionOverwrite(IRole role)
|
||||
{
|
||||
Overwrite value;
|
||||
if (_overwrites.TryGetValue(Id, out value))
|
||||
return value.Permissions;
|
||||
return null;
|
||||
}
|
||||
/// <summary> Downloads a collection of all invites to this channel. </summary>
|
||||
public async Task<IEnumerable<GuildInvite>> GetInvites()
|
||||
{
|
||||
var models = await Discord.BaseClient.GetChannelInvites(Id).ConfigureAwait(false);
|
||||
return models.Select(x => new GuildInvite(Guild, x));
|
||||
}
|
||||
|
||||
/// <summary> Adds or updates the permission overwrite for the given user. </summary>
|
||||
public async Task AddPermissionOverwrite(IUser user, OverwritePermissions perms)
|
||||
{
|
||||
var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue };
|
||||
await Discord.BaseClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false);
|
||||
_overwrites[user.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User });
|
||||
}
|
||||
/// <summary> Adds or updates the permission overwrite for the given role. </summary>
|
||||
public async Task AddPermissionOverwrite(IRole role, OverwritePermissions perms)
|
||||
{
|
||||
var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue };
|
||||
await Discord.BaseClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false);
|
||||
_overwrites[role.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role });
|
||||
}
|
||||
/// <summary> Removes the permission overwrite for the given user, if one exists. </summary>
|
||||
public async Task RemovePermissionOverwrite(IUser user)
|
||||
{
|
||||
await Discord.BaseClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false);
|
||||
|
||||
Overwrite value;
|
||||
_overwrites.TryRemove(user.Id, out value);
|
||||
}
|
||||
/// <summary> Removes the permission overwrite for the given role, if one exists. </summary>
|
||||
public async Task RemovePermissionOverwrite(IRole role)
|
||||
{
|
||||
await Discord.BaseClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false);
|
||||
|
||||
Overwrite value;
|
||||
_overwrites.TryRemove(role.Id, out value);
|
||||
}
|
||||
|
||||
/// <summary> Creates a new invite to this channel. </summary>
|
||||
/// <param name="maxAge"> Time (in seconds) until the invite expires. Set to null to never expire. </param>
|
||||
/// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param>
|
||||
/// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the guild after closing their client. </param>
|
||||
/// <param name="withXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to null. </param>
|
||||
public async Task<GuildInvite> CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false)
|
||||
{
|
||||
var args = new CreateChannelInviteParams
|
||||
{
|
||||
MaxAge = maxAge ?? 0,
|
||||
MaxUses = maxUses ?? 0,
|
||||
Temporary = isTemporary,
|
||||
XkcdPass = withXkcd
|
||||
};
|
||||
var model = await Discord.BaseClient.CreateChannelInvite(Id, args).ConfigureAwait(false);
|
||||
return new GuildInvite(Guild, model);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Delete()
|
||||
{
|
||||
await Discord.BaseClient.DeleteChannel(Id).ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task Update()
|
||||
{
|
||||
var model = await Discord.BaseClient.GetChannel(Id).ConfigureAwait(false);
|
||||
Update(model);
|
||||
}
|
||||
|
||||
IGuild IGuildChannel.Guild => Guild;
|
||||
async Task<IGuildInvite> IGuildChannel.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd)
|
||||
=> await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false);
|
||||
async Task<IEnumerable<IGuildInvite>> IGuildChannel.GetInvites()
|
||||
=> await GetInvites().ConfigureAwait(false);
|
||||
async Task<IEnumerable<IGuildUser>> IGuildChannel.GetUsers()
|
||||
=> await GetUsers().ConfigureAwait(false);
|
||||
async Task<IGuildUser> IGuildChannel.GetUser(ulong id)
|
||||
=> await GetUser(id).ConfigureAwait(false);
|
||||
async Task<IEnumerable<IUser>> IChannel.GetUsers()
|
||||
=> await GetUsers().ConfigureAwait(false);
|
||||
async Task<IUser> IChannel.GetUser(ulong id)
|
||||
=> await GetUser(id).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
122
src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs
Normal file
122
src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Discord.API.Rest;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.Channel;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public class TextChannel : GuildChannel, ITextChannel
|
||||
{
|
||||
private readonly MessageCache _messages;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Topic { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Mention => MentionHelper.Mention(this);
|
||||
|
||||
internal TextChannel(Guild guild, Model model)
|
||||
: base(guild, model)
|
||||
{
|
||||
_messages = new MessageCache(Discord, this);
|
||||
}
|
||||
|
||||
internal override void Update(Model model)
|
||||
{
|
||||
Topic = model.Topic;
|
||||
base.Update(model);
|
||||
}
|
||||
|
||||
public async Task Modify(Action<ModifyTextChannelParams> func)
|
||||
{
|
||||
if (func != null) throw new NullReferenceException(nameof(func));
|
||||
|
||||
var args = new ModifyTextChannelParams();
|
||||
func(args);
|
||||
var model = await Discord.BaseClient.ModifyGuildChannel(Id, args).ConfigureAwait(false);
|
||||
Update(model);
|
||||
}
|
||||
|
||||
protected override async Task<IEnumerable<GuildUser>> GetUsers()
|
||||
{
|
||||
var users = await Guild.GetUsers().ConfigureAwait(false);
|
||||
return users.Where(x => PermissionUtilities.GetValue(PermissionHelper.Resolve(x, this), ChannelPermission.ReadMessages));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Message> GetMessage(ulong id) { throw new NotSupportedException(); } //Not implemented
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<Message>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch)
|
||||
{
|
||||
var args = new GetChannelMessagesParams { Limit = limit };
|
||||
var models = await Discord.BaseClient.GetChannelMessages(Id, args).ConfigureAwait(false);
|
||||
return models.Select(x => new Message(this, x));
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<Message>> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
|
||||
{
|
||||
var args = new GetChannelMessagesParams { Limit = limit };
|
||||
var models = await Discord.BaseClient.GetChannelMessages(Id, args).ConfigureAwait(false);
|
||||
return models.Select(x => new Message(this, x));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Message> SendMessage(string text, bool isTTS = false)
|
||||
{
|
||||
var args = new CreateMessageParams { Content = text, IsTTS = isTTS };
|
||||
var model = await Discord.BaseClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false);
|
||||
return new Message(this, model);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task<Message> SendFile(string filePath, string text = null, bool isTTS = false)
|
||||
{
|
||||
string filename = Path.GetFileName(filePath);
|
||||
using (var file = File.OpenRead(filePath))
|
||||
{
|
||||
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS };
|
||||
var model = await Discord.BaseClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false);
|
||||
return new Message(this, model);
|
||||
}
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task<Message> SendFile(Stream stream, string filename, string text = null, bool isTTS = false)
|
||||
{
|
||||
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS };
|
||||
var model = await Discord.BaseClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false);
|
||||
return new Message(this, model);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteMessages(IEnumerable<IMessage> messages)
|
||||
{
|
||||
await Discord.BaseClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task TriggerTyping()
|
||||
{
|
||||
await Discord.BaseClient.TriggerTypingIndicator(Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => $"{base.ToString()} [Text]";
|
||||
|
||||
async Task<IMessage> IMessageChannel.GetMessage(ulong id)
|
||||
=> await GetMessage(id).ConfigureAwait(false);
|
||||
async Task<IEnumerable<IMessage>> IMessageChannel.GetMessages(int limit)
|
||||
=> await GetMessages(limit).ConfigureAwait(false);
|
||||
async Task<IEnumerable<IMessage>> IMessageChannel.GetMessages(ulong fromMessageId, Direction dir, int limit)
|
||||
=> await GetMessages(fromMessageId, dir, limit).ConfigureAwait(false);
|
||||
async Task<IMessage> IMessageChannel.SendMessage(string text, bool isTTS)
|
||||
=> await SendMessage(text, isTTS).ConfigureAwait(false);
|
||||
async Task<IMessage> IMessageChannel.SendFile(string filePath, string text, bool isTTS)
|
||||
=> await SendFile(filePath, text, isTTS).ConfigureAwait(false);
|
||||
async Task<IMessage> IMessageChannel.SendFile(Stream stream, string filename, string text, bool isTTS)
|
||||
=> await SendFile(stream, filename, text, isTTS).ConfigureAwait(false);
|
||||
async Task IMessageChannel.TriggerTyping()
|
||||
=> await TriggerTyping().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
45
src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs
Normal file
45
src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Discord.API.Rest;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.Channel;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public class VoiceChannel : GuildChannel, IVoiceChannel
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int Bitrate { get; private set; }
|
||||
|
||||
internal VoiceChannel(Guild guild, Model model)
|
||||
: base(guild, model)
|
||||
{
|
||||
}
|
||||
internal override void Update(Model model)
|
||||
{
|
||||
base.Update(model);
|
||||
Bitrate = model.Bitrate;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Modify(Action<ModifyVoiceChannelParams> func)
|
||||
{
|
||||
if (func != null) throw new NullReferenceException(nameof(func));
|
||||
|
||||
var args = new ModifyVoiceChannelParams();
|
||||
func(args);
|
||||
var model = await Discord.BaseClient.ModifyGuildChannel(Id, args).ConfigureAwait(false);
|
||||
Update(model);
|
||||
}
|
||||
|
||||
protected override async Task<IEnumerable<GuildUser>> GetUsers()
|
||||
{
|
||||
var users = await Guild.GetUsers().ConfigureAwait(false);
|
||||
return users.Where(x => PermissionUtilities.GetValue(PermissionHelper.Resolve(x, this), ChannelPermission.Connect));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => $"{base.ToString()} [Voice]";
|
||||
}
|
||||
}
|
||||
374
src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs
Normal file
374
src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs
Normal file
@@ -0,0 +1,374 @@
|
||||
using Discord.API.Rest;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.Guild;
|
||||
using EmbedModel = Discord.API.GuildEmbed;
|
||||
using RoleModel = Discord.API.Role;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
/// <summary> Represents a Discord guild (called a server in the official client). </summary>
|
||||
public class Guild : IGuild
|
||||
{
|
||||
private ConcurrentDictionary<ulong, Role> _roles;
|
||||
private string _iconId, _splashId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ulong Id { get; }
|
||||
internal DiscordClient Discord { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public int AFKTimeout { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public bool IsEmbeddable { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public int VerificationLevel { get; private set; }
|
||||
public int UserCount { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ulong? AFKChannelId { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public ulong? EmbedChannelId { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public ulong OwnerId { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public string VoiceRegionId { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<Emoji> Emojis { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Features { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
|
||||
/// <inheritdoc />
|
||||
public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId);
|
||||
/// <inheritdoc />
|
||||
public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, _splashId);
|
||||
/// <inheritdoc />
|
||||
public ulong DefaultChannelId => Id;
|
||||
/// <inheritdoc />
|
||||
public Role EveryoneRole => GetRole(Id);
|
||||
/// <summary> Gets a collection of all roles in this guild. </summary>
|
||||
public IEnumerable<Role> Roles => _roles?.Select(x => x.Value) ?? Enumerable.Empty<Role>();
|
||||
|
||||
internal Guild(DiscordClient discord, Model model)
|
||||
{
|
||||
Id = model.Id;
|
||||
Discord = discord;
|
||||
|
||||
Update(model);
|
||||
}
|
||||
private void Update(Model model)
|
||||
{
|
||||
AFKChannelId = model.AFKChannelId;
|
||||
AFKTimeout = model.AFKTimeout;
|
||||
EmbedChannelId = model.EmbedChannelId;
|
||||
IsEmbeddable = model.EmbedEnabled;
|
||||
Features = model.Features;
|
||||
_iconId = model.Icon;
|
||||
Name = model.Name;
|
||||
OwnerId = model.OwnerId;
|
||||
VoiceRegionId = model.Region;
|
||||
_splashId = model.Splash;
|
||||
VerificationLevel = model.VerificationLevel;
|
||||
|
||||
if (model.Emojis != null)
|
||||
{
|
||||
var emojis = ImmutableArray.CreateBuilder<Emoji>(model.Emojis.Length);
|
||||
for (int i = 0; i < model.Emojis.Length; i++)
|
||||
emojis.Add(new Emoji(model.Emojis[i]));
|
||||
Emojis = emojis.ToArray();
|
||||
}
|
||||
else
|
||||
Emojis = Array.Empty<Emoji>();
|
||||
|
||||
var roles = new ConcurrentDictionary<ulong, Role>(1, model.Roles?.Length ?? 0);
|
||||
if (model.Roles != null)
|
||||
{
|
||||
for (int i = 0; i < model.Roles.Length; i++)
|
||||
roles[model.Roles[i].Id] = new Role(this, model.Roles[i]);
|
||||
}
|
||||
_roles = roles;
|
||||
}
|
||||
private void Update(EmbedModel model)
|
||||
{
|
||||
IsEmbeddable = model.Enabled;
|
||||
EmbedChannelId = model.ChannelId;
|
||||
}
|
||||
private void Update(IEnumerable<RoleModel> models)
|
||||
{
|
||||
Role role;
|
||||
foreach (var model in models)
|
||||
{
|
||||
if (_roles.TryGetValue(model.Id, out role))
|
||||
role.Update(model);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Update()
|
||||
{
|
||||
var response = await Discord.BaseClient.GetGuild(Id).ConfigureAwait(false);
|
||||
Update(response);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task Modify(Action<ModifyGuildParams> func)
|
||||
{
|
||||
if (func == null) throw new NullReferenceException(nameof(func));
|
||||
|
||||
var args = new ModifyGuildParams();
|
||||
func(args);
|
||||
var model = await Discord.BaseClient.ModifyGuild(Id, args).ConfigureAwait(false);
|
||||
Update(model);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task ModifyEmbed(Action<ModifyGuildEmbedParams> func)
|
||||
{
|
||||
if (func == null) throw new NullReferenceException(nameof(func));
|
||||
|
||||
var args = new ModifyGuildEmbedParams();
|
||||
func(args);
|
||||
var model = await Discord.BaseClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false);
|
||||
|
||||
Update(model);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task ModifyChannels(IEnumerable<ModifyGuildChannelsParams> args)
|
||||
{
|
||||
if (args == null) throw new NullReferenceException(nameof(args));
|
||||
|
||||
await Discord.BaseClient.ModifyGuildChannels(Id, args).ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task ModifyRoles(IEnumerable<ModifyGuildRolesParams> args)
|
||||
{
|
||||
if (args == null) throw new NullReferenceException(nameof(args));
|
||||
|
||||
var models = await Discord.BaseClient.ModifyGuildRoles(Id, args).ConfigureAwait(false);
|
||||
Update(models);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task Leave()
|
||||
{
|
||||
await Discord.BaseClient.LeaveGuild(Id).ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task Delete()
|
||||
{
|
||||
await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<User>> GetBans()
|
||||
{
|
||||
var models = await Discord.BaseClient.GetGuildBans(Id).ConfigureAwait(false);
|
||||
return models.Select(x => new PublicUser(Discord, x));
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public Task AddBan(IUser user, int pruneDays = 0) => AddBan(user, pruneDays);
|
||||
/// <inheritdoc />
|
||||
public async Task AddBan(ulong userId, int pruneDays = 0)
|
||||
{
|
||||
var args = new CreateGuildBanParams()
|
||||
{
|
||||
PruneDays = pruneDays
|
||||
};
|
||||
await Discord.BaseClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public Task RemoveBan(IUser user) => RemoveBan(user.Id);
|
||||
/// <inheritdoc />
|
||||
public async Task RemoveBan(ulong userId)
|
||||
{
|
||||
await Discord.BaseClient.RemoveGuildBan(Id, userId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary> Gets the channel in this guild with the provided id, or null if not found. </summary>
|
||||
public async Task<GuildChannel> GetChannel(ulong id)
|
||||
{
|
||||
var model = await Discord.BaseClient.GetChannel(Id, id).ConfigureAwait(false);
|
||||
if (model != null)
|
||||
return ToChannel(model);
|
||||
return null;
|
||||
}
|
||||
/// <summary> Gets a collection of all channels in this guild. </summary>
|
||||
public async Task<IEnumerable<GuildChannel>> GetChannels()
|
||||
{
|
||||
var models = await Discord.BaseClient.GetGuildChannels(Id).ConfigureAwait(false);
|
||||
return models.Select(x => ToChannel(x));
|
||||
}
|
||||
/// <summary> Creates a new text channel. </summary>
|
||||
public async Task<TextChannel> CreateTextChannel(string name)
|
||||
{
|
||||
if (name == null) throw new ArgumentNullException(nameof(name));
|
||||
|
||||
var args = new CreateGuildChannelParams() { Name = name, Type = ChannelType.Text };
|
||||
var model = await Discord.BaseClient.CreateGuildChannel(Id, args).ConfigureAwait(false);
|
||||
return new TextChannel(this, model);
|
||||
}
|
||||
/// <summary> Creates a new voice channel. </summary>
|
||||
public async Task<VoiceChannel> CreateVoiceChannel(string name)
|
||||
{
|
||||
if (name == null) throw new ArgumentNullException(nameof(name));
|
||||
|
||||
var args = new CreateGuildChannelParams { Name = name, Type = ChannelType.Voice };
|
||||
var model = await Discord.BaseClient.CreateGuildChannel(Id, args).ConfigureAwait(false);
|
||||
return new VoiceChannel(this, model);
|
||||
}
|
||||
|
||||
/// <summary> Gets a collection of all integrations attached to this guild. </summary>
|
||||
public async Task<IEnumerable<GuildIntegration>> GetIntegrations()
|
||||
{
|
||||
var models = await Discord.BaseClient.GetGuildIntegrations(Id).ConfigureAwait(false);
|
||||
return models.Select(x => new GuildIntegration(this, x));
|
||||
}
|
||||
/// <summary> Creates a new integration for this guild. </summary>
|
||||
public async Task<GuildIntegration> CreateIntegration(ulong id, string type)
|
||||
{
|
||||
var args = new CreateGuildIntegrationParams { Id = id, Type = type };
|
||||
var model = await Discord.BaseClient.CreateGuildIntegration(Id, args).ConfigureAwait(false);
|
||||
return new GuildIntegration(this, model);
|
||||
}
|
||||
|
||||
/// <summary> Gets a collection of all invites to this guild. </summary>
|
||||
public async Task<IEnumerable<GuildInvite>> GetInvites()
|
||||
{
|
||||
var models = await Discord.BaseClient.GetGuildInvites(Id).ConfigureAwait(false);
|
||||
return models.Select(x => new GuildInvite(this, x));
|
||||
}
|
||||
/// <summary> Creates a new invite to this guild. </summary>
|
||||
public async Task<GuildInvite> CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false)
|
||||
{
|
||||
if (maxAge <= 0) throw new ArgumentOutOfRangeException(nameof(maxAge));
|
||||
if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses));
|
||||
|
||||
var args = new CreateChannelInviteParams()
|
||||
{
|
||||
MaxAge = maxAge ?? 0,
|
||||
MaxUses = maxUses ?? 0,
|
||||
Temporary = isTemporary,
|
||||
XkcdPass = withXkcd
|
||||
};
|
||||
var model = await Discord.BaseClient.CreateChannelInvite(DefaultChannelId, args).ConfigureAwait(false);
|
||||
return new GuildInvite(this, model);
|
||||
}
|
||||
|
||||
/// <summary> Gets the role in this guild with the provided id, or null if not found. </summary>
|
||||
public Role GetRole(ulong id)
|
||||
{
|
||||
Role result = null;
|
||||
if (_roles?.TryGetValue(id, out result) == true)
|
||||
return result;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary> Creates a new role. </summary>
|
||||
public async Task<Role> CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false)
|
||||
{
|
||||
if (name == null) throw new ArgumentNullException(nameof(name));
|
||||
|
||||
var model = await Discord.BaseClient.CreateGuildRole(Id).ConfigureAwait(false);
|
||||
var role = new Role(this, model);
|
||||
|
||||
await role.Modify(x =>
|
||||
{
|
||||
x.Name = name;
|
||||
x.Permissions = (permissions ?? role.Permissions).RawValue;
|
||||
x.Color = (color ?? Color.Default).RawValue;
|
||||
x.Hoist = isHoisted;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
/// <summary> Gets a collection of all users in this guild. </summary>
|
||||
public async Task<IEnumerable<GuildUser>> GetUsers()
|
||||
{
|
||||
var args = new GetGuildMembersParams();
|
||||
var models = await Discord.BaseClient.GetGuildMembers(Id, args).ConfigureAwait(false);
|
||||
return models.Select(x => new GuildUser(this, x));
|
||||
}
|
||||
/// <summary> Gets a paged collection of all users in this guild. </summary>
|
||||
public async Task<IEnumerable<GuildUser>> GetUsers(int limit, int offset)
|
||||
{
|
||||
var args = new GetGuildMembersParams { Limit = limit, Offset = offset };
|
||||
var models = await Discord.BaseClient.GetGuildMembers(Id, args).ConfigureAwait(false);
|
||||
return models.Select(x => new GuildUser(this, x));
|
||||
}
|
||||
/// <summary> Gets the user in this guild with the provided id, or null if not found. </summary>
|
||||
public async Task<GuildUser> GetUser(ulong id)
|
||||
{
|
||||
var model = await Discord.BaseClient.GetGuildMember(Id, id).ConfigureAwait(false);
|
||||
if (model != null)
|
||||
return new GuildUser(this, model);
|
||||
return null;
|
||||
}
|
||||
/// <summary> Gets a the current user. </summary>
|
||||
public async Task<GuildUser> GetCurrentUser()
|
||||
{
|
||||
var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false);
|
||||
return await GetUser(currentUser.Id).ConfigureAwait(false);
|
||||
}
|
||||
public async Task<int> PruneUsers(int days = 30, bool simulate = false)
|
||||
{
|
||||
var args = new GuildPruneParams() { Days = days };
|
||||
GetGuildPruneCountResponse model;
|
||||
if (simulate)
|
||||
model = await Discord.BaseClient.GetGuildPruneCount(Id, args).ConfigureAwait(false);
|
||||
else
|
||||
model = await Discord.BaseClient.BeginGuildPrune(Id, args).ConfigureAwait(false);
|
||||
return model.Pruned;
|
||||
}
|
||||
|
||||
internal GuildChannel ToChannel(API.Channel model)
|
||||
{
|
||||
switch (model.Type)
|
||||
{
|
||||
case ChannelType.Text:
|
||||
default:
|
||||
return new TextChannel(this, model);
|
||||
case ChannelType.Voice:
|
||||
return new VoiceChannel(this, model);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => Name ?? Id.ToString();
|
||||
|
||||
IEnumerable<Emoji> IGuild.Emojis => Emojis;
|
||||
ulong IGuild.EveryoneRoleId => EveryoneRole.Id;
|
||||
IEnumerable<string> IGuild.Features => Features;
|
||||
|
||||
async Task<IEnumerable<IUser>> IGuild.GetBans()
|
||||
=> await GetBans().ConfigureAwait(false);
|
||||
async Task<IGuildChannel> IGuild.GetChannel(ulong id)
|
||||
=> await GetChannel(id).ConfigureAwait(false);
|
||||
async Task<IEnumerable<IGuildChannel>> IGuild.GetChannels()
|
||||
=> await GetChannels().ConfigureAwait(false);
|
||||
async Task<IGuildInvite> IGuild.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd)
|
||||
=> await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false);
|
||||
async Task<IRole> IGuild.CreateRole(string name, GuildPermissions? permissions, Color? color, bool isHoisted)
|
||||
=> await CreateRole(name, permissions, color, isHoisted).ConfigureAwait(false);
|
||||
async Task<ITextChannel> IGuild.CreateTextChannel(string name)
|
||||
=> await CreateTextChannel(name).ConfigureAwait(false);
|
||||
async Task<IVoiceChannel> IGuild.CreateVoiceChannel(string name)
|
||||
=> await CreateVoiceChannel(name).ConfigureAwait(false);
|
||||
async Task<IEnumerable<IGuildInvite>> IGuild.GetInvites()
|
||||
=> await GetInvites().ConfigureAwait(false);
|
||||
Task<IRole> IGuild.GetRole(ulong id)
|
||||
=> Task.FromResult<IRole>(GetRole(id));
|
||||
Task<IEnumerable<IRole>> IGuild.GetRoles()
|
||||
=> Task.FromResult<IEnumerable<IRole>>(Roles);
|
||||
async Task<IGuildUser> IGuild.GetUser(ulong id)
|
||||
=> await GetUser(id).ConfigureAwait(false);
|
||||
async Task<IGuildUser> IGuild.GetCurrentUser()
|
||||
=> await GetCurrentUser().ConfigureAwait(false);
|
||||
async Task<IEnumerable<IGuildUser>> IGuild.GetUsers()
|
||||
=> await GetUsers().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using Discord.API.Rest;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.Integration;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public class GuildIntegration : IGuildIntegration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ulong Id { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public string Name { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public string Type { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public bool IsSyncing { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public ulong ExpireBehavior { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public ulong ExpireGracePeriod { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public DateTime SyncedAt { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guild Guild { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public Role Role { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public User User { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public IntegrationAccount Account { get; private set; }
|
||||
internal DiscordClient Discord => Guild.Discord;
|
||||
|
||||
internal GuildIntegration(Guild guild, Model model)
|
||||
{
|
||||
Guild = guild;
|
||||
Update(model);
|
||||
}
|
||||
|
||||
private void Update(Model model)
|
||||
{
|
||||
Id = model.Id;
|
||||
Name = model.Name;
|
||||
Type = model.Type;
|
||||
IsEnabled = model.Enabled;
|
||||
IsSyncing = model.Syncing;
|
||||
ExpireBehavior = model.ExpireBehavior;
|
||||
ExpireGracePeriod = model.ExpireGracePeriod;
|
||||
SyncedAt = model.SyncedAt;
|
||||
|
||||
Role = Guild.GetRole(model.RoleId);
|
||||
User = new PublicUser(Discord, model.User);
|
||||
}
|
||||
|
||||
/// <summary> </summary>
|
||||
public async Task Delete()
|
||||
{
|
||||
await Discord.BaseClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false);
|
||||
}
|
||||
/// <summary> </summary>
|
||||
public async Task Modify(Action<ModifyGuildIntegrationParams> func)
|
||||
{
|
||||
if (func == null) throw new NullReferenceException(nameof(func));
|
||||
|
||||
var args = new ModifyGuildIntegrationParams();
|
||||
func(args);
|
||||
var model = await Discord.BaseClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false);
|
||||
|
||||
Update(model);
|
||||
}
|
||||
/// <summary> </summary>
|
||||
public async Task Sync()
|
||||
{
|
||||
await Discord.BaseClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Name ?? Id.ToString()} ({(IsEnabled ? "Enabled" : "Disabled")})";
|
||||
|
||||
IGuild IGuildIntegration.Guild => Guild;
|
||||
IRole IGuildIntegration.Role => Role;
|
||||
IUser IGuildIntegration.User => User;
|
||||
IntegrationAccount IGuildIntegration.Account => Account;
|
||||
}
|
||||
}
|
||||
52
src/Discord.Net/WebSocket/Entities/Invites/GuildInvite.cs
Normal file
52
src/Discord.Net/WebSocket/Entities/Invites/GuildInvite.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.InviteMetadata;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public class GuildInvite : Invite, IGuildInvite
|
||||
{
|
||||
/// <summary> Gets the guild this invite is linked to. </summary>
|
||||
public Guild Guild { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public ulong ChannelId { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsRevoked { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public bool IsTemporary { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public int? MaxAge { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public int? MaxUses { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public int Uses { get; private set; }
|
||||
|
||||
internal override IDiscordClient Discord => Guild.Discord;
|
||||
|
||||
internal GuildInvite(Guild guild, Model model)
|
||||
: base(model)
|
||||
{
|
||||
Guild = guild;
|
||||
|
||||
Update(model); //Causes base.Update(Model) to be run twice, but that's fine.
|
||||
}
|
||||
private void Update(Model model)
|
||||
{
|
||||
base.Update(model);
|
||||
IsRevoked = model.Revoked;
|
||||
IsTemporary = model.Temporary;
|
||||
MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null;
|
||||
MaxUses = model.MaxUses;
|
||||
Uses = model.Uses;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Delete()
|
||||
{
|
||||
await Discord.BaseClient.DeleteInvite(Code).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
IGuild IGuildInvite.Guild => Guild;
|
||||
ulong IInvite.GuildId => Guild.Id;
|
||||
}
|
||||
}
|
||||
146
src/Discord.Net/WebSocket/Entities/Message.cs
Normal file
146
src/Discord.Net/WebSocket/Entities/Message.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using Discord.API.Rest;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.Message;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public class Message : IMessage
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ulong Id { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime? EditedTimestamp { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public bool IsTTS { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public string RawText { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public string Text { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public DateTime Timestamp { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IMessageChannel Channel { get; }
|
||||
/// <inheritdoc />
|
||||
public User Author { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<Attachment> Attachments { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<Embed> Embeds { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<PublicUser> MentionedUsers { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ulong> MentionedChannelIds { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ulong> MentionedRoleIds { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
|
||||
internal DiscordClient Discord => (Channel as TextChannel)?.Discord ?? (Channel as DMChannel).Discord;
|
||||
|
||||
internal Message(IMessageChannel channel, Model model)
|
||||
{
|
||||
Id = model.Id;
|
||||
Channel = channel;
|
||||
Author = new PublicUser(Discord, model.Author);
|
||||
|
||||
Update(model);
|
||||
}
|
||||
private void Update(Model model)
|
||||
{
|
||||
IsTTS = model.IsTextToSpeech;
|
||||
Timestamp = model.Timestamp;
|
||||
EditedTimestamp = model.EditedTimestamp;
|
||||
RawText = model.Content;
|
||||
|
||||
if (model.Attachments.Length > 0)
|
||||
{
|
||||
var attachments = new Attachment[model.Attachments.Length];
|
||||
for (int i = 0; i < attachments.Length; i++)
|
||||
attachments[i] = new Attachment(model.Attachments[i]);
|
||||
Attachments = ImmutableArray.Create(attachments);
|
||||
}
|
||||
else
|
||||
Attachments = Array.Empty<Attachment>();
|
||||
|
||||
if (model.Embeds.Length > 0)
|
||||
{
|
||||
var embeds = new Embed[model.Attachments.Length];
|
||||
for (int i = 0; i < embeds.Length; i++)
|
||||
embeds[i] = new Embed(model.Embeds[i]);
|
||||
Embeds = ImmutableArray.Create(embeds);
|
||||
}
|
||||
else
|
||||
Embeds = Array.Empty<Embed>();
|
||||
|
||||
if (model.Mentions.Length > 0)
|
||||
{
|
||||
var discord = Discord;
|
||||
var builder = ImmutableArray.CreateBuilder<PublicUser>(model.Mentions.Length);
|
||||
for (int i = 0; i < model.Mentions.Length; i++)
|
||||
builder.Add(new PublicUser(discord, model.Mentions[i]));
|
||||
MentionedUsers = builder.ToArray();
|
||||
}
|
||||
else
|
||||
MentionedUsers = Array.Empty<PublicUser>();
|
||||
MentionedChannelIds = MentionHelper.GetChannelMentions(model.Content);
|
||||
MentionedRoleIds = MentionHelper.GetRoleMentions(model.Content);
|
||||
if (model.IsMentioningEveryone)
|
||||
{
|
||||
ulong? guildId = (Channel as IGuildChannel)?.Guild.Id;
|
||||
if (guildId != null)
|
||||
{
|
||||
if (MentionedRoleIds.Count == 0)
|
||||
MentionedRoleIds = ImmutableArray.Create(guildId.Value);
|
||||
else
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<ulong>(MentionedRoleIds.Count + 1);
|
||||
builder.AddRange(MentionedRoleIds);
|
||||
builder.Add(guildId.Value);
|
||||
MentionedRoleIds = builder.ToImmutable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text = MentionHelper.CleanUserMentions(model.Content, model.Mentions);
|
||||
|
||||
Author.Update(model.Author);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Modify(Action<ModifyMessageParams> func)
|
||||
{
|
||||
if (func == null) throw new NullReferenceException(nameof(func));
|
||||
|
||||
var args = new ModifyMessageParams();
|
||||
func(args);
|
||||
var guildChannel = Channel as GuildChannel;
|
||||
|
||||
Model model;
|
||||
if (guildChannel != null)
|
||||
model = await Discord.BaseClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false);
|
||||
else
|
||||
model = await Discord.BaseClient.ModifyMessage(Channel.Id, Id, args).ConfigureAwait(false);
|
||||
Update(model);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Delete()
|
||||
{
|
||||
await Discord.BaseClient.DeleteMessage(Channel.Id, Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Author.ToString()}: {Text}";
|
||||
|
||||
IUser IMessage.Author => Author;
|
||||
IReadOnlyList<Attachment> IMessage.Attachments => Attachments;
|
||||
IReadOnlyList<Embed> IMessage.Embeds => Embeds;
|
||||
IReadOnlyList<ulong> IMessage.MentionedChannelIds => MentionedChannelIds;
|
||||
IReadOnlyList<IUser> IMessage.MentionedUsers => MentionedUsers;
|
||||
}
|
||||
}
|
||||
80
src/Discord.Net/WebSocket/Entities/Role.cs
Normal file
80
src/Discord.Net/WebSocket/Entities/Role.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Discord.API.Rest;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.Role;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public class Role : IRole, IMentionable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ulong Id { get; }
|
||||
/// <summary> Returns the guild this role belongs to. </summary>
|
||||
public Guild Guild { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Color Color { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public bool IsHoisted { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public bool IsManaged { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public string Name { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public GuildPermissions Permissions { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public int Position { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
|
||||
/// <inheritdoc />
|
||||
public bool IsEveryone => Id == Guild.Id;
|
||||
/// <inheritdoc />
|
||||
public string Mention => MentionHelper.Mention(this);
|
||||
internal DiscordClient Discord => Guild.Discord;
|
||||
|
||||
internal Role(Guild guild, Model model)
|
||||
{
|
||||
Id = model.Id;
|
||||
Guild = guild;
|
||||
|
||||
Update(model);
|
||||
}
|
||||
internal void Update(Model model)
|
||||
{
|
||||
Name = model.Name;
|
||||
IsHoisted = model.Hoist.Value;
|
||||
IsManaged = model.Managed.Value;
|
||||
Position = model.Position.Value;
|
||||
Color = new Color(model.Color.Value);
|
||||
Permissions = new GuildPermissions(model.Permissions.Value);
|
||||
}
|
||||
/// <summary> Modifies the properties of this role. </summary>
|
||||
public async Task Modify(Action<ModifyGuildRoleParams> func)
|
||||
{
|
||||
if (func == null) throw new NullReferenceException(nameof(func));
|
||||
|
||||
var args = new ModifyGuildRoleParams();
|
||||
func(args);
|
||||
var response = await Discord.BaseClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false);
|
||||
Update(response);
|
||||
}
|
||||
/// <summary> Deletes this message. </summary>
|
||||
public async Task Delete()
|
||||
=> await Discord.BaseClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Name ?? Id.ToString();
|
||||
|
||||
ulong IRole.GuildId => Guild.Id;
|
||||
|
||||
async Task<IEnumerable<IGuildUser>> IRole.GetUsers()
|
||||
{
|
||||
//A tad hacky, but it works
|
||||
var models = await Discord.BaseClient.GetGuildMembers(Guild.Id, new GetGuildMembersParams()).ConfigureAwait(false);
|
||||
return models.Where(x => x.Roles.Contains(Id)).Select(x => new GuildUser(Guild, x));
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/Discord.Net/WebSocket/Entities/Users/DMUser.cs
Normal file
20
src/Discord.Net/WebSocket/Entities/Users/DMUser.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Model = Discord.API.User;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public class DMUser : User, IDMUser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public DMChannel Channel { get; }
|
||||
|
||||
internal override DiscordClient Discord => Channel.Discord;
|
||||
|
||||
internal DMUser(DMChannel channel, Model model)
|
||||
: base(model)
|
||||
{
|
||||
Channel = channel;
|
||||
}
|
||||
|
||||
IDMChannel IDMUser.Channel => Channel;
|
||||
}
|
||||
}
|
||||
117
src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs
Normal file
117
src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using Discord.API.Rest;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.GuildMember;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public class GuildUser : User, IGuildUser
|
||||
{
|
||||
private ImmutableArray<Role> _roles;
|
||||
|
||||
public Guild Guild { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsDeaf { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public bool IsMute { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public DateTime JoinedAt { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public string Nickname { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<Role> Roles => _roles;
|
||||
internal override DiscordClient Discord => Guild.Discord;
|
||||
|
||||
internal GuildUser(Guild guild, Model model)
|
||||
: base(model.User)
|
||||
{
|
||||
Guild = guild;
|
||||
}
|
||||
internal void Update(Model model)
|
||||
{
|
||||
IsDeaf = model.Deaf;
|
||||
IsMute = model.Mute;
|
||||
JoinedAt = model.JoinedAt.Value;
|
||||
Nickname = model.Nick;
|
||||
|
||||
var roles = ImmutableArray.CreateBuilder<Role>(model.Roles.Length + 1);
|
||||
roles.Add(Guild.EveryoneRole);
|
||||
for (int i = 0; i < model.Roles.Length; i++)
|
||||
roles.Add(Guild.GetRole(model.Roles[i]));
|
||||
_roles = roles.ToImmutable();
|
||||
}
|
||||
|
||||
public async Task Update()
|
||||
{
|
||||
var model = await Discord.BaseClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false);
|
||||
Update(model);
|
||||
}
|
||||
|
||||
public bool HasRole(IRole role)
|
||||
{
|
||||
for (int i = 0; i < _roles.Length; i++)
|
||||
{
|
||||
if (_roles[i].Id == role.Id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task Kick()
|
||||
{
|
||||
await Discord.BaseClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public GuildPermissions GetGuildPermissions()
|
||||
{
|
||||
return new GuildPermissions(PermissionHelper.Resolve(this));
|
||||
}
|
||||
public ChannelPermissions GetPermissions(IGuildChannel channel)
|
||||
{
|
||||
if (channel == null) throw new ArgumentNullException(nameof(channel));
|
||||
return new ChannelPermissions(PermissionHelper.Resolve(this, channel));
|
||||
}
|
||||
|
||||
public async Task Modify(Action<ModifyGuildMemberParams> func)
|
||||
{
|
||||
if (func == null) throw new NullReferenceException(nameof(func));
|
||||
|
||||
var args = new ModifyGuildMemberParams();
|
||||
func(args);
|
||||
|
||||
bool isCurrentUser = (await Discord.GetCurrentUser().ConfigureAwait(false)).Id == Id;
|
||||
if (isCurrentUser && args.Nickname.IsSpecified)
|
||||
{
|
||||
var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value };
|
||||
await Discord.BaseClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false);
|
||||
args.Nickname = new API.Optional<string>(); //Remove
|
||||
}
|
||||
|
||||
if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified)
|
||||
{
|
||||
await Discord.BaseClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false);
|
||||
if (args.Deaf.IsSpecified)
|
||||
IsDeaf = args.Deaf;
|
||||
if (args.Mute.IsSpecified)
|
||||
IsMute = args.Mute;
|
||||
if (args.Nickname.IsSpecified)
|
||||
Nickname = args.Nickname;
|
||||
if (args.Roles.IsSpecified)
|
||||
_roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
IGuild IGuildUser.Guild => Guild;
|
||||
IReadOnlyList<IRole> IGuildUser.Roles => Roles;
|
||||
ulong? IGuildUser.VoiceChannelId => null;
|
||||
|
||||
ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel)
|
||||
=> GetPermissions(channel);
|
||||
}
|
||||
}
|
||||
15
src/Discord.Net/WebSocket/Entities/Users/PublicUser.cs
Normal file
15
src/Discord.Net/WebSocket/Entities/Users/PublicUser.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Model = Discord.API.User;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public class PublicUser : User
|
||||
{
|
||||
internal override DiscordClient Discord { get; }
|
||||
|
||||
internal PublicUser(DiscordClient discord, Model model)
|
||||
: base(model)
|
||||
{
|
||||
Discord = discord;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs
Normal file
48
src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Discord.API.Rest;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.User;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public class SelfUser : User, ISelfUser
|
||||
{
|
||||
internal override DiscordClient Discord { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Email { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public bool IsVerified { get; private set; }
|
||||
|
||||
internal SelfUser(DiscordClient discord, Model model)
|
||||
: base(model)
|
||||
{
|
||||
Discord = discord;
|
||||
}
|
||||
internal override void Update(Model model)
|
||||
{
|
||||
base.Update(model);
|
||||
|
||||
Email = model.Email;
|
||||
IsVerified = model.IsVerified;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Update()
|
||||
{
|
||||
var model = await Discord.BaseClient.GetCurrentUser().ConfigureAwait(false);
|
||||
Update(model);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Modify(Action<ModifyCurrentUserParams> func)
|
||||
{
|
||||
if (func != null) throw new NullReferenceException(nameof(func));
|
||||
|
||||
var args = new ModifyCurrentUserParams();
|
||||
func(args);
|
||||
var model = await Discord.BaseClient.ModifyCurrentUser(args).ConfigureAwait(false);
|
||||
Update(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/Discord.Net/WebSocket/Entities/Users/User.cs
Normal file
65
src/Discord.Net/WebSocket/Entities/Users/User.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Discord.API.Rest;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Model = Discord.API.User;
|
||||
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
public abstract class User : IUser
|
||||
{
|
||||
private string _avatarId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ulong Id { get; }
|
||||
internal abstract DiscordClient Discord { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ushort Discriminator { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public bool IsBot { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public string Username { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId);
|
||||
/// <inheritdoc />
|
||||
public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id);
|
||||
/// <inheritdoc />
|
||||
public string Mention => MentionHelper.Mention(this, false);
|
||||
/// <inheritdoc />
|
||||
public string NicknameMention => MentionHelper.Mention(this, true);
|
||||
|
||||
internal User(Model model)
|
||||
{
|
||||
Id = model.Id;
|
||||
|
||||
Update(model);
|
||||
}
|
||||
internal virtual void Update(Model model)
|
||||
{
|
||||
_avatarId = model.Avatar;
|
||||
Discriminator = model.Discriminator;
|
||||
IsBot = model.Bot;
|
||||
Username = model.Username;
|
||||
}
|
||||
|
||||
public async Task<DMChannel> CreateDMChannel()
|
||||
{
|
||||
var args = new CreateDMChannelParams { RecipientId = Id };
|
||||
var model = await Discord.BaseClient.CreateDMChannel(args).ConfigureAwait(false);
|
||||
|
||||
return new DMChannel(Discord, model);
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Username ?? Id.ToString()}";
|
||||
|
||||
/// <inheritdoc />
|
||||
string IUser.CurrentGame => null;
|
||||
/// <inheritdoc />
|
||||
UserStatus IUser.Status => UserStatus.Unknown;
|
||||
|
||||
/// <inheritdoc />
|
||||
async Task<IDMChannel> IUser.CreateDMChannel()
|
||||
=> await CreateDMChannel().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user