From bd96e473a9e4314fa226f0bcd9c8644de8d71a20 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 15 Jul 2016 19:54:23 -0300 Subject: [PATCH] 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 +++++--- .../Entities/Channels/ChannelType.cs | 9 +- .../Entities/Channels/DMChannel.cs | 3 +- .../Entities/Channels/GroupChannel.cs | 140 ++++++++++++++++++ .../Entities/Channels/IDMChannel.cs | 2 +- .../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 ++++++++++ .../Entities/WebSocket/CachedGuild.cs | 4 +- .../{CachedDMUser.cs => CachedPrivateUser.cs} | 6 +- .../WebSocket/ICachedPrivateChannel.cs | 9 ++ .../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))