| @@ -20,23 +20,22 @@ namespace Discord.API | |||||
| public class DiscordRawClient | public class DiscordRawClient | ||||
| { | { | ||||
| internal event EventHandler<SentRequestEventArgs> SentRequest; | internal event EventHandler<SentRequestEventArgs> SentRequest; | ||||
| private readonly RequestQueue _requestQueue; | private readonly RequestQueue _requestQueue; | ||||
| private readonly IRestClient _restClient; | |||||
| private readonly CancellationToken _cancelToken; | |||||
| private readonly JsonSerializer _serializer; | private readonly JsonSerializer _serializer; | ||||
| private IRestClient _restClient; | |||||
| private CancellationToken _cancelToken; | |||||
| public TokenType AuthTokenType { get; private set; } | public TokenType AuthTokenType { get; private set; } | ||||
| public IRestClient RestClient { get; private set; } | public IRestClient RestClient { get; private set; } | ||||
| public IRequestQueue RequestQueue { 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("accept", "*/*"); | ||||
| _restClient.SetHeader("user-agent", DiscordConfig.UserAgent); | _restClient.SetHeader("user-agent", DiscordConfig.UserAgent); | ||||
| _requestQueue = new RequestQueue(_restClient); | _requestQueue = new RequestQueue(_restClient); | ||||
| _serializer = new JsonSerializer(); | _serializer = new JsonSerializer(); | ||||
| @@ -53,29 +52,40 @@ namespace Discord.API | |||||
| _serializer.ContractResolver = new OptionalContractResolver(); | _serializer.ContractResolver = new OptionalContractResolver(); | ||||
| } | } | ||||
| public void SetToken(TokenType tokenType, string token) | |||||
| public async Task Login(TokenType tokenType, string token, CancellationToken cancelToken) | |||||
| { | { | ||||
| AuthTokenType = tokenType; | 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); | _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 | //Core | ||||
| public Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) | 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) | 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(); | var stopwatch = Stopwatch.StartNew(); | ||||
| string json = null; | string json = null; | ||||
| if (payload != 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) | 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 stopwatch = Stopwatch.StartNew(); | ||||
| var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, multipartArgs, headerOnly), group, bucketId, guildId).ConfigureAwait(false); | var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, multipartArgs, headerOnly), group, bucketId, guildId).ConfigureAwait(false); | ||||
| int bytes = headerOnly ? 0 : (int)responseStream.Length; | int bytes = headerOnly ? 0 : (int)responseStream.Length; | ||||
| @@ -149,11 +163,6 @@ namespace Discord.API | |||||
| //Auth | //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() | public async Task ValidateToken() | ||||
| { | { | ||||
| await Send("GET", "auth/login").ConfigureAwait(false); | 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 ExpireBehavior { get; } | ||||
| ulong ExpireGracePeriod { get; } | ulong ExpireGracePeriod { get; } | ||||
| DateTime SyncedAt { get; } | DateTime SyncedAt { get; } | ||||
| IntegrationAccount Account { get; } | |||||
| IGuild Guild { get; } | IGuild Guild { get; } | ||||
| IUser User { get; } | IUser User { get; } | ||||
| IRole Role { 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 /> | /// <inheritdoc /> | ||||
| public string Id { get; } | public string Id { get; } | ||||
| @@ -1,7 +1,7 @@ | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Invite; | using Model = Discord.API.Invite; | ||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| public abstract class Invite : IInvite | public abstract class Invite : IInvite | ||||
| { | { | ||||
| @@ -17,7 +17,7 @@ namespace Discord.Rest | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null; | public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null; | ||||
| internal abstract DiscordClient Discord { get; } | |||||
| internal abstract IDiscordClient Discord { get; } | |||||
| internal Invite(Model model) | internal Invite(Model model) | ||||
| { | { | ||||
| @@ -15,9 +15,9 @@ namespace Discord.Rest | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public ulong ChannelId => _channelId; | 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) | : base(model) | ||||
| { | { | ||||
| Discord = discord; | Discord = discord; | ||||
| @@ -18,7 +18,7 @@ namespace Discord | |||||
| { | { | ||||
| case ITextChannel _: return _allText; | case ITextChannel _: return _allText; | ||||
| case IVoiceChannel _: return _allVoice; | case IVoiceChannel _: return _allVoice; | ||||
| case IDMChannel _: return _allDM; | |||||
| case IGuildChannel _: return _allDM; | |||||
| default: | default: | ||||
| throw new ArgumentException("Unknown channel type", nameof(channel)); | throw new ArgumentException("Unknown channel type", nameof(channel)); | ||||
| } | } | ||||
| @@ -6,12 +6,11 @@ namespace Discord.Rest | |||||
| public class Connection : IConnection | public class Connection : IConnection | ||||
| { | { | ||||
| public string Id { get; } | 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) | public Connection(Model model) | ||||
| { | { | ||||
| @@ -21,7 +20,7 @@ namespace Discord.Rest | |||||
| Name = model.Name; | Name = model.Name; | ||||
| IsRevoked = model.Revoked; | IsRevoked = model.Revoked; | ||||
| Integrations = model.Integrations; | |||||
| IntegrationIds = model.Integrations; | |||||
| } | } | ||||
| public override string ToString() => $"{Name ?? Id.ToString()} ({Type})"; | public override string ToString() => $"{Name ?? Id.ToString()} ({Type})"; | ||||
| @@ -9,6 +9,6 @@ namespace Discord | |||||
| string Name { get; } | string Name { get; } | ||||
| bool IsRevoked { 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\VoiceRegion.cs" /> | ||||
| <Compile Include="API\Common\VoiceState.cs" /> | <Compile Include="API\Common\VoiceState.cs" /> | ||||
| <Compile Include="API\IOptional.cs" /> | <Compile Include="API\IOptional.cs" /> | ||||
| <Compile Include="API\IWebSocketMessage.cs" /> | |||||
| <Compile Include="API\Optional.cs" /> | <Compile Include="API\Optional.cs" /> | ||||
| <Compile Include="API\Rest\DeleteMessagesParam.cs" /> | <Compile Include="API\Rest\DeleteMessagesParam.cs" /> | ||||
| <Compile Include="API\Rest\GetGuildMembersParams.cs" /> | <Compile Include="API\Rest\GetGuildMembersParams.cs" /> | ||||
| @@ -98,6 +99,7 @@ | |||||
| <Compile Include="API\Rest\ModifyMessageParams.cs" /> | <Compile Include="API\Rest\ModifyMessageParams.cs" /> | ||||
| <Compile Include="API\Rest\ModifyTextChannelParams.cs" /> | <Compile Include="API\Rest\ModifyTextChannelParams.cs" /> | ||||
| <Compile Include="API\Rest\ModifyVoiceChannelParams.cs" /> | <Compile Include="API\Rest\ModifyVoiceChannelParams.cs" /> | ||||
| <Compile Include="API\WebSocketMessage.cs" /> | |||||
| <Compile Include="DiscordConfig.cs" /> | <Compile Include="DiscordConfig.cs" /> | ||||
| <Compile Include="API\DiscordRawClient.cs" /> | <Compile Include="API\DiscordRawClient.cs" /> | ||||
| <Compile Include="Net\Converters\OptionalContractResolver.cs" /> | <Compile Include="Net\Converters\OptionalContractResolver.cs" /> | ||||
| @@ -111,7 +113,6 @@ | |||||
| <Compile Include="Net\Rest\RequestQueue\RestRequest.cs" /> | <Compile Include="Net\Rest\RequestQueue\RestRequest.cs" /> | ||||
| <Compile Include="Rest\DiscordClient.cs" /> | <Compile Include="Rest\DiscordClient.cs" /> | ||||
| <Compile Include="Common\Entities\Guilds\IGuildEmbed.cs" /> | <Compile Include="Common\Entities\Guilds\IGuildEmbed.cs" /> | ||||
| <Compile Include="Common\Entities\Guilds\IIntegrationAccount.cs" /> | |||||
| <Compile Include="Common\Entities\Users\IConnection.cs" /> | <Compile Include="Common\Entities\Users\IConnection.cs" /> | ||||
| <Compile Include="Common\Entities\Guilds\IGuildIntegration.cs" /> | <Compile Include="Common\Entities\Guilds\IGuildIntegration.cs" /> | ||||
| <Compile Include="Common\Entities\Invites\IPublicInvite.cs" /> | <Compile Include="Common\Entities\Invites\IPublicInvite.cs" /> | ||||
| @@ -161,19 +162,19 @@ | |||||
| <Compile Include="Rest\Entities\Channels\VoiceChannel.cs" /> | <Compile Include="Rest\Entities\Channels\VoiceChannel.cs" /> | ||||
| <Compile Include="Rest\Entities\Guilds\GuildEmbed.cs" /> | <Compile Include="Rest\Entities\Guilds\GuildEmbed.cs" /> | ||||
| <Compile Include="Rest\Entities\Guilds\GuildIntegration.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="Common\Helpers\PermissionHelper.cs" /> | ||||
| <Compile Include="Rest\Entities\Invites\GuildInvite.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\Message.cs" /> | ||||
| <Compile Include="Rest\Entities\Role.cs" /> | <Compile Include="Rest\Entities\Role.cs" /> | ||||
| <Compile Include="Rest\Entities\Guilds\UserGuild.cs" /> | <Compile Include="Rest\Entities\Guilds\UserGuild.cs" /> | ||||
| <Compile Include="Rest\Entities\Users\DMUser.cs" /> | <Compile Include="Rest\Entities\Users\DMUser.cs" /> | ||||
| <Compile Include="Rest\Entities\Users\GuildUser.cs" /> | <Compile Include="Rest\Entities\Users\GuildUser.cs" /> | ||||
| <Compile Include="Rest\Entities\Users\PublicUser.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\LogMessageEventArgs.cs" /> | ||||
| <Compile Include="Common\Events\SentRequestEventArgs.cs" /> | <Compile Include="Common\Events\SentRequestEventArgs.cs" /> | ||||
| <Compile Include="Common\Helpers\DateTimeHelper.cs" /> | <Compile Include="Common\Helpers\DateTimeHelper.cs" /> | ||||
| @@ -204,6 +205,23 @@ | |||||
| <Compile Include="Rest\Entities\Users\SelfUser.cs" /> | <Compile Include="Rest\Entities\Users\SelfUser.cs" /> | ||||
| <Compile Include="Rest\Entities\Users\User.cs" /> | <Compile Include="Rest\Entities\Users\User.cs" /> | ||||
| <Compile Include="TokenType.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> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| <None Include="Common\Entities\Users\IVoiceState.cs.old" /> | <None Include="Common\Entities\Users\IVoiceState.cs.old" /> | ||||
| @@ -3,6 +3,8 @@ using System.Reflection; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| //TODO: Add socket config items in their own class | |||||
| public class DiscordConfig | public class DiscordConfig | ||||
| { | { | ||||
| public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; | 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; | public LogSeverity LogLevel { get; set; } = LogSeverity.Info; | ||||
| /// <summary> Gets or sets the provider used to generate new REST connections. </summary> | /// <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 | 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 CanConvert(Type objectType) => objectType == typeof(IEnumerable<ulong[]>); | ||||
| public override bool CanRead => true; | public override bool CanRead => true; | ||||
| @@ -17,13 +17,11 @@ namespace Discord.Net.Rest | |||||
| protected readonly HttpClient _client; | protected readonly HttpClient _client; | ||||
| protected readonly string _baseUrl; | protected readonly string _baseUrl; | ||||
| protected readonly CancellationToken _cancelToken; | |||||
| protected bool _isDisposed; | protected bool _isDisposed; | ||||
| public DefaultRestClient(string baseUrl, CancellationToken cancelToken) | |||||
| public DefaultRestClient(string baseUrl) | |||||
| { | { | ||||
| _baseUrl = baseUrl; | _baseUrl = baseUrl; | ||||
| _cancelToken = cancelToken; | |||||
| _client = new HttpClient(new HttpClientHandler | _client = new HttpClient(new HttpClientHandler | ||||
| { | { | ||||
| @@ -56,18 +54,18 @@ namespace Discord.Net.Rest | |||||
| _client.DefaultRequestHeaders.Add(key, value); | _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); | string uri = Path.Combine(_baseUrl, endpoint); | ||||
| using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | ||||
| { | { | ||||
| if (json != null) | if (json != null) | ||||
| restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); | restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); | ||||
| return await SendInternal(restRequest, _cancelToken, 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); | string uri = Path.Combine(_baseUrl, endpoint); | ||||
| using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | ||||
| @@ -97,7 +95,7 @@ namespace Discord.Net.Rest | |||||
| } | } | ||||
| } | } | ||||
| restRequest.Content = content; | 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.Collections.Generic; | ||||
| using System.IO; | using System.IO; | ||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Net.Rest | namespace Discord.Net.Rest | ||||
| @@ -9,7 +10,7 @@ namespace Discord.Net.Rest | |||||
| { | { | ||||
| void SetHeader(string key, string value); | 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 | 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; } | public IRestClient RestClient { get; } | ||||
| @@ -21,12 +24,26 @@ namespace Discord.Net.Rest | |||||
| _lock = new SemaphoreSlim(1, 1); | _lock = new SemaphoreSlim(1, 1); | ||||
| _globalBuckets = new RequestQueueBucket[Enum.GetValues(typeof(GlobalBucket)).Length]; | _globalBuckets = new RequestQueueBucket[Enum.GetValues(typeof(GlobalBucket)).Length]; | ||||
| _guildBuckets = new Dictionary<ulong, RequestQueueBucket>[Enum.GetValues(typeof(GuildBucket)).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) | internal async Task<Stream> Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId) | ||||
| { | { | ||||
| RequestQueueBucket bucket; | RequestQueueBucket bucket; | ||||
| request.CancelToken = _cancelToken; | |||||
| await Lock().ConfigureAwait(false); | await Lock().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| @@ -129,6 +146,20 @@ namespace Discord.Net.Rest | |||||
| _lock.Release(); | _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) | public async Task Clear(GlobalBucket type) | ||||
| { | { | ||||
| var bucket = _globalBuckets[(int)type]; | var bucket = _globalBuckets[(int)type]; | ||||
| @@ -136,7 +167,7 @@ namespace Discord.Net.Rest | |||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| await bucket.Lock(); | |||||
| await bucket.Lock().ConfigureAwait(false); | |||||
| bucket.Clear(); | bucket.Clear(); | ||||
| } | } | ||||
| finally { bucket.Unlock(); } | finally { bucket.Unlock(); } | ||||
| @@ -152,7 +183,7 @@ namespace Discord.Net.Rest | |||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| await bucket.Lock(); | |||||
| await bucket.Lock().ConfigureAwait(false); | |||||
| bucket.Clear(); | bucket.Clear(); | ||||
| } | } | ||||
| finally { bucket.Unlock(); } | finally { bucket.Unlock(); } | ||||
| @@ -16,7 +16,7 @@ namespace Discord.Net.Rest | |||||
| private readonly ConcurrentQueue<RestRequest> _queue; | private readonly ConcurrentQueue<RestRequest> _queue; | ||||
| private readonly SemaphoreSlim _lock; | private readonly SemaphoreSlim _lock; | ||||
| private Task _resetTask; | private Task _resetTask; | ||||
| private bool _waitingToProcess, _destroyed; //TODO: Remove _destroyed | |||||
| private bool _waitingToProcess; | |||||
| private int _id; | private int _id; | ||||
| public int WindowMaxCount { get; } | public int WindowMaxCount { get; } | ||||
| @@ -49,11 +49,7 @@ namespace Discord.Net.Rest | |||||
| public void Queue(RestRequest request) | public void Queue(RestRequest request) | ||||
| { | { | ||||
| if (_destroyed) throw new Exception(); | |||||
| //Assume this obj's parent is under lock | |||||
| _queue.Enqueue(request); | _queue.Enqueue(request); | ||||
| Debug($"Request queued ({WindowCount}/{WindowMaxCount} + {_queue.Count})"); | |||||
| } | } | ||||
| public async Task ProcessQueue(bool acquireLock = false) | public async Task ProcessQueue(bool acquireLock = false) | ||||
| { | { | ||||
| @@ -81,12 +77,17 @@ namespace Discord.Net.Rest | |||||
| try | 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 | 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 | 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; | var task = _resetTask; | ||||
| if (task != null) | if (task != null) | ||||
| { | { | ||||
| Debug($"External rate limit: Extended to {ex.RetryAfterMilliseconds} ms"); | |||||
| var retryAfter = DateTime.UtcNow.AddMilliseconds(ex.RetryAfterMilliseconds); | var retryAfter = DateTime.UtcNow.AddMilliseconds(ex.RetryAfterMilliseconds); | ||||
| await task.ConfigureAwait(false); | await task.ConfigureAwait(false); | ||||
| int millis = (int)Math.Ceiling((DateTime.UtcNow - retryAfter).TotalMilliseconds); | int millis = (int)Math.Ceiling((DateTime.UtcNow - retryAfter).TotalMilliseconds); | ||||
| _resetTask = ResetAfter(millis); | _resetTask = ResetAfter(millis); | ||||
| } | } | ||||
| else | else | ||||
| { | |||||
| Debug($"External rate limit: Reset in {ex.RetryAfterMilliseconds} ms"); | |||||
| _resetTask = ResetAfter(ex.RetryAfterMilliseconds); | _resetTask = ResetAfter(ex.RetryAfterMilliseconds); | ||||
| } | |||||
| return; | return; | ||||
| } | } | ||||
| catch (HttpException ex) | catch (HttpException ex) | ||||
| @@ -128,13 +125,11 @@ namespace Discord.Net.Rest | |||||
| _queue.TryDequeue(out request); | _queue.TryDequeue(out request); | ||||
| WindowCount++; | WindowCount++; | ||||
| nextRetry = 1000; | nextRetry = 1000; | ||||
| Debug($"Request succeeded ({WindowCount}/{WindowMaxCount} + {_queue.Count})"); | |||||
| if (WindowCount == 1 && WindowSeconds > 0) | if (WindowCount == 1 && WindowSeconds > 0) | ||||
| { | { | ||||
| //First request for this window, schedule a reset | //First request for this window, schedule a reset | ||||
| _resetTask = ResetAfter(WindowSeconds * 1000); | _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); | await _parent.Lock().ConfigureAwait(false); | ||||
| if (_queue.IsEmpty) //Double check, in case a request was queued before we got both locks | if (_queue.IsEmpty) //Double check, in case a request was queued before we got both locks | ||||
| { | |||||
| Debug($"Destroy"); | |||||
| _parent.DestroyGuildBucket((GuildBucket)_bucketId, _guildId); | _parent.DestroyGuildBucket((GuildBucket)_bucketId, _guildId); | ||||
| _destroyed = true; | |||||
| } | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| @@ -179,8 +170,6 @@ namespace Discord.Net.Rest | |||||
| { | { | ||||
| await Lock().ConfigureAwait(false); | await Lock().ConfigureAwait(false); | ||||
| Debug($"Reset"); | |||||
| //Reset the current window count and set our state back to normal | //Reset the current window count and set our state back to normal | ||||
| WindowCount = 0; | WindowCount = 0; | ||||
| _resetTask = null; | _resetTask = null; | ||||
| @@ -188,10 +177,7 @@ namespace Discord.Net.Rest | |||||
| //Wait is over, work through the current queue | //Wait is over, work through the current queue | ||||
| await ProcessQueue().ConfigureAwait(false); | await ProcessQueue().ConfigureAwait(false); | ||||
| } | } | ||||
| finally | |||||
| { | |||||
| Unlock(); | |||||
| } | |||||
| finally { Unlock(); } | |||||
| } | } | ||||
| public async Task Lock() | public async Task Lock() | ||||
| @@ -202,24 +188,5 @@ namespace Discord.Net.Rest | |||||
| { | { | ||||
| _lock.Release(); | _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.Collections.Generic; | ||||
| using System.IO; | using System.IO; | ||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Net.Rest | namespace Discord.Net.Rest | ||||
| { | { | ||||
| internal struct RestRequest | |||||
| internal class RestRequest | |||||
| { | { | ||||
| public string Method { get; } | public string Method { get; } | ||||
| public string Endpoint { get; } | public string Endpoint { get; } | ||||
| public string Json { get; } | public string Json { get; } | ||||
| public bool HeaderOnly { get; } | public bool HeaderOnly { get; } | ||||
| public CancellationToken CancelToken { get; internal set; } | |||||
| public IReadOnlyDictionary<string, object> MultipartParams { get; } | public IReadOnlyDictionary<string, object> MultipartParams { get; } | ||||
| public TaskCompletionSource<Stream> Promise { get; } | public TaskCompletionSource<Stream> Promise { get; } | ||||
| @@ -2,5 +2,5 @@ | |||||
| namespace Discord.Net.Rest | 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.API.Rest; | ||||
| using Discord.Logging; | using Discord.Logging; | ||||
| using Discord.Net; | |||||
| using Discord.Net.Rest; | using Discord.Net.Rest; | ||||
| using Newtonsoft.Json.Linq; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.IO; | using System.IO; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Net; | |||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -43,7 +40,7 @@ namespace Discord.Rest | |||||
| _connectionLock = new SemaphoreSlim(1, 1); | _connectionLock = new SemaphoreSlim(1, 1); | ||||
| _log = new LogManager(config.LogLevel); | _log = new LogManager(config.LogLevel); | ||||
| _userAgent = DiscordConfig.UserAgent; | _userAgent = DiscordConfig.UserAgent; | ||||
| BaseClient = new API.DiscordRawClient(_restClientProvider, _cancelTokenSource.Token); | |||||
| BaseClient = new API.DiscordRawClient(_restClientProvider); | |||||
| _log.Message += (s,e) => Log.Raise(this, e); | _log.Message += (s,e) => Log.Raise(this, e); | ||||
| } | } | ||||
| @@ -69,38 +66,43 @@ namespace Discord.Rest | |||||
| private async Task LoginInternal(string email, string password) | private async Task LoginInternal(string email, string password) | ||||
| { | { | ||||
| if (IsLoggedIn) | if (IsLoggedIn) | ||||
| LogoutInternal(); | |||||
| await LogoutInternal().ConfigureAwait(false); | |||||
| try | try | ||||
| { | { | ||||
| var cancelTokenSource = new CancellationTokenSource(); | |||||
| _cancelTokenSource = new CancellationTokenSource(); | |||||
| var args = new LoginParams { Email = email, Password = password }; | 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) | private async Task LoginInternal(TokenType tokenType, string token, bool validateToken) | ||||
| { | { | ||||
| if (IsLoggedIn) | if (IsLoggedIn) | ||||
| LogoutInternal(); | |||||
| await LogoutInternal().ConfigureAwait(false); | |||||
| try | 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"); | BaseClient.SentRequest += (s, e) => _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); | ||||
| if (validateToken) | if (validateToken) | ||||
| await BaseClient.ValidateToken().ConfigureAwait(false); | |||||
| _cancelTokenSource = cancelTokenSource; | |||||
| { | |||||
| try | |||||
| { | |||||
| await BaseClient.ValidateToken().ConfigureAwait(false); | |||||
| } | |||||
| catch { await BaseClient.Logout().ConfigureAwait(false); } | |||||
| } | |||||
| IsLoggedIn = true; | IsLoggedIn = true; | ||||
| LoggedIn.Raise(this); | LoggedIn.Raise(this); | ||||
| } | } | ||||
| @@ -111,11 +113,11 @@ namespace Discord.Rest | |||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| LogoutInternal(); | |||||
| await LogoutInternal().ConfigureAwait(false); | |||||
| } | } | ||||
| finally { _connectionLock.Release(); } | finally { _connectionLock.Release(); } | ||||
| } | } | ||||
| private void LogoutInternal() | |||||
| private async Task LogoutInternal() | |||||
| { | { | ||||
| bool wasLoggedIn = IsLoggedIn; | bool wasLoggedIn = IsLoggedIn; | ||||
| @@ -125,7 +127,7 @@ namespace Discord.Rest | |||||
| catch { } | catch { } | ||||
| } | } | ||||
| BaseClient.SetToken(TokenType.User, null); | |||||
| await BaseClient.Logout().ConfigureAwait(false); | |||||
| _currentUser = null; | _currentUser = null; | ||||
| if (wasLoggedIn) | if (wasLoggedIn) | ||||
| @@ -1,7 +1,7 @@ | |||||
| using System; | using System; | ||||
| using Model = Discord.API.GuildEmbed; | using Model = Discord.API.GuildEmbed; | ||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| public class GuildEmbed : IGuildEmbed | public class GuildEmbed : IGuildEmbed | ||||
| { | { | ||||
| @@ -12,14 +12,11 @@ namespace Discord.Rest | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public ulong? ChannelId { get; private set; } | public ulong? ChannelId { get; private set; } | ||||
| internal DiscordClient Discord { get; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); | public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); | ||||
| internal GuildEmbed(DiscordClient discord, Model model) | |||||
| internal GuildEmbed(Model model) | |||||
| { | { | ||||
| Discord = discord; | |||||
| Update(model); | Update(model); | ||||
| } | } | ||||
| @@ -82,6 +82,6 @@ namespace Discord.Rest | |||||
| IGuild IGuildIntegration.Guild => Guild; | IGuild IGuildIntegration.Guild => Guild; | ||||
| IRole IGuildIntegration.Role => Role; | IRole IGuildIntegration.Role => Role; | ||||
| IUser IGuildIntegration.User => User; | IUser IGuildIntegration.User => User; | ||||
| IIntegrationAccount IGuildIntegration.Account => Account; | |||||
| IntegrationAccount IGuildIntegration.Account => Account; | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.UserGuild; | using Model = Discord.API.UserGuild; | ||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| public class UserGuild : IUserGuild | public class UserGuild : IUserGuild | ||||
| { | { | ||||
| @@ -10,7 +10,7 @@ namespace Discord.Rest | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public ulong Id { get; } | public ulong Id { get; } | ||||
| internal DiscordClient Discord { get; } | |||||
| internal IDiscordClient Discord { get; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string Name { get; private set; } | public string Name { get; private set; } | ||||
| @@ -22,7 +22,7 @@ namespace Discord.Rest | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); | public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); | ||||
| internal UserGuild(DiscordClient discord, Model model) | |||||
| internal UserGuild(IDiscordClient discord, Model model) | |||||
| { | { | ||||
| Discord = discord; | Discord = discord; | ||||
| Id = model.Id; | Id = model.Id; | ||||
| @@ -40,15 +40,11 @@ namespace Discord.Rest | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public async Task Leave() | 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); | await Discord.BaseClient.LeaveGuild(Id).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public async Task Delete() | 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); | await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -21,7 +21,7 @@ namespace Discord.Rest | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public int Uses { get; private set; } | public int Uses { get; private set; } | ||||
| internal override DiscordClient Discord => Guild.Discord; | |||||
| internal override IDiscordClient Discord => Guild.Discord; | |||||
| internal GuildInvite(Guild guild, Model model) | internal GuildInvite(Guild guild, Model model) | ||||
| : base(model) | : base(model) | ||||
| @@ -135,7 +135,6 @@ namespace Discord.Rest | |||||
| await Discord.BaseClient.DeleteMessage(Channel.Id, Id).ConfigureAwait(false); | await Discord.BaseClient.DeleteMessage(Channel.Id, Id).ConfigureAwait(false); | ||||
| } | } | ||||
| public override string ToString() => $"{Author.ToString()}: {Text}"; | public override string ToString() => $"{Author.ToString()}: {Text}"; | ||||
| IUser IMessage.Author => Author; | IUser IMessage.Author => Author; | ||||
| @@ -45,10 +45,7 @@ namespace Discord.Rest | |||||
| public async Task<DMChannel> CreateDMChannel() | 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); | var model = await Discord.BaseClient.CreateDMChannel(args).ConfigureAwait(false); | ||||
| return new DMChannel(Discord, model); | 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); | |||||
| } | |||||
| } | |||||