| @@ -225,7 +225,7 @@ namespace Discord.Commands | |||
| return false; | |||
| } | |||
| public SearchResult Search(IMessage message, int argPos) => Search(message, message.Text.Substring(argPos)); | |||
| public SearchResult Search(IMessage message, int argPos) => Search(message, message.Content.Substring(argPos)); | |||
| public SearchResult Search(IMessage message, string input) | |||
| { | |||
| string lowerInput = input.ToLowerInvariant(); | |||
| @@ -237,7 +237,7 @@ namespace Discord.Commands | |||
| return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); | |||
| } | |||
| public Task<IResult> Execute(IMessage message, int argPos) => Execute(message, message.Text.Substring(argPos)); | |||
| public Task<IResult> Execute(IMessage message, int argPos) => Execute(message, message.Content.Substring(argPos)); | |||
| public async Task<IResult> Execute(IMessage message, string input) | |||
| { | |||
| var searchResult = Search(message, input); | |||
| @@ -4,7 +4,7 @@ | |||
| { | |||
| public static bool HasCharPrefix(this IMessage msg, char c, ref int argPos) | |||
| { | |||
| var text = msg.Text; | |||
| var text = msg.Content; | |||
| if (text.Length > 0 && text[0] == c) | |||
| { | |||
| argPos = 1; | |||
| @@ -14,7 +14,7 @@ | |||
| } | |||
| public static bool HasStringPrefix(this IMessage msg, string str, ref int argPos) | |||
| { | |||
| var text = msg.Text; | |||
| var text = msg.Content; | |||
| //str = str + ' '; | |||
| if (text.StartsWith(str)) | |||
| { | |||
| @@ -25,7 +25,7 @@ | |||
| } | |||
| public static bool HasMentionPrefix(this IMessage msg, IUser user, ref int argPos) | |||
| { | |||
| var text = msg.Text; | |||
| var text = msg.Content; | |||
| string mention = user.Mention + ' '; | |||
| if (text.StartsWith(mention)) | |||
| { | |||
| @@ -8,5 +8,7 @@ | |||
| => iconId != null ? $"{DiscordConfig.ClientAPIUrl}guilds/{guildId}/icons/{iconId}.jpg" : null; | |||
| public static string GetGuildSplashUrl(ulong guildId, string splashId) | |||
| => splashId != null ? $"{DiscordConfig.ClientAPIUrl}guilds/{guildId}/splashes/{splashId}.jpg" : null; | |||
| public static string GetChannelIconUrl(ulong channelId, string iconId) | |||
| => iconId != null ? $"{DiscordConfig.ClientAPIUrl}channel-icons/{channelId}/{iconId}.jpg" : null; | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| namespace Discord.API | |||
| { | |||
| @@ -7,8 +8,8 @@ namespace Discord.API | |||
| //Shared | |||
| [JsonProperty("id")] | |||
| public ulong Id { get; set; } | |||
| [JsonProperty("is_private")] | |||
| public bool IsPrivate { get; set; } | |||
| [JsonProperty("type")] | |||
| public ChannelType Type { get; set; } | |||
| [JsonProperty("last_message_id")] | |||
| public ulong? LastMessageId { get; set; } | |||
| @@ -17,8 +18,6 @@ namespace Discord.API | |||
| public Optional<ulong> GuildId { get; set; } | |||
| [JsonProperty("name")] | |||
| public Optional<string> Name { get; set; } | |||
| [JsonProperty("type")] | |||
| public Optional<ChannelType> Type { get; set; } | |||
| [JsonProperty("position")] | |||
| public Optional<int> Position { get; set; } | |||
| [JsonProperty("permission_overwrites")] | |||
| @@ -27,6 +26,8 @@ namespace Discord.API | |||
| //TextChannel | |||
| [JsonProperty("topic")] | |||
| public Optional<string> Topic { get; set; } | |||
| [JsonProperty("last_pin_timestamp")] | |||
| public Optional<DateTimeOffset?> LastPinTimestamp { get; set; } | |||
| //VoiceChannel | |||
| [JsonProperty("bitrate")] | |||
| @@ -34,8 +35,12 @@ namespace Discord.API | |||
| [JsonProperty("user_limit")] | |||
| public Optional<int> UserLimit { get; set; } | |||
| //DMChannel | |||
| [JsonProperty("recipient")] | |||
| public Optional<User> Recipient { get; set; } | |||
| //PrivateChannel | |||
| [JsonProperty("recipients")] | |||
| public Optional<User[]> Recipients { get; set; } | |||
| //GroupChannel | |||
| [JsonProperty("icon")] | |||
| public Optional<string> Icon { get; set; } | |||
| } | |||
| } | |||
| @@ -7,6 +7,8 @@ namespace Discord.API | |||
| { | |||
| [JsonProperty("id")] | |||
| public ulong Id { get; set; } | |||
| [JsonProperty("type")] | |||
| public MessageType Type { get; set; } | |||
| [JsonProperty("channel_id")] | |||
| public ulong ChannelId { get; set; } | |||
| [JsonProperty("author")] | |||
| @@ -7,20 +7,20 @@ namespace Discord.API | |||
| [JsonProperty("id")] | |||
| public ulong Id { get; set; } | |||
| [JsonProperty("username")] | |||
| public string Username { get; set; } | |||
| public Optional<string> Username { get; set; } | |||
| [JsonProperty("discriminator")] | |||
| public string Discriminator { get; set; } | |||
| public Optional<string> Discriminator { get; set; } | |||
| [JsonProperty("bot")] | |||
| public bool Bot { get; set; } | |||
| public Optional<bool> Bot { get; set; } | |||
| [JsonProperty("avatar")] | |||
| public string Avatar { get; set; } | |||
| public Optional<string> Avatar { get; set; } | |||
| //CurrentUser | |||
| [JsonProperty("verified")] | |||
| public bool Verified { get; set; } | |||
| public Optional<bool> Verified { get; set; } | |||
| [JsonProperty("email")] | |||
| public string Email { get; set; } | |||
| public Optional<string> Email { get; set; } | |||
| [JsonProperty("mfa_enabled")] | |||
| public bool MfaEnabled { get; set; } | |||
| public Optional<bool> MfaEnabled { get; set; } | |||
| } | |||
| } | |||
| @@ -211,7 +211,7 @@ namespace Discord.API | |||
| if (_gatewayUrl == null) | |||
| { | |||
| var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false); | |||
| _gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.GatewayAPIVersion}&encoding={DiscordConfig.GatewayEncoding}"; | |||
| _gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordConfig.GatewayEncoding}"; | |||
| } | |||
| await _gatewayClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false); | |||
| @@ -551,6 +551,23 @@ namespace Discord.API | |||
| await SendAsync("DELETE", $"channels/{channelId}/pins/{messageId}", options: options).ConfigureAwait(false); | |||
| } | |||
| //Channel Recipients | |||
| public async Task AddGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null) | |||
| { | |||
| Preconditions.GreaterThan(channelId, 0, nameof(channelId)); | |||
| Preconditions.GreaterThan(userId, 0, nameof(userId)); | |||
| await SendAsync("PUT", $"channels/{channelId}/recipients/{userId}", options: options).ConfigureAwait(false); | |||
| } | |||
| public async Task RemoveGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null) | |||
| { | |||
| Preconditions.NotEqual(channelId, 0, nameof(channelId)); | |||
| Preconditions.NotEqual(userId, 0, nameof(userId)); | |||
| await SendAsync("DELETE", $"channels/{channelId}/recipients/{userId}", options: options).ConfigureAwait(false); | |||
| } | |||
| //Guilds | |||
| public async Task<Guild> GetGuildAsync(ulong guildId, RequestOptions options = null) | |||
| { | |||
| @@ -1160,7 +1177,7 @@ namespace Discord.API | |||
| { | |||
| return await SendAsync<IReadOnlyCollection<Connection>>("GET", "users/@me/connections", options: options).ConfigureAwait(false); | |||
| } | |||
| public async Task<IReadOnlyCollection<Channel>> GetMyDMsAsync(RequestOptions options = null) | |||
| public async Task<IReadOnlyCollection<Channel>> GetMyPrivateChannelsAsync(RequestOptions options = null) | |||
| { | |||
| return await SendAsync<IReadOnlyCollection<Channel>>("GET", "users/@me/channels", options: options).ConfigureAwait(false); | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Gateway | |||
| { | |||
| public class RecipientEvent | |||
| { | |||
| [JsonProperty("user")] | |||
| public User User { get; set; } | |||
| [JsonProperty("channel_id")] | |||
| public ulong ChannelId { get; set; } | |||
| } | |||
| } | |||
| @@ -2,6 +2,7 @@ | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| namespace Discord | |||
| { | |||
| @@ -16,12 +17,19 @@ namespace Discord | |||
| private readonly ConcurrentDictionary<ulong, CachedDMChannel> _dmChannels; | |||
| private readonly ConcurrentDictionary<ulong, CachedGuild> _guilds; | |||
| private readonly ConcurrentDictionary<ulong, CachedGlobalUser> _users; | |||
| private readonly ConcurrentHashSet<ulong> _groupChannels; | |||
| internal IReadOnlyCollection<ICachedChannel> Channels => _channels.ToReadOnlyCollection(); | |||
| internal IReadOnlyCollection<CachedDMChannel> DMChannels => _dmChannels.ToReadOnlyCollection(); | |||
| internal IReadOnlyCollection<CachedGroupChannel> GroupChannels => _groupChannels.Select(x => GetChannel(x) as CachedGroupChannel).ToReadOnlyCollection(_groupChannels); | |||
| internal IReadOnlyCollection<CachedGuild> Guilds => _guilds.ToReadOnlyCollection(); | |||
| internal IReadOnlyCollection<CachedGlobalUser> Users => _users.ToReadOnlyCollection(); | |||
| internal IReadOnlyCollection<ICachedPrivateChannel> PrivateChannels => | |||
| _dmChannels.Select(x => x.Value as ICachedPrivateChannel).Concat( | |||
| _groupChannels.Select(x => GetChannel(x) as ICachedPrivateChannel)) | |||
| .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); | |||
| public DataStore(int guildCount, int dmChannelCount) | |||
| { | |||
| double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; | |||
| @@ -30,6 +38,7 @@ namespace Discord | |||
| _dmChannels = new ConcurrentDictionary<ulong, CachedDMChannel>(CollectionConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier)); | |||
| _guilds = new ConcurrentDictionary<ulong, CachedGuild>(CollectionConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); | |||
| _users = new ConcurrentDictionary<ulong, CachedGlobalUser>(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); | |||
| _groupChannels = new ConcurrentHashSet<ulong>(CollectionConcurrencyLevel, (int)(10 * CollectionMultiplier)); | |||
| } | |||
| internal ICachedChannel GetChannel(ulong id) | |||
| @@ -39,18 +48,6 @@ namespace Discord | |||
| return channel; | |||
| return null; | |||
| } | |||
| internal void AddChannel(ICachedChannel channel) | |||
| { | |||
| _channels[channel.Id] = channel; | |||
| } | |||
| internal ICachedChannel RemoveChannel(ulong id) | |||
| { | |||
| ICachedChannel channel; | |||
| if (_channels.TryRemove(id, out channel)) | |||
| return channel; | |||
| return null; | |||
| } | |||
| internal CachedDMChannel GetDMChannel(ulong userId) | |||
| { | |||
| CachedDMChannel channel; | |||
| @@ -58,19 +55,38 @@ namespace Discord | |||
| return channel; | |||
| return null; | |||
| } | |||
| internal void AddDMChannel(CachedDMChannel channel) | |||
| internal void AddChannel(ICachedChannel channel) | |||
| { | |||
| _channels[channel.Id] = channel; | |||
| _dmChannels[channel.Recipient.Id] = channel; | |||
| var dmChannel = channel as CachedDMChannel; | |||
| if (dmChannel != null) | |||
| _dmChannels[dmChannel.Recipient.Id] = dmChannel; | |||
| else | |||
| { | |||
| var groupChannel = channel as CachedGroupChannel; | |||
| if (groupChannel != null) | |||
| _groupChannels.TryAdd(groupChannel.Id); | |||
| } | |||
| } | |||
| internal CachedDMChannel RemoveDMChannel(ulong userId) | |||
| internal ICachedChannel RemoveChannel(ulong id) | |||
| { | |||
| CachedDMChannel channel; | |||
| ICachedChannel ignored; | |||
| if (_dmChannels.TryRemove(userId, out channel)) | |||
| ICachedChannel channel; | |||
| if (_channels.TryRemove(id, out channel)) | |||
| { | |||
| if (_channels.TryRemove(channel.Id, out ignored)) | |||
| return channel; | |||
| var dmChannel = channel as CachedDMChannel; | |||
| if (dmChannel != null) | |||
| { | |||
| CachedDMChannel ignored; | |||
| _dmChannels.TryRemove(dmChannel.Recipient.Id, out ignored); | |||
| } | |||
| else | |||
| { | |||
| var groupChannel = channel as CachedGroupChannel; | |||
| if (groupChannel != null) | |||
| _groupChannels.TryRemove(id); | |||
| } | |||
| return channel; | |||
| } | |||
| return null; | |||
| } | |||
| @@ -10,6 +10,7 @@ using System.Linq; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| using System.Runtime.InteropServices; | |||
| using System.Collections.Concurrent; | |||
| namespace Discord | |||
| { | |||
| @@ -165,16 +166,24 @@ namespace Discord | |||
| return guild.ToChannel(model); | |||
| } | |||
| } | |||
| else if (model.Type == ChannelType.DM) | |||
| return new DMChannel(this, new User(model.Recipients.Value[0]), model); | |||
| else if (model.Type == ChannelType.Group) | |||
| { | |||
| var channel = new GroupChannel(this, model); | |||
| channel.UpdateUsers(model.Recipients.Value, UpdateSource.Creation); | |||
| return channel; | |||
| } | |||
| else | |||
| return new DMChannel(this, new User(model.Recipient.Value), model); | |||
| throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | |||
| } | |||
| return null; | |||
| } | |||
| /// <inheritdoc /> | |||
| public virtual async Task<IReadOnlyCollection<IDMChannel>> GetDMChannelsAsync() | |||
| public virtual async Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync() | |||
| { | |||
| var models = await ApiClient.GetMyDMsAsync().ConfigureAwait(false); | |||
| return models.Select(x => new DMChannel(this, new User(x.Recipient.Value), x)).ToImmutableArray(); | |||
| var models = await ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); | |||
| return models.Select(x => new DMChannel(this, new User(x.Recipients.Value[0]), x)).ToImmutableArray(); | |||
| } | |||
| /// <inheritdoc /> | |||
| @@ -289,9 +298,9 @@ namespace Discord | |||
| private async Task WriteInitialLog() | |||
| { | |||
| if (this is DiscordSocketClient) | |||
| await _clientLogger.InfoAsync($"DiscordSocketClient v{DiscordConfig.Version} (Gateway v{DiscordConfig.GatewayAPIVersion}, {DiscordConfig.GatewayEncoding})").ConfigureAwait(false); | |||
| await _clientLogger.InfoAsync($"DiscordSocketClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, {DiscordConfig.GatewayEncoding})").ConfigureAwait(false); | |||
| else | |||
| await _clientLogger.InfoAsync($"DiscordClient v{DiscordConfig.Version}").ConfigureAwait(false); | |||
| await _clientLogger.InfoAsync($"DiscordClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion})").ConfigureAwait(false); | |||
| await _clientLogger.VerboseAsync($"Runtime: {RuntimeInformation.FrameworkDescription.Trim()} ({ToArchString(RuntimeInformation.ProcessArchitecture)})").ConfigureAwait(false); | |||
| await _clientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false); | |||
| await _clientLogger.VerboseAsync($"Processors: {Environment.ProcessorCount}").ConfigureAwait(false); | |||
| @@ -8,10 +8,10 @@ namespace Discord | |||
| public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; | |||
| public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; | |||
| public const int GatewayAPIVersion = 5; | |||
| public const int APIVersion = 6; | |||
| public const string GatewayEncoding = "json"; | |||
| public const string ClientAPIUrl = "https://discordapp.com/api/"; | |||
| public static readonly string ClientAPIUrl = $"https://discordapp.com/api/v{APIVersion}/"; | |||
| public const string CDNUrl = "https://cdn.discordapp.com/"; | |||
| public const string InviteUrl = "https://discord.gg/"; | |||
| @@ -167,12 +167,12 @@ namespace Discord | |||
| remove { _userPresenceUpdatedEvent.Remove(value); } | |||
| } | |||
| private readonly AsyncEvent<Func<IGuildUser, IPresence, IPresence, Task>> _userPresenceUpdatedEvent = new AsyncEvent<Func<IGuildUser, IPresence, IPresence, Task>>(); | |||
| public event Func<IGuildUser, IVoiceState, IVoiceState, Task> UserVoiceStateUpdated | |||
| public event Func<IUser, IVoiceState, IVoiceState, Task> UserVoiceStateUpdated | |||
| { | |||
| add { _userVoiceStateUpdatedEvent.Add(value); } | |||
| remove { _userVoiceStateUpdatedEvent.Remove(value); } | |||
| } | |||
| private readonly AsyncEvent<Func<IGuildUser, IVoiceState, IVoiceState, Task>> _userVoiceStateUpdatedEvent = new AsyncEvent<Func<IGuildUser, IVoiceState, IVoiceState, Task>>(); | |||
| private readonly AsyncEvent<Func<IUser, IVoiceState, IVoiceState, Task>> _userVoiceStateUpdatedEvent = new AsyncEvent<Func<IUser, IVoiceState, IVoiceState, Task>>(); | |||
| public event Func<ISelfUser, ISelfUser, Task> CurrentUserUpdated | |||
| { | |||
| add { _selfUpdatedEvent.Add(value); } | |||
| @@ -185,6 +185,18 @@ namespace Discord | |||
| remove { _userIsTypingEvent.Remove(value); } | |||
| } | |||
| private readonly AsyncEvent<Func<IUser, IChannel, Task>> _userIsTypingEvent = new AsyncEvent<Func<IUser, IChannel, Task>>(); | |||
| public event Func<IGroupUser, Task> RecipientAdded | |||
| { | |||
| add { _recipientAddedEvent.Add(value); } | |||
| remove { _recipientAddedEvent.Remove(value); } | |||
| } | |||
| private readonly AsyncEvent<Func<IGroupUser, Task>> _recipientAddedEvent = new AsyncEvent<Func<IGroupUser, Task>>(); | |||
| public event Func<IGroupUser, Task> RecipientRemoved | |||
| { | |||
| add { _recipientRemovedEvent.Add(value); } | |||
| remove { _recipientRemovedEvent.Remove(value); } | |||
| } | |||
| private readonly AsyncEvent<Func<IGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<IGroupUser, Task>>(); | |||
| //TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected; | |||
| } | |||
| @@ -57,7 +57,6 @@ namespace Discord | |||
| internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; | |||
| internal IReadOnlyCollection<CachedGuild> Guilds => DataStore.Guilds; | |||
| internal IReadOnlyCollection<CachedDMChannel> DMChannels => DataStore.DMChannels; | |||
| internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection(); | |||
| /// <summary> Creates a new REST/WebSocket discord client. </summary> | |||
| @@ -148,6 +147,7 @@ namespace Discord | |||
| ConnectionState = ConnectionState.Connecting; | |||
| await _gatewayLogger.InfoAsync("Connecting").ConfigureAwait(false); | |||
| try | |||
| { | |||
| _connectTask = new TaskCompletionSource<bool>(); | |||
| @@ -161,7 +161,6 @@ namespace Discord | |||
| await ApiClient.SendIdentifyAsync().ConfigureAwait(false); | |||
| await _connectTask.Task.ConfigureAwait(false); | |||
| ConnectionState = ConnectionState.Connected; | |||
| await _gatewayLogger.InfoAsync("Connected").ConfigureAwait(false); | |||
| } | |||
| @@ -174,6 +173,7 @@ namespace Discord | |||
| /// <inheritdoc /> | |||
| public async Task DisconnectAsync() | |||
| { | |||
| if (_connectTask?.TrySetCanceled() ?? false) return; | |||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| @@ -182,6 +182,17 @@ namespace Discord | |||
| } | |||
| finally { _connectionLock.Release(); } | |||
| } | |||
| private async Task DisconnectAsync(Exception ex) | |||
| { | |||
| if (_connectTask?.TrySetException(ex) ?? false) return; | |||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| _isReconnecting = false; | |||
| await DisconnectInternalAsync(ex).ConfigureAwait(false); | |||
| } | |||
| finally { _connectionLock.Release(); } | |||
| } | |||
| private async Task DisconnectInternalAsync(Exception ex) | |||
| { | |||
| ulong guildId; | |||
| @@ -329,27 +340,39 @@ namespace Discord | |||
| { | |||
| return Task.FromResult<IChannel>(DataStore.GetChannel(id)); | |||
| } | |||
| public override Task<IReadOnlyCollection<IDMChannel>> GetDMChannelsAsync() | |||
| public override Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync() | |||
| { | |||
| return Task.FromResult<IReadOnlyCollection<IDMChannel>>(DMChannels); | |||
| return Task.FromResult<IReadOnlyCollection<IPrivateChannel>>(DataStore.PrivateChannels); | |||
| } | |||
| internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore) | |||
| internal ICachedChannel AddPrivateChannel(API.Channel model, DataStore dataStore) | |||
| { | |||
| var recipient = GetOrAddUser(model.Recipient.Value, dataStore); | |||
| var channel = new CachedDMChannel(this, new CachedDMUser(recipient), model); | |||
| recipient.AddRef(); | |||
| dataStore.AddDMChannel(channel); | |||
| return channel; | |||
| } | |||
| internal CachedDMChannel RemoveDMChannel(ulong id) | |||
| { | |||
| var dmChannel = DataStore.RemoveDMChannel(id); | |||
| if (dmChannel != null) | |||
| switch (model.Type) | |||
| { | |||
| var recipient = dmChannel.Recipient; | |||
| recipient.User.RemoveRef(this); | |||
| case ChannelType.DM: | |||
| { | |||
| var recipients = model.Recipients.Value; | |||
| var user = GetOrAddUser(recipients[0], dataStore); | |||
| var channel = new CachedDMChannel(this, new CachedDMUser(user), model); | |||
| dataStore.AddChannel(channel); | |||
| return channel; | |||
| } | |||
| case ChannelType.Group: | |||
| { | |||
| var channel = new CachedGroupChannel(this, model); | |||
| channel.UpdateUsers(model.Recipients.Value, UpdateSource.Creation, dataStore); | |||
| dataStore.AddChannel(channel); | |||
| return channel; | |||
| } | |||
| default: | |||
| throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | |||
| } | |||
| return dmChannel; | |||
| } | |||
| internal ICachedChannel RemovePrivateChannel(ulong id) | |||
| { | |||
| var channel = DataStore.RemoveChannel(id) as ICachedPrivateChannel; | |||
| foreach (var recipient in channel.Recipients) | |||
| recipient.User.RemoveRef(this); | |||
| return channel; | |||
| } | |||
| /// <inheritdoc /> | |||
| @@ -362,6 +385,11 @@ namespace Discord | |||
| { | |||
| return Task.FromResult<IUser>(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); | |||
| } | |||
| /// <inheritdoc /> | |||
| public override Task<ISelfUser> GetCurrentUserAsync() | |||
| { | |||
| return Task.FromResult<ISelfUser>(_currentUser); | |||
| } | |||
| internal CachedGlobalUser GetOrAddUser(API.User model, DataStore dataStore) | |||
| { | |||
| var user = dataStore.GetOrAddUser(model.Id, _ => new CachedGlobalUser(model)); | |||
| @@ -502,35 +530,43 @@ namespace Discord | |||
| //Connection | |||
| case "READY": | |||
| { | |||
| await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); | |||
| var data = (payload as JToken).ToObject<ReadyEvent>(_serializer); | |||
| var dataStore = new DataStore( data.Guilds.Length, data.PrivateChannels.Length); | |||
| try | |||
| { | |||
| await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); | |||
| var data = (payload as JToken).ToObject<ReadyEvent>(_serializer); | |||
| var dataStore = new DataStore(data.Guilds.Length, data.PrivateChannels.Length); | |||
| var currentUser = new CachedSelfUser(this, data.User); | |||
| int unavailableGuilds = 0; | |||
| for (int i = 0; i < data.Guilds.Length; i++) | |||
| { | |||
| var model = data.Guilds[i]; | |||
| AddGuild(model, dataStore); | |||
| if (model.Unavailable == true) | |||
| unavailableGuilds++; | |||
| } | |||
| for (int i = 0; i < data.PrivateChannels.Length; i++) | |||
| AddPrivateChannel(data.PrivateChannels[i], dataStore); | |||
| var currentUser = new CachedSelfUser(this, data.User); | |||
| int unavailableGuilds = 0; | |||
| //dataStore.GetOrAddUser(data.User.Id, _ => currentUser); | |||
| for (int i = 0; i < data.Guilds.Length; i++) | |||
| _sessionId = data.SessionId; | |||
| _currentUser = currentUser; | |||
| _unavailableGuilds = unavailableGuilds; | |||
| _lastGuildAvailableTime = Environment.TickCount; | |||
| DataStore = dataStore; | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| var model = data.Guilds[i]; | |||
| AddGuild(model, dataStore); | |||
| if (model.Unavailable == true) | |||
| unavailableGuilds++; | |||
| await DisconnectAsync(new Exception("Processing READY failed", ex)); | |||
| return; | |||
| } | |||
| for (int i = 0; i < data.PrivateChannels.Length; i++) | |||
| AddDMChannel(data.PrivateChannels[i], dataStore); | |||
| _sessionId = data.SessionId; | |||
| _currentUser = currentUser; | |||
| _unavailableGuilds = unavailableGuilds; | |||
| _lastGuildAvailableTime = Environment.TickCount; | |||
| DataStore = dataStore; | |||
| _guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token, _clientLogger); | |||
| await _readyEvent.InvokeAsync().ConfigureAwait(false); | |||
| await SyncGuildsAsync().ConfigureAwait(false); | |||
| var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete | |||
| await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); | |||
| } | |||
| @@ -686,11 +722,19 @@ namespace Discord | |||
| var data = (payload as JToken).ToObject<API.Channel>(_serializer); | |||
| ICachedChannel channel = null; | |||
| if (!data.IsPrivate) | |||
| if (data.GuildId.IsSpecified) | |||
| { | |||
| var guild = DataStore.GetGuild(data.GuildId.Value); | |||
| if (guild != null) | |||
| { | |||
| guild.AddChannel(data, DataStore); | |||
| if (!guild.IsSynced) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored CHANNEL_CREATE, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| } | |||
| else | |||
| { | |||
| await _gatewayLogger.WarningAsync("CHANNEL_CREATE referenced an unknown guild.").ConfigureAwait(false); | |||
| @@ -698,7 +742,8 @@ namespace Discord | |||
| } | |||
| } | |||
| else | |||
| channel = AddDMChannel(data, DataStore); | |||
| channel = AddPrivateChannel(data, DataStore); | |||
| if (channel != null) | |||
| await _channelCreatedEvent.InvokeAsync(channel).ConfigureAwait(false); | |||
| } | |||
| @@ -713,6 +758,13 @@ namespace Discord | |||
| { | |||
| var before = channel.Clone(); | |||
| channel.Update(data, UpdateSource.WebSocket); | |||
| if (!((channel as ICachedGuildChannel)?.Guild.IsSynced ?? true)) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored CHANNEL_UPDATE, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| await _channelUpdatedEvent.InvokeAsync(before, channel).ConfigureAwait(false); | |||
| } | |||
| else | |||
| @@ -728,11 +780,19 @@ namespace Discord | |||
| ICachedChannel channel = null; | |||
| var data = (payload as JToken).ToObject<API.Channel>(_serializer); | |||
| if (!data.IsPrivate) | |||
| if (data.GuildId.IsSpecified) | |||
| { | |||
| var guild = DataStore.GetGuild(data.GuildId.Value); | |||
| if (guild != null) | |||
| { | |||
| channel = guild.RemoveChannel(data.Id); | |||
| if (!guild.IsSynced) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored CHANNEL_DELETE, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| } | |||
| else | |||
| { | |||
| await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown guild.").ConfigureAwait(false); | |||
| @@ -740,7 +800,8 @@ namespace Discord | |||
| } | |||
| } | |||
| else | |||
| channel = RemoveDMChannel(data.Recipient.Value.Id); | |||
| channel = RemovePrivateChannel(data.Id); | |||
| if (channel != null) | |||
| await _channelDestroyedEvent.InvokeAsync(channel).ConfigureAwait(false); | |||
| else | |||
| @@ -761,6 +822,13 @@ namespace Discord | |||
| if (guild != null) | |||
| { | |||
| var user = guild.AddUser(data, DataStore); | |||
| if (!guild.IsSynced) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_ADD, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| await _userJoinedEvent.InvokeAsync(user).ConfigureAwait(false); | |||
| } | |||
| else | |||
| @@ -779,6 +847,13 @@ namespace Discord | |||
| if (guild != null) | |||
| { | |||
| var user = guild.GetUser(data.User.Id); | |||
| if (!guild.IsSynced) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_UPDATE, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| if (user != null) | |||
| { | |||
| var before = user.Clone(); | |||
| @@ -807,6 +882,13 @@ namespace Discord | |||
| if (guild != null) | |||
| { | |||
| var user = guild.RemoveUser(data.User.Id); | |||
| if (!guild.IsSynced) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_REMOVE, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| if (user != null) | |||
| { | |||
| user.User.RemoveRef(this); | |||
| @@ -849,6 +931,51 @@ namespace Discord | |||
| } | |||
| } | |||
| break; | |||
| case "CHANNEL_RECIPIENT_ADD": | |||
| { | |||
| await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); | |||
| var data = (payload as JToken).ToObject<RecipientEvent>(_serializer); | |||
| var channel = DataStore.GetChannel(data.ChannelId) as CachedGroupChannel; | |||
| if (channel != null) | |||
| { | |||
| var user = channel.AddUser(data.User, DataStore); | |||
| await _recipientAddedEvent.InvokeAsync(user).ConfigureAwait(false); | |||
| } | |||
| else | |||
| { | |||
| await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_ADD referenced an unknown channel.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| } | |||
| break; | |||
| case "CHANNEL_RECIPIENT_REMOVE": | |||
| { | |||
| await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false); | |||
| var data = (payload as JToken).ToObject<RecipientEvent>(_serializer); | |||
| var channel = DataStore.GetChannel(data.ChannelId) as CachedGroupChannel; | |||
| if (channel != null) | |||
| { | |||
| var user = channel.RemoveUser(data.User.Id); | |||
| if (user != null) | |||
| { | |||
| user.User.RemoveRef(this); | |||
| await _recipientRemovedEvent.InvokeAsync(user).ConfigureAwait(false); | |||
| } | |||
| else | |||
| { | |||
| await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_REMOVE referenced an unknown user.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| } | |||
| else | |||
| { | |||
| await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_ADD referenced an unknown channel.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| } | |||
| break; | |||
| //Roles | |||
| case "GUILD_ROLE_CREATE": | |||
| @@ -860,6 +987,12 @@ namespace Discord | |||
| if (guild != null) | |||
| { | |||
| var role = guild.AddRole(data.Role); | |||
| if (!guild.IsSynced) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_CREATE, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| await _roleCreatedEvent.InvokeAsync(role).ConfigureAwait(false); | |||
| } | |||
| else | |||
| @@ -882,6 +1015,13 @@ namespace Discord | |||
| { | |||
| var before = role.Clone(); | |||
| role.Update(data.Role, UpdateSource.WebSocket); | |||
| if (!guild.IsSynced) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_UPDATE, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| await _roleUpdatedEvent.InvokeAsync(before, role).ConfigureAwait(false); | |||
| } | |||
| else | |||
| @@ -907,7 +1047,15 @@ namespace Discord | |||
| { | |||
| var role = guild.RemoveRole(data.RoleId); | |||
| if (role != null) | |||
| { | |||
| if (!guild.IsSynced) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_DELETE, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| await _roleDeletedEvent.InvokeAsync(role).ConfigureAwait(false); | |||
| } | |||
| else | |||
| { | |||
| await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown role.").ConfigureAwait(false); | |||
| @@ -930,7 +1078,15 @@ namespace Discord | |||
| var data = (payload as JToken).ToObject<GuildBanEvent>(_serializer); | |||
| var guild = DataStore.GetGuild(data.GuildId); | |||
| if (guild != null) | |||
| { | |||
| if (!guild.IsSynced) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored GUILD_BAN_ADD, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| await _userBannedEvent.InvokeAsync(new User(data.User), guild).ConfigureAwait(false); | |||
| } | |||
| else | |||
| { | |||
| await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild.").ConfigureAwait(false); | |||
| @@ -945,7 +1101,15 @@ namespace Discord | |||
| var data = (payload as JToken).ToObject<GuildBanEvent>(_serializer); | |||
| var guild = DataStore.GetGuild(data.GuildId); | |||
| if (guild != null) | |||
| { | |||
| if (!guild.IsSynced) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored GUILD_BAN_REMOVE, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| await _userUnbannedEvent.InvokeAsync(new User(data.User), guild).ConfigureAwait(false); | |||
| } | |||
| else | |||
| { | |||
| await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild.").ConfigureAwait(false); | |||
| @@ -1179,39 +1343,66 @@ namespace Discord | |||
| var data = (payload as JToken).ToObject<API.VoiceState>(_serializer); | |||
| if (data.GuildId.HasValue) | |||
| { | |||
| var guild = DataStore.GetGuild(data.GuildId.Value); | |||
| if (guild != null) | |||
| ICachedUser user; | |||
| VoiceState before, after; | |||
| if (data.GuildId != null) | |||
| { | |||
| if (!guild.IsSynced) | |||
| var guild = DataStore.GetGuild(data.GuildId.Value); | |||
| if (guild != null) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored VOICE_STATE_UPDATE, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| if (!guild.IsSynced) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Ignored VOICE_STATE_UPDATE, guild is not synced yet.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| if (data.ChannelId != null) | |||
| { | |||
| before = guild.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false); | |||
| after = guild.AddOrUpdateVoiceState(data, DataStore); | |||
| } | |||
| else | |||
| { | |||
| before = guild.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false); | |||
| after = new VoiceState(null, data); | |||
| } | |||
| user = guild.GetUser(data.UserId); | |||
| } | |||
| VoiceState before, after; | |||
| if (data.ChannelId != null) | |||
| else | |||
| { | |||
| before = guild.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false); | |||
| after = guild.AddOrUpdateVoiceState(data, DataStore); | |||
| await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| else | |||
| } | |||
| else | |||
| { | |||
| var groupChannel = DataStore.GetChannel(data.ChannelId.Value) as CachedGroupChannel; | |||
| if (groupChannel != null) | |||
| { | |||
| before = guild.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false); | |||
| after = new VoiceState(null, data); | |||
| if (data.ChannelId != null) | |||
| { | |||
| before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false); | |||
| after = groupChannel.AddOrUpdateVoiceState(data, DataStore); | |||
| } | |||
| else | |||
| { | |||
| before = groupChannel.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false); | |||
| after = new VoiceState(null, data); | |||
| } | |||
| user = groupChannel.GetUser(data.UserId); | |||
| } | |||
| var user = guild.GetUser(data.UserId); | |||
| if (user != null) | |||
| await _userVoiceStateUpdatedEvent.InvokeAsync(user, before, after).ConfigureAwait(false); | |||
| else | |||
| { | |||
| await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false); | |||
| await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown channel.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| } | |||
| if (user != null) | |||
| await _userVoiceStateUpdatedEvent.InvokeAsync(user, before, after).ConfigureAwait(false); | |||
| else | |||
| { | |||
| await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); | |||
| await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| } | |||
| @@ -1,9 +1,10 @@ | |||
| namespace Discord | |||
| { | |||
| public enum ChannelType : byte | |||
| public enum ChannelType | |||
| { | |||
| DM, | |||
| Text, | |||
| Voice | |||
| Text = 0, | |||
| DM = 1, | |||
| Voice = 2, | |||
| Group = 3 | |||
| } | |||
| } | |||
| @@ -17,6 +17,7 @@ namespace Discord | |||
| public IUser Recipient { get; private set; } | |||
| public virtual IReadOnlyCollection<IMessage> CachedMessages => ImmutableArray.Create<IMessage>(); | |||
| IReadOnlyCollection<IUser> IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); | |||
| public DMChannel(DiscordClient discord, IUser recipient, Model model) | |||
| : base(model.Id) | |||
| @@ -30,7 +31,7 @@ namespace Discord | |||
| { | |||
| if (/*source == UpdateSource.Rest && */IsAttached) return; | |||
| (Recipient as User).Update(model.Recipient.Value, source); | |||
| (Recipient as User).Update(model.Recipients.Value[0], source); | |||
| } | |||
| public async Task UpdateAsync() | |||
| @@ -0,0 +1,147 @@ | |||
| using Discord.API.Rest; | |||
| using Discord.Extensions; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Diagnostics; | |||
| using System.IO; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.Channel; | |||
| namespace Discord | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| internal class GroupChannel : SnowflakeEntity, IGroupChannel | |||
| { | |||
| protected ConcurrentDictionary<ulong, GroupUser> _users; | |||
| private string _iconId; | |||
| public override DiscordClient Discord { get; } | |||
| public string Name { get; private set; } | |||
| public IReadOnlyCollection<IUser> Recipients => _users.ToReadOnlyCollection(); | |||
| public virtual IReadOnlyCollection<IMessage> CachedMessages => ImmutableArray.Create<IMessage>(); | |||
| public string IconUrl => API.CDN.GetChannelIconUrl(Id, _iconId); | |||
| public GroupChannel(DiscordClient discord, Model model) | |||
| : base(model.Id) | |||
| { | |||
| Discord = discord; | |||
| Update(model, UpdateSource.Creation); | |||
| } | |||
| public virtual void Update(Model model, UpdateSource source) | |||
| { | |||
| if (source == UpdateSource.Rest && IsAttached) return; | |||
| if (model.Name.IsSpecified) | |||
| Name = model.Name.Value; | |||
| if (model.Icon.IsSpecified) | |||
| _iconId = model.Icon.Value; | |||
| if (source != UpdateSource.Creation && model.Recipients.IsSpecified) | |||
| UpdateUsers(model.Recipients.Value, source); | |||
| } | |||
| internal virtual void UpdateUsers(API.User[] models, UpdateSource source) | |||
| { | |||
| if (!IsAttached) | |||
| { | |||
| var users = new ConcurrentDictionary<ulong, GroupUser>(1, (int)(models.Length * 1.05)); | |||
| for (int i = 0; i < models.Length; i++) | |||
| users[models[i].Id] = new GroupUser(this, new User(models[i])); | |||
| _users = users; | |||
| } | |||
| } | |||
| public async Task UpdateAsync() | |||
| { | |||
| if (IsAttached) throw new NotSupportedException(); | |||
| var model = await Discord.ApiClient.GetChannelAsync(Id).ConfigureAwait(false); | |||
| Update(model, UpdateSource.Rest); | |||
| } | |||
| public async Task LeaveAsync() | |||
| { | |||
| await Discord.ApiClient.DeleteChannelAsync(Id).ConfigureAwait(false); | |||
| } | |||
| public async Task AddUserAsync(IUser user) | |||
| { | |||
| await Discord.ApiClient.AddGroupRecipientAsync(Id, user.Id).ConfigureAwait(false); | |||
| } | |||
| public async Task<IUser> GetUserAsync(ulong id) | |||
| { | |||
| GroupUser user; | |||
| if (_users.TryGetValue(id, out user)) | |||
| return user; | |||
| var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); | |||
| if (id == currentUser.Id) | |||
| return currentUser; | |||
| return null; | |||
| } | |||
| public async Task<IReadOnlyCollection<IUser>> GetUsersAsync() | |||
| { | |||
| var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); | |||
| return _users.Select(x => x.Value).Concat<IUser>(ImmutableArray.Create(currentUser)).ToReadOnlyCollection(_users); | |||
| } | |||
| public async Task<IMessage> SendMessageAsync(string text, bool isTTS) | |||
| { | |||
| var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; | |||
| var model = await Discord.ApiClient.CreateDMMessageAsync(Id, args).ConfigureAwait(false); | |||
| return new Message(this, new User(model.Author.Value), model); | |||
| } | |||
| public async Task<IMessage> SendFileAsync(string filePath, string text, bool isTTS) | |||
| { | |||
| 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.ApiClient.UploadDMFileAsync(Id, file, args).ConfigureAwait(false); | |||
| return new Message(this, new User(model.Author.Value), model); | |||
| } | |||
| } | |||
| public async Task<IMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS) | |||
| { | |||
| var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; | |||
| var model = await Discord.ApiClient.UploadDMFileAsync(Id, stream, args).ConfigureAwait(false); | |||
| return new Message(this, new User(model.Author.Value), model); | |||
| } | |||
| public virtual async Task<IMessage> GetMessageAsync(ulong id) | |||
| { | |||
| var model = await Discord.ApiClient.GetChannelMessageAsync(Id, id).ConfigureAwait(false); | |||
| if (model != null) | |||
| return new Message(this, new User(model.Author.Value), model); | |||
| return null; | |||
| } | |||
| public virtual async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit) | |||
| { | |||
| var args = new GetChannelMessagesParams { Limit = limit }; | |||
| var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); | |||
| return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray(); | |||
| } | |||
| public virtual async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) | |||
| { | |||
| var args = new GetChannelMessagesParams { Limit = limit }; | |||
| var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); | |||
| return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray(); | |||
| } | |||
| public async Task DeleteMessagesAsync(IEnumerable<IMessage> messages) | |||
| { | |||
| await Discord.ApiClient.DeleteDMMessagesAsync(Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); | |||
| } | |||
| public async Task TriggerTypingAsync() | |||
| { | |||
| await Discord.ApiClient.TriggerTypingIndicatorAsync(Id).ConfigureAwait(false); | |||
| } | |||
| public override string ToString() => Name; | |||
| private string DebuggerDisplay => $"@{Name} ({Id}, Group)"; | |||
| IMessage IMessageChannel.GetCachedMessage(ulong id) => null; | |||
| } | |||
| } | |||
| @@ -2,7 +2,7 @@ | |||
| namespace Discord | |||
| { | |||
| public interface IDMChannel : IMessageChannel | |||
| public interface IDMChannel : IMessageChannel, IPrivateChannel | |||
| { | |||
| /// <summary> Gets the recipient of all messages in this channel. </summary> | |||
| IUser Recipient { get; } | |||
| @@ -0,0 +1,13 @@ | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| public interface IGroupChannel : IMessageChannel, IPrivateChannel | |||
| { | |||
| /// <summary> Adds a user to this group. </summary> | |||
| Task AddUserAsync(IUser user); | |||
| /// <summary> Leaves this group. </summary> | |||
| Task LeaveAsync(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| using System.Collections.Generic; | |||
| namespace Discord | |||
| { | |||
| public interface IPrivateChannel | |||
| { | |||
| IReadOnlyCollection<IUser> Recipients { get; } | |||
| } | |||
| } | |||
| @@ -296,14 +296,14 @@ namespace Discord | |||
| internal GuildChannel ToChannel(API.Channel model) | |||
| { | |||
| switch (model.Type.Value) | |||
| switch (model.Type) | |||
| { | |||
| case ChannelType.Text: | |||
| return new TextChannel(this, model); | |||
| case ChannelType.Voice: | |||
| return new VoiceChannel(this, model); | |||
| default: | |||
| throw new InvalidOperationException($"Unknown channel type: {model.Type.Value}"); | |||
| throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | |||
| } | |||
| } | |||
| @@ -13,11 +13,13 @@ namespace Discord | |||
| bool IsTTS { get; } | |||
| /// <summary> Returns true if this message was added to its channel's pinned messages. </summary> | |||
| bool IsPinned { get; } | |||
| /// <summary> Returns the text for this message. </summary> | |||
| string Text { get; } | |||
| /// <summary> Returns the content for this message. </summary> | |||
| string Content { get; } | |||
| /// <summary> Gets the time this message was sent. </summary> | |||
| DateTimeOffset Timestamp { get; } | |||
| /// <summary> Gets the type of this message. </summary> | |||
| MessageType Type { get; } | |||
| /// <summary> Gets the channel this message was sent to. </summary> | |||
| IMessageChannel Channel { get; } | |||
| /// <summary> Gets the author of this message. </summary> | |||
| @@ -14,14 +14,15 @@ namespace Discord | |||
| private bool _isMentioningEveryone; | |||
| private long _timestampTicks; | |||
| private long? _editedTimestampTicks; | |||
| public bool IsTTS { get; private set; } | |||
| public string Text { get; private set; } | |||
| public bool IsPinned { get; private set; } | |||
| public MessageType Type { get; } | |||
| public IMessageChannel Channel { get; } | |||
| public IUser Author { get; } | |||
| public bool IsTTS { get; private set; } | |||
| public string Content { get; private set; } | |||
| public bool IsPinned { get; private set; } | |||
| public IReadOnlyCollection<IAttachment> Attachments { get; private set; } | |||
| public IReadOnlyCollection<IEmbed> Embeds { get; private set; } | |||
| public IReadOnlyCollection<ulong> MentionedChannelIds { get; private set; } | |||
| @@ -37,6 +38,7 @@ namespace Discord | |||
| { | |||
| Channel = channel; | |||
| Author = author; | |||
| Type = model.Type; | |||
| if (channel is IGuildChannel) | |||
| { | |||
| @@ -118,7 +120,7 @@ namespace Discord | |||
| MentionedChannelIds = MentionUtils.GetChannelMentions(text, guildChannel.Guild); | |||
| MentionedRoles = MentionUtils.GetRoleMentions(text, guildChannel.Guild); | |||
| } | |||
| Text = text; | |||
| Content = text; | |||
| } | |||
| } | |||
| @@ -163,9 +165,9 @@ namespace Discord | |||
| } | |||
| public string Resolve(int startIndex, int length, UserResolveMode userMode = UserResolveMode.NameOnly) | |||
| => Resolve(Text.Substring(startIndex, length), userMode); | |||
| => Resolve(Content.Substring(startIndex, length), userMode); | |||
| public string Resolve(UserResolveMode userMode = UserResolveMode.NameOnly) | |||
| => Resolve(Text, userMode); | |||
| => Resolve(Content, userMode); | |||
| private string Resolve(string text, UserResolveMode userMode = UserResolveMode.NameOnly) | |||
| { | |||
| @@ -179,7 +181,7 @@ namespace Discord | |||
| return text; | |||
| } | |||
| public override string ToString() => Text; | |||
| private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; | |||
| public override string ToString() => Content; | |||
| private string DebuggerDisplay => $"{Author}: {Content}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| namespace Discord | |||
| { | |||
| public enum MessageType | |||
| { | |||
| Default = 0, | |||
| RecipientAdd = 1, | |||
| RecipientRemove = 2, | |||
| Call = 3, | |||
| ChannelNameChange = 4, | |||
| ChannelIconChange = 5 | |||
| } | |||
| } | |||
| @@ -0,0 +1,47 @@ | |||
| using Discord.API.Rest; | |||
| using System; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| internal class GroupUser : IGroupUser | |||
| { | |||
| public GroupChannel Channel { get; private set; } | |||
| public User User { get; private set; } | |||
| public ulong Id => User.Id; | |||
| public string AvatarUrl => User.AvatarUrl; | |||
| public DateTimeOffset CreatedAt => User.CreatedAt; | |||
| public string Discriminator => User.Discriminator; | |||
| public ushort DiscriminatorValue => User.DiscriminatorValue; | |||
| public bool IsAttached => User.IsAttached; | |||
| public bool IsBot => User.IsBot; | |||
| public string Mention => User.Mention; | |||
| public string NicknameMention => User.NicknameMention; | |||
| public string Username => User.Username; | |||
| public virtual UserStatus Status => UserStatus.Unknown; | |||
| public virtual Game Game => null; | |||
| public DiscordClient Discord => Channel.Discord; | |||
| public GroupUser(GroupChannel channel, User user) | |||
| { | |||
| Channel = channel; | |||
| User = user; | |||
| } | |||
| public async Task KickAsync() | |||
| { | |||
| await Discord.ApiClient.RemoveGroupRecipientAsync(Channel.Id, Id).ConfigureAwait(false); | |||
| } | |||
| public async Task<IDMChannel> CreateDMChannelAsync() | |||
| { | |||
| var args = new CreateDMChannelParams { Recipient = this }; | |||
| var model = await Discord.ApiClient.CreateDMChannelAsync(args).ConfigureAwait(false); | |||
| return new DMChannel(Discord, new User(model.Recipients.Value[0]), model); | |||
| } | |||
| } | |||
| } | |||
| @@ -7,7 +7,6 @@ using System.Linq; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.GuildMember; | |||
| using PresenceModel = Discord.API.Presence; | |||
| using VoiceStateModel = Discord.API.VoiceState; | |||
| namespace Discord | |||
| { | |||
| @@ -141,7 +140,7 @@ namespace Discord | |||
| var args = new CreateDMChannelParams { Recipient = this }; | |||
| var model = await Discord.ApiClient.CreateDMChannelAsync(args).ConfigureAwait(false); | |||
| return new DMChannel(Discord, User, model); | |||
| return new DMChannel(Discord, new User(model.Recipients.Value[0]), model); | |||
| } | |||
| IGuild IGuildUser.Guild => Guild; | |||
| @@ -0,0 +1,13 @@ | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| public interface IGroupUser : IUser | |||
| { | |||
| /// <summary> Kicks this user from this group. </summary> | |||
| Task KickAsync(); | |||
| /// <summary> Returns a private message channel to this user, creating one if it does not already exist. </summary> | |||
| Task<IDMChannel> CreateDMChannelAsync(); | |||
| } | |||
| } | |||
| @@ -31,9 +31,12 @@ namespace Discord | |||
| base.Update(model, source); | |||
| Email = model.Email; | |||
| IsVerified = model.Verified; | |||
| IsMfaEnabled = model.MfaEnabled; | |||
| if (model.Email.IsSpecified) | |||
| Email = model.Email.Value; | |||
| if (model.Verified.IsSpecified) | |||
| IsVerified = model.Verified.Value; | |||
| if (model.MfaEnabled.IsSpecified) | |||
| IsMfaEnabled = model.MfaEnabled.Value; | |||
| } | |||
| public async Task UpdateAsync() | |||
| @@ -31,10 +31,14 @@ namespace Discord | |||
| { | |||
| if (source == UpdateSource.Rest && IsAttached) return; | |||
| _avatarId = model.Avatar; | |||
| DiscriminatorValue = ushort.Parse(model.Discriminator); | |||
| IsBot = model.Bot; | |||
| Username = model.Username; | |||
| if (model.Avatar.IsSpecified) | |||
| _avatarId = model.Avatar.Value; | |||
| if (model.Discriminator.IsSpecified) | |||
| DiscriminatorValue = ushort.Parse(model.Discriminator.Value); | |||
| if (model.Bot.IsSpecified) | |||
| IsBot = model.Bot.Value; | |||
| if (model.Username.IsSpecified) | |||
| Username = model.Username.Value; | |||
| } | |||
| public override string ToString() => $"{Username}#{Discriminator}"; | |||
| @@ -6,13 +6,14 @@ using Model = Discord.API.Channel; | |||
| namespace Discord | |||
| { | |||
| internal class CachedDMChannel : DMChannel, IDMChannel, ICachedChannel, ICachedMessageChannel | |||
| internal class CachedDMChannel : DMChannel, IDMChannel, ICachedChannel, ICachedMessageChannel, ICachedPrivateChannel | |||
| { | |||
| private readonly MessageManager _messages; | |||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||
| public new CachedDMUser Recipient => base.Recipient as CachedDMUser; | |||
| public IReadOnlyCollection<ICachedUser> Members => ImmutableArray.Create<ICachedUser>(Discord.CurrentUser, Recipient); | |||
| IReadOnlyCollection<CachedDMUser> ICachedPrivateChannel.Recipients => ImmutableArray.Create(Recipient); | |||
| public CachedDMChannel(DiscordSocketClient discord, CachedDMUser recipient, Model model) | |||
| : base(discord, recipient, model) | |||
| @@ -1,8 +1,10 @@ | |||
| using System; | |||
| using System.Diagnostics; | |||
| using PresenceModel = Discord.API.Presence; | |||
| namespace Discord | |||
| { | |||
| [DebuggerDisplay("{DebuggerDisplay,nq}")] | |||
| internal class CachedDMUser : ICachedUser | |||
| { | |||
| public CachedGlobalUser User { get; } | |||
| @@ -36,5 +38,8 @@ namespace Discord | |||
| public CachedDMUser Clone() => MemberwiseClone() as CachedDMUser; | |||
| ICachedUser ICachedUser.Clone() => Clone(); | |||
| public override string ToString() => $"{Username}#{Discriminator}"; | |||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; | |||
| } | |||
| } | |||
| @@ -0,0 +1,139 @@ | |||
| using Discord.Extensions; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| using MessageModel = Discord.API.Message; | |||
| using Model = Discord.API.Channel; | |||
| using UserModel = Discord.API.User; | |||
| using VoiceStateModel = Discord.API.VoiceState; | |||
| using Discord.API; | |||
| namespace Discord | |||
| { | |||
| internal class CachedGroupChannel : GroupChannel, IGroupChannel, ICachedChannel, ICachedMessageChannel, ICachedPrivateChannel | |||
| { | |||
| private readonly MessageManager _messages; | |||
| private ConcurrentDictionary<ulong, VoiceState> _voiceStates; | |||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||
| public IReadOnlyCollection<ICachedUser> Members | |||
| => _users.Select(x => x.Value as ICachedUser).Concat(ImmutableArray.Create(Discord.CurrentUser)).ToReadOnlyCollection(() => _users.Count + 1); | |||
| public new IReadOnlyCollection<CachedDMUser> Recipients => _users.Cast<CachedDMUser>().ToReadOnlyCollection(_users); | |||
| public CachedGroupChannel(DiscordSocketClient discord, Model model) | |||
| : base(discord, model) | |||
| { | |||
| if (Discord.MessageCacheSize > 0) | |||
| _messages = new MessageCache(Discord, this); | |||
| else | |||
| _messages = new MessageManager(Discord, this); | |||
| _voiceStates = new ConcurrentDictionary<ulong, VoiceState>(1, 5); | |||
| } | |||
| public override void Update(Model model, UpdateSource source) | |||
| { | |||
| if (source == UpdateSource.Rest && IsAttached) return; | |||
| base.Update(model, source); | |||
| } | |||
| internal void UpdateUsers(UserModel[] models, UpdateSource source, DataStore dataStore) | |||
| { | |||
| var users = new ConcurrentDictionary<ulong, GroupUser>(1, models.Length); | |||
| for (int i = 0; i < models.Length; i++) | |||
| { | |||
| var globalUser = Discord.GetOrAddUser(models[i], dataStore); | |||
| users[models[i].Id] = new CachedGroupUser(this, globalUser); | |||
| } | |||
| _users = users; | |||
| } | |||
| internal override void UpdateUsers(UserModel[] models, UpdateSource source) | |||
| => UpdateUsers(models, source, Discord.DataStore); | |||
| public CachedGroupUser AddUser(UserModel model, DataStore dataStore) | |||
| { | |||
| GroupUser user; | |||
| if (_users.TryGetValue(model.Id, out user)) | |||
| return user as CachedGroupUser; | |||
| else | |||
| { | |||
| var globalUser = Discord.GetOrAddUser(model, dataStore); | |||
| var privateUser = new CachedGroupUser(this, globalUser); | |||
| _users[privateUser.Id] = privateUser; | |||
| return privateUser; | |||
| } | |||
| } | |||
| public ICachedUser GetUser(ulong id) | |||
| { | |||
| GroupUser user; | |||
| if (_users.TryGetValue(id, out user)) | |||
| return user as CachedGroupUser; | |||
| if (id == Discord.CurrentUser.Id) | |||
| return Discord.CurrentUser; | |||
| return null; | |||
| } | |||
| public CachedGroupUser RemoveUser(ulong id) | |||
| { | |||
| GroupUser user; | |||
| if (_users.TryRemove(id, out user)) | |||
| return user as CachedGroupUser; | |||
| return null; | |||
| } | |||
| public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary<ulong, VoiceState> voiceStates = null) | |||
| { | |||
| var voiceChannel = dataStore.GetChannel(model.ChannelId.Value) as CachedVoiceChannel; | |||
| var voiceState = new VoiceState(voiceChannel, model); | |||
| (voiceStates ?? _voiceStates)[model.UserId] = voiceState; | |||
| return voiceState; | |||
| } | |||
| public VoiceState? GetVoiceState(ulong id) | |||
| { | |||
| VoiceState voiceState; | |||
| if (_voiceStates.TryGetValue(id, out voiceState)) | |||
| return voiceState; | |||
| return null; | |||
| } | |||
| public VoiceState? RemoveVoiceState(ulong id) | |||
| { | |||
| VoiceState voiceState; | |||
| if (_voiceStates.TryRemove(id, out voiceState)) | |||
| return voiceState; | |||
| return null; | |||
| } | |||
| public override async Task<IMessage> GetMessageAsync(ulong id) | |||
| { | |||
| return await _messages.DownloadAsync(id).ConfigureAwait(false); | |||
| } | |||
| public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit) | |||
| { | |||
| return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); | |||
| } | |||
| public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) | |||
| { | |||
| return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); | |||
| } | |||
| public CachedMessage AddMessage(ICachedUser author, MessageModel model) | |||
| { | |||
| var msg = new CachedMessage(this, author, model); | |||
| _messages.Add(msg); | |||
| return msg; | |||
| } | |||
| public CachedMessage GetMessage(ulong id) | |||
| { | |||
| return _messages.Get(id); | |||
| } | |||
| public CachedMessage RemoveMessage(ulong id) | |||
| { | |||
| return _messages.Remove(id); | |||
| } | |||
| public CachedDMChannel Clone() => MemberwiseClone() as CachedDMChannel; | |||
| IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); | |||
| ICachedUser ICachedMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id); | |||
| ICachedChannel ICachedChannel.Clone() => Clone(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| using System.Diagnostics; | |||
| namespace Discord | |||
| { | |||
| [DebuggerDisplay("{DebuggerDisplay,nq}")] | |||
| internal class CachedGroupUser : GroupUser, ICachedUser | |||
| { | |||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||
| public new CachedGroupChannel Channel => base.Channel as CachedGroupChannel; | |||
| public new CachedGlobalUser User => base.User as CachedGlobalUser; | |||
| public Presence Presence => User.Presence; //{ get; private set; } | |||
| public override Game Game => Presence.Game; | |||
| public override UserStatus Status => Presence.Status; | |||
| public VoiceState? VoiceState => Channel.GetVoiceState(Id); | |||
| public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; | |||
| public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; | |||
| public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; | |||
| public CachedVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; | |||
| public CachedGroupUser(CachedGroupChannel channel, CachedGlobalUser user) | |||
| : base(channel, user) | |||
| { | |||
| } | |||
| public CachedGroupUser Clone() => MemberwiseClone() as CachedGroupUser; | |||
| ICachedUser ICachedUser.Clone() => Clone(); | |||
| public override string ToString() => $"{Username}#{Discriminator}"; | |||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; | |||
| } | |||
| } | |||
| @@ -208,7 +208,6 @@ namespace Discord | |||
| var user = Discord.GetOrAddUser(model.User, dataStore); | |||
| member = new CachedGuildUser(this, user, model); | |||
| members[user.Id] = member; | |||
| user.AddRef(); | |||
| DownloadedMemberCount++; | |||
| } | |||
| return member; | |||
| @@ -311,14 +310,14 @@ namespace Discord | |||
| new internal ICachedGuildChannel ToChannel(ChannelModel model) | |||
| { | |||
| switch (model.Type.Value) | |||
| switch (model.Type) | |||
| { | |||
| case ChannelType.Text: | |||
| return new CachedTextChannel(this, model); | |||
| case ChannelType.Voice: | |||
| return new CachedVoiceChannel(this, model); | |||
| default: | |||
| throw new InvalidOperationException($"Unknown channel type: {model.Type.Value}"); | |||
| throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| using System.Collections.Generic; | |||
| namespace Discord | |||
| { | |||
| internal interface ICachedPrivateChannel : ICachedChannel, IPrivateChannel | |||
| { | |||
| new IReadOnlyCollection<CachedDMUser> Recipients { get; } | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| using System.Collections; | |||
| using System; | |||
| using System.Collections; | |||
| using System.Collections.Generic; | |||
| using System.Diagnostics; | |||
| using System.Linq; | |||
| @@ -8,24 +9,26 @@ namespace Discord.Extensions | |||
| internal static class CollectionExtensions | |||
| { | |||
| public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source) | |||
| => new ConcurrentDictionaryWrapper<TValue, KeyValuePair<TKey, TValue>>(source, source.Select(x => x.Value)); | |||
| => new ConcurrentDictionaryWrapper<TValue>(source.Select(x => x.Value), () => source.Count); | |||
| public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue, TSource>(this IEnumerable<TValue> query, IReadOnlyCollection<TSource> source) | |||
| => new ConcurrentDictionaryWrapper<TValue, TSource>(source, query); | |||
| => new ConcurrentDictionaryWrapper<TValue>(query, () => source.Count); | |||
| public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue>(this IEnumerable<TValue> query, Func<int> countFunc) | |||
| => new ConcurrentDictionaryWrapper<TValue>(query, countFunc); | |||
| } | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| internal struct ConcurrentDictionaryWrapper<TValue, TSource> : IReadOnlyCollection<TValue> | |||
| internal struct ConcurrentDictionaryWrapper<TValue> : IReadOnlyCollection<TValue> | |||
| { | |||
| private readonly IReadOnlyCollection<TSource> _source; | |||
| private readonly IEnumerable<TValue> _query; | |||
| private readonly Func<int> _countFunc; | |||
| //It's okay that this count is affected by race conditions - we're wrapping a concurrent collection and that's to be expected | |||
| public int Count => _source.Count; | |||
| public int Count => _countFunc(); | |||
| public ConcurrentDictionaryWrapper(IReadOnlyCollection<TSource> source, IEnumerable<TValue> query) | |||
| public ConcurrentDictionaryWrapper(IEnumerable<TValue> query, Func<int> countFunc) | |||
| { | |||
| _source = source; | |||
| _query = query; | |||
| _countFunc = countFunc; | |||
| } | |||
| private string DebuggerDisplay => $"Count = {Count}"; | |||
| @@ -24,7 +24,7 @@ namespace Discord | |||
| Task DisconnectAsync(); | |||
| Task<IChannel> GetChannelAsync(ulong id); | |||
| Task<IReadOnlyCollection<IDMChannel>> GetDMChannelsAsync(); | |||
| Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync(); | |||
| Task<IReadOnlyCollection<IConnection>> GetConnectionsAsync(); | |||
| @@ -1,42 +0,0 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| namespace Discord.Net.Converters | |||
| { | |||
| public class ChannelTypeConverter : JsonConverter | |||
| { | |||
| public static readonly ChannelTypeConverter Instance = new ChannelTypeConverter(); | |||
| public override bool CanConvert(Type objectType) => true; | |||
| public override bool CanRead => true; | |||
| public override bool CanWrite => true; | |||
| public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |||
| { | |||
| switch ((string)reader.Value) | |||
| { | |||
| case "text": | |||
| return ChannelType.Text; | |||
| case "voice": | |||
| return ChannelType.Voice; | |||
| default: | |||
| throw new JsonSerializationException("Unknown channel type"); | |||
| } | |||
| } | |||
| public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | |||
| { | |||
| switch ((ChannelType)value) | |||
| { | |||
| case ChannelType.Text: | |||
| writer.WriteValue("text"); | |||
| break; | |||
| case ChannelType.Voice: | |||
| writer.WriteValue("voice"); | |||
| break; | |||
| default: | |||
| throw new JsonSerializationException("Invalid channel type"); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -75,8 +75,6 @@ namespace Discord.Net.Converters | |||
| } | |||
| //Enums | |||
| if (type == typeof(ChannelType)) | |||
| return ChannelTypeConverter.Instance; | |||
| if (type == typeof(PermissionTarget)) | |||
| return PermissionTargetConverter.Instance; | |||
| if (type == typeof(UserStatus)) | |||