Browse Source

Merge pull request #125 from RogueException/v6

v6 Support
pull/140/head
RogueException GitHub 9 years ago
parent
commit
5bb06b77ff
38 changed files with 863 additions and 199 deletions
  1. +2
    -2
      src/Discord.Net.Commands/CommandService.cs
  2. +3
    -3
      src/Discord.Net.Commands/Extensions/MessageExtensions.cs
  3. +2
    -0
      src/Discord.Net/API/CDN.cs
  4. +12
    -7
      src/Discord.Net/API/Common/Channel.cs
  5. +2
    -0
      src/Discord.Net/API/Common/Message.cs
  6. +7
    -7
      src/Discord.Net/API/Common/User.cs
  7. +19
    -2
      src/Discord.Net/API/DiscordAPIClient.cs
  8. +12
    -0
      src/Discord.Net/API/Gateway/RecipientEvent.cs
  9. +36
    -20
      src/Discord.Net/Data/DataStore.cs
  10. +15
    -6
      src/Discord.Net/DiscordClient.cs
  11. +2
    -2
      src/Discord.Net/DiscordConfig.cs
  12. +14
    -2
      src/Discord.Net/DiscordSocketClient.Events.cs
  13. +253
    -62
      src/Discord.Net/DiscordSocketClient.cs
  14. +5
    -4
      src/Discord.Net/Entities/Channels/ChannelType.cs
  15. +2
    -1
      src/Discord.Net/Entities/Channels/DMChannel.cs
  16. +147
    -0
      src/Discord.Net/Entities/Channels/GroupChannel.cs
  17. +1
    -1
      src/Discord.Net/Entities/Channels/IDMChannel.cs
  18. +13
    -0
      src/Discord.Net/Entities/Channels/IGroupChannel.cs
  19. +9
    -0
      src/Discord.Net/Entities/Channels/IPrivateChannel.cs
  20. +2
    -2
      src/Discord.Net/Entities/Guilds/Guild.cs
  21. +4
    -2
      src/Discord.Net/Entities/Messages/IMessage.cs
  22. +12
    -10
      src/Discord.Net/Entities/Messages/Message.cs
  23. +12
    -0
      src/Discord.Net/Entities/Messages/MessageType.cs
  24. +47
    -0
      src/Discord.Net/Entities/Users/GroupUser.cs
  25. +1
    -2
      src/Discord.Net/Entities/Users/GuildUser.cs
  26. +13
    -0
      src/Discord.Net/Entities/Users/IGroupUser.cs
  27. +6
    -3
      src/Discord.Net/Entities/Users/SelfUser.cs
  28. +8
    -4
      src/Discord.Net/Entities/Users/User.cs
  29. +2
    -1
      src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs
  30. +5
    -0
      src/Discord.Net/Entities/WebSocket/CachedDMUser.cs
  31. +139
    -0
      src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs
  32. +33
    -0
      src/Discord.Net/Entities/WebSocket/CachedGroupUser.cs
  33. +2
    -3
      src/Discord.Net/Entities/WebSocket/CachedGuild.cs
  34. +9
    -0
      src/Discord.Net/Entities/WebSocket/ICachedPrivateChannel.cs
  35. +11
    -8
      src/Discord.Net/Extensions/CollectionExtensions.cs
  36. +1
    -1
      src/Discord.Net/IDiscordClient.cs
  37. +0
    -42
      src/Discord.Net/Net/Converters/ChannelTypeConverter.cs
  38. +0
    -2
      src/Discord.Net/Net/Converters/DiscordContractResolver.cs

+ 2
- 2
src/Discord.Net.Commands/CommandService.cs View File

@@ -225,7 +225,7 @@ namespace Discord.Commands
return false;
}

public SearchResult Search(IMessage message, int argPos) => Search(message, message.Text.Substring(argPos));
public SearchResult Search(IMessage message, int argPos) => Search(message, message.Content.Substring(argPos));
public SearchResult Search(IMessage message, string input)
{
string lowerInput = input.ToLowerInvariant();
@@ -237,7 +237,7 @@ namespace Discord.Commands
return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command.");
}

public Task<IResult> Execute(IMessage message, int argPos) => Execute(message, message.Text.Substring(argPos));
public Task<IResult> Execute(IMessage message, int argPos) => Execute(message, message.Content.Substring(argPos));
public async Task<IResult> Execute(IMessage message, string input)
{
var searchResult = Search(message, input);


+ 3
- 3
src/Discord.Net.Commands/Extensions/MessageExtensions.cs View File

@@ -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))
{


+ 2
- 0
src/Discord.Net/API/CDN.cs View File

@@ -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;
}
}

+ 12
- 7
src/Discord.Net/API/Common/Channel.cs View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System;

namespace Discord.API
{
@@ -7,8 +8,8 @@ namespace Discord.API
//Shared
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("is_private")]
public bool IsPrivate { get; set; }
[JsonProperty("type")]
public ChannelType Type { get; set; }
[JsonProperty("last_message_id")]
public ulong? LastMessageId { get; set; }

@@ -17,8 +18,6 @@ namespace Discord.API
public Optional<ulong> GuildId { get; set; }
[JsonProperty("name")]
public Optional<string> Name { get; set; }
[JsonProperty("type")]
public Optional<ChannelType> Type { get; set; }
[JsonProperty("position")]
public Optional<int> Position { get; set; }
[JsonProperty("permission_overwrites")]
@@ -27,6 +26,8 @@ namespace Discord.API
//TextChannel
[JsonProperty("topic")]
public Optional<string> Topic { get; set; }
[JsonProperty("last_pin_timestamp")]
public Optional<DateTimeOffset?> LastPinTimestamp { get; set; }

//VoiceChannel
[JsonProperty("bitrate")]
@@ -34,8 +35,12 @@ namespace Discord.API
[JsonProperty("user_limit")]
public Optional<int> UserLimit { get; set; }

//DMChannel
[JsonProperty("recipient")]
public Optional<User> Recipient { get; set; }
//PrivateChannel
[JsonProperty("recipients")]
public Optional<User[]> Recipients { get; set; }

//GroupChannel
[JsonProperty("icon")]
public Optional<string> Icon { get; set; }
}
}

+ 2
- 0
src/Discord.Net/API/Common/Message.cs View File

@@ -7,6 +7,8 @@ namespace Discord.API
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("type")]
public MessageType Type { get; set; }
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
[JsonProperty("author")]


+ 7
- 7
src/Discord.Net/API/Common/User.cs View File

@@ -7,20 +7,20 @@ namespace Discord.API
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
public Optional<string> Username { get; set; }
[JsonProperty("discriminator")]
public string Discriminator { get; set; }
public Optional<string> Discriminator { get; set; }
[JsonProperty("bot")]
public bool Bot { get; set; }
public Optional<bool> Bot { get; set; }
[JsonProperty("avatar")]
public string Avatar { get; set; }
public Optional<string> Avatar { get; set; }

