| @@ -1,4 +1,5 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| @@ -7,8 +8,8 @@ namespace Discord.API | |||||
| //Shared | //Shared | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public ulong Id { get; set; } | public ulong Id { get; set; } | ||||
| [JsonProperty("is_private")] | |||||
| public bool IsPrivate { get; set; } | |||||
| [JsonProperty("type")] | |||||
| public ChannelType Type { get; set; } | |||||
| [JsonProperty("last_message_id")] | [JsonProperty("last_message_id")] | ||||
| public ulong? LastMessageId { get; set; } | public ulong? LastMessageId { get; set; } | ||||
| @@ -17,8 +18,6 @@ namespace Discord.API | |||||
| public Optional<ulong> GuildId { get; set; } | public Optional<ulong> GuildId { get; set; } | ||||
| [JsonProperty("name")] | [JsonProperty("name")] | ||||
| public Optional<string> Name { get; set; } | public Optional<string> Name { get; set; } | ||||
| [JsonProperty("type")] | |||||
| public Optional<ChannelType> Type { get; set; } | |||||
| [JsonProperty("position")] | [JsonProperty("position")] | ||||
| public Optional<int> Position { get; set; } | public Optional<int> Position { get; set; } | ||||
| [JsonProperty("permission_overwrites")] | [JsonProperty("permission_overwrites")] | ||||
| @@ -27,6 +26,8 @@ namespace Discord.API | |||||
| //TextChannel | //TextChannel | ||||
| [JsonProperty("topic")] | [JsonProperty("topic")] | ||||
| public Optional<string> Topic { get; set; } | public Optional<string> Topic { get; set; } | ||||
| [JsonProperty("last_pin_timestamp")] | |||||
| public Optional<DateTimeOffset?> LastPinTimestamp { get; set; } | |||||
| //VoiceChannel | //VoiceChannel | ||||
| [JsonProperty("bitrate")] | [JsonProperty("bitrate")] | ||||
| @@ -35,7 +36,7 @@ namespace Discord.API | |||||
| public Optional<int> UserLimit { get; set; } | public Optional<int> UserLimit { get; set; } | ||||
| //DMChannel | //DMChannel | ||||
| [JsonProperty("recipient")] | |||||
| public Optional<User> Recipient { get; set; } | |||||
| [JsonProperty("recipients")] | |||||
| public Optional<User[]> Recipients { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,12 @@ | |||||
| namespace Discord.API.Common | |||||
| { | |||||
| public enum MessageType | |||||
| { | |||||
| Default = 0, | |||||
| RecipientAdd = 1, | |||||
| RecipientRemove = 2, | |||||
| Call = 3, | |||||
| ChannelNameChange = 4, | |||||
| ChannelIconChange = 5 | |||||
| } | |||||
| } | |||||
| @@ -211,7 +211,7 @@ namespace Discord.API | |||||
| if (_gatewayUrl == null) | if (_gatewayUrl == null) | ||||
| { | { | ||||
| var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false); | 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); | await _gatewayClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false); | ||||
| @@ -1160,7 +1160,7 @@ namespace Discord.API | |||||
| { | { | ||||
| return await SendAsync<IReadOnlyCollection<Connection>>("GET", "users/@me/connections", options: options).ConfigureAwait(false); | return await SendAsync<IReadOnlyCollection<Connection>>("GET", "users/@me/connections", options: options).ConfigureAwait(false); | ||||
| } | } | ||||
| public async Task<IReadOnlyCollection<Channel>> GetMyDMsAsync(RequestOptions options = null) | |||||
| public async Task<IReadOnlyCollection<Channel>> GetMyPrivateChannelsAsync(RequestOptions options = null) | |||||
| { | { | ||||
| return await SendAsync<IReadOnlyCollection<Channel>>("GET", "users/@me/channels", options: options).ConfigureAwait(false); | return await SendAsync<IReadOnlyCollection<Channel>>("GET", "users/@me/channels", options: options).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -2,6 +2,7 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -16,12 +17,19 @@ namespace Discord | |||||
| private readonly ConcurrentDictionary<ulong, CachedDMChannel> _dmChannels; | private readonly ConcurrentDictionary<ulong, CachedDMChannel> _dmChannels; | ||||
| private readonly ConcurrentDictionary<ulong, CachedGuild> _guilds; | private readonly ConcurrentDictionary<ulong, CachedGuild> _guilds; | ||||
| private readonly ConcurrentDictionary<ulong, CachedGlobalUser> _users; | private readonly ConcurrentDictionary<ulong, CachedGlobalUser> _users; | ||||
| private readonly ConcurrentHashSet<ulong> _groupChannels; | |||||
| internal IReadOnlyCollection<ICachedChannel> Channels => _channels.ToReadOnlyCollection(); | internal IReadOnlyCollection<ICachedChannel> Channels => _channels.ToReadOnlyCollection(); | ||||
| internal IReadOnlyCollection<CachedDMChannel> DMChannels => _dmChannels.ToReadOnlyCollection(); | internal IReadOnlyCollection<CachedDMChannel> DMChannels => _dmChannels.ToReadOnlyCollection(); | ||||
| internal IReadOnlyCollection<CachedGroupChannel> GroupChannels => _groupChannels.Select(x => GetChannel(x) as CachedGroupChannel).ToReadOnlyCollection(_groupChannels); | |||||
| internal IReadOnlyCollection<CachedGuild> Guilds => _guilds.ToReadOnlyCollection(); | internal IReadOnlyCollection<CachedGuild> Guilds => _guilds.ToReadOnlyCollection(); | ||||
| internal IReadOnlyCollection<CachedGlobalUser> Users => _users.ToReadOnlyCollection(); | internal IReadOnlyCollection<CachedGlobalUser> Users => _users.ToReadOnlyCollection(); | ||||
| internal IReadOnlyCollection<ICachedPrivateChannel> PrivateChannels => | |||||
| _dmChannels.Select(x => x.Value as ICachedPrivateChannel).Concat( | |||||
| _groupChannels.Select(x => GetChannel(x) as ICachedPrivateChannel)) | |||||
| .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); | |||||
| public DataStore(int guildCount, int dmChannelCount) | public DataStore(int guildCount, int dmChannelCount) | ||||
| { | { | ||||
| double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; | double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; | ||||
| @@ -30,6 +38,7 @@ namespace Discord | |||||
| _dmChannels = new ConcurrentDictionary<ulong, CachedDMChannel>(CollectionConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier)); | _dmChannels = new ConcurrentDictionary<ulong, CachedDMChannel>(CollectionConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier)); | ||||
| _guilds = new ConcurrentDictionary<ulong, CachedGuild>(CollectionConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); | _guilds = new ConcurrentDictionary<ulong, CachedGuild>(CollectionConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); | ||||
| _users = new ConcurrentDictionary<ulong, CachedGlobalUser>(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); | _users = new ConcurrentDictionary<ulong, CachedGlobalUser>(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); | ||||
| _groupChannels = new ConcurrentHashSet<ulong>(CollectionConcurrencyLevel, (int)(10 * CollectionMultiplier)); | |||||
| } | } | ||||
| internal ICachedChannel GetChannel(ulong id) | internal ICachedChannel GetChannel(ulong id) | ||||
| @@ -39,18 +48,6 @@ namespace Discord | |||||
| return channel; | return channel; | ||||
| return null; | 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) | internal CachedDMChannel GetDMChannel(ulong userId) | ||||
| { | { | ||||
| CachedDMChannel channel; | CachedDMChannel channel; | ||||
| @@ -58,19 +55,25 @@ namespace Discord | |||||
| return channel; | return channel; | ||||
| return null; | return null; | ||||
| } | } | ||||
| internal void AddDMChannel(CachedDMChannel channel) | |||||
| internal void AddChannel(ICachedChannel channel) | |||||
| { | { | ||||
| _channels[channel.Id] = 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; | return null; | ||||
| } | } | ||||
| @@ -10,6 +10,7 @@ using System.Linq; | |||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using System.Runtime.InteropServices; | using System.Runtime.InteropServices; | ||||
| using System.Collections.Concurrent; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -165,16 +166,26 @@ namespace Discord | |||||
| return guild.ToChannel(model); | 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<ulong, IUser>(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 | else | ||||
| return new DMChannel(this, new User(model.Recipient.Value), model); | |||||
| throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | |||||
| } | } | ||||
| return null; | return null; | ||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual async Task<IReadOnlyCollection<IDMChannel>> GetDMChannelsAsync() | |||||
| public virtual async Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync() | |||||
| { | { | ||||
| var models = await ApiClient.GetMyDMsAsync().ConfigureAwait(false); | |||||
| return models.Select(x => new DMChannel(this, new User(x.Recipient.Value), x)).ToImmutableArray(); | |||||
| var models = await ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); | |||||
| return models.Select(x => new DMChannel(this, new User(x.Recipients.Value[0]), x)).ToImmutableArray(); | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -289,9 +300,9 @@ namespace Discord | |||||
| private async Task WriteInitialLog() | private async Task WriteInitialLog() | ||||
| { | { | ||||
| if (this is DiscordSocketClient) | 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 | 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($"Runtime: {RuntimeInformation.FrameworkDescription.Trim()} ({ToArchString(RuntimeInformation.ProcessArchitecture)})").ConfigureAwait(false); | ||||
| await _clientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false); | await _clientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false); | ||||
| await _clientLogger.VerboseAsync($"Processors: {Environment.ProcessorCount}").ConfigureAwait(false); | await _clientLogger.VerboseAsync($"Processors: {Environment.ProcessorCount}").ConfigureAwait(false); | ||||
| @@ -8,10 +8,10 @@ namespace Discord | |||||
| public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; | public static string 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 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 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 CDNUrl = "https://cdn.discordapp.com/"; | ||||
| public const string InviteUrl = "https://discord.gg/"; | public const string InviteUrl = "https://discord.gg/"; | ||||
| @@ -57,7 +57,6 @@ namespace Discord | |||||
| internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; | internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; | ||||
| internal IReadOnlyCollection<CachedGuild> Guilds => DataStore.Guilds; | internal IReadOnlyCollection<CachedGuild> Guilds => DataStore.Guilds; | ||||
| internal IReadOnlyCollection<CachedDMChannel> DMChannels => DataStore.DMChannels; | |||||
| internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection(); | internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection(); | ||||
| /// <summary> Creates a new REST/WebSocket discord client. </summary> | /// <summary> Creates a new REST/WebSocket discord client. </summary> | ||||
| @@ -329,27 +328,42 @@ namespace Discord | |||||
| { | { | ||||
| return Task.FromResult<IChannel>(DataStore.GetChannel(id)); | return Task.FromResult<IChannel>(DataStore.GetChannel(id)); | ||||
| } | } | ||||
| public override Task<IReadOnlyCollection<IDMChannel>> GetDMChannelsAsync() | |||||
| public override Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync() | |||||
| { | { | ||||
| return Task.FromResult<IReadOnlyCollection<IDMChannel>>(DMChannels); | |||||
| return Task.FromResult<IReadOnlyCollection<IPrivateChannel>>(DataStore.PrivateChannels); | |||||
| } | } | ||||
| internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore) | |||||
| internal ICachedChannel AddPrivateChannel(API.Channel model, DataStore dataStore) | |||||
| { | { | ||||
| var recipient = GetOrAddUser(model.Recipient.Value, dataStore); | |||||
| var channel = new CachedDMChannel(this, new CachedDMUser(recipient), model); | |||||
| recipient.AddRef(); | |||||
| dataStore.AddDMChannel(channel); | |||||
| return channel; | |||||
| } | |||||
| internal CachedDMChannel RemoveDMChannel(ulong id) | |||||
| { | |||||
| var dmChannel = DataStore.RemoveDMChannel(id); | |||||
| if (dmChannel != null) | |||||
| switch (model.Type) | |||||
| { | { | ||||
| var recipient = dmChannel.Recipient; | |||||
| recipient.User.RemoveRef(this); | |||||
| case ChannelType.DM: | |||||
| { | |||||
| var recipients = model.Recipients.Value; | |||||
| var user = GetOrAddUser(recipients[0], dataStore); | |||||
| var channel = new CachedDMChannel(this, new CachedPrivateUser(user), model); | |||||
| dataStore.AddChannel(channel); | |||||
| return channel; | |||||
| } | |||||
| case ChannelType.Group: | |||||
| { | |||||
| var recipients = model.Recipients.Value; | |||||
| var users = new ConcurrentDictionary<ulong, IUser>(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; | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -362,6 +376,11 @@ namespace Discord | |||||
| { | { | ||||
| return Task.FromResult<IUser>(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); | return Task.FromResult<IUser>(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public override Task<ISelfUser> GetCurrentUserAsync() | |||||
| { | |||||
| return Task.FromResult<ISelfUser>(_currentUser); | |||||
| } | |||||
| internal CachedGlobalUser GetOrAddUser(API.User model, DataStore dataStore) | internal CachedGlobalUser GetOrAddUser(API.User model, DataStore dataStore) | ||||
| { | { | ||||
| var user = dataStore.GetOrAddUser(model.Id, _ => new CachedGlobalUser(model)); | var user = dataStore.GetOrAddUser(model.Id, _ => new CachedGlobalUser(model)); | ||||
| @@ -518,7 +537,7 @@ namespace Discord | |||||
| unavailableGuilds++; | unavailableGuilds++; | ||||
| } | } | ||||
| for (int i = 0; i < data.PrivateChannels.Length; i++) | for (int i = 0; i < data.PrivateChannels.Length; i++) | ||||
| AddDMChannel(data.PrivateChannels[i], dataStore); | |||||
| AddPrivateChannel(data.PrivateChannels[i], dataStore); | |||||
| _sessionId = data.SessionId; | _sessionId = data.SessionId; | ||||
| _currentUser = currentUser; | _currentUser = currentUser; | ||||
| @@ -686,7 +705,7 @@ namespace Discord | |||||
| var data = (payload as JToken).ToObject<API.Channel>(_serializer); | var data = (payload as JToken).ToObject<API.Channel>(_serializer); | ||||
| ICachedChannel channel = null; | ICachedChannel channel = null; | ||||
| if (!data.IsPrivate) | |||||
| if (data.GuildId.IsSpecified) | |||||
| { | { | ||||
| var guild = DataStore.GetGuild(data.GuildId.Value); | var guild = DataStore.GetGuild(data.GuildId.Value); | ||||
| if (guild != null) | if (guild != null) | ||||
| @@ -698,7 +717,8 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| else | else | ||||
| channel = AddDMChannel(data, DataStore); | |||||
| channel = AddPrivateChannel(data, DataStore); | |||||
| if (channel != null) | if (channel != null) | ||||
| await _channelCreatedEvent.InvokeAsync(channel).ConfigureAwait(false); | await _channelCreatedEvent.InvokeAsync(channel).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -728,7 +748,7 @@ namespace Discord | |||||
| ICachedChannel channel = null; | ICachedChannel channel = null; | ||||
| var data = (payload as JToken).ToObject<API.Channel>(_serializer); | var data = (payload as JToken).ToObject<API.Channel>(_serializer); | ||||
| if (!data.IsPrivate) | |||||
| if (data.GuildId.IsSpecified) | |||||
| { | { | ||||
| var guild = DataStore.GetGuild(data.GuildId.Value); | var guild = DataStore.GetGuild(data.GuildId.Value); | ||||
| if (guild != null) | if (guild != null) | ||||
| @@ -740,7 +760,8 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| else | else | ||||
| channel = RemoveDMChannel(data.Recipient.Value.Id); | |||||
| channel = RemovePrivateChannel(data.Id); | |||||
| if (channel != null) | if (channel != null) | ||||
| await _channelDestroyedEvent.InvokeAsync(channel).ConfigureAwait(false); | await _channelDestroyedEvent.InvokeAsync(channel).ConfigureAwait(false); | ||||
| else | else | ||||
| @@ -1,9 +1,10 @@ | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public enum ChannelType : byte | |||||
| public enum ChannelType | |||||
| { | { | ||||
| DM, | |||||
| Text, | |||||
| Voice | |||||
| Text = 0, | |||||
| DM = 1, | |||||
| Voice = 2, | |||||
| Group = 3 | |||||
| } | } | ||||
| } | } | ||||
| @@ -17,6 +17,7 @@ namespace Discord | |||||
| public IUser Recipient { get; private set; } | public IUser Recipient { get; private set; } | ||||
| public virtual IReadOnlyCollection<IMessage> CachedMessages => ImmutableArray.Create<IMessage>(); | public virtual IReadOnlyCollection<IMessage> CachedMessages => ImmutableArray.Create<IMessage>(); | ||||
| IReadOnlyCollection<IUser> IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); | |||||
| public DMChannel(DiscordClient discord, IUser recipient, Model model) | public DMChannel(DiscordClient discord, IUser recipient, Model model) | ||||
| : base(model.Id) | : base(model.Id) | ||||
| @@ -30,7 +31,7 @@ namespace Discord | |||||
| { | { | ||||
| if (/*source == UpdateSource.Rest && */IsAttached) return; | 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() | public async Task UpdateAsync() | ||||
| @@ -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<ulong, IUser> _users; | |||||
| public override DiscordClient Discord { get; } | |||||
| public string Name { get; private set; } | |||||
| public IReadOnlyCollection<IUser> Recipients => _users.ToReadOnlyCollection(); | |||||
| public virtual IReadOnlyCollection<IMessage> CachedMessages => ImmutableArray.Create<IMessage>(); | |||||
| public GroupChannel(DiscordClient discord, ConcurrentDictionary<ulong, IUser> 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<ulong, IUser>(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<IUser> 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<IReadOnlyCollection<IUser>> GetUsersAsync() | |||||
| { | |||||
| var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); | |||||
| return _users.Select(x => x.Value).Concat(ImmutableArray.Create(currentUser)).ToReadOnlyCollection(_users, 1); | |||||
| } | |||||
| public async Task<IMessage> SendMessageAsync(string text, bool isTTS) | |||||
| { | |||||
| var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; | |||||
| var model = await Discord.ApiClient.CreateDMMessageAsync(Id, args).ConfigureAwait(false); | |||||
| return new Message(this, new User(model.Author.Value), model); | |||||
| } | |||||
| public async Task<IMessage> SendFileAsync(string filePath, string text, bool isTTS) | |||||
| { | |||||
| string filename = Path.GetFileName(filePath); | |||||
| using (var file = File.OpenRead(filePath)) | |||||
| { | |||||
| var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; | |||||
| var model = await Discord.ApiClient.UploadDMFileAsync(Id, file, args).ConfigureAwait(false); | |||||
| return new Message(this, new User(model.Author.Value), model); | |||||
| } | |||||
| } | |||||
| public async Task<IMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS) | |||||
| { | |||||
| var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; | |||||
| var model = await Discord.ApiClient.UploadDMFileAsync(Id, stream, args).ConfigureAwait(false); | |||||
| return new Message(this, new User(model.Author.Value), model); | |||||
| } | |||||
| public virtual async Task<IMessage> GetMessageAsync(ulong id) | |||||
| { | |||||
| var model = await Discord.ApiClient.GetChannelMessageAsync(Id, id).ConfigureAwait(false); | |||||
| if (model != null) | |||||
| return new Message(this, new User(model.Author.Value), model); | |||||
| return null; | |||||
| } | |||||
| public virtual async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit) | |||||
| { | |||||
| var args = new GetChannelMessagesParams { Limit = limit }; | |||||
| var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); | |||||
| return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray(); | |||||
| } | |||||
| public virtual async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) | |||||
| { | |||||
| var args = new GetChannelMessagesParams { Limit = limit }; | |||||
| var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); | |||||
| return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray(); | |||||
| } | |||||
| public async Task DeleteMessagesAsync(IEnumerable<IMessage> messages) | |||||
| { | |||||
| await Discord.ApiClient.DeleteDMMessagesAsync(Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); | |||||
| } | |||||
| public async Task TriggerTypingAsync() | |||||
| { | |||||
| await Discord.ApiClient.TriggerTypingIndicatorAsync(Id).ConfigureAwait(false); | |||||
| } | |||||
| public override string ToString() => Name; | |||||
| private string DebuggerDisplay => $"@{Name} ({Id}, Group)"; | |||||
| IMessage IMessageChannel.GetCachedMessage(ulong id) => null; | |||||
| } | |||||
| } | |||||
| @@ -2,7 +2,7 @@ | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public interface IDMChannel : IMessageChannel | |||||
| public interface IDMChannel : IMessageChannel, IPrivateChannel | |||||
| { | { | ||||
| /// <summary> Gets the recipient of all messages in this channel. </summary> | /// <summary> Gets the recipient of all messages in this channel. </summary> | ||||
| IUser Recipient { get; } | IUser Recipient { get; } | ||||
| @@ -0,0 +1,10 @@ | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IGroupChannel : IMessageChannel, IPrivateChannel | |||||
| { | |||||
| /// <summary> Leaves this group. </summary> | |||||
| Task LeaveAsync(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| using System.Collections.Generic; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IPrivateChannel | |||||
| { | |||||
| IReadOnlyCollection<IUser> Recipients { get; } | |||||
| } | |||||
| } | |||||
| @@ -296,14 +296,14 @@ namespace Discord | |||||
| internal GuildChannel ToChannel(API.Channel model) | internal GuildChannel ToChannel(API.Channel model) | ||||
| { | { | ||||
| switch (model.Type.Value) | |||||
| switch (model.Type) | |||||
| { | { | ||||
| case ChannelType.Text: | case ChannelType.Text: | ||||
| return new TextChannel(this, model); | return new TextChannel(this, model); | ||||
| case ChannelType.Voice: | case ChannelType.Voice: | ||||
| return new VoiceChannel(this, model); | return new VoiceChannel(this, model); | ||||
| default: | default: | ||||
| throw new InvalidOperationException($"Unknown channel type: {model.Type.Value}"); | |||||
| throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | |||||
| } | } | ||||
| } | } | ||||
| @@ -141,7 +141,7 @@ namespace Discord | |||||
| var args = new CreateDMChannelParams { Recipient = this }; | var args = new CreateDMChannelParams { Recipient = this }; | ||||
| var model = await Discord.ApiClient.CreateDMChannelAsync(args).ConfigureAwait(false); | 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; | IGuild IGuildUser.Guild => Guild; | ||||
| @@ -6,15 +6,16 @@ using Model = Discord.API.Channel; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| internal class CachedDMChannel : DMChannel, IDMChannel, ICachedChannel, ICachedMessageChannel | |||||
| internal class CachedDMChannel : DMChannel, IDMChannel, ICachedChannel, ICachedMessageChannel, ICachedPrivateChannel | |||||
| { | { | ||||
| private readonly MessageManager _messages; | private readonly MessageManager _messages; | ||||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | 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<ICachedUser> Members => ImmutableArray.Create<ICachedUser>(Discord.CurrentUser, Recipient); | public IReadOnlyCollection<ICachedUser> Members => ImmutableArray.Create<ICachedUser>(Discord.CurrentUser, Recipient); | ||||
| IReadOnlyCollection<CachedPrivateUser> 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) | : base(discord, recipient, model) | ||||
| { | { | ||||
| if (Discord.MessageCacheSize > 0) | if (Discord.MessageCacheSize > 0) | ||||
| @@ -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<ICachedUser> Members | |||||
| => _users.Select(x => x.Value).Concat(ImmutableArray.Create(Discord.CurrentUser)).Cast<ICachedUser>().ToReadOnlyCollection(_users, 1); | |||||
| public new IReadOnlyCollection<CachedPrivateUser> Recipients => _users.Cast<CachedPrivateUser>().ToReadOnlyCollection(_users); | |||||
| public CachedGroupChannel(DiscordSocketClient discord, ConcurrentDictionary<ulong, IUser> 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<ulong, IUser>(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<IMessage> GetMessageAsync(ulong id) | |||||
| { | |||||
| return await _messages.DownloadAsync(id).ConfigureAwait(false); | |||||
| } | |||||
| public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit) | |||||
| { | |||||
| return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); | |||||
| } | |||||
| public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) | |||||
| { | |||||
| return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); | |||||
| } | |||||
| public CachedMessage AddMessage(ICachedUser author, MessageModel model) | |||||
| { | |||||
| var msg = new CachedMessage(this, author, model); | |||||
| _messages.Add(msg); | |||||
| return msg; | |||||
| } | |||||
| public CachedMessage GetMessage(ulong id) | |||||
| { | |||||
| return _messages.Get(id); | |||||
| } | |||||
| public CachedMessage RemoveMessage(ulong id) | |||||
| { | |||||
| return _messages.Remove(id); | |||||
| } | |||||
| public CachedDMChannel Clone() => MemberwiseClone() as CachedDMChannel; | |||||
| IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); | |||||
| ICachedUser ICachedMessageChannel.GetUser(ulong id, bool skipCheck) | |||||
| { | |||||
| 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(); | |||||
| } | |||||
| } | |||||
| @@ -311,14 +311,14 @@ namespace Discord | |||||
| new internal ICachedGuildChannel ToChannel(ChannelModel model) | new internal ICachedGuildChannel ToChannel(ChannelModel model) | ||||
| { | { | ||||
| switch (model.Type.Value) | |||||
| switch (model.Type) | |||||
| { | { | ||||
| case ChannelType.Text: | case ChannelType.Text: | ||||
| return new CachedTextChannel(this, model); | return new CachedTextChannel(this, model); | ||||
| case ChannelType.Voice: | case ChannelType.Voice: | ||||
| return new CachedVoiceChannel(this, model); | return new CachedVoiceChannel(this, model); | ||||
| default: | default: | ||||
| throw new InvalidOperationException($"Unknown channel type: {model.Type.Value}"); | |||||
| throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | |||||
| } | } | ||||
| } | } | ||||
| @@ -3,7 +3,7 @@ using PresenceModel = Discord.API.Presence; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| internal class CachedDMUser : ICachedUser | |||||
| internal class CachedPrivateUser : ICachedUser | |||||
| { | { | ||||
| public CachedGlobalUser User { get; } | public CachedGlobalUser User { get; } | ||||
| @@ -24,7 +24,7 @@ namespace Discord | |||||
| public string NicknameMention => User.NicknameMention; | public string NicknameMention => User.NicknameMention; | ||||
| public string Username => User.Username; | public string Username => User.Username; | ||||
| public CachedDMUser(CachedGlobalUser user) | |||||
| public CachedPrivateUser(CachedGlobalUser user) | |||||
| { | { | ||||
| User = user; | User = user; | ||||
| } | } | ||||
| @@ -34,7 +34,7 @@ namespace Discord | |||||
| User.Update(model, source); | User.Update(model, source); | ||||
| } | } | ||||
| public CachedDMUser Clone() => MemberwiseClone() as CachedDMUser; | |||||
| public CachedPrivateUser Clone() => MemberwiseClone() as CachedPrivateUser; | |||||
| ICachedUser ICachedUser.Clone() => Clone(); | ICachedUser ICachedUser.Clone() => Clone(); | ||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,9 @@ | |||||
| using System.Collections.Generic; | |||||
| namespace Discord | |||||
| { | |||||
| internal interface ICachedPrivateChannel : ICachedChannel, IPrivateChannel | |||||
| { | |||||
| new IReadOnlyCollection<CachedPrivateUser> Recipients { get; } | |||||
| } | |||||
| } | |||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Collections; | |||||
| using System; | |||||
| using System.Collections; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| @@ -8,24 +9,26 @@ namespace Discord.Extensions | |||||
| internal static class CollectionExtensions | internal static class CollectionExtensions | ||||
| { | { | ||||
| public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source) | public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source) | ||||
| => new ConcurrentDictionaryWrapper<TValue, KeyValuePair<TKey, TValue>>(source, source.Select(x => x.Value)); | |||||
| public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue, TSource>(this IEnumerable<TValue> query, IReadOnlyCollection<TSource> source) | |||||
| => new ConcurrentDictionaryWrapper<TValue, TSource>(source, query); | |||||
| => new ConcurrentDictionaryWrapper<TValue>(source.Select(x => x.Value), () => source.Count); | |||||
| public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue, TSource>(this IEnumerable<TValue> query, IReadOnlyCollection<TSource> source, int countOffset = 0) | |||||
| => new ConcurrentDictionaryWrapper<TValue>(query, () => source.Count + countOffset); | |||||
| public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue>(this IEnumerable<TValue> query, Func<int> countFunc) | |||||
| => new ConcurrentDictionaryWrapper<TValue>(query, countFunc); | |||||
| } | } | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| internal struct ConcurrentDictionaryWrapper<TValue, TSource> : IReadOnlyCollection<TValue> | |||||
| internal struct ConcurrentDictionaryWrapper<TValue> : IReadOnlyCollection<TValue> | |||||
| { | { | ||||
| private readonly IReadOnlyCollection<TSource> _source; | |||||
| private readonly IEnumerable<TValue> _query; | private readonly IEnumerable<TValue> _query; | ||||
| private readonly Func<int> _countFunc; | |||||
| //It's okay that this count is affected by race conditions - we're wrapping a concurrent collection and that's to be expected | //It's okay that this count is affected by race conditions - we're wrapping a concurrent collection and that's to be expected | ||||
| public int Count => _source.Count; | |||||
| public int Count => _countFunc(); | |||||
| public ConcurrentDictionaryWrapper(IReadOnlyCollection<TSource> source, IEnumerable<TValue> query) | |||||
| public ConcurrentDictionaryWrapper(IEnumerable<TValue> query, Func<int> countFunc) | |||||
| { | { | ||||
| _source = source; | |||||
| _query = query; | _query = query; | ||||
| _countFunc = countFunc; | |||||
| } | } | ||||
| private string DebuggerDisplay => $"Count = {Count}"; | private string DebuggerDisplay => $"Count = {Count}"; | ||||
| @@ -24,7 +24,7 @@ namespace Discord | |||||
| Task DisconnectAsync(); | Task DisconnectAsync(); | ||||
| Task<IChannel> GetChannelAsync(ulong id); | Task<IChannel> GetChannelAsync(ulong id); | ||||
| Task<IReadOnlyCollection<IDMChannel>> GetDMChannelsAsync(); | |||||
| Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync(); | |||||
| Task<IReadOnlyCollection<IConnection>> GetConnectionsAsync(); | Task<IReadOnlyCollection<IConnection>> GetConnectionsAsync(); | ||||
| @@ -1,42 +0,0 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System; | |||||
| namespace Discord.Net.Converters | |||||
| { | |||||
| public class ChannelTypeConverter : JsonConverter | |||||
| { | |||||
| public static readonly ChannelTypeConverter Instance = new ChannelTypeConverter(); | |||||
| public override bool CanConvert(Type objectType) => true; | |||||
| public override bool CanRead => true; | |||||
| public override bool CanWrite => true; | |||||
| public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |||||
| { | |||||
| switch ((string)reader.Value) | |||||
| { | |||||
| case "text": | |||||
| return ChannelType.Text; | |||||
| case "voice": | |||||
| return ChannelType.Voice; | |||||
| default: | |||||
| throw new JsonSerializationException("Unknown channel type"); | |||||
| } | |||||
| } | |||||
| public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | |||||
| { | |||||
| switch ((ChannelType)value) | |||||
| { | |||||
| case ChannelType.Text: | |||||
| writer.WriteValue("text"); | |||||
| break; | |||||
| case ChannelType.Voice: | |||||
| writer.WriteValue("voice"); | |||||
| break; | |||||
| default: | |||||
| throw new JsonSerializationException("Invalid channel type"); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -75,8 +75,6 @@ namespace Discord.Net.Converters | |||||
| } | } | ||||
| //Enums | //Enums | ||||
| if (type == typeof(ChannelType)) | |||||
| return ChannelTypeConverter.Instance; | |||||
| if (type == typeof(PermissionTarget)) | if (type == typeof(PermissionTarget)) | ||||
| return PermissionTargetConverter.Instance; | return PermissionTargetConverter.Instance; | ||||
| if (type == typeof(UserStatus)) | if (type == typeof(UserStatus)) | ||||