From bd96e473a9e4314fa226f0bcd9c8644de8d71a20 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 15 Jul 2016 19:54:23 -0300 Subject: [PATCH 01/12] Started adding v6 support --- src/Discord.Net/API/Common/Channel.cs | 13 +- src/Discord.Net/API/Common/MessageType.cs | 12 ++ src/Discord.Net/API/DiscordAPIClient.cs | 4 +- src/Discord.Net/Data/DataStore.cs | 43 ++++--- src/Discord.Net/DiscordClient.cs | 23 +++- src/Discord.Net/DiscordConfig.cs | 4 +- src/Discord.Net/DiscordSocketClient.cs | 65 ++++++---- src/Discord.Net/Entities/Channels/ChannelType.cs | 9 +- src/Discord.Net/Entities/Channels/DMChannel.cs | 3 +- src/Discord.Net/Entities/Channels/GroupChannel.cs | 140 +++++++++++++++++++++ src/Discord.Net/Entities/Channels/IDMChannel.cs | 2 +- src/Discord.Net/Entities/Channels/IGroupChannel.cs | 10 ++ .../Entities/Channels/IPrivateChannel.cs | 9 ++ src/Discord.Net/Entities/Guilds/Guild.cs | 4 +- src/Discord.Net/Entities/Users/GuildUser.cs | 2 +- .../Entities/WebSocket/CachedDMChannel.cs | 7 +- .../Entities/WebSocket/CachedGroupChannel.cs | 80 ++++++++++++ src/Discord.Net/Entities/WebSocket/CachedGuild.cs | 4 +- .../{CachedDMUser.cs => CachedPrivateUser.cs} | 6 +- .../Entities/WebSocket/ICachedPrivateChannel.cs | 9 ++ src/Discord.Net/Extensions/CollectionExtensions.cs | 21 ++-- src/Discord.Net/IDiscordClient.cs | 2 +- .../Net/Converters/ChannelTypeConverter.cs | 42 ------- .../Net/Converters/DiscordContractResolver.cs | 2 - 24 files changed, 387 insertions(+), 129 deletions(-) create mode 100644 src/Discord.Net/API/Common/MessageType.cs create mode 100644 src/Discord.Net/Entities/Channels/GroupChannel.cs create mode 100644 src/Discord.Net/Entities/Channels/IGroupChannel.cs create mode 100644 src/Discord.Net/Entities/Channels/IPrivateChannel.cs create mode 100644 src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs rename src/Discord.Net/Entities/WebSocket/{CachedDMUser.cs => CachedPrivateUser.cs} (85%) create mode 100644 src/Discord.Net/Entities/WebSocket/ICachedPrivateChannel.cs delete mode 100644 src/Discord.Net/Net/Converters/ChannelTypeConverter.cs diff --git a/src/Discord.Net/API/Common/Channel.cs b/src/Discord.Net/API/Common/Channel.cs index b0789b111..99fa87d87 100644 --- a/src/Discord.Net/API/Common/Channel.cs +++ b/src/Discord.Net/API/Common/Channel.cs @@ -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 GuildId { get; set; } [JsonProperty("name")] public Optional Name { get; set; } - [JsonProperty("type")] - public Optional Type { get; set; } [JsonProperty("position")] public Optional Position { get; set; } [JsonProperty("permission_overwrites")] @@ -27,6 +26,8 @@ namespace Discord.API //TextChannel [JsonProperty("topic")] public Optional Topic { get; set; } + [JsonProperty("last_pin_timestamp")] + public Optional LastPinTimestamp { get; set; } //VoiceChannel [JsonProperty("bitrate")] @@ -35,7 +36,7 @@ namespace Discord.API public Optional UserLimit { get; set; } //DMChannel - [JsonProperty("recipient")] - public Optional Recipient { get; set; } + [JsonProperty("recipients")] + public Optional Recipients { get; set; } } } diff --git a/src/Discord.Net/API/Common/MessageType.cs b/src/Discord.Net/API/Common/MessageType.cs new file mode 100644 index 000000000..837c20cd2 --- /dev/null +++ b/src/Discord.Net/API/Common/MessageType.cs @@ -0,0 +1,12 @@ +namespace Discord.API.Common +{ + public enum MessageType + { + Default = 0, + RecipientAdd = 1, + RecipientRemove = 2, + Call = 3, + ChannelNameChange = 4, + ChannelIconChange = 5 + } +} diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index df3e7403c..c5dc1c83b 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -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); @@ -1160,7 +1160,7 @@ namespace Discord.API { return await SendAsync>("GET", "users/@me/connections", options: options).ConfigureAwait(false); } - public async Task> GetMyDMsAsync(RequestOptions options = null) + public async Task> GetMyPrivateChannelsAsync(RequestOptions options = null) { return await SendAsync>("GET", "users/@me/channels", options: options).ConfigureAwait(false); } diff --git a/src/Discord.Net/Data/DataStore.cs b/src/Discord.Net/Data/DataStore.cs index 59b62e180..c767904b3 100644 --- a/src/Discord.Net/Data/DataStore.cs +++ b/src/Discord.Net/Data/DataStore.cs @@ -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 _dmChannels; private readonly ConcurrentDictionary _guilds; private readonly ConcurrentDictionary _users; + private readonly ConcurrentHashSet _groupChannels; internal IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); internal IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection(); + internal IReadOnlyCollection GroupChannels => _groupChannels.Select(x => GetChannel(x) as CachedGroupChannel).ToReadOnlyCollection(_groupChannels); internal IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); internal IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + internal IReadOnlyCollection 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(CollectionConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier)); _guilds = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); _users = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); + _groupChannels = new ConcurrentHashSet(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,25 @@ 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; } - 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); + } + return channel; } return null; } diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index d80b374f4..8bb10063a 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -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,26 @@ 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 recipients = model.Recipients.Value; + var users = new ConcurrentDictionary(1, recipients.Length); + for (int i = 0; i < recipients.Length; i++) + users[recipients[i].Id] = new User(recipients[i]); + return new GroupChannel(this, users, model); + } else - return new DMChannel(this, new User(model.Recipient.Value), model); + throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); } return null; } /// - public virtual async Task> GetDMChannelsAsync() + public virtual async Task> 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(); } /// @@ -289,9 +300,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); diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index 75d5b7a21..938369f88 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -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/"; diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index f3948a1eb..256fd9391 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -57,7 +57,6 @@ namespace Discord internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; internal IReadOnlyCollection Guilds => DataStore.Guilds; - internal IReadOnlyCollection DMChannels => DataStore.DMChannels; internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); /// Creates a new REST/WebSocket discord client. @@ -329,27 +328,42 @@ namespace Discord { return Task.FromResult(DataStore.GetChannel(id)); } - public override Task> GetDMChannelsAsync() + public override Task> GetPrivateChannelsAsync() { - return Task.FromResult>(DMChannels); + return Task.FromResult>(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 CachedPrivateUser(user), model); + dataStore.AddChannel(channel); + return channel; + } + case ChannelType.Group: + { + var recipients = model.Recipients.Value; + var users = new ConcurrentDictionary(1, recipients.Length); + for (int i = 0; i < recipients.Length; i++) + users[recipients[i].Id] = new CachedPrivateUser(GetOrAddUser(recipients[i], dataStore)); + var channel = new CachedGroupChannel(this, users, model); + 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; } /// @@ -362,6 +376,11 @@ namespace Discord { return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); } + /// + public override Task GetCurrentUserAsync() + { + return Task.FromResult(_currentUser); + } internal CachedGlobalUser GetOrAddUser(API.User model, DataStore dataStore) { var user = dataStore.GetOrAddUser(model.Id, _ => new CachedGlobalUser(model)); @@ -518,7 +537,7 @@ namespace Discord unavailableGuilds++; } for (int i = 0; i < data.PrivateChannels.Length; i++) - AddDMChannel(data.PrivateChannels[i], dataStore); + AddPrivateChannel(data.PrivateChannels[i], dataStore); _sessionId = data.SessionId; _currentUser = currentUser; @@ -686,7 +705,7 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); ICachedChannel channel = null; - if (!data.IsPrivate) + if (data.GuildId.IsSpecified) { var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) @@ -698,7 +717,8 @@ namespace Discord } } else - channel = AddDMChannel(data, DataStore); + channel = AddPrivateChannel(data, DataStore); + if (channel != null) await _channelCreatedEvent.InvokeAsync(channel).ConfigureAwait(false); } @@ -728,7 +748,7 @@ namespace Discord ICachedChannel channel = null; var data = (payload as JToken).ToObject(_serializer); - if (!data.IsPrivate) + if (data.GuildId.IsSpecified) { var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) @@ -740,7 +760,8 @@ namespace Discord } } else - channel = RemoveDMChannel(data.Recipient.Value.Id); + channel = RemovePrivateChannel(data.Id); + if (channel != null) await _channelDestroyedEvent.InvokeAsync(channel).ConfigureAwait(false); else diff --git a/src/Discord.Net/Entities/Channels/ChannelType.cs b/src/Discord.Net/Entities/Channels/ChannelType.cs index e6a3a1e00..f05f1598e 100644 --- a/src/Discord.Net/Entities/Channels/ChannelType.cs +++ b/src/Discord.Net/Entities/Channels/ChannelType.cs @@ -1,9 +1,10 @@ namespace Discord { - public enum ChannelType : byte + public enum ChannelType { - DM, - Text, - Voice + Text = 0, + DM = 1, + Voice = 2, + Group = 3 } } diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs index a5adc4284..858adfa7f 100644 --- a/src/Discord.Net/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -17,6 +17,7 @@ namespace Discord public IUser Recipient { get; private set; } public virtual IReadOnlyCollection CachedMessages => ImmutableArray.Create(); + IReadOnlyCollection 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() diff --git a/src/Discord.Net/Entities/Channels/GroupChannel.cs b/src/Discord.Net/Entities/Channels/GroupChannel.cs new file mode 100644 index 000000000..9b8526cfe --- /dev/null +++ b/src/Discord.Net/Entities/Channels/GroupChannel.cs @@ -0,0 +1,140 @@ +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 _users; + + public override DiscordClient Discord { get; } + public string Name { get; private set; } + + public IReadOnlyCollection Recipients => _users.ToReadOnlyCollection(); + public virtual IReadOnlyCollection CachedMessages => ImmutableArray.Create(); + + public GroupChannel(DiscordClient discord, ConcurrentDictionary recipients, Model model) + : base(model.Id) + { + Discord = discord; + _users = recipients; + + Update(model, UpdateSource.Creation); + } + public void Update(Model model, UpdateSource source) + { + if (source == UpdateSource.Rest && IsAttached) return; + + if (model.Name.IsSpecified) + Name = model.Name.Value; + + if (source != UpdateSource.Creation && model.Recipients.IsSpecified) + UpdateUsers(model.Recipients.Value, source); + } + + protected virtual void UpdateUsers(API.User[] models, UpdateSource source) + { + if (!IsAttached) + { + var users = new ConcurrentDictionary(1, (int)(models.Length * 1.05)); + for (int i = 0; i < models.Length; i++) + users[models[i].Id] = 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 GetUserAsync(ulong id) + { + IUser 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> GetUsersAsync() + { + var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); + return _users.Select(x => x.Value).Concat(ImmutableArray.Create(currentUser)).ToReadOnlyCollection(_users, 1); + } + + public async Task 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 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 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 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> 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> 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 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; + } +} diff --git a/src/Discord.Net/Entities/Channels/IDMChannel.cs b/src/Discord.Net/Entities/Channels/IDMChannel.cs index b6bbb39d6..a5a3a4168 100644 --- a/src/Discord.Net/Entities/Channels/IDMChannel.cs +++ b/src/Discord.Net/Entities/Channels/IDMChannel.cs @@ -2,7 +2,7 @@ namespace Discord { - public interface IDMChannel : IMessageChannel + public interface IDMChannel : IMessageChannel, IPrivateChannel { /// Gets the recipient of all messages in this channel. IUser Recipient { get; } diff --git a/src/Discord.Net/Entities/Channels/IGroupChannel.cs b/src/Discord.Net/Entities/Channels/IGroupChannel.cs new file mode 100644 index 000000000..209838568 --- /dev/null +++ b/src/Discord.Net/Entities/Channels/IGroupChannel.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Discord +{ + public interface IGroupChannel : IMessageChannel, IPrivateChannel + { + /// Leaves this group. + Task LeaveAsync(); + } +} \ No newline at end of file diff --git a/src/Discord.Net/Entities/Channels/IPrivateChannel.cs b/src/Discord.Net/Entities/Channels/IPrivateChannel.cs new file mode 100644 index 000000000..9fe7e2147 --- /dev/null +++ b/src/Discord.Net/Entities/Channels/IPrivateChannel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Discord +{ + public interface IPrivateChannel + { + IReadOnlyCollection Recipients { get; } + } +} diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index 1e400cce0..cc360813d 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -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}"); } } diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 5a9e19278..eb7989de1 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -141,7 +141,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; diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs index dae7cf92d..c3014d074 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs @@ -6,15 +6,16 @@ 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 new CachedPrivateUser Recipient => base.Recipient as CachedPrivateUser; public IReadOnlyCollection Members => ImmutableArray.Create(Discord.CurrentUser, Recipient); + IReadOnlyCollection ICachedPrivateChannel.Recipients => ImmutableArray.Create(Recipient); - public CachedDMChannel(DiscordSocketClient discord, CachedDMUser recipient, Model model) + public CachedDMChannel(DiscordSocketClient discord, CachedPrivateUser recipient, Model model) : base(discord, recipient, model) { if (Discord.MessageCacheSize > 0) diff --git a/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs new file mode 100644 index 000000000..4d71337ad --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs @@ -0,0 +1,80 @@ +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 Discord.API; + +namespace Discord +{ + internal class CachedGroupChannel : GroupChannel, IGroupChannel, ICachedChannel, ICachedMessageChannel + { + private readonly MessageManager _messages; + + public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + public IReadOnlyCollection Members + => _users.Select(x => x.Value).Concat(ImmutableArray.Create(Discord.CurrentUser)).Cast().ToReadOnlyCollection(_users, 1); + public new IReadOnlyCollection Recipients => _users.Cast().ToReadOnlyCollection(_users); + + public CachedGroupChannel(DiscordSocketClient discord, ConcurrentDictionary recipients, Model model) + : base(discord, recipients, model) + { + if (Discord.MessageCacheSize > 0) + _messages = new MessageCache(Discord, this); + else + _messages = new MessageManager(Discord, this); + } + + protected override void UpdateUsers(API.User[] models, UpdateSource source) + { + var users = new ConcurrentDictionary(1, models.Length); + for (int i = 0; i < models.Length; i++) + users[models[i].Id] = new CachedPrivateUser(Discord.GetOrAddUser(models[i], Discord.DataStore)); + _users = users; + } + + public override async Task GetMessageAsync(ulong id) + { + return await _messages.DownloadAsync(id).ConfigureAwait(false); + } + public override async Task> GetMessagesAsync(int limit) + { + return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); + } + public override async Task> 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) + { + IUser user; + if (_users.TryGetValue(id, out user)) + return user as ICachedUser; + if (id == Discord.CurrentUser.Id) + return Discord.CurrentUser; + return null; + } + ICachedChannel ICachedChannel.Clone() => Clone(); + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 569347d73..26bca90e3 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -311,14 +311,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}"); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs b/src/Discord.Net/Entities/WebSocket/CachedPrivateUser.cs similarity index 85% rename from src/Discord.Net/Entities/WebSocket/CachedDMUser.cs rename to src/Discord.Net/Entities/WebSocket/CachedPrivateUser.cs index 9cb5ffdb3..56e069c1b 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedPrivateUser.cs @@ -3,7 +3,7 @@ using PresenceModel = Discord.API.Presence; namespace Discord { - internal class CachedDMUser : ICachedUser + internal class CachedPrivateUser : ICachedUser { public CachedGlobalUser User { get; } @@ -24,7 +24,7 @@ namespace Discord public string NicknameMention => User.NicknameMention; public string Username => User.Username; - public CachedDMUser(CachedGlobalUser user) + public CachedPrivateUser(CachedGlobalUser user) { User = user; } @@ -34,7 +34,7 @@ namespace Discord User.Update(model, source); } - public CachedDMUser Clone() => MemberwiseClone() as CachedDMUser; + public CachedPrivateUser Clone() => MemberwiseClone() as CachedPrivateUser; ICachedUser ICachedUser.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/ICachedPrivateChannel.cs b/src/Discord.Net/Entities/WebSocket/ICachedPrivateChannel.cs new file mode 100644 index 000000000..79fba737a --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/ICachedPrivateChannel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Discord +{ + internal interface ICachedPrivateChannel : ICachedChannel, IPrivateChannel + { + new IReadOnlyCollection Recipients { get; } + } +} diff --git a/src/Discord.Net/Extensions/CollectionExtensions.cs b/src/Discord.Net/Extensions/CollectionExtensions.cs index 921379bfc..d2cef8a64 100644 --- a/src/Discord.Net/Extensions/CollectionExtensions.cs +++ b/src/Discord.Net/Extensions/CollectionExtensions.cs @@ -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 ToReadOnlyCollection(this IReadOnlyDictionary source) - => new ConcurrentDictionaryWrapper>(source, source.Select(x => x.Value)); - public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source) - => new ConcurrentDictionaryWrapper(source, query); + => new ConcurrentDictionaryWrapper(source.Select(x => x.Value), () => source.Count); + public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source, int countOffset = 0) + => new ConcurrentDictionaryWrapper(query, () => source.Count + countOffset); + public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, Func countFunc) + => new ConcurrentDictionaryWrapper(query, countFunc); } [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal struct ConcurrentDictionaryWrapper : IReadOnlyCollection + internal struct ConcurrentDictionaryWrapper : IReadOnlyCollection { - private readonly IReadOnlyCollection _source; private readonly IEnumerable _query; + private readonly Func _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 source, IEnumerable query) + public ConcurrentDictionaryWrapper(IEnumerable query, Func countFunc) { - _source = source; _query = query; + _countFunc = countFunc; } private string DebuggerDisplay => $"Count = {Count}"; diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index 2163c7dcd..4753a7a30 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -24,7 +24,7 @@ namespace Discord Task DisconnectAsync(); Task GetChannelAsync(ulong id); - Task> GetDMChannelsAsync(); + Task> GetPrivateChannelsAsync(); Task> GetConnectionsAsync(); diff --git a/src/Discord.Net/Net/Converters/ChannelTypeConverter.cs b/src/Discord.Net/Net/Converters/ChannelTypeConverter.cs deleted file mode 100644 index 48bcbd755..000000000 --- a/src/Discord.Net/Net/Converters/ChannelTypeConverter.cs +++ /dev/null @@ -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"); - } - } - } -} diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs index d0f51dc59..b79694cd0 100644 --- a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs @@ -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)) From f05fa6bb2e21631b6169a1a41e67b930f3f867f5 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 15 Jul 2016 20:09:52 -0300 Subject: [PATCH 02/12] Added ToString and DebuggerDisplay to CachedPrivateUser --- src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs | 1 - src/Discord.Net/Entities/WebSocket/CachedPrivateUser.cs | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs index 4d71337ad..d64812ae2 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading.Tasks; using MessageModel = Discord.API.Message; using Model = Discord.API.Channel; -using Discord.API; namespace Discord { diff --git a/src/Discord.Net/Entities/WebSocket/CachedPrivateUser.cs b/src/Discord.Net/Entities/WebSocket/CachedPrivateUser.cs index 56e069c1b..b920c22ce 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedPrivateUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedPrivateUser.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics; using PresenceModel = Discord.API.Presence; namespace Discord { + [DebuggerDisplay("{DebuggerDisplay,nq}")] internal class CachedPrivateUser : ICachedUser { public CachedGlobalUser User { get; } @@ -36,5 +38,8 @@ namespace Discord public CachedPrivateUser Clone() => MemberwiseClone() as CachedPrivateUser; ICachedUser ICachedUser.Clone() => Clone(); + + public override string ToString() => $"{Username}#{Discriminator}"; + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; } } From ba66469248dad3d9ca4f06a8ebeda8aef2297082 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 15 Jul 2016 22:12:14 -0300 Subject: [PATCH 03/12] Fixed User nullref errors --- src/Discord.Net/API/Common/User.cs | 14 +++++++------- src/Discord.Net/Data/DataStore.cs | 13 +++++++++++++ src/Discord.Net/DiscordSocketClient.cs | 1 - src/Discord.Net/Entities/Users/SelfUser.cs | 9 ++++++--- src/Discord.Net/Entities/Users/User.cs | 12 ++++++++---- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net/API/Common/User.cs b/src/Discord.Net/API/Common/User.cs index 9c4ff6911..8c379ac55 100644 --- a/src/Discord.Net/API/Common/User.cs +++ b/src/Discord.Net/API/Common/User.cs @@ -7,20 +7,20 @@ namespace Discord.API [JsonProperty("id")] public ulong Id { get; set; } [JsonProperty("username")] - public string Username { get; set; } + public Optional Username { get; set; } [JsonProperty("discriminator")] - public string Discriminator { get; set; } + public Optional Discriminator { get; set; } [JsonProperty("bot")] - public bool Bot { get; set; } + public Optional Bot { get; set; } [JsonProperty("avatar")] - public string Avatar { get; set; } + public Optional Avatar { get; set; } //CurrentUser [JsonProperty("verified")] - public bool Verified { get; set; } + public Optional Verified { get; set; } [JsonProperty("email")] - public string Email { get; set; } + public Optional Email { get; set; } [JsonProperty("mfa_enabled")] - public bool MfaEnabled { get; set; } + public Optional MfaEnabled { get; set; } } } diff --git a/src/Discord.Net/Data/DataStore.cs b/src/Discord.Net/Data/DataStore.cs index c767904b3..882fc7677 100644 --- a/src/Discord.Net/Data/DataStore.cs +++ b/src/Discord.Net/Data/DataStore.cs @@ -58,9 +58,16 @@ namespace Discord internal void AddChannel(ICachedChannel channel) { _channels[channel.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 ICachedChannel RemoveChannel(ulong id) { @@ -73,6 +80,12 @@ namespace Discord 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; diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 256fd9391..093b1fcc1 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -528,7 +528,6 @@ namespace Discord 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++) { var model = data.Guilds[i]; diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index 91cd08b92..e4f67b5d6 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -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() diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs index f1f14607c..501bfb354 100644 --- a/src/Discord.Net/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -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}"; From a13b10ed5df338f14522b5babb070521461d21cc Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 15 Jul 2016 22:12:34 -0300 Subject: [PATCH 04/12] Fixed group channels not being returned from GetPrivateChannelsAsync --- src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs index d64812ae2..da94a5b31 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs @@ -9,7 +9,7 @@ using Model = Discord.API.Channel; namespace Discord { - internal class CachedGroupChannel : GroupChannel, IGroupChannel, ICachedChannel, ICachedMessageChannel + internal class CachedGroupChannel : GroupChannel, IGroupChannel, ICachedChannel, ICachedMessageChannel, ICachedPrivateChannel { private readonly MessageManager _messages; From d94bd7373c47beb97b171f90249629895a50e6e0 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 15 Jul 2016 22:29:35 -0300 Subject: [PATCH 05/12] Added MessageType --- src/Discord.Net/API/Common/Message.cs | 2 ++ src/Discord.Net/Entities/Messages/IMessage.cs | 2 ++ src/Discord.Net/Entities/Messages/Message.cs | 10 ++++++---- .../{API/Common => Entities/Messages}/MessageType.cs | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) rename src/Discord.Net/{API/Common => Entities/Messages}/MessageType.cs (86%) diff --git a/src/Discord.Net/API/Common/Message.cs b/src/Discord.Net/API/Common/Message.cs index be5305114..54f9f986e 100644 --- a/src/Discord.Net/API/Common/Message.cs +++ b/src/Discord.Net/API/Common/Message.cs @@ -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")] diff --git a/src/Discord.Net/Entities/Messages/IMessage.cs b/src/Discord.Net/Entities/Messages/IMessage.cs index df620a516..ca9134557 100644 --- a/src/Discord.Net/Entities/Messages/IMessage.cs +++ b/src/Discord.Net/Entities/Messages/IMessage.cs @@ -18,6 +18,8 @@ namespace Discord /// Gets the time this message was sent. DateTimeOffset Timestamp { get; } + /// Gets the type of this message. + MessageType Type { get; } /// Gets the channel this message was sent to. IMessageChannel Channel { get; } /// Gets the author of this message. diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index db3929af3..77cf5ba04 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -14,14 +14,15 @@ namespace Discord private bool _isMentioningEveryone; private long _timestampTicks; private long? _editedTimestampTicks; + + public MessageType Type { get; } + public IMessageChannel Channel { get; } + public IUser Author { get; } public bool IsTTS { get; private set; } public string Text { get; private set; } public bool IsPinned { get; private set; } - - public IMessageChannel Channel { get; } - public IUser Author { get; } - + public IReadOnlyCollection Attachments { get; private set; } public IReadOnlyCollection Embeds { get; private set; } public IReadOnlyCollection MentionedChannelIds { get; private set; } @@ -37,6 +38,7 @@ namespace Discord { Channel = channel; Author = author; + Type = model.Type; if (channel is IGuildChannel) { diff --git a/src/Discord.Net/API/Common/MessageType.cs b/src/Discord.Net/Entities/Messages/MessageType.cs similarity index 86% rename from src/Discord.Net/API/Common/MessageType.cs rename to src/Discord.Net/Entities/Messages/MessageType.cs index 837c20cd2..9ab107d76 100644 --- a/src/Discord.Net/API/Common/MessageType.cs +++ b/src/Discord.Net/Entities/Messages/MessageType.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Common +namespace Discord { public enum MessageType { From da2002b520583df9fed6c3382d7bdf6c4113a74f Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 15 Jul 2016 22:34:27 -0300 Subject: [PATCH 06/12] Renamed Message.Text -> Message.Content --- src/Discord.Net.Commands/CommandService.cs | 4 ++-- src/Discord.Net.Commands/Extensions/MessageExtensions.cs | 6 +++--- src/Discord.Net/Entities/Messages/IMessage.cs | 4 ++-- src/Discord.Net/Entities/Messages/Message.cs | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 864b69a91..f762ae366 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -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 Execute(IMessage message, int argPos) => Execute(message, message.Text.Substring(argPos)); + public Task Execute(IMessage message, int argPos) => Execute(message, message.Content.Substring(argPos)); public async Task Execute(IMessage message, string input) { var searchResult = Search(message, input); diff --git a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs index f2dd3adfb..6618f4b5e 100644 --- a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs @@ -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)) { diff --git a/src/Discord.Net/Entities/Messages/IMessage.cs b/src/Discord.Net/Entities/Messages/IMessage.cs index ca9134557..e9077302b 100644 --- a/src/Discord.Net/Entities/Messages/IMessage.cs +++ b/src/Discord.Net/Entities/Messages/IMessage.cs @@ -13,8 +13,8 @@ namespace Discord bool IsTTS { get; } /// Returns true if this message was added to its channel's pinned messages. bool IsPinned { get; } - /// Returns the text for this message. - string Text { get; } + /// Returns the content for this message. + string Content { get; } /// Gets the time this message was sent. DateTimeOffset Timestamp { get; } diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index 77cf5ba04..186ca7308 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -20,7 +20,7 @@ namespace Discord public IUser Author { get; } public bool IsTTS { get; private set; } - public string Text { get; private set; } + public string Content { get; private set; } public bool IsPinned { get; private set; } public IReadOnlyCollection Attachments { get; private set; } @@ -120,7 +120,7 @@ namespace Discord MentionedChannelIds = MentionUtils.GetChannelMentions(text, guildChannel.Guild); MentionedRoles = MentionUtils.GetRoleMentions(text, guildChannel.Guild); } - Text = text; + Content = text; } } @@ -165,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) { @@ -181,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]" : "")}"; } } From 6815fdb5f263491ce2d7ec0cb4ac0d6bc0f4e140 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 15 Jul 2016 22:43:18 -0300 Subject: [PATCH 07/12] Added support for group channel icons --- src/Discord.Net/API/CDN.cs | 2 ++ src/Discord.Net/API/Common/Channel.cs | 6 +++++- src/Discord.Net/Entities/Channels/GroupChannel.cs | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/API/CDN.cs b/src/Discord.Net/API/CDN.cs index 260feba75..27f0864cd 100644 --- a/src/Discord.Net/API/CDN.cs +++ b/src/Discord.Net/API/CDN.cs @@ -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; } } diff --git a/src/Discord.Net/API/Common/Channel.cs b/src/Discord.Net/API/Common/Channel.cs index 99fa87d87..44c59c1ad 100644 --- a/src/Discord.Net/API/Common/Channel.cs +++ b/src/Discord.Net/API/Common/Channel.cs @@ -35,8 +35,12 @@ namespace Discord.API [JsonProperty("user_limit")] public Optional UserLimit { get; set; } - //DMChannel + //PrivateChannel [JsonProperty("recipients")] public Optional Recipients { get; set; } + + //GroupChannel + [JsonProperty("icon")] + public Optional Icon { get; set; } } } diff --git a/src/Discord.Net/Entities/Channels/GroupChannel.cs b/src/Discord.Net/Entities/Channels/GroupChannel.cs index 9b8526cfe..65dad771c 100644 --- a/src/Discord.Net/Entities/Channels/GroupChannel.cs +++ b/src/Discord.Net/Entities/Channels/GroupChannel.cs @@ -16,12 +16,14 @@ namespace Discord internal class GroupChannel : SnowflakeEntity, IGroupChannel { protected ConcurrentDictionary _users; + private string _iconId; public override DiscordClient Discord { get; } public string Name { get; private set; } public IReadOnlyCollection Recipients => _users.ToReadOnlyCollection(); public virtual IReadOnlyCollection CachedMessages => ImmutableArray.Create(); + public string IconUrl => API.CDN.GetChannelIconUrl(Id, _iconId); public GroupChannel(DiscordClient discord, ConcurrentDictionary recipients, Model model) : base(model.Id) @@ -37,6 +39,8 @@ namespace Discord 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); From c5f291f214cde06e09d5e56c9cfde0a4e815ac91 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 15 Jul 2016 22:47:31 -0300 Subject: [PATCH 08/12] Added more guild sync checks --- src/Discord.Net/DiscordSocketClient.cs | 81 ++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 093b1fcc1..9ac32f122 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -708,7 +708,15 @@ namespace Discord { 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); @@ -732,6 +740,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 @@ -751,7 +766,15 @@ namespace Discord { 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); @@ -781,6 +804,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 @@ -799,6 +829,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(); @@ -827,6 +864,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); @@ -880,6 +924,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 @@ -902,6 +952,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 @@ -927,7 +984,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); @@ -950,7 +1015,15 @@ namespace Discord var data = (payload as JToken).ToObject(_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); @@ -965,7 +1038,15 @@ namespace Discord var data = (payload as JToken).ToObject(_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); From 432e8d1e0fb54aa83a8a99415152f3a9dbff2de8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 16 Jul 2016 10:23:57 -0300 Subject: [PATCH 09/12] Added voice states to group channels --- src/Discord.Net/DiscordSocketClient.Events.cs | 4 +- src/Discord.Net/DiscordSocketClient.cs | 65 +++++++++++++++------- src/Discord.Net/Entities/Channels/GroupChannel.cs | 6 +- .../Entities/WebSocket/CachedGroupChannel.cs | 43 +++++++++++++- src/Discord.Net/Extensions/CollectionExtensions.cs | 4 +- 5 files changed, 95 insertions(+), 27 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.Events.cs b/src/Discord.Net/DiscordSocketClient.Events.cs index 092b19674..698a940e4 100644 --- a/src/Discord.Net/DiscordSocketClient.Events.cs +++ b/src/Discord.Net/DiscordSocketClient.Events.cs @@ -167,12 +167,12 @@ namespace Discord remove { _userPresenceUpdatedEvent.Remove(value); } } private readonly AsyncEvent> _userPresenceUpdatedEvent = new AsyncEvent>(); - public event Func UserVoiceStateUpdated + public event Func UserVoiceStateUpdated { add { _userVoiceStateUpdatedEvent.Add(value); } remove { _userVoiceStateUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); + private readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); public event Func CurrentUserUpdated { add { _selfUpdatedEvent.Add(value); } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 9ac32f122..7375d1088 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -1280,39 +1280,66 @@ namespace Discord var data = (payload as JToken).ToObject(_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; } } diff --git a/src/Discord.Net/Entities/Channels/GroupChannel.cs b/src/Discord.Net/Entities/Channels/GroupChannel.cs index 65dad771c..a11b58800 100644 --- a/src/Discord.Net/Entities/Channels/GroupChannel.cs +++ b/src/Discord.Net/Entities/Channels/GroupChannel.cs @@ -30,10 +30,10 @@ namespace Discord { Discord = discord; _users = recipients; - + Update(model, UpdateSource.Creation); } - public void Update(Model model, UpdateSource source) + public virtual void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; @@ -41,7 +41,7 @@ namespace Discord Name = model.Name.Value; if (model.Icon.IsSpecified) _iconId = model.Icon.Value; - + if (source != UpdateSource.Creation && model.Recipients.IsSpecified) UpdateUsers(model.Recipients.Value, source); } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs index da94a5b31..5320727fa 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs @@ -6,16 +6,18 @@ using System.Linq; using System.Threading.Tasks; using MessageModel = Discord.API.Message; using Model = Discord.API.Channel; +using VoiceStateModel = Discord.API.VoiceState; namespace Discord { internal class CachedGroupChannel : GroupChannel, IGroupChannel, ICachedChannel, ICachedMessageChannel, ICachedPrivateChannel { private readonly MessageManager _messages; + private ConcurrentDictionary _voiceStates; public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public IReadOnlyCollection Members - => _users.Select(x => x.Value).Concat(ImmutableArray.Create(Discord.CurrentUser)).Cast().ToReadOnlyCollection(_users, 1); + => _users.Select(x => x.Value).Concat(ImmutableArray.Create(Discord.CurrentUser)).Cast().ToReadOnlyCollection(() => _users.Count + 1); public new IReadOnlyCollection Recipients => _users.Cast().ToReadOnlyCollection(_users); public CachedGroupChannel(DiscordSocketClient discord, ConcurrentDictionary recipients, Model model) @@ -25,6 +27,13 @@ namespace Discord _messages = new MessageCache(Discord, this); else _messages = new MessageManager(Discord, this); + _voiceStates = new ConcurrentDictionary(1, 5); + } + public override void Update(Model model, UpdateSource source) + { + if (source == UpdateSource.Rest && IsAttached) return; + + base.Update(model, source); } protected override void UpdateUsers(API.User[] models, UpdateSource source) @@ -35,6 +44,38 @@ namespace Discord _users = users; } + public ICachedUser GetUser(ulong id) + { + IUser user; + if (_users.TryGetValue(id, out user)) + return user as ICachedUser; + if (id == Discord.CurrentUser.Id) + return Discord.CurrentUser; + return null; + } + + public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary 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 GetMessageAsync(ulong id) { return await _messages.DownloadAsync(id).ConfigureAwait(false); diff --git a/src/Discord.Net/Extensions/CollectionExtensions.cs b/src/Discord.Net/Extensions/CollectionExtensions.cs index d2cef8a64..91c9f030f 100644 --- a/src/Discord.Net/Extensions/CollectionExtensions.cs +++ b/src/Discord.Net/Extensions/CollectionExtensions.cs @@ -10,8 +10,8 @@ namespace Discord.Extensions { public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyDictionary source) => new ConcurrentDictionaryWrapper(source.Select(x => x.Value), () => source.Count); - public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source, int countOffset = 0) - => new ConcurrentDictionaryWrapper(query, () => source.Count + countOffset); + public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source) + => new ConcurrentDictionaryWrapper(query, () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, Func countFunc) => new ConcurrentDictionaryWrapper(query, countFunc); } From d0c5eab2dd30d5ea12b68f8de09a3c01a6400fc2 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 16 Jul 2016 10:24:30 -0300 Subject: [PATCH 10/12] Minor fix --- src/Discord.Net/Entities/Channels/GroupChannel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/Entities/Channels/GroupChannel.cs b/src/Discord.Net/Entities/Channels/GroupChannel.cs index a11b58800..0fda28f9d 100644 --- a/src/Discord.Net/Entities/Channels/GroupChannel.cs +++ b/src/Discord.Net/Entities/Channels/GroupChannel.cs @@ -82,7 +82,7 @@ namespace Discord public async Task> GetUsersAsync() { var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); - return _users.Select(x => x.Value).Concat(ImmutableArray.Create(currentUser)).ToReadOnlyCollection(_users, 1); + return _users.Select(x => x.Value).Concat(ImmutableArray.Create(currentUser)).ToReadOnlyCollection(_users); } public async Task SendMessageAsync(string text, bool isTTS) From af7ee37e44ce87f1a77796a0c9561df9c404b8c9 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 18 Jul 2016 13:37:57 -0300 Subject: [PATCH 11/12] Added IGroupUser, GroupChannel.Add/Remove Recipient, Add/Remove Recipient events --- src/Discord.Net/API/DiscordAPIClient.cs | 17 +++ src/Discord.Net/API/Gateway/RecipientEvent.cs | 12 +++ src/Discord.Net/DiscordClient.cs | 8 +- src/Discord.Net/DiscordSocketClient.Events.cs | 12 +++ src/Discord.Net/DiscordSocketClient.cs | 117 ++++++++++++++++----- src/Discord.Net/Entities/Channels/GroupChannel.cs | 19 ++-- src/Discord.Net/Entities/Channels/IGroupChannel.cs | 3 + src/Discord.Net/Entities/Users/GroupUser.cs | 47 +++++++++ src/Discord.Net/Entities/Users/GuildUser.cs | 1 - src/Discord.Net/Entities/Users/IGroupUser.cs | 13 +++ .../Entities/WebSocket/CachedDMChannel.cs | 6 +- .../{CachedPrivateUser.cs => CachedDMUser.cs} | 6 +- .../Entities/WebSocket/CachedGroupChannel.cs | 52 +++++---- .../Entities/WebSocket/CachedGroupUser.cs | 33 ++++++ src/Discord.Net/Entities/WebSocket/CachedGuild.cs | 1 - .../Entities/WebSocket/ICachedPrivateChannel.cs | 2 +- 16 files changed, 282 insertions(+), 67 deletions(-) create mode 100644 src/Discord.Net/API/Gateway/RecipientEvent.cs create mode 100644 src/Discord.Net/Entities/Users/GroupUser.cs create mode 100644 src/Discord.Net/Entities/Users/IGroupUser.cs rename src/Discord.Net/Entities/WebSocket/{CachedPrivateUser.cs => CachedDMUser.cs} (87%) create mode 100644 src/Discord.Net/Entities/WebSocket/CachedGroupUser.cs diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index c5dc1c83b..5f5486780 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -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 GetGuildAsync(ulong guildId, RequestOptions options = null) { diff --git a/src/Discord.Net/API/Gateway/RecipientEvent.cs b/src/Discord.Net/API/Gateway/RecipientEvent.cs new file mode 100644 index 000000000..6cac8f27f --- /dev/null +++ b/src/Discord.Net/API/Gateway/RecipientEvent.cs @@ -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; } + } +} diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 8bb10063a..48bc52f15 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -170,11 +170,9 @@ namespace Discord return new DMChannel(this, new User(model.Recipients.Value[0]), model); else if (model.Type == ChannelType.Group) { - var recipients = model.Recipients.Value; - var users = new ConcurrentDictionary(1, recipients.Length); - for (int i = 0; i < recipients.Length; i++) - users[recipients[i].Id] = new User(recipients[i]); - return new GroupChannel(this, users, model); + var channel = new GroupChannel(this, model); + channel.UpdateUsers(model.Recipients.Value, UpdateSource.Creation); + return channel; } else throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); diff --git a/src/Discord.Net/DiscordSocketClient.Events.cs b/src/Discord.Net/DiscordSocketClient.Events.cs index 698a940e4..7182a7070 100644 --- a/src/Discord.Net/DiscordSocketClient.Events.cs +++ b/src/Discord.Net/DiscordSocketClient.Events.cs @@ -185,6 +185,18 @@ namespace Discord remove { _userIsTypingEvent.Remove(value); } } private readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); + public event Func RecipientAdded + { + add { _recipientAddedEvent.Add(value); } + remove { _recipientAddedEvent.Remove(value); } + } + private readonly AsyncEvent> _recipientAddedEvent = new AsyncEvent>(); + public event Func RecipientRemoved + { + add { _recipientRemovedEvent.Add(value); } + remove { _recipientRemovedEvent.Remove(value); } + } + private readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); //TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected; } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 7375d1088..4b2d42882 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -147,6 +147,7 @@ namespace Discord ConnectionState = ConnectionState.Connecting; await _gatewayLogger.InfoAsync("Connecting").ConfigureAwait(false); + try { _connectTask = new TaskCompletionSource(); @@ -160,7 +161,6 @@ namespace Discord await ApiClient.SendIdentifyAsync().ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); - ConnectionState = ConnectionState.Connected; await _gatewayLogger.InfoAsync("Connected").ConfigureAwait(false); } @@ -173,6 +173,7 @@ namespace Discord /// public async Task DisconnectAsync() { + if (_connectTask?.TrySetCanceled() ?? false) return; await _connectionLock.WaitAsync().ConfigureAwait(false); try { @@ -181,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; @@ -340,17 +352,14 @@ namespace Discord { var recipients = model.Recipients.Value; var user = GetOrAddUser(recipients[0], dataStore); - var channel = new CachedDMChannel(this, new CachedPrivateUser(user), model); + var channel = new CachedDMChannel(this, new CachedDMUser(user), model); dataStore.AddChannel(channel); return channel; } case ChannelType.Group: { - var recipients = model.Recipients.Value; - var users = new ConcurrentDictionary(1, recipients.Length); - for (int i = 0; i < recipients.Length; i++) - users[recipients[i].Id] = new CachedPrivateUser(GetOrAddUser(recipients[i], dataStore)); - var channel = new CachedGroupChannel(this, users, model); + var channel = new CachedGroupChannel(this, model); + channel.UpdateUsers(model.Recipients.Value, UpdateSource.Creation); dataStore.AddChannel(channel); return channel; } @@ -521,34 +530,43 @@ namespace Discord //Connection case "READY": { - await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_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(_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 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); + + _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++) - AddPrivateChannel(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); } @@ -913,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(_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(_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": diff --git a/src/Discord.Net/Entities/Channels/GroupChannel.cs b/src/Discord.Net/Entities/Channels/GroupChannel.cs index 0fda28f9d..0e9347166 100644 --- a/src/Discord.Net/Entities/Channels/GroupChannel.cs +++ b/src/Discord.Net/Entities/Channels/GroupChannel.cs @@ -15,7 +15,7 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] internal class GroupChannel : SnowflakeEntity, IGroupChannel { - protected ConcurrentDictionary _users; + protected ConcurrentDictionary _users; private string _iconId; public override DiscordClient Discord { get; } @@ -25,11 +25,10 @@ namespace Discord public virtual IReadOnlyCollection CachedMessages => ImmutableArray.Create(); public string IconUrl => API.CDN.GetChannelIconUrl(Id, _iconId); - public GroupChannel(DiscordClient discord, ConcurrentDictionary recipients, Model model) + public GroupChannel(DiscordClient discord, Model model) : base(model.Id) { Discord = discord; - _users = recipients; Update(model, UpdateSource.Creation); } @@ -46,13 +45,13 @@ namespace Discord UpdateUsers(model.Recipients.Value, source); } - protected virtual void UpdateUsers(API.User[] models, UpdateSource source) + internal virtual void UpdateUsers(API.User[] models, UpdateSource source) { if (!IsAttached) { - var users = new ConcurrentDictionary(1, (int)(models.Length * 1.05)); + var users = new ConcurrentDictionary(1, (int)(models.Length * 1.05)); for (int i = 0; i < models.Length; i++) - users[models[i].Id] = new User(models[i]); + users[models[i].Id] = new GroupUser(this, new User(models[i])); _users = users; } } @@ -69,9 +68,13 @@ namespace Discord 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 GetUserAsync(ulong id) { - IUser user; + GroupUser user; if (_users.TryGetValue(id, out user)) return user; var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); @@ -82,7 +85,7 @@ namespace Discord public async Task> GetUsersAsync() { var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); - return _users.Select(x => x.Value).Concat(ImmutableArray.Create(currentUser)).ToReadOnlyCollection(_users); + return _users.Select(x => x.Value).Concat(ImmutableArray.Create(currentUser)).ToReadOnlyCollection(_users); } public async Task SendMessageAsync(string text, bool isTTS) diff --git a/src/Discord.Net/Entities/Channels/IGroupChannel.cs b/src/Discord.Net/Entities/Channels/IGroupChannel.cs index 209838568..6b71f76b6 100644 --- a/src/Discord.Net/Entities/Channels/IGroupChannel.cs +++ b/src/Discord.Net/Entities/Channels/IGroupChannel.cs @@ -4,6 +4,9 @@ namespace Discord { public interface IGroupChannel : IMessageChannel, IPrivateChannel { + /// Adds a user to this group. + Task AddUserAsync(IUser user); + /// Leaves this group. Task LeaveAsync(); } diff --git a/src/Discord.Net/Entities/Users/GroupUser.cs b/src/Discord.Net/Entities/Users/GroupUser.cs new file mode 100644 index 000000000..af32e4395 --- /dev/null +++ b/src/Discord.Net/Entities/Users/GroupUser.cs @@ -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 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); + } + } +} diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index eb7989de1..60b1b6e38 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -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 { diff --git a/src/Discord.Net/Entities/Users/IGroupUser.cs b/src/Discord.Net/Entities/Users/IGroupUser.cs new file mode 100644 index 000000000..8ed53616c --- /dev/null +++ b/src/Discord.Net/Entities/Users/IGroupUser.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace Discord +{ + public interface IGroupUser : IUser + { + /// Kicks this user from this group. + Task KickAsync(); + + /// Returns a private message channel to this user, creating one if it does not already exist. + Task CreateDMChannelAsync(); + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs index c3014d074..c22aa645b 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs @@ -11,11 +11,11 @@ namespace Discord private readonly MessageManager _messages; public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new CachedPrivateUser Recipient => base.Recipient as CachedPrivateUser; + public new CachedDMUser Recipient => base.Recipient as CachedDMUser; public IReadOnlyCollection Members => ImmutableArray.Create(Discord.CurrentUser, Recipient); - IReadOnlyCollection ICachedPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + IReadOnlyCollection ICachedPrivateChannel.Recipients => ImmutableArray.Create(Recipient); - public CachedDMChannel(DiscordSocketClient discord, CachedPrivateUser recipient, Model model) + public CachedDMChannel(DiscordSocketClient discord, CachedDMUser recipient, Model model) : base(discord, recipient, model) { if (Discord.MessageCacheSize > 0) diff --git a/src/Discord.Net/Entities/WebSocket/CachedPrivateUser.cs b/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs similarity index 87% rename from src/Discord.Net/Entities/WebSocket/CachedPrivateUser.cs rename to src/Discord.Net/Entities/WebSocket/CachedDMUser.cs index b920c22ce..c25b8ac7a 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedPrivateUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs @@ -5,7 +5,7 @@ using PresenceModel = Discord.API.Presence; namespace Discord { [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal class CachedPrivateUser : ICachedUser + internal class CachedDMUser : ICachedUser { public CachedGlobalUser User { get; } @@ -26,7 +26,7 @@ namespace Discord public string NicknameMention => User.NicknameMention; public string Username => User.Username; - public CachedPrivateUser(CachedGlobalUser user) + public CachedDMUser(CachedGlobalUser user) { User = user; } @@ -36,7 +36,7 @@ namespace Discord User.Update(model, source); } - public CachedPrivateUser Clone() => MemberwiseClone() as CachedPrivateUser; + public CachedDMUser Clone() => MemberwiseClone() as CachedDMUser; ICachedUser ICachedUser.Clone() => Clone(); public override string ToString() => $"{Username}#{Discriminator}"; diff --git a/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs index 5320727fa..b8f7606a1 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs @@ -6,6 +6,7 @@ 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; namespace Discord @@ -17,11 +18,11 @@ namespace Discord public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public IReadOnlyCollection Members - => _users.Select(x => x.Value).Concat(ImmutableArray.Create(Discord.CurrentUser)).Cast().ToReadOnlyCollection(() => _users.Count + 1); - public new IReadOnlyCollection Recipients => _users.Cast().ToReadOnlyCollection(_users); + => _users.Select(x => x.Value as ICachedUser).Concat(ImmutableArray.Create(Discord.CurrentUser)).ToReadOnlyCollection(() => _users.Count + 1); + public new IReadOnlyCollection Recipients => _users.Cast().ToReadOnlyCollection(_users); - public CachedGroupChannel(DiscordSocketClient discord, ConcurrentDictionary recipients, Model model) - : base(discord, recipients, model) + public CachedGroupChannel(DiscordSocketClient discord, Model model) + : base(discord, model) { if (Discord.MessageCacheSize > 0) _messages = new MessageCache(Discord, this); @@ -36,23 +37,46 @@ namespace Discord base.Update(model, source); } - protected override void UpdateUsers(API.User[] models, UpdateSource source) + internal override void UpdateUsers(UserModel[] models, UpdateSource source) { - var users = new ConcurrentDictionary(1, models.Length); + var users = new ConcurrentDictionary(1, models.Length); for (int i = 0; i < models.Length; i++) - users[models[i].Id] = new CachedPrivateUser(Discord.GetOrAddUser(models[i], Discord.DataStore)); + { + var globalUser = Discord.GetOrAddUser(models[i], Discord.DataStore); + users[models[i].Id] = new CachedGroupUser(this, globalUser); + } _users = users; } + 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) { - IUser user; + GroupUser user; if (_users.TryGetValue(id, out user)) - return user as ICachedUser; + 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 voiceStates = null) { @@ -106,15 +130,7 @@ namespace Discord public CachedDMChannel Clone() => MemberwiseClone() as CachedDMChannel; IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); - ICachedUser ICachedMessageChannel.GetUser(ulong id, bool skipCheck) - { - IUser user; - if (_users.TryGetValue(id, out user)) - return user as ICachedUser; - if (id == Discord.CurrentUser.Id) - return Discord.CurrentUser; - return null; - } + ICachedUser ICachedMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id); ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGroupUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGroupUser.cs new file mode 100644 index 000000000..8735bbd11 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedGroupUser.cs @@ -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})"; + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 26bca90e3..da90f9a6f 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -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; diff --git a/src/Discord.Net/Entities/WebSocket/ICachedPrivateChannel.cs b/src/Discord.Net/Entities/WebSocket/ICachedPrivateChannel.cs index 79fba737a..3b9081521 100644 --- a/src/Discord.Net/Entities/WebSocket/ICachedPrivateChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/ICachedPrivateChannel.cs @@ -4,6 +4,6 @@ namespace Discord { internal interface ICachedPrivateChannel : ICachedChannel, IPrivateChannel { - new IReadOnlyCollection Recipients { get; } + new IReadOnlyCollection Recipients { get; } } } From 506c857963f6fe5e18c37a98da7d4d192daf163c Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 18 Jul 2016 13:47:44 -0300 Subject: [PATCH 12/12] Fixed null DataStore during GroupChannel update --- src/Discord.Net/DiscordSocketClient.cs | 2 +- src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 4b2d42882..973357625 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -359,7 +359,7 @@ namespace Discord case ChannelType.Group: { var channel = new CachedGroupChannel(this, model); - channel.UpdateUsers(model.Recipients.Value, UpdateSource.Creation); + channel.UpdateUsers(model.Recipients.Value, UpdateSource.Creation, dataStore); dataStore.AddChannel(channel); return channel; } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs index b8f7606a1..6b217d217 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs @@ -8,6 +8,7 @@ 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 { @@ -37,16 +38,18 @@ namespace Discord base.Update(model, source); } - internal override void UpdateUsers(UserModel[] models, UpdateSource source) + internal void UpdateUsers(UserModel[] models, UpdateSource source, DataStore dataStore) { var users = new ConcurrentDictionary(1, models.Length); for (int i = 0; i < models.Length; i++) { - var globalUser = Discord.GetOrAddUser(models[i], Discord.DataStore); + 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) {