//CurrentUser
[JsonProperty("verified")]
public bool Verified { get; set; }
public Optional<bool> Verified { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
public Optional<string> Email { get; set; }
[JsonProperty("mfa_enabled")]
public bool MfaEnabled { get; set; }
public Optional<bool> MfaEnabled { get; set; }
}
}

+ 19
- 2
src/Discord.Net/API/DiscordAPIClient.cs View File

@@ -211,7 +211,7 @@ namespace Discord.API
if (_gatewayUrl == null)
{
var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false);
_gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.GatewayAPIVersion}&encoding={DiscordConfig.GatewayEncoding}";
_gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordConfig.GatewayEncoding}";
}
await _gatewayClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false);

@@ -551,6 +551,23 @@ namespace Discord.API
await SendAsync("DELETE", $"channels/{channelId}/pins/{messageId}", options: options).ConfigureAwait(false);
}

//Channel Recipients
public async Task AddGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null)
{
Preconditions.GreaterThan(channelId, 0, nameof(channelId));
Preconditions.GreaterThan(userId, 0, nameof(userId));

await SendAsync("PUT", $"channels/{channelId}/recipients/{userId}", options: options).ConfigureAwait(false);

}
public async Task RemoveGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));
Preconditions.NotEqual(userId, 0, nameof(userId));

await SendAsync("DELETE", $"channels/{channelId}/recipients/{userId}", options: options).ConfigureAwait(false);
}

//Guilds
public async Task<Guild> GetGuildAsync(ulong guildId, RequestOptions options = null)
{
@@ -1160,7 +1177,7 @@ namespace Discord.API
{
return await SendAsync<IReadOnlyCollection<Connection>>("GET", "users/@me/connections", options: options).ConfigureAwait(false);
}
public async Task<IReadOnlyCollection<Channel>> GetMyDMsAsync(RequestOptions options = null)
public async Task<IReadOnlyCollection<Channel>> GetMyPrivateChannelsAsync(RequestOptions options = null)
{
return await SendAsync<IReadOnlyCollection<Channel>>("GET", "users/@me/channels", options: options).ConfigureAwait(false);
}


+ 12
- 0
src/Discord.Net/API/Gateway/RecipientEvent.cs View File

@@ -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; }
}
}

+ 36
- 20
src/Discord.Net/Data/DataStore.cs View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Discord
{
@@ -16,12 +17,19 @@ namespace Discord
private readonly ConcurrentDictionary<ulong, CachedDMChannel> _dmChannels;
private readonly ConcurrentDictionary<ulong, CachedGuild> _guilds;
private readonly ConcurrentDictionary<ulong, CachedGlobalUser> _users;
private readonly ConcurrentHashSet<ulong> _groupChannels;

internal IReadOnlyCollection<ICachedChannel> Channels => _channels.ToReadOnlyCollection();
internal IReadOnlyCollection<CachedDMChannel> DMChannels => _dmChannels.ToReadOnlyCollection();
internal IReadOnlyCollection<CachedGroupChannel> GroupChannels => _groupChannels.Select(x => GetChannel(x) as CachedGroupChannel).ToReadOnlyCollection(_groupChannels);
internal IReadOnlyCollection<CachedGuild> Guilds => _guilds.ToReadOnlyCollection();
internal IReadOnlyCollection<CachedGlobalUser> Users => _users.ToReadOnlyCollection();

internal IReadOnlyCollection<ICachedPrivateChannel> PrivateChannels =>
_dmChannels.Select(x => x.Value as ICachedPrivateChannel).Concat(
_groupChannels.Select(x => GetChannel(x) as ICachedPrivateChannel))
.ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count);

public DataStore(int guildCount, int dmChannelCount)
{
double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount;
@@ -30,6 +38,7 @@ namespace Discord
_dmChannels = new ConcurrentDictionary<ulong, CachedDMChannel>(CollectionConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier));
_guilds = new ConcurrentDictionary<ulong, CachedGuild>(CollectionConcurrencyLevel, (int)(guildCount * CollectionMultiplier));
_users = new ConcurrentDictionary<ulong, CachedGlobalUser>(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier));
_groupChannels = new ConcurrentHashSet<ulong>(CollectionConcurrencyLevel, (int)(10 * CollectionMultiplier));
}

internal ICachedChannel GetChannel(ulong id)
@@ -39,18 +48,6 @@ namespace Discord
return channel;
return null;
}
internal void AddChannel(ICachedChannel channel)
{
_channels[channel.Id] = channel;
}
internal ICachedChannel RemoveChannel(ulong id)
{
ICachedChannel channel;
if (_channels.TryRemove(id, out channel))
return channel;
return null;
}

internal CachedDMChannel GetDMChannel(ulong userId)
{
CachedDMChannel channel;
@@ -58,19 +55,38 @@ namespace Discord
return channel;
return null;
}
internal void AddDMChannel(CachedDMChannel channel)
internal void AddChannel(ICachedChannel channel)
{
_channels[channel.Id] = channel;
_dmChannels[channel.Recipient.Id] = channel;

var dmChannel = channel as CachedDMChannel;
if (dmChannel != null)
_dmChannels[dmChannel.Recipient.Id] = dmChannel;
else
{
var groupChannel = channel as CachedGroupChannel;
if (groupChannel != null)
_groupChannels.TryAdd(groupChannel.Id);
}
}
internal CachedDMChannel RemoveDMChannel(ulong userId)
internal ICachedChannel RemoveChannel(ulong id)
{
CachedDMChannel channel;
ICachedChannel ignored;
if (_dmChannels.TryRemove(userId, out channel))
ICachedChannel channel;
if (_channels.TryRemove(id, out channel))
{
if (_channels.TryRemove(channel.Id, out ignored))
return channel;
var dmChannel = channel as CachedDMChannel;
if (dmChannel != null)
{
CachedDMChannel ignored;
_dmChannels.TryRemove(dmChannel.Recipient.Id, out ignored);
}
else
{
var groupChannel = channel as CachedGroupChannel;
if (groupChannel != null)
_groupChannels.TryRemove(id);
}
return channel;
}
return null;
}


+ 15
- 6
src/Discord.Net/DiscordClient.cs View File

@@ -10,6 +10,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Collections.Concurrent;

