| @@ -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); | |||
| @@ -0,0 +1,9 @@ | |||
| namespace Discord.API | |||
| { | |||
| public interface IWebSocketMessage | |||
| { | |||
| int OpCode { get; } | |||
| object Payload { get; } | |||
| bool IsPrivate { get; } | |||
| } | |||
| } | |||
| @@ -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); | |||
| @@ -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)); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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]"; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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)); | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||