namespace Discord
{
@@ -165,16 +166,24 @@ namespace Discord
return guild.ToChannel(model);
}
}
else if (model.Type == ChannelType.DM)
return new DMChannel(this, new User(model.Recipients.Value[0]), model);
else if (model.Type == ChannelType.Group)
{
var channel = new GroupChannel(this, model);
channel.UpdateUsers(model.Recipients.Value, UpdateSource.Creation);
return channel;
}
else
return new DMChannel(this, new User(model.Recipient.Value), model);
throw new InvalidOperationException($"Unexpected channel type: {model.Type}");
}
return null;
}
/// <inheritdoc />
public virtual async Task<IReadOnlyCollection<IDMChannel>> GetDMChannelsAsync()
public virtual async Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync()
{
var models = await ApiClient.GetMyDMsAsync().ConfigureAwait(false);
return models.Select(x => new DMChannel(this, new User(x.Recipient.Value), x)).ToImmutableArray();
var models = await ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false);
return models.Select(x => new DMChannel(this, new User(x.Recipients.Value[0]), x)).ToImmutableArray();
}

/// <inheritdoc />
@@ -289,9 +298,9 @@ namespace Discord
private async Task WriteInitialLog()
{
if (this is DiscordSocketClient)
await _clientLogger.InfoAsync($"DiscordSocketClient v{DiscordConfig.Version} (Gateway v{DiscordConfig.GatewayAPIVersion}, {DiscordConfig.GatewayEncoding})").ConfigureAwait(false);
await _clientLogger.InfoAsync($"DiscordSocketClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, {DiscordConfig.GatewayEncoding})").ConfigureAwait(false);
else
await _clientLogger.InfoAsync($"DiscordClient v{DiscordConfig.Version}").ConfigureAwait(false);
await _clientLogger.InfoAsync($"DiscordClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion})").ConfigureAwait(false);
await _clientLogger.VerboseAsync($"Runtime: {RuntimeInformation.FrameworkDescription.Trim()} ({ToArchString(RuntimeInformation.ProcessArchitecture)})").ConfigureAwait(false);
await _clientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false);
await _clientLogger.VerboseAsync($"Processors: {Environment.ProcessorCount}").ConfigureAwait(false);


+ 2
- 2
src/Discord.Net/DiscordConfig.cs View File

@@ -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/";



+ 14
- 2
src/Discord.Net/DiscordSocketClient.Events.cs View File

@@ -167,12 +167,12 @@ namespace Discord
remove { _userPresenceUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IGuildUser, IPresence, IPresence, Task>> _userPresenceUpdatedEvent = new AsyncEvent<Func<IGuildUser, IPresence, IPresence, Task>>();
public event Func<IGuildUser, IVoiceState, IVoiceState, Task> UserVoiceStateUpdated
public event Func<IUser, IVoiceState, IVoiceState, Task> UserVoiceStateUpdated
{
add { _userVoiceStateUpdatedEvent.Add(value); }
remove { _userVoiceStateUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IGuildUser, IVoiceState, IVoiceState, Task>> _userVoiceStateUpdatedEvent = new AsyncEvent<Func<IGuildUser, IVoiceState, IVoiceState, Task>>();
private readonly AsyncEvent<Func<IUser, IVoiceState, IVoiceState, Task>> _userVoiceStateUpdatedEvent = new AsyncEvent<Func<IUser, IVoiceState, IVoiceState, Task>>();
public event Func<ISelfUser, ISelfUser, Task> CurrentUserUpdated
{
add { _selfUpdatedEvent.Add(value); }
@@ -185,6 +185,18 @@ namespace Discord
remove { _userIsTypingEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IUser, IChannel, Task>> _userIsTypingEvent = new AsyncEvent<Func<IUser, IChannel, Task>>();
public event Func<IGroupUser, Task> RecipientAdded
{
add { _recipientAddedEvent.Add(value); }
remove { _recipientAddedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IGroupUser, Task>> _recipientAddedEvent = new AsyncEvent<Func<IGroupUser, Task>>();
public event Func<IGroupUser, Task> RecipientRemoved
{
add { _recipientRemovedEvent.Add(value); }
remove { _recipientRemovedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<IGroupUser, Task>>();

//TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected;
}


+ 253
- 62
src/Discord.Net/DiscordSocketClient.cs View File

@@ -57,7 +57,6 @@ namespace Discord

internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser;
internal IReadOnlyCollection<CachedGuild> Guilds => DataStore.Guilds;
internal IReadOnlyCollection<CachedDMChannel> DMChannels => DataStore.DMChannels;
internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection();

/// <summary> Creates a new REST/WebSocket discord client. </summary>
@@ -148,6 +147,7 @@ namespace Discord

ConnectionState = ConnectionState.Connecting;
await _gatewayLogger.InfoAsync("Connecting").ConfigureAwait(false);
try
{
_connectTask = new TaskCompletionSource<bool>();
@@ -161,7 +161,6 @@ namespace Discord
await ApiClient.SendIdentifyAsync().ConfigureAwait(false);

await _connectTask.Task.ConfigureAwait(false);
ConnectionState = ConnectionState.Connected;
await _gatewayLogger.InfoAsync("Connected").ConfigureAwait(false);
}
@@ -174,6 +173,7 @@ namespace Discord
/// <inheritdoc />
public async Task DisconnectAsync()
{
if (_connectTask?.TrySetCanceled() ?? false) return;
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
@@ -182,6 +182,17 @@ namespace Discord
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectAsync(Exception ex)
{
if (_connectTask?.TrySetException(ex) ?? false) return;
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
_isReconnecting = false;
await DisconnectInternalAsync(ex).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectInternalAsync(Exception ex)
{
ulong guildId;
@@ -329,27 +340,39 @@ namespace Discord
{
return Task.FromResult<IChannel>(DataStore.GetChannel(id));
}
public override Task<IReadOnlyCollection<IDMChannel>> GetDMChannelsAsync()
public override Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync()
{
return Task.FromResult<IReadOnlyCollection<IDMChannel>>(DMChannels);
return Task.FromResult<IReadOnlyCollection<IPrivateChannel>>(DataStore.PrivateChannels);
}
internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore)
internal ICachedChannel AddPrivateChannel(API.Channel model, DataStore dataStore)
{
var recipient = GetOrAddUser(model.Recipient.Value, dataStore);
var channel = new CachedDMChannel(this, new CachedDMUser(recipient), model);
recipient.AddRef();
dataStore.AddDMChannel(channel);
return channel;
}
internal CachedDMChannel RemoveDMChannel(ulong id)
{
var dmChannel = DataStore.RemoveDMChannel(id);
if (dmChannel != null)
switch (model.Type)
{
var recipient = dmChannel.Recipient;
recipient.User.RemoveRef(this);
case ChannelType.DM:
{
var recipients = model.Recipients.Value;
var user = GetOrAddUser(recipients[0], dataStore);
var channel = new CachedDMChannel(this, new CachedDMUser(user), model);
dataStore.AddChannel(channel);
return channel;
}
case ChannelType.Group:
{
var channel = new CachedGroupChannel(this, model);
channel.UpdateUsers(model.Recipients.Value, UpdateSource.Creation, dataStore);
dataStore.AddChannel(channel);
return channel;
}
default:
throw new InvalidOperationException($"Unexpected channel type: {model.Type}");
}
return dmChannel;
}
internal ICachedChannel RemovePrivateChannel(ulong id)
{
var channel = DataStore.RemoveChannel(id) as ICachedPrivateChannel;
foreach (var recipient in channel.Recipients)
recipient.User.RemoveRef(this);
return channel;
}

/// <inheritdoc />
@@ -362,6 +385,11 @@ namespace Discord
{
return Task.FromResult<IUser>(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault());
}
/// <inheritdoc />
public override Task<ISelfUser> GetCurrentUserAsync()
{
return Task.FromResult<ISelfUser>(_currentUser);
}
internal CachedGlobalUser GetOrAddUser(API.User model, DataStore dataStore)
{
var user = dataStore.GetOrAddUser(model.Id, _ => new CachedGlobalUser(model));
@@ -502,35 +530,43 @@ namespace Discord
//Connection
case "READY":
{
await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false);
var data = (payload as JToken).ToObject<ReadyEvent>(_serializer);
var dataStore = new DataStore( data.Guilds.Length, data.PrivateChannels.Length);
try
{
await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<ReadyEvent>(_serializer);
var dataStore = new DataStore(data.Guilds.Length, data.PrivateChannels.Length);

var currentUser = new CachedSelfUser(this, data.User);
int unavailableGuilds = 0;
for (int i = 0; i < data.Guilds.Length; i++)
{
var model = data.Guilds[i];
AddGuild(model, dataStore);
if (model.Unavailable == true)
unavailableGuilds++;
}
for (int i = 0; i < data.PrivateChannels.Length; i++)
AddPrivateChannel(data.PrivateChannels[i], dataStore);

var currentUser = new CachedSelfUser(this, data.User);
int unavailableGuilds = 0;
//dataStore.GetOrAddUser(data.User.Id, _ => currentUser);
for (int i = 0; i < data.Guilds.Length; i++)
_sessionId = data.SessionId;
_currentUser = currentUser;
_unavailableGuilds = unavailableGuilds;
_lastGuildAvailableTime = Environment.TickCount;
DataStore = dataStore;

}
catch (Exception ex)
{
var model = data.Guilds[i];
AddGuild(model, dataStore);
if (model.Unavailable == true)
unavailableGuilds++;
await DisconnectAsync(new Exception("Processing READY failed", ex));
return;
}
for (int i = 0; i < data.PrivateChannels.Length; i++)
AddDMChannel(data.PrivateChannels[i], dataStore);

_sessionId = data.SessionId;
_currentUser = currentUser;
_unavailableGuilds = unavailableGuilds;
_lastGuildAvailableTime = Environment.TickCount;
DataStore = dataStore;

_guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token, _clientLogger);

await _readyEvent.InvokeAsync().ConfigureAwait(false);
await SyncGuildsAsync().ConfigureAwait(false);

var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete
await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false);
}
@@ -686,11 +722,19 @@ namespace Discord

var data = (payload as JToken).ToObject<API.Channel>(_serializer);
ICachedChannel channel = null;
if (!data.IsPrivate)
if (data.GuildId.IsSpecified)
{
var guild = DataStore.GetGuild(data.GuildId.Value);
if (guild != null)
{
guild.AddChannel(data, DataStore);

if (!guild.IsSynced)
{
await _gatewayLogger.DebugAsync("Ignored CHANNEL_CREATE, guild is not synced yet.").ConfigureAwait(false);
return;
}
}
else
{
await _gatewayLogger.WarningAsync("CHANNEL_CREATE referenced an unknown guild.").ConfigureAwait(false);
@@ -698,7 +742,8 @@ namespace Discord
}
}
else
channel = AddDMChannel(data, DataStore);
channel = AddPrivateChannel(data, DataStore);

if (channel != null)
await _channelCreatedEvent.InvokeAsync(channel).ConfigureAwait(false);
}
@@ -713,6 +758,13 @@ namespace Discord
{
var before = channel.Clone();
channel.Update(data, UpdateSource.WebSocket);

if (!((channel as ICachedGuildChannel)?.Guild.IsSynced ?? true))
{
await _gatewayLogger.DebugAsync("Ignored CHANNEL_UPDATE, guild is not synced yet.").ConfigureAwait(false);
return;
}

await _channelUpdatedEvent.InvokeAsync(before, channel).ConfigureAwait(false);
}
else
@@ -728,11 +780,19 @@ namespace Discord

ICachedChannel channel = null;
var data = (payload as JToken).ToObject<API.Channel>(_serializer);
if (!data.IsPrivate)
if (data.GuildId.IsSpecified)
{
var guild = DataStore.GetGuild(data.GuildId.Value);
if (guild != null)
{
channel = guild.RemoveChannel(data.Id);

if (!guild.IsSynced)
{
await _gatewayLogger.DebugAsync("Ignored CHANNEL_DELETE, guild is not synced yet.").ConfigureAwait(false);
return;
}
}
else
{
await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown guild.").ConfigureAwait(false);
@@ -740,7 +800,8 @@ namespace Discord
}
}
else
channel = RemoveDMChannel(data.Recipient.Value.Id);
channel = RemovePrivateChannel(data.Id);

if (channel != null)
await _channelDestroyedEvent.InvokeAsync(channel).ConfigureAwait(false);
else
@@ -761,6 +822,13 @@ namespace Discord
if (guild != null)
{
var user = guild.AddUser(data, DataStore);

if (!guild.IsSynced)
{
await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_ADD, guild is not synced yet.").ConfigureAwait(false);
return;
}

await _userJoinedEvent.InvokeAsync(user).ConfigureAwait(false);
}
else
@@ -779,6 +847,13 @@ namespace Discord
if (guild != null)
{
var user = guild.GetUser(data.User.Id);

if (!guild.IsSynced)
{
await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_UPDATE, guild is not synced yet.").ConfigureAwait(false);
return;
}

if (user != null)
{
var before = user.Clone();
@@ -807,6 +882,13 @@ namespace Discord
if (guild != null)
{
var user = guild.RemoveUser(data.User.Id);

if (!guild.IsSynced)
{
await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_REMOVE, guild is not synced yet.").ConfigureAwait(false);
return;
}

if (user != null)
{
user.User.RemoveRef(this);
@@ -849,6 +931,51 @@ namespace Discord
}
}
break;
case "CHANNEL_RECIPIENT_ADD":
{
await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<RecipientEvent>(_serializer);
var channel = DataStore.GetChannel(data.ChannelId) as CachedGroupChannel;
if (channel != null)
{
var user = channel.AddUser(data.User, DataStore);
await _recipientAddedEvent.InvokeAsync(user).ConfigureAwait(false);
}
else
{
await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_ADD referenced an unknown channel.").ConfigureAwait(false);
return;
}
}
break;
case "CHANNEL_RECIPIENT_REMOVE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<RecipientEvent>(_serializer);
var channel = DataStore.GetChannel(data.ChannelId) as CachedGroupChannel;
if (channel != null)
{
var user = channel.RemoveUser(data.User.Id);
if (user != null)
{
user.User.RemoveRef(this);
await _recipientRemovedEvent.InvokeAsync(user).ConfigureAwait(false);
}
else
{
await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_REMOVE referenced an unknown user.").ConfigureAwait(false);
return;
}
}
else
{
await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_ADD referenced an unknown channel.").ConfigureAwait(false);
return;
}
}
break;

//Roles
case "GUILD_ROLE_CREATE":
@@ -860,6 +987,12 @@ namespace Discord
if (guild != null)
{
var role = guild.AddRole(data.Role);

if (!guild.IsSynced)
{
await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_CREATE, guild is not synced yet.").ConfigureAwait(false);
return;
}
await _roleCreatedEvent.InvokeAsync(role).ConfigureAwait(false);
}
else
@@ -882,6 +1015,13 @@ namespace Discord
{
var before = role.Clone();
role.Update(data.Role, UpdateSource.WebSocket);

if (!guild.IsSynced)
{
await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_UPDATE, guild is not synced yet.").ConfigureAwait(false);
return;
}

await _roleUpdatedEvent.InvokeAsync(before, role).ConfigureAwait(false);
}
else
@@ -907,7 +1047,15 @@ namespace Discord
{
var role = guild.RemoveRole(data.RoleId);
if (role != null)
{
if (!guild.IsSynced)
{
await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_DELETE, guild is not synced yet.").ConfigureAwait(false);
return;
}

await _roleDeletedEvent.InvokeAsync(role).ConfigureAwait(false);
}
else
{
await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown role.").ConfigureAwait(false);
@@ -930,7 +1078,15 @@ namespace Discord
var data = (payload as JToken).ToObject<GuildBanEvent>(_serializer);
var guild = DataStore.GetGuild(data.GuildId);
if (guild != null)
{
if (!guild.IsSynced)
{
await _gatewayLogger.DebugAsync("Ignored GUILD_BAN_ADD, guild is not synced yet.").ConfigureAwait(false);
return;
}

await _userBannedEvent.InvokeAsync(new User(data.User), guild).ConfigureAwait(false);
}
else
{
await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild.").ConfigureAwait(false);
@@ -945,7 +1101,15 @@ namespace Discord
var data = (payload as JToken).ToObject<GuildBanEvent>(_serializer);
var guild = DataStore.GetGuild(data.GuildId);
if (guild != null)
{
if (!guild.IsSynced)
{
await _gatewayLogger.DebugAsync("Ignored GUILD_BAN_REMOVE, guild is not synced yet.").ConfigureAwait(false);
return;
}

await _userUnbannedEvent.InvokeAsync(new User(data.User), guild).ConfigureAwait(false);
}
else
{
await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild.").ConfigureAwait(false);
@@ -1179,39 +1343,66 @@ namespace Discord
var data = (payload as JToken).ToObject<API.VoiceState>(_serializer);
if (data.GuildId.HasValue)
{
var guild = DataStore.GetGuild(data.GuildId.Value);
if (guild != null)
ICachedUser user;
VoiceState before, after;
if (data.GuildId != null)
{
if (!guild.IsSynced)
var guild = DataStore.GetGuild(data.GuildId.Value);
if (guild != null)
{
await _gatewayLogger.DebugAsync("Ignored VOICE_STATE_UPDATE, guild is not synced yet.").ConfigureAwait(false);
return;
if (!guild.IsSynced)
{
await _gatewayLogger.DebugAsync("Ignored VOICE_STATE_UPDATE, guild is not synced yet.").ConfigureAwait(false);
return;
}

if (data.ChannelId != null)
{
before = guild.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false);
after = guild.AddOrUpdateVoiceState(data, DataStore);
}
else
{
before = guild.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false);
after = new VoiceState(null, data);
}
user = guild.GetUser(data.UserId);
}

VoiceState before, after;
if (data.ChannelId != null)
else
{
before = guild.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false);
after = guild.AddOrUpdateVoiceState(data, DataStore);
await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false);
return;
}
else
}
else
{
var groupChannel = DataStore.GetChannel(data.ChannelId.Value) as CachedGroupChannel;
if (groupChannel != null)
{
before = guild.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false);
after = new VoiceState(null, data);
if (data.ChannelId != null)
{
before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false);
after = groupChannel.AddOrUpdateVoiceState(data, DataStore);
}
else
{
before = groupChannel.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false);
after = new VoiceState(null, data);
}
user = groupChannel.GetUser(data.UserId);
}

var user = guild.GetUser(data.UserId);
if (user != null)
await _userVoiceStateUpdatedEvent.InvokeAsync(user, before, after).ConfigureAwait(false);
else
{
await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false);
await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown channel.").ConfigureAwait(false);
return;
}
}

if (user != null)
await _userVoiceStateUpdatedEvent.InvokeAsync(user, before, after).ConfigureAwait(false);
else
{
await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false);
await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false);
return;
}
}


+ 5
- 4
src/Discord.Net/Entities/Channels/ChannelType.cs View File

@@ -1,9 +1,10 @@
namespace Discord
{
public enum ChannelType : byte
public enum ChannelType
{
DM,
Text,
Voice
Text = 0,
DM = 1,
Voice = 2,
Group = 3
}
}

+ 2
- 1
src/Discord.Net/Entities/Channels/DMChannel.cs View File

@@ -17,6 +17,7 @@ namespace Discord
public IUser Recipient { get; private set; }

public virtual IReadOnlyCollection<IMessage> CachedMessages => ImmutableArray.Create<IMessage>();
IReadOnlyCollection<IUser> IPrivateChannel.Recipients => ImmutableArray.Create(Recipient);

public DMChannel(DiscordClient discord, IUser recipient, Model model)
: base(model.Id)
@@ -30,7 +31,7 @@ namespace Discord
{
if (/*source == UpdateSource.Rest && */IsAttached) return;
(Recipient as User).Update(model.Recipient.Value, source);
(Recipient as User).Update(model.Recipients.Value[0], source);
}

public async Task UpdateAsync()


+ 147
- 0
src/Discord.Net/Entities/Channels/GroupChannel.cs View File

@@ -0,0 +1,147 @@
using Discord.API.Rest;
using Discord.Extensions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;

namespace Discord
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
internal class GroupChannel : SnowflakeEntity, IGroupChannel
{
protected ConcurrentDictionary<ulong, GroupUser> _users;
private string _iconId;
public override DiscordClient Discord { get; }
public string Name { get; private set; }

public IReadOnlyCollection<IUser> Recipients => _users.ToReadOnlyCollection();
public virtual IReadOnlyCollection<IMessage> CachedMessages => ImmutableArray.Create<IMessage>();
public string IconUrl => API.CDN.GetChannelIconUrl(Id, _iconId);

public GroupChannel(DiscordClient discord, Model model)
: base(model.Id)
{
Discord = discord;

Update(model, UpdateSource.Creation);
}
public virtual void Update(Model model, UpdateSource source)
{
if (source == UpdateSource.Rest && IsAttached) return;

if (model.Name.IsSpecified)
Name = model.Name.Value;
if (model.Icon.IsSpecified)
_iconId = model.Icon.Value;

if (source != UpdateSource.Creation && model.Recipients.IsSpecified)
UpdateUsers(model.Recipients.Value, source);
}

internal virtual void UpdateUsers(API.User[] models, UpdateSource source)
{
if (!IsAttached)
{
var users = new ConcurrentDictionary<ulong, GroupUser>(1, (int)(models.Length * 1.05));
for (int i = 0; i < models.Length; i++)
users[models[i].Id] = new GroupUser(this, new User(models[i]));
_users = users;
}
}

public async Task UpdateAsync()
{
if (IsAttached) throw new NotSupportedException();

var model = await Discord.ApiClient.GetChannelAsync(Id).ConfigureAwait(false);
Update(model, UpdateSource.Rest);
}
public async Task LeaveAsync()
{
await Discord.ApiClient.DeleteChannelAsync(Id).ConfigureAwait(false);
}

public async Task AddUserAsync(IUser user)
{
await Discord.ApiClient.AddGroupRecipientAsync(Id, user.Id).ConfigureAwait(false);
}
public async Task<IUser> GetUserAsync(ulong id)
{
GroupUser user;
if (_users.TryGetValue(id, out user))
return user;
var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false);
if (id == currentUser.Id)
return currentUser;
return null;
}
public async Task<IReadOnlyCollection<IUser>> GetUsersAsync()
{
var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false);
return _users.Select(x => x.Value).Concat<IUser>(ImmutableArray.Create(currentUser)).ToReadOnlyCollection(_users);
}

public async Task<IMessage> SendMessageAsync(string text, bool isTTS)
{
var args = new CreateMessageParams { Content = text, IsTTS = isTTS };
var model = await Discord.ApiClient.CreateDMMessageAsync(Id, args).ConfigureAwait(false);
return new Message(this, new User(model.Author.Value), model);
}
public async Task<IMessage> SendFileAsync(string filePath, string text, bool isTTS)
{
string filename = Path.GetFileName(filePath);
using (var file = File.OpenRead(filePath))
{
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS };
var model = await Discord.ApiClient.UploadDMFileAsync(Id, file, args).ConfigureAwait(false);
return new Message(this, new User(model.Author.Value), model);
}
}
public async Task<IMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS)
{
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS };
var model = await Discord.ApiClient.UploadDMFileAsync(Id, stream, args).ConfigureAwait(false);
return new Message(this, new User(model.Author.Value), model);
}
public virtual async Task<IMessage> GetMessageAsync(ulong id)
{
var model = await Discord.ApiClient.GetChannelMessageAsync(Id, id).ConfigureAwait(false);
if (model != null)
return new Message(this, new User(model.Author.Value), model);
return null;
}
public virtual async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit)
{
var args = new GetChannelMessagesParams { Limit = limit };
var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false);
return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray();
}
public virtual async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit)
{
var args = new GetChannelMessagesParams { Limit = limit };
var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false);
return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray();
}
public async Task DeleteMessagesAsync(IEnumerable<IMessage> messages)
{
await Discord.ApiClient.DeleteDMMessagesAsync(Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false);
}

public async Task TriggerTypingAsync()
{
await Discord.ApiClient.TriggerTypingIndicatorAsync(Id).ConfigureAwait(false);
}

public override string ToString() => Name;
private string DebuggerDisplay => $"@{Name} ({Id}, Group)";

IMessage IMessageChannel.GetCachedMessage(ulong id) => null;
}
}

+ 1
- 1
src/Discord.Net/Entities/Channels/IDMChannel.cs View File

@@ -2,7 +2,7 @@

namespace Discord
{
public interface IDMChannel : IMessageChannel
public interface IDMChannel : IMessageChannel, IPrivateChannel
{
/// <summary> Gets the recipient of all messages in this channel. </summary>
IUser Recipient { get; }


+ 13
- 0
src/Discord.Net/Entities/Channels/IGroupChannel.cs View File

@@ -0,0 +1,13 @@
using System.Threading.Tasks;

namespace Discord
{
public interface IGroupChannel : IMessageChannel, IPrivateChannel
{
/// <summary> Adds a user to this group. </summary>
Task AddUserAsync(IUser user);

/// <summary> Leaves this group. </summary>
Task LeaveAsync();
}
}

+ 9
- 0
src/Discord.Net/Entities/Channels/IPrivateChannel.cs View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;

namespace Discord
{
public interface IPrivateChannel
{
IReadOnlyCollection<IUser> Recipients { get; }
}
}

+ 2
- 2
src/Discord.Net/Entities/Guilds/Guild.cs View File

@@ -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}");
}
}



+ 4
- 2
src/Discord.Net/Entities/Messages/IMessage.cs View File

@@ -13,11 +13,13 @@ namespace Discord
bool IsTTS { get; }
/// <summary> Returns true if this message was added to its channel's pinned messages. </summary>
bool IsPinned { get; }
/// <summary> Returns the text for this message. </summary>
string Text { get; }
/// <summary> Returns the content for this message. </summary>
string Content { get; }
/// <summary> Gets the time this message was sent. </summary>
DateTimeOffset Timestamp { get; }

/// <summary> Gets the type of this message. </summary>
MessageType Type { get; }
/// <summary> Gets the channel this message was sent to. </summary>
IMessageChannel Channel { get; }
/// <summary> Gets the author of this message. </summary>


+ 12
- 10
src/Discord.Net/Entities/Messages/Message.cs View File

@@ -14,14 +14,15 @@ namespace Discord
private bool _isMentioningEveryone;
private long _timestampTicks;
private long? _editedTimestampTicks;

public bool IsTTS { get; private set; }
public string Text { get; private set; }
public bool IsPinned { get; private set; }
public MessageType Type { get; }
public IMessageChannel Channel { get; }
public IUser Author { get; }

public bool IsTTS { get; private set; }
public string Content { get; private set; }
public bool IsPinned { get; private set; }

public IReadOnlyCollection<IAttachment> Attachments { get; private set; }
public IReadOnlyCollection<IEmbed> Embeds { get; private set; }
public IReadOnlyCollection<ulong> MentionedChannelIds { get; private set; }
@@ -37,6 +38,7 @@ namespace Discord
{
Channel = channel;
Author = author;
Type = model.Type;

if (channel is IGuildChannel)
{
@@ -118,7 +120,7 @@ namespace Discord
MentionedChannelIds = MentionUtils.GetChannelMentions(text, guildChannel.Guild);
MentionedRoles = MentionUtils.GetRoleMentions(text, guildChannel.Guild);
}
Text = text;
Content = text;
}
}

@@ -163,9 +165,9 @@ namespace Discord
}
public string Resolve(int startIndex, int length, UserResolveMode userMode = UserResolveMode.NameOnly)
=> Resolve(Text.Substring(startIndex, length), userMode);
=> Resolve(Content.Substring(startIndex, length), userMode);
public string Resolve(UserResolveMode userMode = UserResolveMode.NameOnly)
=> Resolve(Text, userMode);
=> Resolve(Content, userMode);
private string Resolve(string text, UserResolveMode userMode = UserResolveMode.NameOnly)
{
@@ -179,7 +181,7 @@ namespace Discord
return text;
}

public override string ToString() => Text;
private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}";
public override string ToString() => Content;
private string DebuggerDisplay => $"{Author}: {Content}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}";
}
}

+ 12
- 0
src/Discord.Net/Entities/Messages/MessageType.cs View File

@@ -0,0 +1,12 @@
namespace Discord
{
public enum MessageType
{
Default = 0,
RecipientAdd = 1,
RecipientRemove = 2,
Call = 3,
ChannelNameChange = 4,
ChannelIconChange = 5
}
}

+ 47
- 0
src/Discord.Net/Entities/Users/GroupUser.cs View File

@@ -0,0 +1,47 @@
using Discord.API.Rest;
using System;
using System.Threading.Tasks;

namespace Discord
{
internal class GroupUser : IGroupUser
{
public GroupChannel Channel { get; private set; }
public User User { get; private set; }

public ulong Id => User.Id;
public string AvatarUrl => User.AvatarUrl;
public DateTimeOffset CreatedAt => User.CreatedAt;
public string Discriminator => User.Discriminator;
public ushort DiscriminatorValue => User.DiscriminatorValue;
public bool IsAttached => User.IsAttached;
public bool IsBot => User.IsBot;
public string Mention => User.Mention;
public string NicknameMention => User.NicknameMention;
public string Username => User.Username;

public virtual UserStatus Status => UserStatus.Unknown;
public virtual Game Game => null;

public DiscordClient Discord => Channel.Discord;

public GroupUser(GroupChannel channel, User user)
{
Channel = channel;
User = user;
}

public async Task KickAsync()
{
await Discord.ApiClient.RemoveGroupRecipientAsync(Channel.Id, Id).ConfigureAwait(false);
}

public async Task<IDMChannel> CreateDMChannelAsync()
{
var args = new CreateDMChannelParams { Recipient = this };
var model = await Discord.ApiClient.CreateDMChannelAsync(args).ConfigureAwait(false);

return new DMChannel(Discord, new User(model.Recipients.Value[0]), model);
}
}
}

+ 1
- 2
src/Discord.Net/Entities/Users/GuildUser.cs View File

@@ -7,7 +7,6 @@ using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.GuildMember;
using PresenceModel = Discord.API.Presence;
using VoiceStateModel = Discord.API.VoiceState;

namespace Discord
{
@@ -141,7 +140,7 @@ namespace Discord
var args = new CreateDMChannelParams { Recipient = this };
var model = await Discord.ApiClient.CreateDMChannelAsync(args).ConfigureAwait(false);

return new DMChannel(Discord, User, model);
return new DMChannel(Discord, new User(model.Recipients.Value[0]), model);
}

IGuild IGuildUser.Guild => Guild;


+ 13
- 0
src/Discord.Net/Entities/Users/IGroupUser.cs View File

@@ -0,0 +1,13 @@
using System.Threading.Tasks;

namespace Discord
{
public interface IGroupUser : IUser
{
/// <summary> Kicks this user from this group. </summary>
Task KickAsync();

/// <summary> Returns a private message channel to this user, creating one if it does not already exist. </summary>
Task<IDMChannel> CreateDMChannelAsync();
}
}

+ 6
- 3
src/Discord.Net/Entities/Users/SelfUser.cs View File

@@ -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()


+ 8
- 4
src/Discord.Net/Entities/Users/User.cs View File

@@ -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}";


+ 2
- 1
src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs View File

@@ -6,13 +6,14 @@ using Model = Discord.API.Channel;

namespace Discord
{
internal class CachedDMChannel : DMChannel, IDMChannel, ICachedChannel, ICachedMessageChannel
internal class CachedDMChannel : DMChannel, IDMChannel, ICachedChannel, ICachedMessageChannel, ICachedPrivateChannel
{
private readonly MessageManager _messages;

public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public new CachedDMUser Recipient => base.Recipient as CachedDMUser;
public IReadOnlyCollection<ICachedUser> Members => ImmutableArray.Create<ICachedUser>(Discord.CurrentUser, Recipient);
IReadOnlyCollection<CachedDMUser> ICachedPrivateChannel.Recipients => ImmutableArray.Create(Recipient);

public CachedDMChannel(DiscordSocketClient discord, CachedDMUser recipient, Model model)
: base(discord, recipient, model)


+ 5
- 0
src/Discord.Net/Entities/WebSocket/CachedDMUser.cs View File

@@ -1,8 +1,10 @@
using System;
using System.Diagnostics;
using PresenceModel = Discord.API.Presence;

namespace Discord
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
internal class CachedDMUser : ICachedUser
{
public CachedGlobalUser User { get; }
@@ -36,5 +38,8 @@ namespace Discord

public CachedDMUser Clone() => MemberwiseClone() as CachedDMUser;
ICachedUser ICachedUser.Clone() => Clone();

public override string ToString() => $"{Username}#{Discriminator}";
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})";
}
}

+ 139
- 0
src/Discord.Net/Entities/WebSocket/CachedGroupChannel.cs View File

@@ -0,0 +1,139 @@
using Discord.Extensions;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using MessageModel = Discord.API.Message;
using Model = Discord.API.Channel;
using UserModel = Discord.API.User;
using VoiceStateModel = Discord.API.VoiceState;
using Discord.API;

namespace Discord
{
internal class CachedGroupChannel : GroupChannel, IGroupChannel, ICachedChannel, ICachedMessageChannel, ICachedPrivateChannel
{
private readonly MessageManager _messages;
private ConcurrentDictionary<ulong, VoiceState> _voiceStates;

public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public IReadOnlyCollection<ICachedUser> Members
=> _users.Select(x => x.Value as ICachedUser).Concat(ImmutableArray.Create(Discord.CurrentUser)).ToReadOnlyCollection(() => _users.Count + 1);
public new IReadOnlyCollection<CachedDMUser> Recipients => _users.Cast<CachedDMUser>().ToReadOnlyCollection(_users);

public CachedGroupChannel(DiscordSocketClient discord, Model model)
: base(discord, model)
{
if (Discord.MessageCacheSize > 0)
_messages = new MessageCache(Discord, this);
else
_messages = new MessageManager(Discord, this);
_voiceStates = new ConcurrentDictionary<ulong, VoiceState>(1, 5);
}
public override void Update(Model model, UpdateSource source)
{
if (source == UpdateSource.Rest && IsAttached) return;

base.Update(model, source);
}

internal void UpdateUsers(UserModel[] models, UpdateSource source, DataStore dataStore)
{
var users = new ConcurrentDictionary<ulong, GroupUser>(1, models.Length);
for (int i = 0; i < models.Length; i++)
{
var globalUser = Discord.GetOrAddUser(models[i], dataStore);
users[models[i].Id] = new CachedGroupUser(this, globalUser);
}
_users = users;
}
internal override void UpdateUsers(UserModel[] models, UpdateSource source)
=> UpdateUsers(models, source, Discord.DataStore);

public CachedGroupUser AddUser(UserModel model, DataStore dataStore)
{
GroupUser user;
if (_users.TryGetValue(model.Id, out user))
return user as CachedGroupUser;
else
{
var globalUser = Discord.GetOrAddUser(model, dataStore);
var privateUser = new CachedGroupUser(this, globalUser);
_users[privateUser.Id] = privateUser;
return privateUser;
}
}
public ICachedUser GetUser(ulong id)
{
GroupUser user;
if (_users.TryGetValue(id, out user))
return user as CachedGroupUser;
if (id == Discord.CurrentUser.Id)
return Discord.CurrentUser;
return null;
}
public CachedGroupUser RemoveUser(ulong id)
{
GroupUser user;
if (_users.TryRemove(id, out user))
return user as CachedGroupUser;
return null;
}

public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary<ulong, VoiceState> voiceStates = null)
{
var voiceChannel = dataStore.GetChannel(model.ChannelId.Value) as CachedVoiceChannel;
var voiceState = new VoiceState(voiceChannel, model);
(voiceStates ?? _voiceStates)[model.UserId] = voiceState;
return voiceState;
}
public VoiceState? GetVoiceState(ulong id)
{
VoiceState voiceState;
if (_voiceStates.TryGetValue(id, out voiceState))
return voiceState;
return null;
}
public VoiceState? RemoveVoiceState(ulong id)
{
VoiceState voiceState;
if (_voiceStates.TryRemove(id, out voiceState))
return voiceState;
return null;
}

public override async Task<IMessage> GetMessageAsync(ulong id)
{
return await _messages.DownloadAsync(id).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit)
{
return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit)
{
return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false);
}
public CachedMessage AddMessage(ICachedUser author, MessageModel model)
{
var msg = new CachedMessage(this, author, model);
_messages.Add(msg);
return msg;
}
public CachedMessage GetMessage(ulong id)
{
return _messages.Get(id);
}
public CachedMessage RemoveMessage(ulong id)
{
return _messages.Remove(id);
}

public CachedDMChannel Clone() => MemberwiseClone() as CachedDMChannel;

IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id);
ICachedUser ICachedMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id);
ICachedChannel ICachedChannel.Clone() => Clone();
}
}

+ 33
- 0
src/Discord.Net/Entities/WebSocket/CachedGroupUser.cs View File

@@ -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})";
}
}

+ 2
- 3
src/Discord.Net/Entities/WebSocket/CachedGuild.cs View File

@@ -208,7 +208,6 @@ namespace Discord
var user = Discord.GetOrAddUser(model.User, dataStore);
member = new CachedGuildUser(this, user, model);
members[user.Id] = member;
user.AddRef();
DownloadedMemberCount++;
}
return member;
@@ -311,14 +310,14 @@ namespace Discord

new internal ICachedGuildChannel ToChannel(ChannelModel model)
{
switch (model.Type.Value)
switch (model.Type)
{
case ChannelType.Text:
return new CachedTextChannel(this, model);
case ChannelType.Voice:
return new CachedVoiceChannel(this, model);
default:
throw new InvalidOperationException($"Unknown channel type: {model.Type.Value}");
throw new InvalidOperationException($"Unexpected channel type: {model.Type}");
}
}



+ 9
- 0
src/Discord.Net/Entities/WebSocket/ICachedPrivateChannel.cs View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;

namespace Discord
{
internal interface ICachedPrivateChannel : ICachedChannel, IPrivateChannel
{
new IReadOnlyCollection<CachedDMUser> Recipients { get; }
}
}

+ 11
- 8
src/Discord.Net/Extensions/CollectionExtensions.cs View File

@@ -1,4 +1,5 @@
using System.Collections;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -8,24 +9,26 @@ namespace Discord.Extensions
internal static class CollectionExtensions
{
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source)
=> new ConcurrentDictionaryWrapper<TValue, KeyValuePair<TKey, TValue>>(source, source.Select(x => x.Value));
=> new ConcurrentDictionaryWrapper<TValue>(source.Select(x => x.Value), () => source.Count);
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue, TSource>(this IEnumerable<TValue> query, IReadOnlyCollection<TSource> source)
=> new ConcurrentDictionaryWrapper<TValue, TSource>(source, query);
=> new ConcurrentDictionaryWrapper<TValue>(query, () => source.Count);
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue>(this IEnumerable<TValue> query, Func<int> countFunc)
=> new ConcurrentDictionaryWrapper<TValue>(query, countFunc);
}

[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
internal struct ConcurrentDictionaryWrapper<TValue, TSource> : IReadOnlyCollection<TValue>
internal struct ConcurrentDictionaryWrapper<TValue> : IReadOnlyCollection<TValue>
{
private readonly IReadOnlyCollection<TSource> _source;
private readonly IEnumerable<TValue> _query;
private readonly Func<int> _countFunc;

//It's okay that this count is affected by race conditions - we're wrapping a concurrent collection and that's to be expected
public int Count => _source.Count;
public int Count => _countFunc();
public ConcurrentDictionaryWrapper(IReadOnlyCollection<TSource> source, IEnumerable<TValue> query)
public ConcurrentDictionaryWrapper(IEnumerable<TValue> query, Func<int> countFunc)
{
_source = source;
_query = query;
_countFunc = countFunc;
}

private string DebuggerDisplay => $"Count = {Count}";


+ 1
- 1
src/Discord.Net/IDiscordClient.cs View File

@@ -24,7 +24,7 @@ namespace Discord
Task DisconnectAsync();

Task<IChannel> GetChannelAsync(ulong id);
Task<IReadOnlyCollection<IDMChannel>> GetDMChannelsAsync();
Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync();

Task<IReadOnlyCollection<IConnection>> GetConnectionsAsync();



+ 0
- 42
src/Discord.Net/Net/Converters/ChannelTypeConverter.cs View File

@@ -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");
}
}
}
}

+ 0
- 2
src/Discord.Net/Net/Converters/DiscordContractResolver.cs View File

@@ -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))


Loading…
Cancel
Save