Browse Source

Restructured to better merge REST and WebSocket entities

tags/1.0-rc
RogueException 9 years ago
parent
commit
8c2fa21b81
100 changed files with 2636 additions and 1028 deletions
  1. +6
    -8
      README.md
  2. +1
    -1
      src/Discord.Net/API/Common/Connection.cs
  3. +1
    -1
      src/Discord.Net/API/Common/Game.cs
  4. +14
    -0
      src/Discord.Net/API/Common/Presence.cs
  5. +14
    -0
      src/Discord.Net/API/Common/Relationship.cs
  6. +9
    -0
      src/Discord.Net/API/Common/RelationshipType.cs
  7. +0
    -2
      src/Discord.Net/API/Common/VoiceState.cs
  8. +243
    -223
      src/Discord.Net/API/DiscordAPIClient.cs
  9. +24
    -0
      src/Discord.Net/API/Gateway/Common/ExtendedGuild.cs
  10. +8
    -2
      src/Discord.Net/API/Gateway/GatewayOpCodes.cs
  11. +10
    -0
      src/Discord.Net/API/Gateway/GuildBanEvent.cs
  12. +12
    -0
      src/Discord.Net/API/Gateway/GuildRoleDeleteEvent.cs
  13. +3
    -1
      src/Discord.Net/API/Gateway/ReadyEvent.cs
  14. +1
    -1
      src/Discord.Net/API/Rest/DeleteMessagesParams.cs
  15. +0
    -12
      src/Discord.Net/API/Rest/LoginParams.cs
  16. +0
    -10
      src/Discord.Net/API/Rest/LoginResponse.cs
  17. +1
    -8
      src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs
  18. +4
    -0
      src/Discord.Net/Data/DataStoreProvider.cs
  19. +90
    -0
      src/Discord.Net/Data/DefaultDataStore.cs
  20. +24
    -0
      src/Discord.Net/Data/IDataStore.cs
  21. +11
    -0
      src/Discord.Net/Data/SharedDataStore.cs
  22. +47
    -89
      src/Discord.Net/DiscordClient.cs
  23. +1
    -1
      src/Discord.Net/DiscordConfig.cs
  24. +708
    -0
      src/Discord.Net/DiscordSocketClient.cs
  25. +7
    -7
      src/Discord.Net/DiscordSocketConfig.cs
  26. +126
    -0
      src/Discord.Net/Entities/Channels/DMChannel.cs
  27. +59
    -84
      src/Discord.Net/Entities/Channels/GuildChannel.cs
  28. +2
    -2
      src/Discord.Net/Entities/Channels/IChannel.cs
  29. +4
    -4
      src/Discord.Net/Entities/Channels/IGuildChannel.cs
  30. +11
    -11
      src/Discord.Net/Entities/Channels/IMessageChannel.cs
  31. +116
    -0
      src/Discord.Net/Entities/Channels/TextChannel.cs
  32. +21
    -13
      src/Discord.Net/Entities/Channels/VoiceChannel.cs
  33. +16
    -0
      src/Discord.Net/Entities/Entity.cs
  34. +1
    -1
      src/Discord.Net/Entities/Guilds/Emoji.cs
  35. +81
    -141
      src/Discord.Net/Entities/Guilds/Guild.cs
  36. +18
    -0
      src/Discord.Net/Entities/Guilds/GuildEmbed.cs
  37. +16
    -30
      src/Discord.Net/Entities/Guilds/GuildIntegration.cs
  38. +18
    -19
      src/Discord.Net/Entities/Guilds/IGuild.cs
  39. +0
    -8
      src/Discord.Net/Entities/Guilds/IGuildEmbed.cs
  40. +1
    -1
      src/Discord.Net/Entities/Guilds/IUserGuild.cs
  41. +3
    -1
      src/Discord.Net/Entities/Guilds/IVoiceRegion.cs
  42. +0
    -3
      src/Discord.Net/Entities/Guilds/IntegrationAccount.cs
  43. +11
    -19
      src/Discord.Net/Entities/Guilds/UserGuild.cs
  44. +2
    -8
      src/Discord.Net/Entities/Guilds/VoiceRegion.cs
  45. +4
    -0
      src/Discord.Net/Entities/IEntity.cs
  46. +1
    -1
      src/Discord.Net/Entities/IUpdateable.cs
  47. +1
    -1
      src/Discord.Net/Entities/Invites/IInvite.cs
  48. +13
    -26
      src/Discord.Net/Entities/Invites/Invite.cs
  49. +6
    -9
      src/Discord.Net/Entities/Invites/InviteMetadata.cs
  50. +4
    -4
      src/Discord.Net/Entities/Messages/Embed.cs
  51. +5
    -3
      src/Discord.Net/Entities/Messages/EmbedProvider.cs
  52. +8
    -5
      src/Discord.Net/Entities/Messages/EmbedThumbnail.cs
  53. +12
    -0
      src/Discord.Net/Entities/Messages/IEmbed.cs
  54. +7
    -8
      src/Discord.Net/Entities/Messages/IMessage.cs
  55. +41
    -49
      src/Discord.Net/Entities/Messages/Message.cs
  56. +1
    -1
      src/Discord.Net/Entities/Permissions/ChannelPermissions.cs
  57. +1
    -1
      src/Discord.Net/Entities/Permissions/GuildPermissions.cs
  58. +7
    -4
      src/Discord.Net/Entities/Permissions/Overwrite.cs
  59. +1
    -1
      src/Discord.Net/Entities/Permissions/OverwritePermissions.cs
  60. +15
    -14
      src/Discord.Net/Entities/Permissions/Permissions.cs
  61. +1
    -4
      src/Discord.Net/Entities/Roles/IRole.cs
  62. +18
    -35
      src/Discord.Net/Entities/Roles/Role.cs
  63. +15
    -0
      src/Discord.Net/Entities/SnowflakeEntity.cs
  64. +9
    -0
      src/Discord.Net/Entities/UpdateSource.cs
  65. +2
    -3
      src/Discord.Net/Entities/Users/Connection.cs
  66. +7
    -7
      src/Discord.Net/Entities/Users/Game.cs
  67. +43
    -43
      src/Discord.Net/Entities/Users/GuildUser.cs
  68. +1
    -1
      src/Discord.Net/Entities/Users/IConnection.cs
  69. +4
    -4
      src/Discord.Net/Entities/Users/IGuildUser.cs
  70. +10
    -0
      src/Discord.Net/Entities/Users/IPresence.cs
  71. +1
    -5
      src/Discord.Net/Entities/Users/IUser.cs
  72. +15
    -19
      src/Discord.Net/Entities/Users/SelfUser.cs
  73. +16
    -32
      src/Discord.Net/Entities/Users/User.cs
  74. +70
    -0
      src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs
  75. +171
    -0
      src/Discord.Net/Entities/WebSocket/CachedGuild.cs
  76. +16
    -0
      src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs
  77. +17
    -0
      src/Discord.Net/Entities/WebSocket/CachedMessage.cs
  78. +58
    -0
      src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs
  79. +16
    -0
      src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs
  80. +73
    -0
      src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs
  81. +38
    -0
      src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs
  82. +6
    -0
      src/Discord.Net/Entities/WebSocket/ICachedChannel.cs
  83. +7
    -0
      src/Discord.Net/Entities/WebSocket/ICachedEntity.cs
  84. +6
    -0
      src/Discord.Net/Entities/WebSocket/ICachedGuildChannel.cs
  85. +16
    -0
      src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs
  86. +4
    -4
      src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old
  87. +14
    -0
      src/Discord.Net/Entities/WebSocket/Presence.cs
  88. +31
    -0
      src/Discord.Net/Extensions/CollectionExtensions.cs
  89. +14
    -0
      src/Discord.Net/Extensions/DiscordClientExtensions.cs
  90. +1
    -1
      src/Discord.Net/Extensions/EventExtensions.cs
  91. +12
    -0
      src/Discord.Net/Extensions/GuildExtensions.cs
  92. +6
    -11
      src/Discord.Net/IDiscordClient.cs
  93. +4
    -3
      src/Discord.Net/Logging/LogManager.cs
  94. +1
    -1
      src/Discord.Net/Logging/Logger.cs
  95. +17
    -7
      src/Discord.Net/Net/Converters/DiscordContractResolver.cs
  96. +1
    -4
      src/Discord.Net/Net/Converters/OptionalConverter.cs
  97. +4
    -2
      src/Discord.Net/Net/HttpException.cs
  98. +16
    -0
      src/Discord.Net/Net/Queue/BucketDefinition.cs
  99. +1
    -1
      src/Discord.Net/Net/Queue/BucketGroup.cs
  100. +2
    -3
      src/Discord.Net/Net/Queue/GlobalBucket.cs

+ 6
- 8
README.md View File

@@ -5,9 +5,6 @@ An unofficial .Net API Wrapper for the Discord client (http://discordapp.com).

Check out the [documentation](http://rtd.discord.foxbot.me/en/docs-dev/index.html) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx).

##### Warning: Some of the documentation is outdated.
It's current being rewritten. Until that's done, feel free to use my [DiscordBot](https://github.com/RogueException/DiscordBot) repo for reference.

### Installation
You can download Discord.Net and its extensions from NuGet:
- [Discord.Net](https://www.nuget.org/packages/Discord.Net/)
@@ -16,9 +13,10 @@ You can download Discord.Net and its extensions from NuGet:
- [Discord.Net.Audio](https://www.nuget.org/packages/Discord.Net.Audio/)

### Compiling
In order to compile Discord.Net, you require at least the following:
- [Visual Studio 2015](https://www.visualstudio.com/downloads/download-visual-studio-vs)
- [Visual Studio 2015 Update 2](https://www.visualstudio.com/en-us/news/vs2015-update2-vs.aspx)
- [Visual Studio .Net Core Plugin](https://www.microsoft.com/net/core#windows)
In order to compile Discord.Net, you require the following:
#### Visual Studio 2015
- [VS2015 Update 2](https://www.visualstudio.com/en-us/news/vs2015-update2-vs.aspx)
- [.Net Core SDK + VS Plugin](https://www.microsoft.com/net/core#windows)
- NuGet 3.3+ (available through Visual Studio)

#### CLI
- [.Net Core SDK](https://www.microsoft.com/net/core#windows)

+ 1
- 1
src/Discord.Net/API/Common/Connection.cs View File

@@ -15,6 +15,6 @@ namespace Discord.API
public bool Revoked { get; set; }

[JsonProperty("integrations")]
public IEnumerable<ulong> Integrations { get; set; }
public IReadOnlyCollection<ulong> Integrations { get; set; }
}
}

+ 1
- 1
src/Discord.Net/API/Common/Game.cs View File

@@ -9,6 +9,6 @@ namespace Discord.API
[JsonProperty("url")]
public string StreamUrl { get; set; }
[JsonProperty("type")]
public StreamType StreamType { get; set; }
public StreamType? StreamType { get; set; }
}
}

+ 14
- 0
src/Discord.Net/API/Common/Presence.cs View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;

namespace Discord.API
{
public class Presence
{
[JsonProperty("user")]
public User User { get; set; }
[JsonProperty("status")]
public UserStatus Status { get; set; }
[JsonProperty("game")]
public Game Game { get; set; }
}
}

+ 14
- 0
src/Discord.Net/API/Common/Relationship.cs View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;

namespace Discord.API
{
public class Relationship
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("user")]
public User User { get; set; }
[JsonProperty("type")]
public RelationshipType Type { get; set; }
}
}

+ 9
- 0
src/Discord.Net/API/Common/RelationshipType.cs View File

@@ -0,0 +1,9 @@
namespace Discord.API
{
public enum RelationshipType
{
Friend = 1,
Blocked = 2,
Pending = 4
}
}

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

@@ -4,8 +4,6 @@ namespace Discord.API
{
public class VoiceState
{
[JsonProperty("guild_id")]
public ulong? GuildId { get; set; }
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
[JsonProperty("user_id")]


+ 243
- 223
src/Discord.Net/API/DiscordAPIClient.cs
File diff suppressed because it is too large
View File


+ 24
- 0
src/Discord.Net/API/Gateway/Common/ExtendedGuild.cs View File

@@ -0,0 +1,24 @@
using Newtonsoft.Json;
using System;

namespace Discord.API.Gateway
{
public class ExtendedGuild : Guild
{
[JsonProperty("unavailable")]
public bool? Unavailable { get; set; }
[JsonProperty("member_count")]
public int MemberCount { get; set; }
[JsonProperty("large")]
public bool Large { get; set; }

[JsonProperty("presences")]
public Presence[] Presences { get; set; }
[JsonProperty("members")]
public GuildMember[] Members { get; set; }
[JsonProperty("channels")]
public Channel[] Channels { get; set; }
[JsonProperty("joined_at")]
public DateTime JoinedAt { get; set; }
}
}

+ 8
- 2
src/Discord.Net/API/Gateway/GatewayOpCodes.cs View File

@@ -12,13 +12,19 @@
StatusUpdate = 3,
/// <summary> C→S - Used to join a particular voice channel. </summary>
VoiceStateUpdate = 4,
/// <summary> C→S - Used to ensure the server's voice server is alive. Only send this if voice connection fails or suddenly drops. </summary>
/// <summary> C→S - Used to ensure the guild's voice server is alive. </summary>
VoiceServerPing = 5,
/// <summary> C→S - Used to resume a connection after a redirect occurs. </summary>
Resume = 6,
/// <summary> C←S - Used to notify a client that they must reconnect to another gateway. </summary>
Reconnect = 7,
/// <summary> C→S - Used to request all members that were withheld by large_threshold </summary>
RequestGuildMembers = 8
RequestGuildMembers = 8,
/// <summary> S→C - Used to notify the client that their session has expired and cannot be resumed. </summary>
InvalidSession = 9,
/// <summary> S→C - Used to provide information to the client immediately on connection. </summary>
Hello = 10,
/// <summary> S→C - Used to reply to a client's heartbeat. </summary>
HeartbeatAck = 11
}
}

+ 10
- 0
src/Discord.Net/API/Gateway/GuildBanEvent.cs View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Discord.API.Gateway
{
public class GuildBanEvent : User
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
}
}

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

@@ -0,0 +1,12 @@
using Newtonsoft.Json;

namespace Discord.API.Gateway
{
public class GuildRoleDeleteEvent
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("role_id")]
public ulong RoleId { get; set; }
}
}

+ 3
- 1
src/Discord.Net/API/Gateway/ReadyEvent.cs View File

@@ -23,11 +23,13 @@ namespace Discord.API.Gateway
[JsonProperty("read_state")]
public ReadState[] ReadStates { get; set; }
[JsonProperty("guilds")]
public Guild[] Guilds { get; set; }
public ExtendedGuild[] Guilds { get; set; }
[JsonProperty("private_channels")]
public Channel[] PrivateChannels { get; set; }
[JsonProperty("heartbeat_interval")]
public int HeartbeatInterval { get; set; }
[JsonProperty("relationships")]
public Relationship[] Relationships { get; set; }

//Ignored
[JsonProperty("user_settings")]


src/Discord.Net/API/Rest/DeleteMessagesParam.cs → src/Discord.Net/API/Rest/DeleteMessagesParams.cs View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;

namespace Discord.API.Rest
{
public class DeleteMessagesParam
public class DeleteMessagesParams
{
[JsonProperty("messages")]
public IEnumerable<ulong> MessageIds { get; set; }

+ 0
- 12
src/Discord.Net/API/Rest/LoginParams.cs View File

@@ -1,12 +0,0 @@
using Newtonsoft.Json;

namespace Discord.API.Rest
{
public class LoginParams
{
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("password")]
public string Password { get; set; }
}
}

+ 0
- 10
src/Discord.Net/API/Rest/LoginResponse.cs View File

@@ -1,10 +0,0 @@
using Newtonsoft.Json;

namespace Discord.API.Rest
{
public class LoginResponse
{
[JsonProperty("token")]
public string Token { get; set; }
}
}

+ 1
- 8
src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs View File

@@ -1,5 +1,4 @@
using Discord.Net.Converters;
using Newtonsoft.Json;
using Newtonsoft.Json;
using System.IO;

namespace Discord.API.Rest
@@ -8,12 +7,6 @@ namespace Discord.API.Rest
{
[JsonProperty("username")]
public Optional<string> Username { get; set; }
[JsonProperty("email")]
public Optional<string> Email { get; set; }
[JsonProperty("password")]
public Optional<string> Password { get; set; }
[JsonProperty("new_password")]
public Optional<string> NewPassword { get; set; }
[JsonProperty("avatar"), Image]
public Optional<Stream> Avatar { get; set; }
}


+ 4
- 0
src/Discord.Net/Data/DataStoreProvider.cs View File

@@ -0,0 +1,4 @@
namespace Discord.Data
{
public delegate DataStore DataStoreProvider(int shardId, int totalShards, int guildCount, int dmCount);
}

+ 90
- 0
src/Discord.Net/Data/DefaultDataStore.cs View File

@@ -0,0 +1,90 @@
using Discord.Extensions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Discord.Data
{
public class DefaultDataStore : DataStore
{
private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149
private const double AverageUsersPerGuild = 47.78; //Source: Googie2149
private const double CollectionMultiplier = 1.05; //Add buffer to handle growth
private const double CollectionConcurrencyLevel = 1; //WebSocket updater/event handler. //TODO: Needs profiling, increase to 2?

private readonly ConcurrentDictionary<ulong, ICachedChannel> _channels;
private readonly ConcurrentDictionary<ulong, CachedGuild> _guilds;
private readonly ConcurrentDictionary<ulong, CachedPublicUser> _users;

internal override IReadOnlyCollection<ICachedChannel> Channels => _channels.ToReadOnlyCollection();
internal override IReadOnlyCollection<CachedGuild> Guilds => _guilds.ToReadOnlyCollection();
internal override IReadOnlyCollection<CachedPublicUser> Users => _users.ToReadOnlyCollection();

public DefaultDataStore(int guildCount, int dmChannelCount)
{
double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount;
double estimatedUsersCount = guildCount * AverageUsersPerGuild;
_channels = new ConcurrentDictionary<ulong, ICachedChannel>(1, (int)(estimatedChannelCount * CollectionMultiplier));
_guilds = new ConcurrentDictionary<ulong, CachedGuild>(1, (int)(guildCount * CollectionMultiplier));
_users = new ConcurrentDictionary<ulong, CachedPublicUser>(1, (int)(estimatedUsersCount * CollectionMultiplier));
}

internal override ICachedChannel GetChannel(ulong id)
{
ICachedChannel channel;
if (_channels.TryGetValue(id, out channel))
return channel;
return null;
}
internal override void AddChannel(ICachedChannel channel)
{
_channels[channel.Id] = channel;
}
internal override ICachedChannel RemoveChannel(ulong id)
{
ICachedChannel channel;
if (_channels.TryRemove(id, out channel))
return channel;
return null;
}

internal override CachedGuild GetGuild(ulong id)
{
CachedGuild guild;
if (_guilds.TryGetValue(id, out guild))
return guild;
return null;
}
internal override void AddGuild(CachedGuild guild)
{
_guilds[guild.Id] = guild;
}
internal override CachedGuild RemoveGuild(ulong id)
{
CachedGuild guild;
if (_guilds.TryRemove(id, out guild))
return guild;
return null;
}

internal override CachedPublicUser GetUser(ulong id)
{
CachedPublicUser user;
if (_users.TryGetValue(id, out user))
return user;
return null;
}
internal override CachedPublicUser GetOrAddUser(ulong id, Func<ulong, CachedPublicUser> userFactory)
{
return _users.GetOrAdd(id, userFactory);
}
internal override CachedPublicUser RemoveUser(ulong id)
{
CachedPublicUser user;
if (_users.TryRemove(id, out user))
return user;
return null;
}
}
}

+ 24
- 0
src/Discord.Net/Data/IDataStore.cs View File

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

namespace Discord.Data
{
public abstract class DataStore
{
internal abstract IReadOnlyCollection<ICachedChannel> Channels { get; }
internal abstract IReadOnlyCollection<CachedGuild> Guilds { get; }
internal abstract IReadOnlyCollection<CachedPublicUser> Users { get; }

internal abstract ICachedChannel GetChannel(ulong id);
internal abstract void AddChannel(ICachedChannel channel);
internal abstract ICachedChannel RemoveChannel(ulong id);

internal abstract CachedGuild GetGuild(ulong id);
internal abstract void AddGuild(CachedGuild guild);
internal abstract CachedGuild RemoveGuild(ulong id);

internal abstract CachedPublicUser GetUser(ulong id);
internal abstract CachedPublicUser GetOrAddUser(ulong userId, Func<ulong, CachedPublicUser> userFactory);
internal abstract CachedPublicUser RemoveUser(ulong id);
}
}

+ 11
- 0
src/Discord.Net/Data/SharedDataStore.cs View File

@@ -0,0 +1,11 @@
namespace Discord.Data
{
//TODO: Implement
//TODO: CachedPublicUser's GuildCount system is not at all multi-writer threadsafe!
//TODO: CachedPublicUser's Update method is not multi-writer threadsafe!
//TODO: Are there other multiwriters across shards?

/*public class SharedDataStore
{
}*/
}

src/Discord.Net/Rest/DiscordClient.cs → src/Discord.Net/DiscordClient.cs View File

@@ -1,43 +1,38 @@
using Discord.API.Rest;
using Discord.Extensions;
using Discord.Logging;
using Discord.Net;
using Discord.Net.Queue;
using Discord.Net.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Rest
namespace Discord
{
//TODO: Docstrings
//TODO: Log Internal/External REST Rate Limits, 502s
//TODO: Log Logins/Logouts
public sealed class DiscordClient : IDiscordClient, IDisposable
public class DiscordClient : IDiscordClient
{
public event Func<LogMessage, Task> Log;
public event Func<Task> LoggedIn, LoggedOut;

private readonly Logger _discordLogger, _restLogger;
private readonly SemaphoreSlim _connectionLock;
private readonly RestClientProvider _restClientProvider;
private readonly LogManager _log;
private readonly RequestQueue _requestQueue;
private bool _isDisposed;
private SelfUser _currentUser;
internal readonly Logger _discordLogger, _restLogger;
internal readonly SemaphoreSlim _connectionLock;
internal readonly LogManager _log;
internal readonly RequestQueue _requestQueue;
internal bool _isDisposed;
internal SelfUser _currentUser;

public LoginState LoginState { get; private set; }
public API.DiscordApiClient ApiClient { get; private set; }
public IRequestQueue RequestQueue => _requestQueue;

public DiscordClient(DiscordConfig config = null)
{
if (config == null)
config = new DiscordConfig();
_log = new LogManager(config.LogLevel);
_log.Message += async msg => await Log.Raise(msg).ConfigureAwait(false);
_discordLogger = _log.CreateLogger("Discord");
@@ -49,26 +44,17 @@ namespace Discord.Rest
ApiClient = new API.DiscordApiClient(config.RestClientProvider, requestQueue: _requestQueue);
ApiClient.SentRequest += async (method, endpoint, millis) => await _log.Verbose("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
}

public async Task Login(string email, string password)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await LoginInternal(TokenType.User, null, email, password, true, false).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
public async Task Login(TokenType tokenType, string token, bool validateToken = true)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await LoginInternal(tokenType, token, null, null, false, validateToken).ConfigureAwait(false);
await LoginInternal(tokenType, token, validateToken).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task LoginInternal(TokenType tokenType, string token, string email, string password, bool useEmail, bool validateToken)
private async Task LoginInternal(TokenType tokenType, string token, bool validateToken)
{
if (LoginState != LoginState.LoggedOut)
await LogoutInternal().ConfigureAwait(false);
@@ -76,13 +62,7 @@ namespace Discord.Rest

try
{
if (useEmail)
{
var args = new LoginParams { Email = email, Password = password };
await ApiClient.Login(args).ConfigureAwait(false);
}
else
await ApiClient.Login(tokenType, token).ConfigureAwait(false);
await ApiClient.Login(tokenType, token).ConfigureAwait(false);

if (validateToken)
{
@@ -96,6 +76,8 @@ namespace Discord.Rest
}
}

await OnLogin().ConfigureAwait(false);

LoginState = LoginState.LoggedIn;
}
catch (Exception)
@@ -106,6 +88,7 @@ namespace Discord.Rest

await LoggedIn.Raise().ConfigureAwait(false);
}
protected virtual Task OnLogin() => Task.CompletedTask;

public async Task Logout()
{
@@ -122,6 +105,8 @@ namespace Discord.Rest
LoginState = LoginState.LoggingOut;

await ApiClient.Logout().ConfigureAwait(false);
await OnLogout().ConfigureAwait(false);

_currentUser = null;

@@ -129,14 +114,15 @@ namespace Discord.Rest

await LoggedOut.Raise().ConfigureAwait(false);
}
protected virtual Task OnLogout() => Task.CompletedTask;

public async Task<IEnumerable<Connection>> GetConnections()
public async Task<IReadOnlyCollection<IConnection>> GetConnections()
{
var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false);
return models.Select(x => new Connection(x));
return models.Select(x => new Connection(x)).ToImmutableArray();
}

public async Task<IChannel> GetChannel(ulong id)
public virtual async Task<IChannel> GetChannel(ulong id)
{
var model = await ApiClient.GetChannel(id).ConfigureAwait(false);
if (model != null)
@@ -151,17 +137,17 @@ namespace Discord.Rest
}
}
else
return new DMChannel(this, model);
return new DMChannel(this, new User(this, model.Recipient), model);
}
return null;
}
public async Task<IEnumerable<DMChannel>> GetDMChannels()
public virtual async Task<IReadOnlyCollection<IDMChannel>> GetDMChannels()
{
var models = await ApiClient.GetCurrentUserDMs().ConfigureAwait(false);
return models.Select(x => new DMChannel(this, x));
return models.Select(x => new DMChannel(this, new User(this, x.Recipient), x)).ToImmutableArray();
}

public async Task<Invite> GetInvite(string inviteIdOrXkcd)
public virtual async Task<IInvite> GetInvite(string inviteIdOrXkcd)
{
var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false);
if (model != null)
@@ -169,48 +155,48 @@ namespace Discord.Rest
return null;
}

public async Task<Guild> GetGuild(ulong id)
public virtual async Task<IGuild> GetGuild(ulong id)
{
var model = await ApiClient.GetGuild(id).ConfigureAwait(false);
if (model != null)
return new Guild(this, model);
return null;
}
public async Task<GuildEmbed> GetGuildEmbed(ulong id)
public virtual async Task<GuildEmbed?> GetGuildEmbed(ulong id)
{
var model = await ApiClient.GetGuildEmbed(id).ConfigureAwait(false);
if (model != null)
return new GuildEmbed(model);
return null;
}
public async Task<IEnumerable<UserGuild>> GetGuilds()
public virtual async Task<IReadOnlyCollection<IUserGuild>> GetGuilds()
{
var models = await ApiClient.GetCurrentUserGuilds().ConfigureAwait(false);
return models.Select(x => new UserGuild(this, x));
return models.Select(x => new UserGuild(this, x)).ToImmutableArray();

}
public async Task<Guild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null)
public virtual async Task<IGuild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null)
{
var args = new CreateGuildParams();
var model = await ApiClient.CreateGuild(args).ConfigureAwait(false);
return new Guild(this, model);
}

public async Task<User> GetUser(ulong id)
public virtual async Task<IUser> GetUser(ulong id)
{
var model = await ApiClient.GetUser(id).ConfigureAwait(false);
if (model != null)
return new PublicUser(this, model);
return new User(this, model);
return null;
}
public async Task<User> GetUser(string username, ushort discriminator)
public virtual async Task<IUser> GetUser(string username, ushort discriminator)
{
var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false);
if (model != null)
return new PublicUser(this, model);
return new User(this, model);
return null;
}
public async Task<SelfUser> GetCurrentUser()
public virtual async Task<ISelfUser> GetCurrentUser()
{
var user = _currentUser;
if (user == null)
@@ -221,60 +207,32 @@ namespace Discord.Rest
}
return user;
}
public async Task<IEnumerable<User>> QueryUsers(string query, int limit)
public virtual async Task<IReadOnlyCollection<IUser>> QueryUsers(string query, int limit)
{
var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false);
return models.Select(x => new PublicUser(this, x));
return models.Select(x => new User(this, x)).ToImmutableArray();
}

public async Task<IEnumerable<VoiceRegion>> GetVoiceRegions()
public virtual async Task<IReadOnlyCollection<IVoiceRegion>> GetVoiceRegions()
{
var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false);
return models.Select(x => new VoiceRegion(x));
return models.Select(x => new VoiceRegion(x)).ToImmutableArray();
}
public async Task<VoiceRegion> GetVoiceRegion(string id)
public virtual async Task<IVoiceRegion> GetVoiceRegion(string id)
{
var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false);
return models.Select(x => new VoiceRegion(x)).Where(x => x.Id == id).FirstOrDefault();
}

void Dispose(bool disposing)
internal void Dispose(bool disposing)
{
if (!_isDisposed)
_isDisposed = true;
}
public void Dispose() => Dispose(true);
ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected;
WebSocket.Data.IDataStore IDiscordClient.DataStore => null;

Task IDiscordClient.Connect() { return Task.FromException(new NotSupportedException("This client does not support websocket connections.")); }
Task IDiscordClient.Disconnect() { return Task.FromException(new NotSupportedException("This client does not support websocket connections.")); }
async Task<IChannel> IDiscordClient.GetChannel(ulong id)
=> await GetChannel(id).ConfigureAwait(false);
async Task<IEnumerable<IDMChannel>> IDiscordClient.GetDMChannels()
=> await GetDMChannels().ConfigureAwait(false);
async Task<IEnumerable<IConnection>> IDiscordClient.GetConnections()
=> await GetConnections().ConfigureAwait(false);
async Task<IInvite> IDiscordClient.GetInvite(string inviteIdOrXkcd)
=> await GetInvite(inviteIdOrXkcd).ConfigureAwait(false);
async Task<IGuild> IDiscordClient.GetGuild(ulong id)
=> await GetGuild(id).ConfigureAwait(false);
async Task<IEnumerable<IUserGuild>> IDiscordClient.GetGuilds()
=> await GetGuilds().ConfigureAwait(false);
async Task<IGuild> IDiscordClient.CreateGuild(string name, IVoiceRegion region, Stream jpegIcon)
=> await CreateGuild(name, region, jpegIcon).ConfigureAwait(false);
async Task<IUser> IDiscordClient.GetUser(ulong id)
=> await GetUser(id).ConfigureAwait(false);
async Task<IUser> IDiscordClient.GetUser(string username, ushort discriminator)
=> await GetUser(username, discriminator).ConfigureAwait(false);
async Task<ISelfUser> IDiscordClient.GetCurrentUser()
=> await GetCurrentUser().ConfigureAwait(false);
async Task<IEnumerable<IUser>> IDiscordClient.QueryUsers(string query, int limit)
=> await QueryUsers(query, limit).ConfigureAwait(false);
async Task<IEnumerable<IVoiceRegion>> IDiscordClient.GetVoiceRegions()
=> await GetVoiceRegions().ConfigureAwait(false);
async Task<IVoiceRegion> IDiscordClient.GetVoiceRegion(string id)
=> await GetVoiceRegion(id).ConfigureAwait(false);
Task IDiscordClient.Connect() { throw new NotSupportedException(); }
Task IDiscordClient.Disconnect() { throw new NotSupportedException(); }
}
}

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

@@ -10,7 +10,7 @@ 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 = 3; //TODO: Upgrade to 4
public const int GatewayAPIVersion = 5;
public const string GatewayEncoding = "json";

public const string ClientAPIUrl = "https://discordapp.com/api/";


+ 708
- 0
src/Discord.Net/DiscordSocketClient.cs View File

@@ -0,0 +1,708 @@
using Discord.API;
using Discord.API.Gateway;
using Discord.Data;
using Discord.Extensions;
using Discord.Logging;
using Discord.Net.Converters;
using Discord.Net.WebSockets;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;

namespace Discord
{
//TODO: Remove unnecessary `as` casts
//TODO: Add docstrings
public class DiscordSocketClient : DiscordClient, IDiscordClient
{
public event Func<Task> Connected, Disconnected;
public event Func<Task> Ready;
//public event Func<Channel> VoiceConnected, VoiceDisconnected;
/*public event Func<IChannel, Task> ChannelCreated, ChannelDestroyed;
public event Func<IChannel, IChannel, Task> ChannelUpdated;
public event Func<IMessage, Task> MessageReceived, MessageDeleted;
public event Func<IMessage, IMessage, Task> MessageUpdated;
public event Func<IRole, Task> RoleCreated, RoleDeleted;
public event Func<IRole, IRole, Task> RoleUpdated;
public event Func<IGuild, Task> JoinedGuild, LeftGuild, GuildAvailable, GuildUnavailable;
public event Func<IGuild, IGuild, Task> GuildUpdated;
public event Func<IUser, Task> UserJoined, UserLeft, UserBanned, UserUnbanned;
public event Func<IUser, IUser, Task> UserUpdated;
public event Func<ISelfUser, ISelfUser, Task> CurrentUserUpdated;
public event Func<IChannel, IUser, Task> UserIsTyping;*/

private readonly ConcurrentQueue<ulong> _largeGuilds;
private readonly Logger _gatewayLogger;
private readonly DataStoreProvider _dataStoreProvider;
private readonly JsonSerializer _serializer;
private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay;
private readonly bool _enablePreUpdateEvents;
private readonly int _largeThreshold;
private readonly int _totalShards;
private ImmutableDictionary<string, VoiceRegion> _voiceRegions;
private string _sessionId;

public int ShardId { get; }
public ConnectionState ConnectionState { get; private set; }
public IWebSocketClient GatewaySocket { get; private set; }
internal int MessageCacheSize { get; private set; }
//internal bool UsePermissionCache { get; private set; }
internal DataStore DataStore { get; private set; }

internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser;
internal IReadOnlyCollection<CachedGuild> Guilds
{
get
{
var guilds = DataStore.Guilds;
return guilds.Select(x => x as CachedGuild).ToReadOnlyCollection(guilds);
}
}
internal IReadOnlyCollection<CachedDMChannel> DMChannels
{
get
{
var users = DataStore.Users;
return users.Select(x => (x as CachedPublicUser).DMChannel).Where(x => x != null).ToReadOnlyCollection(users);
}
}
internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection();

public DiscordSocketClient(DiscordSocketConfig config = null)
{
if (config == null)
config = new DiscordSocketConfig();

ShardId = config.ShardId;
_totalShards = config.TotalShards;

_connectionTimeout = config.ConnectionTimeout;
_reconnectDelay = config.ReconnectDelay;
_failedReconnectDelay = config.FailedReconnectDelay;
_dataStoreProvider = config.DataStoreProvider;

MessageCacheSize = config.MessageCacheSize;
//UsePermissionCache = config.UsePermissionsCache;
_enablePreUpdateEvents = config.EnablePreUpdateEvents;
_largeThreshold = config.LargeThreshold;
_gatewayLogger = _log.CreateLogger("Gateway");
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };
ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Verbose($"Sent Op {opCode}");
ApiClient.ReceivedGatewayEvent += ProcessMessage;
GatewaySocket = config.WebSocketProvider();

_voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>();
_largeGuilds = new ConcurrentQueue<ulong>();
}

protected override async Task OnLogin()
{
var voiceRegions = await ApiClient.GetVoiceRegions().ConfigureAwait(false);
_voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id);
}
protected override async Task OnLogout()
{
if (ConnectionState != ConnectionState.Disconnected)
await DisconnectInternal().ConfigureAwait(false);

_voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>();
}

public async Task Connect()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await ConnectInternal().ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task ConnectInternal()
{
if (LoginState != LoginState.LoggedIn)
throw new InvalidOperationException("You must log in before connecting.");

ConnectionState = ConnectionState.Connecting;
try
{
await ApiClient.Connect().ConfigureAwait(false);

ConnectionState = ConnectionState.Connected;
}
catch (Exception)
{
await DisconnectInternal().ConfigureAwait(false);
throw;
}

await Connected.Raise().ConfigureAwait(false);
}
public async Task Disconnect()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternal().ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectInternal()
{
ulong guildId;

if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting;

await ApiClient.Disconnect().ConfigureAwait(false);
while (_largeGuilds.TryDequeue(out guildId)) { }

ConnectionState = ConnectionState.Disconnected;

await Disconnected.Raise().ConfigureAwait(false);
}
public override Task<IVoiceRegion> GetVoiceRegion(string id)
{
VoiceRegion region;
if (_voiceRegions.TryGetValue(id, out region))
return Task.FromResult<IVoiceRegion>(region);
return Task.FromResult<IVoiceRegion>(null);
}

public override Task<IGuild> GetGuild(ulong id)
{
return Task.FromResult<IGuild>(DataStore.GetGuild(id));
}
internal CachedGuild AddCachedGuild(API.Gateway.ExtendedGuild model, DataStore dataStore = null)
{
var guild = new CachedGuild(this, model);
for (int i = 0; i < model.Channels.Length; i++)
AddCachedChannel(model.Channels[i], dataStore);
DataStore.AddGuild(guild);
if (model.Large)
_largeGuilds.Enqueue(model.Id);
return guild;
}
internal CachedGuild RemoveCachedGuild(ulong id, DataStore dataStore = null)
{
var guild = DataStore.RemoveGuild(id) as CachedGuild;
foreach (var channel in guild.Channels)
guild.RemoveCachedChannel(channel.Id);
foreach (var user in guild.Members)
guild.RemoveCachedUser(user.Id);
return guild;
}
internal CachedGuild GetCachedGuild(ulong id) => DataStore.GetGuild(id) as CachedGuild;

public override Task<IChannel> GetChannel(ulong id)
{
return Task.FromResult<IChannel>(DataStore.GetChannel(id));
}
internal ICachedChannel AddCachedChannel(API.Channel model, DataStore dataStore = null)
{
if (model.IsPrivate)
{
var recipient = AddCachedUser(model.Recipient);
return recipient.SetDMChannel(model);
}
else
{
var guild = GetCachedGuild(model.GuildId.Value);
return guild.AddCachedChannel(model);
}
}
internal ICachedChannel RemoveCachedChannel(ulong id, DataStore dataStore = null)
{
var channel = DataStore.RemoveChannel(id) as ICachedChannel;
var dmChannel = channel as CachedDMChannel;
if (dmChannel != null)
{
var recipient = dmChannel.Recipient;
recipient.RemoveDMChannel(id);
}
return channel;
}
internal ICachedChannel GetCachedChannel(ulong id) => DataStore.GetChannel(id) as ICachedChannel;

public override Task<IUser> GetUser(ulong id)
{
return Task.FromResult<IUser>(DataStore.GetUser(id));
}
public override Task<IUser> GetUser(string username, ushort discriminator)
{
return Task.FromResult<IUser>(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault());
}
internal CachedPublicUser AddCachedUser(API.User model, DataStore dataStore = null)
{
var user = DataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)) as CachedPublicUser;
user.AddRef();
return user;
}
internal CachedPublicUser RemoveCachedUser(ulong id, DataStore dataStore = null)
{
var user = DataStore.GetUser(id) as CachedPublicUser;
user.RemoveRef();
return user;
}

private async Task ProcessMessage(GatewayOpCodes opCode, string type, JToken payload)
{
try
{
switch (opCode)
{
case GatewayOpCodes.Dispatch:
switch (type)
{
//Global
case "READY":
{
//TODO: Make downloading large guilds optional
var data = payload.ToObject<ReadyEvent>(_serializer);
var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length);

_currentUser = new CachedSelfUser(this,data.User);

for (int i = 0; i < data.Guilds.Length; i++)
AddCachedGuild(data.Guilds[i], dataStore);
for (int i = 0; i < data.PrivateChannels.Length; i++)
AddCachedChannel(data.PrivateChannels[i], dataStore);

_sessionId = data.SessionId;
DataStore = dataStore;

await Ready().ConfigureAwait(false);
}
break;

//Guilds
/*case "GUILD_CREATE":
{
var data = payload.ToObject<ExtendedGuild>(_serializer);
var guild = new CachedGuild(this, data);
DataStore.AddGuild(guild);

if (data.Unavailable == false)
type = "GUILD_AVAILABLE";
else
await JoinedGuild.Raise(guild).ConfigureAwait(false);

if (!data.Large)
await GuildAvailable.Raise(guild);
else
_largeGuilds.Enqueue(data.Id);
}
break;
case "GUILD_UPDATE":
{
var data = payload.ToObject<API.Guild>(_serializer);
var guild = DataStore.GetGuild(data.Id);
if (guild != null)
{
var before = _enablePreUpdateEvents ? guild.Clone() : null;
guild.Update(data);
await GuildUpdated.Raise(before, guild);
}
else
await _gatewayLogger.Warning("GUILD_UPDATE referenced an unknown guild.");
}
break;
case "GUILD_DELETE":
{
var data = payload.ToObject<ExtendedGuild>(_serializer);
var guild = DataStore.RemoveGuild(data.Id);
if (guild != null)
{
if (data.Unavailable == true)
type = "GUILD_UNAVAILABLE";

await GuildUnavailable.Raise(guild);
if (data.Unavailable != true)
await LeftGuild.Raise(guild);
}
else
await _gatewayLogger.Warning("GUILD_DELETE referenced an unknown guild.");
}
break;

//Channels
case "CHANNEL_CREATE":
{
var data = payload.ToObject<API.Channel>(_serializer);

IChannel channel = null;
if (data.GuildId != null)
{
var guild = GetCachedGuild(data.GuildId.Value);
if (guild != null)
channel = guild.AddCachedChannel(data.Id, true);
else
await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild.");
}
else
channel = AddCachedPrivateChannel(data.Id, data.Recipient.Id);
if (channel != null)
{
channel.Update(data);
await ChannelCreated.Raise(channel);
}
}
break;
case "CHANNEL_UPDATE":
{
var data = payload.ToObject<API.Channel>(_serializer);
var channel = DataStore.GetChannel(data.Id) as Channel;
if (channel != null)
{
var before = _enablePreUpdateEvents ? channel.Clone() : null;
channel.Update(data);
await ChannelUpdated.Raise(before, channel);
}
else
await _gatewayLogger.Warning("CHANNEL_UPDATE referenced an unknown channel.");
}
break;
case "CHANNEL_DELETE":
{
var data = payload.ToObject<API.Channel>(_serializer);
var channel = RemoveCachedChannel(data.Id);
if (channel != null)
await ChannelDestroyed.Raise(channel);
else
await _gatewayLogger.Warning("CHANNEL_DELETE referenced an unknown channel.");
}
break;

//Members
case "GUILD_MEMBER_ADD":
{
var data = payload.ToObject<API.GuildMember>(_serializer);
var guild = GetGuild(data.GuildId.Value);
if (guild != null)
{
var user = guild.AddCachedUser(data.User.Id, true, true);
user.Update(data);
user.UpdateActivity();
UserJoined.Raise(user);
}
else
await _gatewayLogger.Warning("GUILD_MEMBER_ADD referenced an unknown guild.");
}
break;
case "GUILD_MEMBER_UPDATE":
{
var data = payload.ToObject<API.GuildMember>(_serializer);
var guild = GetGuild(data.GuildId.Value);
if (guild != null)
{
var user = guild.GetCachedUser(data.User.Id);
if (user != null)
{
var before = _enablePreUpdateEvents ? user.Clone() : null;
user.Update(data);
await UserUpdated.Raise(before, user);
}
else
await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user.");
}
else
await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild.");
}
break;
case "GUILD_MEMBER_REMOVE":
{
var data = payload.ToObject<API.GuildMember>(_serializer);
var guild = GetGuild(data.GuildId.Value);
if (guild != null)
{
var user = guild.RemoveCachedUser(data.User.Id);
if (user != null)
{
user.GlobalUser.RemoveGuild();
if (user.GuildCount == 0 && user.DMChannel == null)
DataStore.RemoveUser(user.Id);
await UserLeft.Raise(user);
}
else
await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user.");
}
else
await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild.");
}
break;
case "GUILD_MEMBERS_CHUNK":
{
var data = payload.ToObject<GuildMembersChunkEvent>(_serializer);
var guild = GetCachedGuild(data.GuildId);
if (guild != null)
{
foreach (var memberData in data.Members)
{
var user = guild.AddCachedUser(memberData.User.Id, true, false);
user.Update(memberData);
}

if (guild.CurrentUserCount >= guild.UserCount) //Finished downloading for there
await GuildAvailable.Raise(guild);
}
else
await _gatewayLogger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild.");
}
break;

//Roles
case "GUILD_ROLE_CREATE":
{
var data = payload.ToObject<GuildRoleCreateEvent>(_serializer);
var guild = GetCachedGuild(data.GuildId);
if (guild != null)
{
var role = guild.AddCachedRole(data.Data.Id);
role.Update(data.Data, false);
RoleCreated.Raise(role);
}
else
await _gatewayLogger.Warning("GUILD_ROLE_CREATE referenced an unknown guild.");
}
break;
case "GUILD_ROLE_UPDATE":
{
var data = payload.ToObject<GuildRoleUpdateEvent>(_serializer);
var guild = GetCachedGuild(data.GuildId);
if (guild != null)
{
var role = guild.GetRole(data.Data.Id);
if (role != null)
{
var before = _enablePreUpdateEvents ? role.Clone() : null;
role.Update(data.Data, true);
RoleUpdated.Raise(before, role);
}
else
await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown role.");
}
else
await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild.");
}
break;
case "GUILD_ROLE_DELETE":
{
var data = payload.ToObject<GuildRoleDeleteEvent>(_serializer);
var guild = DataStore.GetGuild(data.GuildId) as CachedGuild;
if (guild != null)
{
var role = guild.RemoveRole(data.RoleId);
if (role != null)
RoleDeleted.Raise(role);
else
await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown role.");
}
else
await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown guild.");
}
break;

//Bans
case "GUILD_BAN_ADD":
{
var data = payload.ToObject<GuildBanEvent>(_serializer);
var guild = GetCachedGuild(data.GuildId);
if (guild != null)
await UserBanned.Raise(new User(this, data));
else
await _gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown guild.");
}
break;
case "GUILD_BAN_REMOVE":
{
var data = payload.ToObject<GuildBanEvent>(_serializer);
var guild = GetCachedGuild(data.GuildId);
if (guild != null)
await UserUnbanned.Raise(new User(this, data));
else
await _gatewayLogger.Warning("GUILD_BAN_REMOVE referenced an unknown guild.");
}
break;

//Messages
case "MESSAGE_CREATE":
{
var data = payload.ToObject<API.Message>(_serializer);

var channel = DataStore.GetChannel(data.ChannelId);
if (channel != null)
{
var user = channel.GetUser(data.Author.Id);

if (user != null)
{
bool isAuthor = data.Author.Id == CurrentUser.Id;
var msg = channel.AddMessage(data.Id, user, data.Timestamp.Value);

msg.Update(data);

MessageReceived.Raise(msg);
}
else
await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown user.");
}
else
await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown channel.");
}
break;
case "MESSAGE_UPDATE":
{
var data = payload.ToObject<API.Message>(_serializer);
var channel = GetCachedChannel(data.ChannelId);
if (channel != null)
{
var msg = channel.GetMessage(data.Id, data.Author?.Id);
var before = _enablePreUpdateEvents ? msg.Clone() : null;
msg.Update(data);
MessageUpdated.Raise(before, msg);
}
else
await _gatewayLogger.Warning("MESSAGE_UPDATE referenced an unknown channel.");
}
break;
case "MESSAGE_DELETE":
{
var data = payload.ToObject<API.Message>(_serializer);
var channel = GetCachedChannel(data.ChannelId);
if (channel != null)
{
var msg = channel.RemoveMessage(data.Id);
MessageDeleted.Raise(msg);
}
else
await _gatewayLogger.Warning("MESSAGE_DELETE referenced an unknown channel.");
}
break;

//Statuses
case "PRESENCE_UPDATE":
{
var data = payload.ToObject<API.Presence>(_serializer);
User user;
Guild guild;
if (data.GuildId == null)
{
guild = null;
user = GetPrivateChannel(data.User.Id)?.Recipient;
}
else
{
guild = GetGuild(data.GuildId.Value);
if (guild == null)
{
await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown guild.");
break;
}
else
user = guild.GetUser(data.User.Id);
}

if (user != null)
{
var before = _enablePreUpdateEvents ? user.Clone() : null;
user.Update(data);
UserUpdated.Raise(before, user);
}
else
{
//Occurs when a user leaves a guild
//await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown user.");
}
}
break;
case "TYPING_START":
{
var data = payload.ToObject<TypingStartEvent>(_serializer);
var channel = GetCachedChannel(data.ChannelId);
if (channel != null)
{
var user = channel.GetUser(data.UserId);
if (user != null)
{
await UserIsTyping.Raise(channel, user);
user.UpdateActivity();
}
}
else
await _gatewayLogger.Warning("TYPING_START referenced an unknown channel.");
}
break;

//Voice
case "VOICE_STATE_UPDATE":
{
var data = payload.ToObject<API.VoiceState>(_serializer);
var guild = GetGuild(data.GuildId);
if (guild != null)
{
var user = guild.GetUser(data.UserId);
if (user != null)
{
var before = _enablePreUpdateEvents ? user.Clone() : null;
user.Update(data);
UserUpdated.Raise(before, user);
}
else
{
//Occurs when a user leaves a guild
//await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown user.");
}
}
else
await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown guild.");
}
break;

//Settings
case "USER_UPDATE":
{
var data = payload.ToObject<SelfUser>(_serializer);
if (data.Id == CurrentUser.Id)
{
var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null;
CurrentUser.Update(data);
await CurrentUserUpdated.Raise(before, CurrentUser).ConfigureAwait(false);
}
}
break;*/

//Ignored
case "USER_SETTINGS_UPDATE":
case "MESSAGE_ACK": //TODO: Add (User only)
case "GUILD_EMOJIS_UPDATE": //TODO: Add
case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add
case "VOICE_SERVER_UPDATE": //TODO: Add
case "RESUMED": //TODO: Add
await _gatewayLogger.Debug($"Ignored message {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false);
return;

//Others
default:
await _gatewayLogger.Warning($"Unknown message {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false);
return;
}
break;
}
}
catch (Exception ex)
{
await _gatewayLogger.Error($"Error handling msg {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false);
return;
}
await _gatewayLogger.Debug($"Received {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false);
}
}
}

src/Discord.Net/WebSocket/DiscordSocketConfig.cs → src/Discord.Net/DiscordSocketConfig.cs View File

@@ -1,7 +1,7 @@
using Discord.Net.WebSockets;
using Discord.WebSocket.Data;
using Discord.Data;
using Discord.Net.WebSockets;

namespace Discord.WebSocket
namespace Discord
{
public class DiscordSocketConfig : DiscordConfig
{
@@ -15,15 +15,15 @@ namespace Discord.WebSocket
/// <summary> Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. </summary>
public int ReconnectDelay { get; set; } = 1000;
/// <summary> Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. </summary>
public int FailedReconnectDelay { get; set; } = 15000;
public int FailedReconnectDelay { get; set; } = 15000;

/// <summary> Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. </summary>
public int MessageCacheSize { get; set; } = 100;
/// <summary>
/*/// <summary>
/// Gets or sets whether the permissions cache should be used.
/// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster while increasing memory usage.
/// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster at the expense of increased memory usage.
/// </summary>
public bool UsePermissionsCache { get; set; } = true;
public bool UsePermissionsCache { get; set; } = false;*/
/// <summary> Gets or sets whether the a copy of a model is generated on an update event to allow you to check which properties changed. </summary>
public bool EnablePreUpdateEvents { get; set; } = true;
/// <summary>

+ 126
- 0
src/Discord.Net/Entities/Channels/DMChannel.cs View File

@@ -0,0 +1,126 @@
using Discord.API.Rest;
using System;
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 DMChannel : SnowflakeEntity, IDMChannel
{
public override DiscordClient Discord { get; }
public User Recipient { get; private set; }

public virtual IReadOnlyCollection<IMessage> CachedMessages => ImmutableArray.Create<IMessage>();

public DMChannel(DiscordClient discord, User recipient, Model model)
: base(model.Id)
{
Discord = discord;
Recipient = recipient;

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

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

var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false);
Update(model, UpdateSource.Rest);
}
public async Task Close()
{
await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false);
}

public virtual async Task<IUser> GetUser(ulong id)
{
var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false);
if (id == Recipient.Id)
return Recipient;
else if (id == currentUser.Id)
return currentUser;
else
return null;
}
public virtual async Task<IReadOnlyCollection<IUser>> GetUsers()
{
var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false);
return ImmutableArray.Create<IUser>(currentUser, Recipient);
}
public virtual async Task<IReadOnlyCollection<IUser>> GetUsers(int limit, int offset)
{
var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false);
return new IUser[] { currentUser, Recipient }.Skip(offset).Take(limit).ToImmutableArray();
}

public async Task<IMessage> SendMessage(string text, bool isTTS)
{
var args = new CreateMessageParams { Content = text, IsTTS = isTTS };
var model = await Discord.ApiClient.CreateDMMessage(Id, args).ConfigureAwait(false);
return new Message(this, new User(Discord, model.Author), model);
}
public async Task<IMessage> SendFile(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.UploadDMFile(Id, file, args).ConfigureAwait(false);
return new Message(this, new User(Discord, model.Author), model);
}
}
public async Task<IMessage> SendFile(Stream stream, string filename, string text, bool isTTS)
{
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS };
var model = await Discord.ApiClient.UploadDMFile(Id, stream, args).ConfigureAwait(false);
return new Message(this, new User(Discord, model.Author), model);
}
public virtual async Task<IMessage> GetMessage(ulong id)
{
var model = await Discord.ApiClient.GetChannelMessage(Id, id).ConfigureAwait(false);
if (model != null)
return new Message(this, new User(Discord, model.Author), model);
return null;
}
public virtual async Task<IReadOnlyCollection<IMessage>> GetMessages(int limit)
{
var args = new GetChannelMessagesParams { Limit = limit };
var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false);
return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray();
}
public virtual async Task<IReadOnlyCollection<IMessage>> GetMessages(ulong fromMessageId, Direction dir, int limit)
{
var args = new GetChannelMessagesParams { Limit = limit };
var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false);
return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray();
}
public async Task DeleteMessages(IEnumerable<IMessage> messages)
{
await Discord.ApiClient.DeleteDMMessages(Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false);
}

public async Task TriggerTyping()
{
await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false);
}
public override string ToString() => '@' + Recipient.ToString();
private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)";

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

src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs → src/Discord.Net/Entities/Channels/GuildChannel.cs View File

@@ -1,42 +1,39 @@
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.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;

namespace Discord.Rest
namespace Discord
{
public abstract class GuildChannel : IGuildChannel
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
internal abstract class GuildChannel : SnowflakeEntity, IGuildChannel
{
private ConcurrentDictionary<ulong, Overwrite> _overwrites;

/// <inheritdoc />
public ulong Id { get; }
/// <summary> Gets the guild this channel is a member of. </summary>
public Guild Guild { get; }

/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public int Position { get; private set; }

/// <inheritdoc />
public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id);
/// <inheritdoc />
public IReadOnlyDictionary<ulong, Overwrite> PermissionOverwrites => _overwrites;
internal DiscordClient Discord => Guild.Discord;
public Guild Guild { get; private set; }

public override DiscordClient Discord => Guild.Discord;

internal GuildChannel(Guild guild, Model model)
public GuildChannel(Guild guild, Model model)
: base(model.Id)
{
Id = model.Id;
Guild = guild;

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

Name = model.Name;
Position = model.Position;

@@ -49,6 +46,13 @@ namespace Discord.Rest
_overwrites = newOverwrites;
}

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

var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false);
Update(model, UpdateSource.Rest);
}
public async Task Modify(Action<ModifyGuildChannelParams> func)
{
if (func != null) throw new NullReferenceException(nameof(func));
@@ -56,10 +60,35 @@ namespace Discord.Rest
var args = new ModifyGuildChannelParams();
func(args);
var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false);
Update(model);
Update(model, UpdateSource.Rest);
}
/// <inheritdoc />
public async Task Delete()
{
await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false);
}

public abstract Task<IGuildUser> GetUser(ulong id);
public abstract Task<IReadOnlyCollection<IGuildUser>> GetUsers();
public abstract Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset);

public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvites()
{
var models = await Discord.ApiClient.GetChannelInvites(Id).ConfigureAwait(false);
return models.Select(x => new InviteMetadata(Discord, x)).ToImmutableArray();
}
public async Task<IInviteMetadata> CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd)
{
var args = new CreateChannelInviteParams
{
MaxAge = maxAge ?? 0,
MaxUses = maxUses ?? 0,
Temporary = isTemporary,
XkcdPass = withXkcd
};
var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false);
return new InviteMetadata(Discord, model);
}

public OverwritePermissions? GetPermissionOverwrite(IUser user)
{
Overwrite value;
@@ -67,7 +96,6 @@ namespace Discord.Rest
return value.Permissions;
return null;
}
/// <inheritdoc />
public OverwritePermissions? GetPermissionOverwrite(IRole role)
{
Overwrite value;
@@ -75,28 +103,19 @@ namespace Discord.Rest
return value.Permissions;
return null;
}
/// <summary> Downloads a collection of all invites to this channel. </summary>
public async Task<IEnumerable<InviteMetadata>> GetInvites()
{
var models = await Discord.ApiClient.GetChannelInvites(Id).ConfigureAwait(false);
return models.Select(x => new InviteMetadata(Discord, x));
}

/// <inheritdoc />
public async Task AddPermissionOverwrite(IUser user, OverwritePermissions perms)
{
var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue };
await Discord.ApiClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false);
_overwrites[user.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User });
}
/// <inheritdoc />
public async Task AddPermissionOverwrite(IRole role, OverwritePermissions perms)
{
var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue };
await Discord.ApiClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false);
_overwrites[role.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role });
}
/// <inheritdoc />
public async Task RemovePermissionOverwrite(IUser user)
{
await Discord.ApiClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false);
@@ -104,7 +123,6 @@ namespace Discord.Rest
Overwrite value;
_overwrites.TryRemove(user.Id, out value);
}
/// <inheritdoc />
public async Task RemovePermissionOverwrite(IRole role)
{
await Discord.ApiClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false);
@@ -112,58 +130,15 @@ namespace Discord.Rest
Overwrite value;
_overwrites.TryRemove(role.Id, out value);
}

/// <summary> Creates a new invite to this channel. </summary>
/// <param name="maxAge"> Time (in seconds) until the invite expires. Set to null to never expire. </param>
/// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param>
/// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the guild after closing their client. </param>
/// <param name="withXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to null. </param>
public async Task<InviteMetadata> CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false)
{
var args = new CreateChannelInviteParams
{
MaxAge = maxAge ?? 0,
MaxUses = maxUses ?? 0,
Temporary = isTemporary,
XkcdPass = withXkcd
};
var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false);
return new InviteMetadata(Discord, model);
}

/// <inheritdoc />
public async Task Delete()
{
await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task Update()
{
var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false);
Update(model);
}

/// <inheritdoc />
public override string ToString() => Name;

protected abstract Task<GuildUser> GetUserInternal(ulong id);
protected abstract Task<IEnumerable<GuildUser>> GetUsersInternal();
protected abstract Task<IEnumerable<GuildUser>> GetUsersInternal(int limit, int offset);

private string DebuggerDisplay => $"{Name} ({Id})";
IGuild IGuildChannel.Guild => Guild;
async Task<IInviteMetadata> IGuildChannel.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd)
=> await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false);
async Task<IEnumerable<IInviteMetadata>> IGuildChannel.GetInvites()
=> await GetInvites().ConfigureAwait(false);
async Task<IEnumerable<IGuildUser>> IGuildChannel.GetUsers()
=> await GetUsersInternal().ConfigureAwait(false);
async Task<IEnumerable<IUser>> IChannel.GetUsers()
=> await GetUsersInternal().ConfigureAwait(false);
async Task<IEnumerable<IUser>> IChannel.GetUsers(int limit, int offset)
=> await GetUsersInternal(limit, offset).ConfigureAwait(false);
async Task<IGuildUser> IGuildChannel.GetUser(ulong id)
=> await GetUserInternal(id).ConfigureAwait(false);
async Task<IUser> IChannel.GetUser(ulong id)
=> await GetUserInternal(id).ConfigureAwait(false);
IReadOnlyCollection<Overwrite> IGuildChannel.PermissionOverwrites => _overwrites.ToReadOnlyCollection();

async Task<IUser> IChannel.GetUser(ulong id) => await GetUser(id).ConfigureAwait(false);
async Task<IReadOnlyCollection<IUser>> IChannel.GetUsers() => await GetUsers().ConfigureAwait(false);
async Task<IReadOnlyCollection<IUser>> IChannel.GetUsers(int limit, int offset) => await GetUsers(limit, offset).ConfigureAwait(false);
}
}

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

@@ -6,9 +6,9 @@ namespace Discord
public interface IChannel : ISnowflakeEntity
{
/// <summary> Gets a collection of all users in this channel. </summary>
Task<IEnumerable<IUser>> GetUsers();
Task<IReadOnlyCollection<IUser>> GetUsers();
/// <summary> Gets a paginated collection of all users in this channel. </summary>
Task<IEnumerable<IUser>> GetUsers(int limit, int offset = 0);
Task<IReadOnlyCollection<IUser>> GetUsers(int limit, int offset = 0);
/// <summary> Gets a user in this channel with the provided id.</summary>
Task<IUser> GetUser(ulong id);
}


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

@@ -22,11 +22,11 @@ namespace Discord
/// <param name="withXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to null. </param>
Task<IInviteMetadata> CreateInvite(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false);
/// <summary> Returns a collection of all invites to this channel. </summary>
Task<IEnumerable<IInviteMetadata>> GetInvites();
Task<IReadOnlyCollection<IInviteMetadata>> GetInvites();

/// <summary> Gets a collection of permission overwrites for this channel. </summary>
IReadOnlyDictionary<ulong, Overwrite> PermissionOverwrites { get; }
IReadOnlyCollection<Overwrite> PermissionOverwrites { get; }
/// <summary> Modifies this guild channel. </summary>
Task Modify(Action<ModifyGuildChannelParams> func);

@@ -44,7 +44,7 @@ namespace Discord
Task AddPermissionOverwrite(IUser user, OverwritePermissions permissions);

/// <summary> Gets a collection of all users in this channel. </summary>
new Task<IEnumerable<IGuildUser>> GetUsers();
new Task<IReadOnlyCollection<IGuildUser>> GetUsers();
/// <summary> Gets a user in this channel with the provided id.</summary>
new Task<IGuildUser> GetUser(ulong id);
}

+ 11
- 11
src/Discord.Net/Entities/Channels/IMessageChannel.cs View File

@@ -7,25 +7,25 @@ namespace Discord
public interface IMessageChannel : IChannel
{
/// <summary> Gets all messages in this channel's cache. </summary>
IEnumerable<IMessage> CachedMessages { get; }
IReadOnlyCollection<IMessage> CachedMessages { get; }

/// <summary> Gets the message from this channel's cache with the given id, or null if none was found. </summary>
Task<IMessage> GetCachedMessage(ulong id);

/// <summary> Gets the last N messages from this message channel. </summary>
Task<IEnumerable<IMessage>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch);
/// <summary> Gets a collection of messages in this channel. </summary>
Task<IEnumerable<IMessage>> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch);

/// <summary> Sends a message to this text channel. </summary>
/// <summary> Sends a message to this message channel. </summary>
Task<IMessage> SendMessage(string text, bool isTTS = false);
/// <summary> Sends a file to this text channel, with an optional caption. </summary>
Task<IMessage> SendFile(string filePath, string text = null, bool isTTS = false);
/// <summary> Sends a file to this text channel, with an optional caption. </summary>
Task<IMessage> SendFile(Stream stream, string filename, string text = null, bool isTTS = false);
/// <summary> Gets a message from this message channel with the given id, or null if not found. </summary>
Task<IMessage> GetMessage(ulong id);
/// <summary> Gets the message from this channel's cache with the given id, or null if not found. </summary>
IMessage GetCachedMessage(ulong id);
/// <summary> Gets the last N messages from this message channel. </summary>
Task<IReadOnlyCollection<IMessage>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch);
/// <summary> Gets a collection of messages in this channel. </summary>
Task<IReadOnlyCollection<IMessage>> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch);
/// <summary> Bulk deletes multiple messages. </summary>
Task DeleteMessages(IEnumerable<IMessage> messages);

/// <summary> Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds.</summary>
Task TriggerTyping();


+ 116
- 0
src/Discord.Net/Entities/Channels/TextChannel.cs View File

@@ -0,0 +1,116 @@
using Discord.API.Rest;
using System;
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 TextChannel : GuildChannel, ITextChannel
{
public string Topic { get; private set; }
public string Mention => MentionUtils.Mention(this);
public virtual IReadOnlyCollection<IMessage> CachedMessages => ImmutableArray.Create<IMessage>();

public TextChannel(Guild guild, Model model)
: base(guild, model)
{
}
protected override void Update(Model model, UpdateSource source)
{
if (source == UpdateSource.Rest && IsAttached) return;

Topic = model.Topic;
base.Update(model, UpdateSource.Rest);
}

public async Task Modify(Action<ModifyTextChannelParams> func)
{
if (func != null) throw new NullReferenceException(nameof(func));

var args = new ModifyTextChannelParams();
func(args);
var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false);
Update(model, UpdateSource.Rest);
}
public override async Task<IGuildUser> GetUser(ulong id)
{
var user = await Guild.GetUser(id).ConfigureAwait(false);
if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages))
return user;
return null;
}
public override async Task<IReadOnlyCollection<IGuildUser>> GetUsers()
{
var users = await Guild.GetUsers().ConfigureAwait(false);
return users.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray();
}
public override async Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset)
{
var users = await Guild.GetUsers(limit, offset).ConfigureAwait(false);
return users.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray();
}

public async Task<IMessage> SendMessage(string text, bool isTTS)
{
var args = new CreateMessageParams { Content = text, IsTTS = isTTS };
var model = await Discord.ApiClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false);
return new Message(this, new User(Discord, model.Author), model);
}
public async Task<IMessage> SendFile(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.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false);
return new Message(this, new User(Discord, model.Author), model);
}
}
public async Task<IMessage> SendFile(Stream stream, string filename, string text, bool isTTS)
{
var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS };
var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false);
return new Message(this, new User(Discord, model.Author), model);
}
public virtual async Task<IMessage> GetMessage(ulong id)
{
var model = await Discord.ApiClient.GetChannelMessage(Id, id).ConfigureAwait(false);
if (model != null)
return new Message(this, new User(Discord, model.Author), model);
return null;
}
public virtual async Task<IReadOnlyCollection<IMessage>> GetMessages(int limit)
{
var args = new GetChannelMessagesParams { Limit = limit };
var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false);
return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray();
}
public virtual async Task<IReadOnlyCollection<IMessage>> GetMessages(ulong fromMessageId, Direction dir, int limit)
{
var args = new GetChannelMessagesParams { Limit = limit };
var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false);
return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray();
}
public async Task DeleteMessages(IEnumerable<IMessage> messages)
{
await Discord.ApiClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false);
}
public async Task TriggerTyping()
{
await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false);
}

private string DebuggerDisplay => $"{Name} ({Id}, Text)";

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

src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs → src/Discord.Net/Entities/Channels/VoiceChannel.cs View File

@@ -5,28 +5,27 @@ using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Channel;

namespace Discord.Rest
namespace Discord
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class VoiceChannel : GuildChannel, IVoiceChannel
internal class VoiceChannel : GuildChannel, IVoiceChannel
{
/// <inheritdoc />
public int Bitrate { get; private set; }
/// <inheritdoc />
public int UserLimit { get; private set; }

internal VoiceChannel(Guild guild, Model model)
public VoiceChannel(Guild guild, Model model)
: base(guild, model)
{
}
internal override void Update(Model model)
protected override void Update(Model model, UpdateSource source)
{
base.Update(model);
if (source == UpdateSource.Rest && IsAttached) return;

base.Update(model, UpdateSource.Rest);
Bitrate = model.Bitrate;
UserLimit = model.UserLimit;
}

/// <inheritdoc />
public async Task Modify(Action<ModifyVoiceChannelParams> func)
{
if (func != null) throw new NullReferenceException(nameof(func));
@@ -34,12 +33,21 @@ namespace Discord.Rest
var args = new ModifyVoiceChannelParams();
func(args);
var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false);
Update(model);
Update(model, UpdateSource.Rest);
}

protected override Task<GuildUser> GetUserInternal(ulong id) { throw new NotSupportedException(); }
protected override Task<IEnumerable<GuildUser>> GetUsersInternal() { throw new NotSupportedException(); }
protected override Task<IEnumerable<GuildUser>> GetUsersInternal(int limit, int offset) { throw new NotSupportedException(); }
public override Task<IGuildUser> GetUser(ulong id)
{
throw new NotSupportedException();
}
public override Task<IReadOnlyCollection<IGuildUser>> GetUsers()
{
throw new NotSupportedException();
}
public override Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset)
{
throw new NotSupportedException();
}

private string DebuggerDisplay => $"{Name} ({Id}, Voice)";
}

+ 16
- 0
src/Discord.Net/Entities/Entity.cs View File

@@ -0,0 +1,16 @@
namespace Discord
{
internal abstract class Entity<T> : IEntity<T>
{
public T Id { get; }

public abstract DiscordClient Discord { get; }

public bool IsAttached => this is ICachedEntity<T>;

public Entity(T id)
{
Id = id;
}
}
}

+ 1
- 1
src/Discord.Net/Entities/Guilds/Emoji.cs View File

@@ -11,7 +11,7 @@ namespace Discord
public bool RequireColons { get; }
public IImmutableList<ulong> RoleIds { get; }

internal Emoji(Model model)
public Emoji(Model model)
{
Id = model.Id;
Name = model.Name;


src/Discord.Net/Rest/Entities/Guilds/Guild.cs → src/Discord.Net/Entities/Guilds/Guild.cs View File

@@ -1,77 +1,60 @@
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.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Guild;
using EmbedModel = Discord.API.GuildEmbed;
using Model = Discord.API.Guild;
using RoleModel = Discord.API.Role;
using System.Diagnostics;

namespace Discord.Rest
namespace Discord
{
/// <summary> Represents a Discord guild (called a server in the official client). </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class Guild : IGuild
internal class Guild : SnowflakeEntity, IGuild
{
private ConcurrentDictionary<ulong, Role> _roles;
private string _iconId, _splashId;

/// <inheritdoc />
public ulong Id { get; }
internal DiscordClient Discord { get; }

/// <inheritdoc />
protected ConcurrentDictionary<ulong, Role> _roles;
protected string _iconId, _splashId;
public string Name { get; private set; }
/// <inheritdoc />
public int AFKTimeout { get; private set; }
/// <inheritdoc />
public bool IsEmbeddable { get; private set; }
/// <inheritdoc />
public int VerificationLevel { get; private set; }

/// <inheritdoc />
public ulong? AFKChannelId { get; private set; }
/// <inheritdoc />
public ulong? EmbedChannelId { get; private set; }
/// <inheritdoc />
public ulong OwnerId { get; private set; }
/// <inheritdoc />
public string VoiceRegionId { get; private set; }
/// <inheritdoc />
public IReadOnlyList<Emoji> Emojis { get; private set; }
/// <inheritdoc />
public IReadOnlyList<string> Features { get; private set; }
public override DiscordClient Discord { get; }
public ImmutableArray<Emoji> Emojis { get; protected set; }
public ImmutableArray<string> Features { get; protected set; }

/// <inheritdoc />
public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id);
/// <inheritdoc />
public ulong DefaultChannelId => Id;
public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId);
/// <inheritdoc />
public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, _splashId);
/// <inheritdoc />
public ulong DefaultChannelId => Id;
/// <inheritdoc />

public Role EveryoneRole => GetRole(Id);
/// <summary> Gets a collection of all roles in this guild. </summary>
public IEnumerable<Role> Roles => _roles?.Select(x => x.Value) ?? Enumerable.Empty<Role>();
public IReadOnlyCollection<IRole> Roles => _roles.ToReadOnlyCollection();

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

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

AFKChannelId = model.AFKChannelId;
AFKTimeout = model.AFKTimeout;
EmbedChannelId = model.EmbedChannelId;
AFKTimeout = model.AFKTimeout;
IsEmbeddable = model.EmbedEnabled;
Features = model.Features;
Features = model.Features.ToImmutableArray();
_iconId = model.Icon;
Name = model.Name;
OwnerId = model.OwnerId;
@@ -84,10 +67,10 @@ namespace Discord.Rest
var emojis = ImmutableArray.CreateBuilder<Emoji>(model.Emojis.Length);
for (int i = 0; i < model.Emojis.Length; i++)
emojis.Add(new Emoji(model.Emojis[i]));
Emojis = emojis.ToArray();
Emojis = emojis.ToImmutableArray();
}
else
Emojis = Array.Empty<Emoji>();
Emojis = ImmutableArray.Create<Emoji>();

var roles = new ConcurrentDictionary<ulong, Role>(1, model.Roles?.Length ?? 0);
if (model.Roles != null)
@@ -97,28 +80,32 @@ namespace Discord.Rest
}
_roles = roles;
}
private void Update(EmbedModel model)
public void Update(EmbedModel model, UpdateSource source)
{
if (source == UpdateSource.Rest && IsAttached) return;

IsEmbeddable = model.Enabled;
EmbedChannelId = model.ChannelId;
}
private void Update(IEnumerable<RoleModel> models)
public void Update(IEnumerable<RoleModel> models, UpdateSource source)
{
if (source == UpdateSource.Rest && IsAttached) return;

Role role;
foreach (var model in models)
{
if (_roles.TryGetValue(model.Id, out role))
role.Update(model);
role.Update(model, UpdateSource.Rest);
}
}

/// <inheritdoc />
public async Task Update()
{
if (IsAttached) throw new NotSupportedException();

var response = await Discord.ApiClient.GetGuild(Id).ConfigureAwait(false);
Update(response);
Update(response, UpdateSource.Rest);
}
/// <inheritdoc />
public async Task Modify(Action<ModifyGuildParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));
@@ -126,9 +113,8 @@ namespace Discord.Rest
var args = new ModifyGuildParams();
func(args);
var model = await Discord.ApiClient.ModifyGuild(Id, args).ConfigureAwait(false);
Update(model);
Update(model, UpdateSource.Rest);
}
/// <inheritdoc />
public async Task ModifyEmbed(Action<ModifyGuildEmbedParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));
@@ -136,68 +122,57 @@ namespace Discord.Rest
var args = new ModifyGuildEmbedParams();
func(args);
var model = await Discord.ApiClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false);
Update(model);
Update(model, UpdateSource.Rest);
}
/// <inheritdoc />
public async Task ModifyChannels(IEnumerable<ModifyGuildChannelsParams> args)
{
//TODO: Update channels
await Discord.ApiClient.ModifyGuildChannels(Id, args).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task ModifyRoles(IEnumerable<ModifyGuildRolesParams> args)
{
var models = await Discord.ApiClient.ModifyGuildRoles(Id, args).ConfigureAwait(false);
Update(models);
Update(models, UpdateSource.Rest);
}
/// <inheritdoc />
public async Task Leave()
{
await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task Delete()
{
await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false);
}

/// <inheritdoc />
public async Task<IEnumerable<User>> GetBans()
public async Task<IReadOnlyCollection<IUser>> GetBans()
{
var models = await Discord.ApiClient.GetGuildBans(Id).ConfigureAwait(false);
return models.Select(x => new PublicUser(Discord, x));
return models.Select(x => new User(Discord, x)).ToImmutableArray();
}
/// <inheritdoc />
public Task AddBan(IUser user, int pruneDays = 0) => AddBan(user, pruneDays);
/// <inheritdoc />
public async Task AddBan(ulong userId, int pruneDays = 0)
{
var args = new CreateGuildBanParams() { PruneDays = pruneDays };
await Discord.ApiClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false);
}
/// <inheritdoc />
public Task RemoveBan(IUser user) => RemoveBan(user.Id);
/// <inheritdoc />
public async Task RemoveBan(ulong userId)
{
await Discord.ApiClient.RemoveGuildBan(Id, userId).ConfigureAwait(false);
}

/// <summary> Gets the channel in this guild with the provided id, or null if not found. </summary>
public async Task<GuildChannel> GetChannel(ulong id)
public virtual async Task<IGuildChannel> GetChannel(ulong id)
{
var model = await Discord.ApiClient.GetChannel(Id, id).ConfigureAwait(false);
if (model != null)
return ToChannel(model);
return null;
}
/// <summary> Gets a collection of all channels in this guild. </summary>
public async Task<IEnumerable<GuildChannel>> GetChannels()
public virtual async Task<IReadOnlyCollection<IGuildChannel>> GetChannels()
{
var models = await Discord.ApiClient.GetGuildChannels(Id).ConfigureAwait(false);
return models.Select(x => ToChannel(x));
return models.Select(x => ToChannel(x)).ToImmutableArray();
}
/// <summary> Creates a new text channel. </summary>
public async Task<TextChannel> CreateTextChannel(string name)
public async Task<ITextChannel> CreateTextChannel(string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));

@@ -205,8 +180,7 @@ namespace Discord.Rest
var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false);
return new TextChannel(this, model);
}
/// <summary> Creates a new voice channel. </summary>
public async Task<VoiceChannel> CreateVoiceChannel(string name)
public async Task<IVoiceChannel> CreateVoiceChannel(string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));

@@ -214,29 +188,25 @@ namespace Discord.Rest
var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false);
return new VoiceChannel(this, model);
}

/// <summary> Gets a collection of all integrations attached to this guild. </summary>
public async Task<IEnumerable<GuildIntegration>> GetIntegrations()
public async Task<IReadOnlyCollection<IGuildIntegration>> GetIntegrations()
{
var models = await Discord.ApiClient.GetGuildIntegrations(Id).ConfigureAwait(false);
return models.Select(x => new GuildIntegration(this, x));
return models.Select(x => new GuildIntegration(this, x)).ToImmutableArray();
}
/// <summary> Creates a new integration for this guild. </summary>
public async Task<GuildIntegration> CreateIntegration(ulong id, string type)
public async Task<IGuildIntegration> CreateIntegration(ulong id, string type)
{
var args = new CreateGuildIntegrationParams { Id = id, Type = type };
var model = await Discord.ApiClient.CreateGuildIntegration(Id, args).ConfigureAwait(false);
return new GuildIntegration(this, model);
}

/// <summary> Gets a collection of all invites to this guild. </summary>
public async Task<IEnumerable<InviteMetadata>> GetInvites()
public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvites()
{
var models = await Discord.ApiClient.GetGuildInvites(Id).ConfigureAwait(false);
return models.Select(x => new InviteMetadata(Discord, x));
return models.Select(x => new InviteMetadata(Discord, x)).ToImmutableArray();
}
/// <summary> Creates a new invite to this guild. </summary>
public async Task<InviteMetadata> CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false)
public async Task<IInviteMetadata> CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false)
{
if (maxAge <= 0) throw new ArgumentOutOfRangeException(nameof(maxAge));
if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses));
@@ -251,18 +221,15 @@ namespace Discord.Rest
var model = await Discord.ApiClient.CreateChannelInvite(DefaultChannelId, args).ConfigureAwait(false);
return new InviteMetadata(Discord, model);
}

/// <summary> Gets the role in this guild with the provided id, or null if not found. </summary>
public Role GetRole(ulong id)
{
Role result = null;
if (_roles?.TryGetValue(id, out result) == true)
return result;
return null;
}

/// <summary> Creates a new role. </summary>
public async Task<Role> CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false)
}
public async Task<IRole> CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false)
{
if (name == null) throw new ArgumentNullException(nameof(name));
@@ -280,34 +247,30 @@ namespace Discord.Rest
return role;
}

/// <summary> Gets a collection of all users in this guild. </summary>
public async Task<IEnumerable<GuildUser>> GetUsers()
{
var args = new GetGuildMembersParams();
var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false);
return models.Select(x => new GuildUser(this, x));
}
/// <summary> Gets a paged collection of all users in this guild. </summary>
public async Task<IEnumerable<GuildUser>> GetUsers(int limit, int offset)
{
var args = new GetGuildMembersParams { Limit = limit, Offset = offset };
var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false);
return models.Select(x => new GuildUser(this, x));
}
/// <summary> Gets the user in this guild with the provided id, or null if not found. </summary>
public async Task<GuildUser> GetUser(ulong id)
public virtual async Task<IGuildUser> GetUser(ulong id)
{
var model = await Discord.ApiClient.GetGuildMember(Id, id).ConfigureAwait(false);
if (model != null)
return new GuildUser(this, model);
return new GuildUser(this, new User(Discord, model.User), model);
return null;
}
/// <summary> Gets a the current user. </summary>
public async Task<GuildUser> GetCurrentUser()
public virtual async Task<IGuildUser> GetCurrentUser()
{
var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false);
return await GetUser(currentUser.Id).ConfigureAwait(false);
}
public virtual async Task<IReadOnlyCollection<IGuildUser>> GetUsers()
{
var args = new GetGuildMembersParams();
var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false);
return models.Select(x => new GuildUser(this, new User(Discord, x.User), x)).ToImmutableArray();
}
public virtual async Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset)
{
var args = new GetGuildMembersParams { Limit = limit, Offset = offset };
var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false);
return models.Select(x => new GuildUser(this, new User(Discord, x.User), x)).ToImmutableArray();
}
public async Task<int> PruneUsers(int days = 30, bool simulate = false)
{
var args = new GuildPruneParams() { Days = days };
@@ -324,45 +287,22 @@ namespace Discord.Rest
switch (model.Type)
{
case ChannelType.Text:
default:
return new TextChannel(this, model);
case ChannelType.Voice:
return new VoiceChannel(this, model);
default:
throw new InvalidOperationException($"Unknown channel type: {model.Type}");
}
}

public override string ToString() => Name;

private string DebuggerDisplay => $"{Name} ({Id})";

IEnumerable<Emoji> IGuild.Emojis => Emojis;
ulong IGuild.EveryoneRoleId => EveryoneRole.Id;
IEnumerable<string> IGuild.Features => Features;
IRole IGuild.EveryoneRole => EveryoneRole;
IReadOnlyCollection<Emoji> IGuild.Emojis => Emojis;
IReadOnlyCollection<string> IGuild.Features => Features;

async Task<IEnumerable<IUser>> IGuild.GetBans()
=> await GetBans().ConfigureAwait(false);
async Task<IGuildChannel> IGuild.GetChannel(ulong id)
=> await GetChannel(id).ConfigureAwait(false);
async Task<IEnumerable<IGuildChannel>> IGuild.GetChannels()
=> await GetChannels().ConfigureAwait(false);
async Task<IInviteMetadata> IGuild.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd)
=> await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false);
async Task<IRole> IGuild.CreateRole(string name, GuildPermissions? permissions, Color? color, bool isHoisted)
=> await CreateRole(name, permissions, color, isHoisted).ConfigureAwait(false);
async Task<ITextChannel> IGuild.CreateTextChannel(string name)
=> await CreateTextChannel(name).ConfigureAwait(false);
async Task<IVoiceChannel> IGuild.CreateVoiceChannel(string name)
=> await CreateVoiceChannel(name).ConfigureAwait(false);
async Task<IEnumerable<IInviteMetadata>> IGuild.GetInvites()
=> await GetInvites().ConfigureAwait(false);
Task<IRole> IGuild.GetRole(ulong id)
=> Task.FromResult<IRole>(GetRole(id));
Task<IEnumerable<IRole>> IGuild.GetRoles()
=> Task.FromResult<IEnumerable<IRole>>(Roles);
async Task<IGuildUser> IGuild.GetUser(ulong id)
=> await GetUser(id).ConfigureAwait(false);
async Task<IGuildUser> IGuild.GetCurrentUser()
=> await GetCurrentUser().ConfigureAwait(false);
async Task<IEnumerable<IGuildUser>> IGuild.GetUsers()
=> await GetUsers().ConfigureAwait(false);
IRole IGuild.GetRole(ulong id) => GetRole(id);
}
}

+ 18
- 0
src/Discord.Net/Entities/Guilds/GuildEmbed.cs View File

@@ -0,0 +1,18 @@
using Model = Discord.API.GuildEmbed;

namespace Discord
{
public struct GuildEmbed
{
public bool IsEnabled { get; private set; }
public ulong? ChannelId { get; private set; }

public GuildEmbed(bool isEnabled, ulong? channelId)
{
ChannelId = channelId;
IsEnabled = isEnabled;
}
internal GuildEmbed(Model model)
: this(model.Enabled, model.ChannelId) { }
}
}

src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs → src/Discord.Net/Entities/Guilds/GuildIntegration.cs View File

@@ -4,47 +4,37 @@ using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Integration;

namespace Discord.Rest
namespace Discord
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class GuildIntegration : IGuildIntegration
internal class GuildIntegration : Entity<ulong>, IGuildIntegration
{
/// <inheritdoc />
public ulong Id { get; private set; }
/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public string Type { get; private set; }
/// <inheritdoc />
public bool IsEnabled { get; private set; }
/// <inheritdoc />
public bool IsSyncing { get; private set; }
/// <inheritdoc />
public ulong ExpireBehavior { get; private set; }
/// <inheritdoc />
public ulong ExpireGracePeriod { get; private set; }
/// <inheritdoc />
public DateTime SyncedAt { get; private set; }

/// <inheritdoc />
public Guild Guild { get; private set; }
/// <inheritdoc />
public Role Role { get; private set; }
/// <inheritdoc />
public User User { get; private set; }
/// <inheritdoc />
public IntegrationAccount Account { get; private set; }
internal DiscordClient Discord => Guild.Discord;
internal GuildIntegration(Guild guild, Model model)

public override DiscordClient Discord => Guild.Discord;

public GuildIntegration(Guild guild, Model model)
: base(model.Id)
{
Guild = guild;
Update(model);
Update(model, UpdateSource.Creation);
}

private void Update(Model model)
private void Update(Model model, UpdateSource source)
{
Id = model.Id;
if (source == UpdateSource.Rest && IsAttached) return;

Name = model.Name;
Type = model.Type;
IsEnabled = model.Enabled;
@@ -53,16 +43,14 @@ namespace Discord.Rest
ExpireGracePeriod = model.ExpireGracePeriod;
SyncedAt = model.SyncedAt;

Role = Guild.GetRole(model.RoleId);
User = new PublicUser(Discord, model.User);
Role = Guild.GetRole(model.RoleId) as Role;
User = new User(Discord, model.User);
}

/// <summary> </summary>
public async Task Delete()
{
await Discord.ApiClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false);
}
/// <summary> </summary>
public async Task Modify(Action<ModifyGuildIntegrationParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));
@@ -71,9 +59,8 @@ namespace Discord.Rest
func(args);
var model = await Discord.ApiClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false);

Update(model);
Update(model, UpdateSource.Rest);
}
/// <summary> </summary>
public async Task Sync()
{
await Discord.ApiClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false);
@@ -83,8 +70,7 @@ namespace Discord.Rest
private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})";

IGuild IGuildIntegration.Guild => Guild;
IRole IGuildIntegration.Role => Role;
IUser IGuildIntegration.User => User;
IntegrationAccount IGuildIntegration.Account => Account;
IRole IGuildIntegration.Role => Role;
}
}

+ 18
- 19
src/Discord.Net/Entities/Guilds/IGuild.cs View File

@@ -7,13 +7,17 @@ namespace Discord
{
public interface IGuild : IDeletable, ISnowflakeEntity, IUpdateable
{
/// <summary> Gets the name of this guild. </summary>
string Name { get; }
/// <summary> Gets the amount of time (in seconds) a user must be inactive in a voice channel for until they are automatically moved to the AFK voice channel, if one is set. </summary>
int AFKTimeout { get; }
/// <summary> Returns true if this guild is embeddable (e.g. widget) </summary>
bool IsEmbeddable { get; }
/// <summary> Gets the name of this guild. </summary>
string Name { get; }
int VerificationLevel { get; }
/// <summary> Returns the url to this guild's icon, or null if one is not set. </summary>
string IconUrl { get; }
/// <summary> Returns the url to this guild's splash image, or null if one is not set. </summary>
string SplashUrl { get; }

/// <summary> Gets the id of the AFK voice channel for this guild if set, or null if not. </summary>
ulong? AFKChannelId { get; }
@@ -21,22 +25,19 @@ namespace Discord
ulong DefaultChannelId { get; }
/// <summary> Gets the id of the embed channel for this guild if set, or null if not. </summary>
ulong? EmbedChannelId { get; }
/// <summary> Gets the id of the role containing all users in this guild. </summary>
ulong EveryoneRoleId { get; }
/// <summary> Gets the id of the user that created this guild. </summary>
ulong OwnerId { get; }
/// <summary> Gets the id of the server region hosting this guild's voice channels. </summary>
/// <summary> Gets the id of the region hosting this guild's voice channels. </summary>
string VoiceRegionId { get; }

/// <summary> Returns the url to this server's icon, or null if one is not set. </summary>
string IconUrl { get; }
/// <summary> Returns the url to this server's splash image, or null if one is not set. </summary>
string SplashUrl { get; }

/// <summary> Gets the built-in role containing all users in this guild. </summary>
IRole EveryoneRole { get; }
/// <summary> Gets a collection of all custom emojis for this guild. </summary>
IEnumerable<Emoji> Emojis { get; }
IReadOnlyCollection<Emoji> Emojis { get; }
/// <summary> Gets a collection of all extra features added to this guild. </summary>
IEnumerable<string> Features { get; }
IReadOnlyCollection<string> Features { get; }
/// <summary> Gets a collection of all roles in this guild. </summary>
IReadOnlyCollection<IRole> Roles { get; }

/// <summary> Modifies this guild. </summary>
Task Modify(Action<ModifyGuildParams> func);
@@ -50,7 +51,7 @@ namespace Discord
Task Leave();

/// <summary> Gets a collection of all users banned on this guild. </summary>
Task<IEnumerable<IUser>> GetBans();
Task<IReadOnlyCollection<IUser>> GetBans();
/// <summary> Bans the provided user from this guild and optionally prunes their recent messages. </summary>
Task AddBan(IUser user, int pruneDays = 0);
/// <summary> Bans the provided user id from this guild and optionally prunes their recent messages. </summary>
@@ -61,7 +62,7 @@ namespace Discord
Task RemoveBan(ulong userId);

/// <summary> Gets a collection of all channels in this guild. </summary>
Task<IEnumerable<IGuildChannel>> GetChannels();
Task<IReadOnlyCollection<IGuildChannel>> GetChannels();
/// <summary> Gets the channel in this guild with the provided id, or null if not found. </summary>
Task<IGuildChannel> GetChannel(ulong id);
/// <summary> Creates a new text channel. </summary>
@@ -70,7 +71,7 @@ namespace Discord
Task<IVoiceChannel> CreateVoiceChannel(string name);

/// <summary> Gets a collection of all invites to this guild. </summary>
Task<IEnumerable<IInviteMetadata>> GetInvites();
Task<IReadOnlyCollection<IInviteMetadata>> GetInvites();
/// <summary> Creates a new invite to this guild. </summary>
/// <param name="maxAge"> The time (in seconds) until the invite expires. Set to null to never expire. </param>
/// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param>
@@ -78,15 +79,13 @@ namespace Discord
/// <param name="withXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to null. </param>
Task<IInviteMetadata> CreateInvite(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false);

/// <summary> Gets a collection of all roles in this guild. </summary>
Task<IEnumerable<IRole>> GetRoles();
/// <summary> Gets the role in this guild with the provided id, or null if not found. </summary>
Task<IRole> GetRole(ulong id);
IRole GetRole(ulong id);
/// <summary> Creates a new role. </summary>
Task<IRole> CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false);

/// <summary> Gets a collection of all users in this guild. </summary>
Task<IEnumerable<IGuildUser>> GetUsers();
Task<IReadOnlyCollection<IGuildUser>> GetUsers();
/// <summary> Gets the user in this guild with the provided id, or null if not found. </summary>
Task<IGuildUser> GetUser(ulong id);
/// <summary> Gets the current user for this guild. </summary>


+ 0
- 8
src/Discord.Net/Entities/Guilds/IGuildEmbed.cs View File

@@ -1,8 +0,0 @@
namespace Discord
{
public interface IGuildEmbed : ISnowflakeEntity
{
bool IsEnabled { get; }
ulong? ChannelId { get; }
}
}

+ 1
- 1
src/Discord.Net/Entities/Guilds/IUserGuild.cs View File

@@ -4,7 +4,7 @@
{
/// <summary> Gets the name of this guild. </summary>
string Name { get; }
/// <summary> Returns the url to this server's icon, or null if one is not set. </summary>
/// <summary> Returns the url to this guild's icon, or null if one is not set. </summary>
string IconUrl { get; }
/// <summary> Returns true if the current user owns this guild. </summary>
bool IsOwner { get; }


+ 3
- 1
src/Discord.Net/Entities/Guilds/IVoiceRegion.cs View File

@@ -1,7 +1,9 @@
namespace Discord
{
public interface IVoiceRegion : IEntity<string>
public interface IVoiceRegion
{
/// <summary> Gets the unique identifier for this voice region. </summary>
string Id { get; }
/// <summary> Gets the name of this voice region. </summary>
string Name { get; }
/// <summary> Returns true if this voice region is exclusive to VIP accounts. </summary>


+ 0
- 3
src/Discord.Net/Entities/Guilds/IntegrationAccount.cs View File

@@ -5,10 +5,7 @@ namespace Discord
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public struct IntegrationAccount
{
/// <inheritdoc />
public string Id { get; }

/// <inheritdoc />
public string Name { get; private set; }

public override string ToString() => Name;


src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs → src/Discord.Net/Entities/Guilds/UserGuild.cs View File

@@ -1,50 +1,42 @@
using System;
using System.Diagnostics;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.UserGuild;

namespace Discord
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class UserGuild : IUserGuild
internal class UserGuild : SnowflakeEntity, IUserGuild
{
private string _iconId;

/// <inheritdoc />
public ulong Id { get; }
internal IDiscordClient Discord { get; }

/// <inheritdoc />
public string Name { get; private set; }
public bool IsOwner { get; private set; }
public GuildPermissions Permissions { get; private set; }

/// <inheritdoc />
public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id);
/// <inheritdoc />
public override DiscordClient Discord { get; }

public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId);

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

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

_iconId = model.Icon;
IsOwner = model.Owner;
Name = model.Name;
Permissions = new GuildPermissions(model.Permissions);
}
/// <inheritdoc />
public async Task Leave()
{
await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task Delete()
{
await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false);

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

@@ -4,22 +4,16 @@ using Model = Discord.API.VoiceRegion;
namespace Discord
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class VoiceRegion : IVoiceRegion
internal class VoiceRegion : IVoiceRegion
{
/// <inheritdoc />
public string Id { get; }
/// <inheritdoc />
public string Name { get; }
/// <inheritdoc />
public bool IsVip { get; }
/// <inheritdoc />
public bool IsOptimal { get; }
/// <inheritdoc />
public string SampleHostname { get; }
/// <inheritdoc />
public int SamplePort { get; }

internal VoiceRegion(Model model)
public VoiceRegion(Model model)
{
Id = model.Id;
Name = model.Name;


+ 4
- 0
src/Discord.Net/Entities/IEntity.cs View File

@@ -4,5 +4,9 @@ namespace Discord
{
/// <summary> Gets the unique identifier for this object. </summary>
TId Id { get; }

//TODO: What do we do when an object is destroyed due to reconnect? This summary isn't correct.
/// <summary> Returns true if this object is getting live updates from the DiscordClient. </summary>
bool IsAttached { get;}
}
}

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

@@ -4,7 +4,7 @@ namespace Discord
{
public interface IUpdateable
{
/// <summary> Ensures this objects's cached properties reflect its current state on the Discord server. </summary>
/// <summary> Updates this object's properties with its current state. </summary>
Task Update();
}
}

+ 1
- 1
src/Discord.Net/Entities/Invites/IInvite.cs View File

@@ -18,7 +18,7 @@ namespace Discord
/// <summary> Gets the id of the guild this invite is linked to. </summary>
ulong GuildId { get; }

/// <summary> Accepts this invite and joins the target server. This will fail on bot accounts. </summary>
/// <summary> Accepts this invite and joins the target guild. This will fail on bot accounts. </summary>
Task Accept();
}
}

+ 13
- 26
src/Discord.Net/Entities/Invites/Invite.cs View File

@@ -5,38 +5,31 @@ using Model = Discord.API.Invite;
namespace Discord
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class Invite : IInvite
internal class Invite : Entity<string>, IInvite
{
/// <inheritdoc />
public string Code { get; }
internal IDiscordClient Discord { get; }
public string ChannelName { get; private set; }
public string GuildName { get; private set; }
public string XkcdCode { get; private set; }

/// <inheritdoc />
public ulong GuildId { get; private set; }
/// <inheritdoc />
public ulong ChannelId { get; private set; }
/// <inheritdoc />
public string XkcdCode { get; private set; }
/// <inheritdoc />
public string GuildName { get; private set; }
/// <inheritdoc />
public string ChannelName { get; private set; }
public ulong GuildId { get; private set; }
public override DiscordClient Discord { get; }

/// <inheritdoc />
public string Code => Id;
public string Url => $"{DiscordConfig.InviteUrl}/{XkcdCode ?? Code}";
/// <inheritdoc />
public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null;

internal Invite(IDiscordClient discord, Model model)
public Invite(DiscordClient discord, Model model)
: base(model.Code)
{
Discord = discord;
Code = model.Code;

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

XkcdCode = model.XkcdPass;
GuildId = model.Guild.Id;
ChannelId = model.Channel.Id;
@@ -44,22 +37,16 @@ namespace Discord
ChannelName = model.Channel.Name;
}

/// <inheritdoc />
public async Task Accept()
{
await Discord.ApiClient.AcceptInvite(Code).ConfigureAwait(false);
}

/// <inheritdoc />
public async Task Delete()
{
await Discord.ApiClient.DeleteInvite(Code).ConfigureAwait(false);
}

/// <inheritdoc />
public override string ToString() => XkcdUrl ?? Url;
private string DebuggerDisplay => $"{XkcdUrl ?? Url} ({GuildName} / {ChannelName})";

string IEntity<string>.Id => Code;
}
}

+ 6
- 9
src/Discord.Net/Entities/Invites/InviteMetadata.cs View File

@@ -2,26 +2,23 @@

namespace Discord
{
public class InviteMetadata : Invite, IInviteMetadata
internal class InviteMetadata : Invite, IInviteMetadata
{
/// <inheritdoc />
public bool IsRevoked { get; private set; }
/// <inheritdoc />
public bool IsTemporary { get; private set; }
/// <inheritdoc />
public int? MaxAge { get; private set; }
/// <inheritdoc />
public int? MaxUses { get; private set; }
/// <inheritdoc />
public int Uses { get; private set; }

internal InviteMetadata(IDiscordClient client, Model model)
public InviteMetadata(DiscordClient client, Model model)
: base(client, model)
{
Update(model);
Update(model, UpdateSource.Creation);
}
private void Update(Model model)
private void Update(Model model, UpdateSource source)
{
if (source == UpdateSource.Rest && IsAttached) return;

IsRevoked = model.Revoked;
IsTemporary = model.Temporary;
MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null;


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

@@ -2,16 +2,16 @@

namespace Discord
{
public struct Embed
internal class Embed : IEmbed
{
public string Description { get; }
public string Url { get; }
public string Type { get; }
public string Title { get; }
public string Description { get; }
public string Type { get; }
public EmbedProvider Provider { get; }
public EmbedThumbnail Thumbnail { get; }

internal Embed(Model model)
public Embed(Model model)
{
Url = model.Url;
Type = model.Type;


+ 5
- 3
src/Discord.Net/Entities/Messages/EmbedProvider.cs View File

@@ -7,10 +7,12 @@ namespace Discord
public string Name { get; }
public string Url { get; }

internal EmbedProvider(Model model)
public EmbedProvider(string name, string url)
{
Name = model.Name;
Url = model.Url;
Name = name;
Url = url;
}
internal EmbedProvider(Model model)
: this(model.Name, model.Url) { }
}
}

+ 8
- 5
src/Discord.Net/Entities/Messages/EmbedThumbnail.cs View File

@@ -9,12 +9,15 @@ namespace Discord
public int? Height { get; }
public int? Width { get; }

internal EmbedThumbnail(Model model)
public EmbedThumbnail(string url, string proxyUrl, int? height, int? width)
{
Url = model.Url;
ProxyUrl = model.ProxyUrl;
Height = model.Height;
Width = model.Width;
Url = url;
ProxyUrl = proxyUrl;
Height = height;
Width = width;
}

internal EmbedThumbnail(Model model)
: this(model.Url, model.ProxyUrl, model.Height, model.Width) { }
}
}

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

@@ -0,0 +1,12 @@
namespace Discord
{
public interface IEmbed
{
string Url { get; }
string Type { get; }
string Title { get; }
string Description { get; }
EmbedProvider Provider { get; }
EmbedThumbnail Thumbnail { get; }
}
}

+ 7
- 8
src/Discord.Net/Entities/Messages/IMessage.cs View File

@@ -5,7 +5,7 @@ using System.Collections.Generic;

namespace Discord
{
public interface IMessage : IDeletable, ISnowflakeEntity
public interface IMessage : IDeletable, ISnowflakeEntity, IUpdateable
{
/// <summary> Gets the time of this message's last edit, if any. </summary>
DateTime? EditedTimestamp { get; }
@@ -16,23 +16,22 @@ namespace Discord
/// <summary> Returns the text for this message after mention processing. </summary>
string Text { get; }
/// <summary> Gets the time this message was sent. </summary>
DateTime Timestamp { get; } //TODO: Is this different from IHasSnowflake.CreatedAt?
DateTime Timestamp { get; }

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

/// <summary> Returns a collection of all attachments included in this message. </summary>
IReadOnlyList<Attachment> Attachments { get; }
IReadOnlyCollection<Attachment> Attachments { get; }
/// <summary> Returns a collection of all embeds included in this message. </summary>
IReadOnlyList<Embed> Embeds { get; }
IReadOnlyCollection<IEmbed> Embeds { get; }
/// <summary> Returns a collection of channel ids mentioned in this message. </summary>
IReadOnlyList<ulong> MentionedChannelIds { get; }
IReadOnlyCollection<ulong> MentionedChannelIds { get; }
/// <summary> Returns a collection of role ids mentioned in this message. </summary>
IReadOnlyList<ulong> MentionedRoleIds { get; }
IReadOnlyCollection<ulong> MentionedRoleIds { get; }
/// <summary> Returns a collection of user ids mentioned in this message. </summary>
IReadOnlyList<IUser> MentionedUsers { get; }
IReadOnlyCollection<IUser> MentionedUsers { get; }

/// <summary> Modifies this message. </summary>
Task Modify(Action<ModifyMessageParams> func);


src/Discord.Net/Rest/Entities/Message.cs → src/Discord.Net/Entities/Messages/Message.cs View File

@@ -6,55 +6,40 @@ using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Message;

namespace Discord.Rest
namespace Discord
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class Message : IMessage
{
/// <inheritdoc />
public ulong Id { get; }

/// <inheritdoc />
internal class Message : SnowflakeEntity, IMessage
{
public DateTime? EditedTimestamp { get; private set; }
/// <inheritdoc />
public bool IsTTS { get; private set; }
/// <inheritdoc />
public string RawText { get; private set; }
/// <inheritdoc />
public string Text { get; private set; }
/// <inheritdoc />
public DateTime Timestamp { get; private set; }

/// <inheritdoc />
public IMessageChannel Channel { get; }
/// <inheritdoc />
public IUser Author { get; }
public ImmutableArray<Attachment> Attachments { get; private set; }
public ImmutableArray<Embed> Embeds { get; private set; }
public ImmutableArray<ulong> MentionedChannelIds { get; private set; }
public ImmutableArray<ulong> MentionedRoleIds { get; private set; }
public ImmutableArray<User> MentionedUsers { get; private set; }

public override DiscordClient Discord => (Channel as Entity<ulong>).Discord;

/// <inheritdoc />
public IReadOnlyList<Attachment> Attachments { get; private set; }
/// <inheritdoc />
public IReadOnlyList<Embed> Embeds { get; private set; }
/// <inheritdoc />
public IReadOnlyList<IUser> MentionedUsers { get; private set; }
/// <inheritdoc />
public IReadOnlyList<ulong> MentionedChannelIds { get; private set; }
/// <inheritdoc />
public IReadOnlyList<ulong> MentionedRoleIds { get; private set; }

/// <inheritdoc />
public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id);
internal DiscordClient Discord => (Channel as TextChannel)?.Discord ?? (Channel as DMChannel).Discord;

internal Message(IMessageChannel channel, Model model)
public Message(IMessageChannel channel, IUser author, Model model)
: base(model.Id)
{
Id = model.Id;
Channel = channel;
Author = new PublicUser(Discord, model.Author);
Author = author;

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

var guildChannel = Channel as GuildChannel;
var guild = guildChannel?.Guild;
var discord = Discord;
@@ -72,7 +57,7 @@ namespace Discord.Rest
Attachments = ImmutableArray.Create(attachments);
}
else
Attachments = Array.Empty<Attachment>();
Attachments = ImmutableArray.Create<Attachment>();

if (model.Embeds.Length > 0)
{
@@ -82,17 +67,17 @@ namespace Discord.Rest
Embeds = ImmutableArray.Create(embeds);
}
else
Embeds = Array.Empty<Embed>();
Embeds = ImmutableArray.Create<Embed>();

if (guildChannel != null && model.Mentions.Length > 0)
{
var mentions = new PublicUser[model.Mentions.Length];
var mentions = new User[model.Mentions.Length];
for (int i = 0; i < model.Mentions.Length; i++)
mentions[i] = new PublicUser(discord, model.Mentions[i]);
mentions[i] = new User(discord, model.Mentions[i]);
MentionedUsers = ImmutableArray.Create(mentions);
}
else
MentionedUsers = Array.Empty<PublicUser>();
MentionedUsers = ImmutableArray.Create<User>();

if (guildChannel != null)
{
@@ -105,14 +90,20 @@ namespace Discord.Rest
}
else
{
MentionedChannelIds = Array.Empty<ulong>();
MentionedRoleIds = Array.Empty<ulong>();
MentionedChannelIds = ImmutableArray.Create<ulong>();
MentionedRoleIds = ImmutableArray.Create<ulong>();
}
Text = MentionUtils.CleanUserMentions(model.Content, model.Mentions);
}

/// <inheritdoc />
public async Task Update()
{
if (IsAttached) throw new NotSupportedException();

var model = await Discord.ApiClient.GetChannelMessage(Channel.Id, Id).ConfigureAwait(false);
Update(model, UpdateSource.Rest);
}
public async Task Modify(Action<ModifyMessageParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));
@@ -126,10 +117,8 @@ namespace Discord.Rest
model = await Discord.ApiClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false);
else
model = await Discord.ApiClient.ModifyDMMessage(Channel.Id, Id, args).ConfigureAwait(false);
Update(model);
}

/// <inheritdoc />
Update(model, UpdateSource.Rest);
}
public async Task Delete()
{
var guildChannel = Channel as GuildChannel;
@@ -140,9 +129,12 @@ namespace Discord.Rest
}

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

IUser IMessage.Author => Author;
IReadOnlyList<IUser> IMessage.MentionedUsers => MentionedUsers;
IReadOnlyCollection<Attachment> IMessage.Attachments => Attachments;
IReadOnlyCollection<IEmbed> IMessage.Embeds => Embeds;
IReadOnlyCollection<ulong> IMessage.MentionedChannelIds => MentionedChannelIds;
IReadOnlyCollection<ulong> IMessage.MentionedRoleIds => MentionedRoleIds;
IReadOnlyCollection<IUser> IMessage.MentionedUsers => MentionedUsers;
}
}

+ 1
- 1
src/Discord.Net/Entities/Permissions/ChannelPermissions.cs View File

@@ -144,7 +144,7 @@ namespace Discord
}
return perms;
}
/// <inheritdoc />
public override string ToString() => RawValue.ToString();
private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})";
}


+ 1
- 1
src/Discord.Net/Entities/Permissions/GuildPermissions.cs View File

@@ -144,7 +144,7 @@ namespace Discord
}
return perms;
}
/// <inheritdoc />
public override string ToString() => RawValue.ToString();
private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})";
}


+ 7
- 4
src/Discord.Net/Entities/Permissions/Overwrite.cs View File

@@ -12,11 +12,14 @@ namespace Discord
public OverwritePermissions Permissions { get; }

/// <summary> Creates a new Overwrite with provided target information and modified permissions. </summary>
internal Overwrite(Model model)
public Overwrite(ulong targetId, PermissionTarget targetType, OverwritePermissions permissions)
{
TargetId = model.TargetId;
TargetType = model.TargetType;
Permissions = new OverwritePermissions(model.Allow, model.Deny);
TargetId = targetId;
TargetType = targetType;
Permissions = permissions;
}

internal Overwrite(Model model)
: this(model.TargetId, model.TargetType, new OverwritePermissions(model.Allow, model.Deny)) { }
}
}

+ 1
- 1
src/Discord.Net/Entities/Permissions/OverwritePermissions.cs View File

@@ -135,7 +135,7 @@ namespace Discord
}
return perms;
}
/// <inheritdoc />
public override string ToString() => $"Allow {AllowValue}, Deny {DenyValue}";
private string DebuggerDisplay =>
$"Allow {AllowValue} ({string.Join(", ", ToAllowList())})\n" +


+ 15
- 14
src/Discord.Net/Entities/Permissions/Permissions.cs View File

@@ -90,8 +90,8 @@ namespace Discord
{
var roles = user.Roles;
ulong newPermissions = 0;
for (int i = 0; i < roles.Count; i++)
newPermissions |= roles[i].Permissions.RawValue;
foreach (var role in roles)
newPermissions |= role.Permissions.RawValue;
return newPermissions;
}

@@ -110,25 +110,26 @@ namespace Discord
{
//Start with this user's guild permissions
resolvedPermissions = guildPermissions;
var overwrites = channel.PermissionOverwrites;

Overwrite entry;
OverwritePermissions? perms;
var roles = user.Roles;
if (roles.Count > 0)
{
for (int i = 0; i < roles.Count; i++)
ulong deniedPermissions = 0UL, allowedPermissions = 0UL;
foreach (var role in roles)
{
if (overwrites.TryGetValue(roles[i].Id, out entry))
resolvedPermissions &= ~entry.Permissions.DenyValue;
}
for (int i = 0; i < roles.Count; i++)
{
if (overwrites.TryGetValue(roles[i].Id, out entry))
resolvedPermissions |= entry.Permissions.AllowValue;
perms = channel.GetPermissionOverwrite(role);
if (perms != null)
{
deniedPermissions |= perms.Value.DenyValue;
allowedPermissions |= perms.Value.AllowValue;
}
}
resolvedPermissions = (resolvedPermissions & ~deniedPermissions) | allowedPermissions;
}
if (overwrites.TryGetValue(user.Id, out entry))
resolvedPermissions = (resolvedPermissions & ~entry.Permissions.DenyValue) | entry.Permissions.AllowValue;
perms = channel.GetPermissionOverwrite(user);
if (perms != null)
resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue;

#if CSHARP7
switch (channel)


+ 1
- 4
src/Discord.Net/Entities/Roles/IRole.cs View File

@@ -11,7 +11,7 @@ namespace Discord
Color Color { get; }
/// <summary> Returns true if users of this role are separated in the user list. </summary>
bool IsHoisted { get; }
/// <summary> Returns true if this role is automatically managed by the Discord server. </summary>
/// <summary> Returns true if this role is automatically managed by Discord. </summary>
bool IsManaged { get; }
/// <summary> Gets the name of this role. </summary>
string Name { get; }
@@ -25,8 +25,5 @@ namespace Discord

/// <summary> Modifies this role. </summary>
Task Modify(Action<ModifyGuildRoleParams> func);

/// <summary> Returns a collection of all users that have been assigned this role. </summary>
Task<IEnumerable<IGuildUser>> GetUsers();
}
}

src/Discord.Net/Rest/Entities/Role.cs → src/Discord.Net/Entities/Roles/Role.cs View File

@@ -1,51 +1,41 @@
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Role;

namespace Discord.Rest
namespace Discord
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class Role : IRole, IMentionable
internal class Role : SnowflakeEntity, IRole, IMentionable
{
/// <inheritdoc />
public ulong Id { get; }
/// <summary> Returns the guild this role belongs to. </summary>
public Guild Guild { get; }

/// <inheritdoc />
public Color Color { get; private set; }
/// <inheritdoc />
public bool IsHoisted { get; private set; }
/// <inheritdoc />
public bool IsManaged { get; private set; }
/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public GuildPermissions Permissions { get; private set; }
/// <inheritdoc />
public int Position { get; private set; }

/// <inheritdoc />
public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id);
/// <inheritdoc />
public bool IsEveryone => Id == Guild.Id;
/// <inheritdoc />
public string Mention => MentionUtils.Mention(this);
internal DiscordClient Discord => Guild.Discord;
public override DiscordClient Discord => Guild.Discord;

internal Role(Guild guild, Model model)
public Role(Guild guild, Model model)
: base(model.Id)
{
Id = model.Id;
Guild = guild;

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

Name = model.Name;
IsHoisted = model.Hoist.Value;
IsManaged = model.Managed.Value;
@@ -53,7 +43,7 @@ namespace Discord.Rest
Color = new Color(model.Color.Value);
Permissions = new GuildPermissions(model.Permissions.Value);
}
/// <summary> Modifies the properties of this role. </summary>
public async Task Modify(Action<ModifyGuildRoleParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));
@@ -61,23 +51,16 @@ namespace Discord.Rest
var args = new ModifyGuildRoleParams();
func(args);
var response = await Discord.ApiClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false);
Update(response);
Update(response, UpdateSource.Rest);
}
/// <summary> Deletes this message. </summary>
public async Task Delete()
=> await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false);

/// <inheritdoc />
{
await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false);
}
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id})";

ulong IRole.GuildId => Guild.Id;
async Task<IEnumerable<IGuildUser>> IRole.GetUsers()
{
//TODO: Rethink this, it isn't paginated or anything...
var models = await Discord.ApiClient.GetGuildMembers(Guild.Id, new GetGuildMembersParams()).ConfigureAwait(false);
return models.Where(x => x.Roles.Contains(Id)).Select(x => new GuildUser(Guild, x));
}
}
}

+ 15
- 0
src/Discord.Net/Entities/SnowflakeEntity.cs View File

@@ -0,0 +1,15 @@
using System;

namespace Discord
{
internal abstract class SnowflakeEntity : Entity<ulong>, ISnowflakeEntity
{
//TODO: Candidate for Extension Property. Lets us remove this class.
public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id);

public SnowflakeEntity(ulong id)
: base(id)
{
}
}
}

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

@@ -0,0 +1,9 @@
namespace Discord
{
internal enum UpdateSource
{
Creation,
Rest,
WebSocket
}
}

+ 2
- 3
src/Discord.Net/Entities/Users/Connection.cs View File

@@ -5,19 +5,18 @@ using Model = Discord.API.Connection;
namespace Discord
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class Connection : IConnection
internal class Connection : IConnection
{
public string Id { get; }
public string Type { get; }
public string Name { get; }
public bool IsRevoked { get; }

public IEnumerable<ulong> IntegrationIds { get; }
public IReadOnlyCollection<ulong> IntegrationIds { get; }

public Connection(Model model)
{
Id = model.Id;

Type = model.Type;
Name = model.Name;
IsRevoked = model.Revoked;


+ 7
- 7
src/Discord.Net/Entities/Users/Game.cs View File

@@ -1,4 +1,6 @@
namespace Discord
using Model = Discord.API.Game;

namespace Discord
{
public struct Game
{
@@ -6,17 +8,15 @@
public string StreamUrl { get; }
public StreamType StreamType { get; }

public Game(string name)
{
Name = name;
StreamUrl = null;
StreamType = StreamType.NotStreaming;
}
public Game(string name, string streamUrl, StreamType type)
{
Name = name;
StreamUrl = streamUrl;
StreamType = type;
}
public Game(string name)
: this(name, null, StreamType.NotStreaming) { }
internal Game(Model model)
: this(model.Name, model.StreamUrl, model.StreamType ?? StreamType.NotStreaming) { }
}
}

src/Discord.Net/Rest/Entities/Users/GuildUser.cs → src/Discord.Net/Entities/Users/GuildUser.cs View File

@@ -6,70 +6,64 @@ using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.GuildMember;

namespace Discord.Rest
namespace Discord
{
public class GuildUser : User, IGuildUser
internal class GuildUser : IGuildUser, ISnowflakeEntity
{
private ImmutableArray<Role> _roles;

public Guild Guild { get; }

/// <inheritdoc />
public bool IsDeaf { get; private set; }
/// <inheritdoc />
public bool IsMute { get; private set; }
/// <inheritdoc />
public DateTime JoinedAt { get; private set; }
/// <inheritdoc />
public string Nickname { get; private set; }

/// <inheritdoc />
public GuildPermissions GuildPermissions { get; private set; }

/// <inheritdoc />
public IReadOnlyList<Role> Roles => _roles;
internal override DiscordClient Discord => Guild.Discord;
public Guild Guild { get; private set; }
public User User { get; private set; }
public ImmutableArray<Role> Roles { get; private set; }

public ulong Id => User.Id;
public string AvatarUrl => User.AvatarUrl;
public DateTime CreatedAt => User.CreatedAt;
public ushort Discriminator => User.Discriminator;
public Game? Game => User.Game;
public bool IsAttached => User.IsAttached;
public bool IsBot => User.IsBot;
public string Mention => User.Mention;
public UserStatus Status => User.Status;
public string Username => User.Username;

internal GuildUser(Guild guild, Model model)
: base(model.User)
public DiscordClient Discord => Guild.Discord;

public GuildUser(Guild guild, User user, Model model)
{
Guild = guild;

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

IsDeaf = model.Deaf;
IsMute = model.Mute;
JoinedAt = model.JoinedAt.Value;
Nickname = model.Nick;

var roles = ImmutableArray.CreateBuilder<Role>(model.Roles.Length + 1);
roles.Add(Guild.EveryoneRole);
roles.Add(Guild.EveryoneRole as Role);
for (int i = 0; i < model.Roles.Length; i++)
roles.Add(Guild.GetRole(model.Roles[i]));
_roles = roles.ToImmutable();
roles.Add(Guild.GetRole(model.Roles[i]) as Role);
Roles = roles.ToImmutable();

GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this));
}

public async Task Update()
{
var model = await Discord.ApiClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false);
Update(model);
}
if (IsAttached) throw new NotSupportedException();

public async Task Kick()
{
await Discord.ApiClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false);
}

public ChannelPermissions GetPermissions(IGuildChannel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue));
var model = await Discord.ApiClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false);
Update(model, UpdateSource.Rest);
}

public async Task Modify(Action<ModifyGuildMemberParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));
@@ -82,7 +76,7 @@ namespace Discord.Rest
{
var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value ?? "" };
await Discord.ApiClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false);
args.Nickname = new API.Optional<string>(); //Remove
args.Nickname = new Optional<string>(); //Remove
}

if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified)
@@ -95,18 +89,24 @@ namespace Discord.Rest
if (args.Nickname.IsSpecified)
Nickname = args.Nickname.Value ?? "";
if (args.Roles.IsSpecified)
_roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray();
Roles = args.Roles.Value.Select(x => Guild.GetRole(x) as Role).Where(x => x != null).ToImmutableArray();
}
}
public async Task Kick()
{
await Discord.ApiClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false);
}

public ChannelPermissions GetPermissions(IGuildChannel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue));
}

public Task<IDMChannel> CreateDMChannel() => User.CreateDMChannel();

IGuild IGuildUser.Guild => Guild;
IReadOnlyList<IRole> IGuildUser.Roles => Roles;
IReadOnlyCollection<IRole> IGuildUser.Roles => Roles;
IVoiceChannel IGuildUser.VoiceChannel => null;

GuildPermissions IGuildUser.GetGuildPermissions()
=> GuildPermissions;
ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel)
=> GetPermissions(channel);
}
}

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

@@ -9,6 +9,6 @@ namespace Discord
string Name { get; }
bool IsRevoked { get; }

IEnumerable<ulong> IntegrationIds { get; }
IReadOnlyCollection<ulong> IntegrationIds { get; }
}
}

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

@@ -16,16 +16,16 @@ namespace Discord
DateTime JoinedAt { get; }
/// <summary> Gets the nickname for this user. </summary>
string Nickname { get; }
/// <summary> Gets the guild-level permissions granted to this user by their roles. </summary>
GuildPermissions GuildPermissions { get; }

/// <summary> Gets the guild for this guild-user pair. </summary>
IGuild Guild { get; }
/// <summary> Returns a collection of the roles this user is a member of in this guild, including the guild's @everyone role. </summary>
IReadOnlyList<IRole> Roles { get; }
IReadOnlyCollection<IRole> Roles { get; }
/// <summary> Gets the voice channel this user is currently in, if any. </summary>
IVoiceChannel VoiceChannel { get; }

/// <summary> Gets the guild-level permissions granted to this user by their roles. </summary>
GuildPermissions GetGuildPermissions();
/// <summary> Gets the channel-level permissions granted to this user for a given channel. </summary>
ChannelPermissions GetPermissions(IGuildChannel channel);

@@ -34,4 +34,4 @@ namespace Discord
/// <summary> Modifies this user's properties in this guild. </summary>
Task Modify(Action<ModifyGuildMemberParams> func);
}
}
}

+ 10
- 0
src/Discord.Net/Entities/Users/IPresence.cs View File

@@ -0,0 +1,10 @@
namespace Discord
{
public interface IPresence
{
/// <summary> Gets the game this user is currently playing, if any. </summary>
Game? Game { get; }
/// <summary> Gets the current status of this user. </summary>
UserStatus Status { get; }
}
}

+ 1
- 5
src/Discord.Net/Entities/Users/IUser.cs View File

@@ -2,18 +2,14 @@ using System.Threading.Tasks;

namespace Discord
{
public interface IUser : ISnowflakeEntity, IMentionable
public interface IUser : ISnowflakeEntity, IMentionable, IPresence
{
/// <summary> Gets the url to this user's avatar. </summary>
string AvatarUrl { get; }
/// <summary> Gets the game this user is currently playing, if any. </summary>
Game? CurrentGame { get; }
/// <summary> Gets the per-username unique id for this user. </summary>
ushort Discriminator { get; }
/// <summary> Returns true if this user is a bot account. </summary>
bool IsBot { get; }
/// <summary> Gets the current status of this user. </summary>
UserStatus Status { get; }
/// <summary> Gets the username for this user. </summary>
string Username { get; }



src/Discord.Net/Rest/Entities/Users/SelfUser.cs → src/Discord.Net/Entities/Users/SelfUser.cs View File

@@ -3,38 +3,34 @@ using System;
using System.Threading.Tasks;
using Model = Discord.API.User;

namespace Discord.Rest
namespace Discord
{
public class SelfUser : User, ISelfUser
{
internal override DiscordClient Discord { get; }

/// <inheritdoc />
internal class SelfUser : User, ISelfUser
{
public string Email { get; private set; }
/// <inheritdoc />
public bool IsVerified { get; private set; }

internal SelfUser(DiscordClient discord, Model model)
: base(model)
public SelfUser(DiscordClient discord, Model model)
: base(discord, model)
{
Discord = discord;
}
internal override void Update(Model model)
public override void Update(Model model, UpdateSource source)
{
base.Update(model);
if (source == UpdateSource.Rest && IsAttached) return;

base.Update(model, source);

Email = model.Email;
IsVerified = model.IsVerified;
}

/// <inheritdoc />
public async Task Update()
{
var model = await Discord.ApiClient.GetCurrentUser().ConfigureAwait(false);
Update(model);
}
if (IsAttached) throw new NotSupportedException();

/// <inheritdoc />
var model = await Discord.ApiClient.GetCurrentUser().ConfigureAwait(false);
Update(model, UpdateSource.Rest);
}
public async Task Modify(Action<ModifyCurrentUserParams> func)
{
if (func != null) throw new NullReferenceException(nameof(func));
@@ -42,7 +38,7 @@ namespace Discord.Rest
var args = new ModifyCurrentUserParams();
func(args);
var model = await Discord.ApiClient.ModifyCurrentUser(args).ConfigureAwait(false);
Update(model);
Update(model, UpdateSource.Rest);
}
}
}

src/Discord.Net/Rest/Entities/Users/User.cs → src/Discord.Net/Entities/Users/User.cs View File

@@ -1,68 +1,52 @@
using Discord.API.Rest;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.User;

namespace Discord.Rest
namespace Discord
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public abstract class User : IUser
internal class User : SnowflakeEntity, IUser
{
private string _avatarId;

/// <inheritdoc />
public ulong Id { get; }
internal abstract DiscordClient Discord { get; }

/// <inheritdoc />
public ushort Discriminator { get; private set; }
/// <inheritdoc />
public bool IsBot { get; private set; }
/// <inheritdoc />
public string Username { get; private set; }

/// <inheritdoc />
public override DiscordClient Discord { get; }

public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId);
/// <inheritdoc />
public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id);
/// <inheritdoc />
public virtual Game? Game => null;
public string Mention => MentionUtils.Mention(this, false);
/// <inheritdoc />
public string NicknameMention => MentionUtils.Mention(this, true);
public virtual UserStatus Status => UserStatus.Unknown;

internal User(Model model)
public User(DiscordClient discord, Model model)
: base(model.Id)
{
Id = model.Id;

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

_avatarId = model.Avatar;
Discriminator = model.Discriminator;
IsBot = model.Bot;
Username = model.Username;
}

protected virtual async Task<DMChannel> CreateDMChannelInternal()
public async Task<IDMChannel> CreateDMChannel()
{
var args = new CreateDMChannelParams { RecipientId = Id };
var model = await Discord.ApiClient.CreateDMChannel(args).ConfigureAwait(false);

return new DMChannel(Discord, model);
return new DMChannel(Discord, this, model);
}

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

/// <inheritdoc />
Game? IUser.CurrentGame => null;
/// <inheritdoc />
UserStatus IUser.Status => UserStatus.Unknown;

/// <inheritdoc />
async Task<IDMChannel> IUser.CreateDMChannel()
=> await CreateDMChannelInternal().ConfigureAwait(false);
}
}

+ 70
- 0
src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs View File

@@ -0,0 +1,70 @@
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;

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

public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public new CachedPublicUser Recipient => base.Recipient as CachedPublicUser;
public IReadOnlyCollection<IUser> Members => ImmutableArray.Create<IUser>(Discord.CurrentUser, Recipient);

public CachedDMChannel(DiscordSocketClient discord, CachedPublicUser recipient, Model model)
: base(discord, recipient, model)
{
_messages = new MessageCache(Discord, this);
}

public override Task<IUser> GetUser(ulong id) => Task.FromResult(GetCachedUser(id));
public override Task<IReadOnlyCollection<IUser>> GetUsers() => Task.FromResult(Members);
public override Task<IReadOnlyCollection<IUser>> GetUsers(int limit, int offset)
=> Task.FromResult<IReadOnlyCollection<IUser>>(Members.Skip(offset).Take(limit).ToImmutableArray());
public IUser GetCachedUser(ulong id)
{
var currentUser = Discord.CurrentUser;
if (id == Recipient.Id)
return Recipient;
else if (id == currentUser.Id)
return currentUser;
else
return null;
}

public override async Task<IMessage> GetMessage(ulong id)
{
return await _messages.Download(id).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessages(int limit)
{
return await _messages.Download(null, Direction.Before, limit).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessages(ulong fromMessageId, Direction dir, int limit)
{
return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false);
}
public CachedMessage AddCachedMessage(IUser author, MessageModel model)
{
var msg = new CachedMessage(this, author, model);
_messages.Add(msg);
return msg;
}
public CachedMessage GetCachedMessage(ulong id)
{
return _messages.Get(id);
}
public CachedMessage RemoveCachedMessage(ulong id)
{
return _messages.Remove(id);
}

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

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

+ 171
- 0
src/Discord.Net/Entities/WebSocket/CachedGuild.cs View File

@@ -0,0 +1,171 @@
using Discord.Data;
using Discord.Extensions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using ChannelModel = Discord.API.Channel;
using ExtendedModel = Discord.API.Gateway.ExtendedGuild;
using MemberModel = Discord.API.GuildMember;
using Model = Discord.API.Guild;
using PresenceModel = Discord.API.Presence;

namespace Discord
{
internal class CachedGuild : Guild, ICachedEntity<ulong>
{
private ConcurrentHashSet<ulong> _channels;
private ConcurrentDictionary<ulong, CachedGuildUser> _members;
private ConcurrentDictionary<ulong, Presence> _presences;
private int _userCount;

public bool Available { get; private set; } //TODO: Add to IGuild

public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public CachedGuildUser CurrentUser => GetCachedUser(Discord.CurrentUser.Id);
public IReadOnlyCollection<ICachedGuildChannel> Channels => _channels.Select(x => GetCachedChannel(x)).ToReadOnlyCollection(_channels);
public IReadOnlyCollection<CachedGuildUser> Members => _members.ToReadOnlyCollection();

public CachedGuild(DiscordSocketClient discord, Model model) : base(discord, model)
{
}

public void Update(ExtendedModel model, UpdateSource source, DataStore dataStore)
{
if (source == UpdateSource.Rest && IsAttached) return;
Available = !(model.Unavailable ?? false);
if (!Available)
{
if (_channels == null)
_channels = new ConcurrentHashSet<ulong>();
if (_members == null)
_members = new ConcurrentDictionary<ulong, CachedGuildUser>();
if (_presences == null)
_presences = new ConcurrentDictionary<ulong, Presence>();
if (_roles == null)
_roles = new ConcurrentDictionary<ulong, Role>();
if (Emojis == null)
Emojis = ImmutableArray.Create<Emoji>();
if (Features == null)
Features = ImmutableArray.Create<string>();
return;
}

base.Update(model as Model, source);

_userCount = model.MemberCount;

var channels = new ConcurrentHashSet<ulong>();
if (model.Channels != null)
{
for (int i = 0; i < model.Channels.Length; i++)
AddCachedChannel(model.Channels[i], channels, dataStore);
}
_channels = channels;

var presences = new ConcurrentDictionary<ulong, Presence>();
if (model.Presences != null)
{
for (int i = 0; i < model.Presences.Length; i++)
AddCachedPresence(model.Presences[i], presences);
}
_presences = presences;

var members = new ConcurrentDictionary<ulong, CachedGuildUser>();
if (model.Members != null)
{
for (int i = 0; i < model.Members.Length; i++)
AddCachedUser(model.Members[i], members, dataStore);
}
_members = members;
}

public override Task<IGuildChannel> GetChannel(ulong id) => Task.FromResult<IGuildChannel>(GetCachedChannel(id));
public override Task<IReadOnlyCollection<IGuildChannel>> GetChannels() => Task.FromResult<IReadOnlyCollection<IGuildChannel>>(Channels);
public ICachedGuildChannel AddCachedChannel(ChannelModel model, ConcurrentHashSet<ulong> channels = null, DataStore dataStore = null)
{
var channel = ToChannel(model);
(dataStore ?? Discord.DataStore).AddChannel(channel);
(channels ?? _channels).TryAdd(model.Id);
return channel;
}
public ICachedGuildChannel GetCachedChannel(ulong id)
{
return Discord.DataStore.GetChannel(id) as ICachedGuildChannel;
}
public ICachedGuildChannel RemoveCachedChannel(ulong id, ConcurrentHashSet<ulong> channels = null, DataStore dataStore = null)
{
(channels ?? _channels).TryRemove(id);
return (dataStore ?? Discord.DataStore).RemoveChannel(id) as ICachedGuildChannel;
}

public Presence AddCachedPresence(PresenceModel model, ConcurrentDictionary<ulong, Presence> presences = null)
{
var game = model.Game != null ? new Game(model.Game) : (Game?)null;
var presence = new Presence(model.Status, game);
(presences ?? _presences)[model.User.Id] = presence;
return presence;
}
public Presence? GetCachedPresence(ulong id)
{
Presence presence;
if (_presences.TryGetValue(id, out presence))
return presence;
return null;
}
public Presence? RemoveCachedPresence(ulong id)
{
Presence presence;
if (_presences.TryRemove(id, out presence))
return presence;
return null;
}

public override Task<IGuildUser> GetUser(ulong id) => Task.FromResult<IGuildUser>(GetCachedUser(id));
public override Task<IGuildUser> GetCurrentUser()
=> Task.FromResult<IGuildUser>(CurrentUser);
public override Task<IReadOnlyCollection<IGuildUser>> GetUsers()
=> Task.FromResult<IReadOnlyCollection<IGuildUser>>(Members);
//TODO: Is there a better way of exposing pagination?
public override Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset)
=> Task.FromResult<IReadOnlyCollection<IGuildUser>>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray());
public CachedGuildUser AddCachedUser(MemberModel model, ConcurrentDictionary<ulong, CachedGuildUser> members = null, DataStore dataStore = null)
{
var user = Discord.AddCachedUser(model.User);
var member = new CachedGuildUser(this, user, model);
(members ?? _members)[user.Id] = member;
user.AddRef();
return member;
}
public CachedGuildUser GetCachedUser(ulong id)
{
CachedGuildUser member;
if (_members.TryGetValue(id, out member))
return member;
return null;
}
public CachedGuildUser RemoveCachedUser(ulong id)
{
CachedGuildUser member;
if (_members.TryRemove(id, out member))
return member;
return null;
}

new internal ICachedGuildChannel ToChannel(ChannelModel model)
{
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}");
}
}
}
}

+ 16
- 0
src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs View File

@@ -0,0 +1,16 @@
using Model = Discord.API.GuildMember;

namespace Discord
{
internal class CachedGuildUser : GuildUser, ICachedEntity<ulong>
{
public VoiceChannel VoiceChannel { get; private set; }

public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;

public CachedGuildUser(CachedGuild guild, CachedPublicUser user, Model model)
: base(guild, user, model)
{
}
}
}

+ 17
- 0
src/Discord.Net/Entities/WebSocket/CachedMessage.cs View File

@@ -0,0 +1,17 @@
using Model = Discord.API.Message;

namespace Discord
{
internal class CachedMessage : Message, ICachedEntity<ulong>
{
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public new ICachedMessageChannel Channel => base.Channel as ICachedMessageChannel;

public CachedMessage(ICachedMessageChannel channel, IUser author, Model model)
: base(channel, author, model)
{
}

public CachedMessage Clone() => MemberwiseClone() as CachedMessage;
}
}

+ 58
- 0
src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs View File

@@ -0,0 +1,58 @@
using ChannelModel = Discord.API.Channel;
using Model = Discord.API.User;

namespace Discord
{
internal class CachedPublicUser : User, ICachedEntity<ulong>
{
private int _references;

public CachedDMChannel DMChannel { get; private set; }

public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;

public CachedPublicUser(DiscordSocketClient discord, Model model)
: base(discord, model)
{
}

public CachedDMChannel SetDMChannel(ChannelModel model)
{
lock (this)
{
var channel = new CachedDMChannel(Discord, this, model);
DMChannel = channel;
return channel;
}
}
public CachedDMChannel RemoveDMChannel(ulong id)
{
lock (this)
{
var channel = DMChannel;
if (channel.Id == id)
{
DMChannel = null;
return channel;
}
return null;
}
}

public void AddRef()
{
lock (this)
_references++;
}
public void RemoveRef()
{
lock (this)
{
if (--_references == 0 && DMChannel == null)
Discord.RemoveCachedUser(Id);
}
}

public CachedPublicUser Clone() => MemberwiseClone() as CachedPublicUser;
}
}

+ 16
- 0
src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs View File

@@ -0,0 +1,16 @@
using Model = Discord.API.User;

namespace Discord
{
internal class CachedSelfUser : SelfUser, ICachedEntity<ulong>
{
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;

public CachedSelfUser(DiscordSocketClient discord, Model model)
: base(discord, model)
{
}

public CachedSelfUser Clone() => MemberwiseClone() as CachedSelfUser;
}
}

+ 73
- 0
src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs View File

@@ -0,0 +1,73 @@
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;

namespace Discord
{
internal class CachedTextChannel : TextChannel, ICachedGuildChannel, ICachedMessageChannel
{
private readonly MessageCache _messages;

public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public new CachedGuild Guild => base.Guild as CachedGuild;

public IReadOnlyCollection<IGuildUser> Members
=> Guild.Members.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray();

public CachedTextChannel(CachedGuild guild, Model model)
: base(guild, model)
{
_messages = new MessageCache(Discord, this);
}

public override Task<IGuildUser> GetUser(ulong id) => Task.FromResult(GetCachedUser(id));
public override Task<IReadOnlyCollection<IGuildUser>> GetUsers() => Task.FromResult(Members);
public override Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset)
=> Task.FromResult<IReadOnlyCollection<IGuildUser>>(Members.Skip(offset).Take(limit).ToImmutableArray());
public IGuildUser GetCachedUser(ulong id)
{
var user = Guild.GetCachedUser(id);
if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages))
return user;
return null;
}

public override async Task<IMessage> GetMessage(ulong id)
{
return await _messages.Download(id).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch)
{
return await _messages.Download(null, Direction.Before, limit).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
{
return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false);
}

public CachedMessage AddCachedMessage(IUser author, MessageModel model)
{
var msg = new CachedMessage(this, author, model);
_messages.Add(msg);
return msg;
}
public CachedMessage GetCachedMessage(ulong id)
{
return _messages.Get(id);
}
public CachedMessage RemoveCachedMessage(ulong id)
{
return _messages.Remove(id);
}

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

IReadOnlyCollection<IUser> ICachedMessageChannel.Members => Members;

IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id);
IUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id);
}
}

+ 38
- 0
src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;

namespace Discord
{
internal class CachedVoiceChannel : VoiceChannel, ICachedGuildChannel
{
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public new CachedGuild Guild => base.Guild as CachedGuild;

public IReadOnlyCollection<IGuildUser> Members
=> Guild.Members.Where(x => x.VoiceChannel.Id == Id).ToImmutableArray();

public CachedVoiceChannel(CachedGuild guild, Model model)
: base(guild, model)
{
}

public override Task<IGuildUser> GetUser(ulong id)
=> Task.FromResult(GetCachedUser(id));
public override Task<IReadOnlyCollection<IGuildUser>> GetUsers()
=> Task.FromResult(Members);
public override Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset)
=> Task.FromResult<IReadOnlyCollection<IGuildUser>>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray());
public IGuildUser GetCachedUser(ulong id)
{
var user = Guild.GetCachedUser(id);
if (user != null && user.VoiceChannel.Id == Id)
return user;
return null;
}

public CachedVoiceChannel Clone() => MemberwiseClone() as CachedVoiceChannel;
}
}

+ 6
- 0
src/Discord.Net/Entities/WebSocket/ICachedChannel.cs View File

@@ -0,0 +1,6 @@
namespace Discord
{
internal interface ICachedChannel : IChannel, ICachedEntity<ulong>
{
}
}

+ 7
- 0
src/Discord.Net/Entities/WebSocket/ICachedEntity.cs View File

@@ -0,0 +1,7 @@
namespace Discord
{
interface ICachedEntity<T> : IEntity<T>
{
DiscordSocketClient Discord { get; }
}
}

+ 6
- 0
src/Discord.Net/Entities/WebSocket/ICachedGuildChannel.cs View File

@@ -0,0 +1,6 @@
namespace Discord
{
internal interface ICachedGuildChannel : ICachedChannel, IGuildChannel
{
}
}

+ 16
- 0
src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
using MessageModel = Discord.API.Message;

namespace Discord
{
internal interface ICachedMessageChannel : ICachedChannel, IMessageChannel
{
IReadOnlyCollection<IUser> Members { get; }

CachedMessage AddCachedMessage(IUser author, MessageModel model);
new CachedMessage GetCachedMessage(ulong id);
CachedMessage RemoveCachedMessage(ulong id);

IUser GetCachedUser(ulong id);
}
}

src/Discord.Net/Entities/Users/IVoiceState.cs.old → src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old View File

@@ -3,7 +3,7 @@ using Model = Discord.API.MemberVoiceState;

namespace Discord.WebSocket
{
public class VoiceState
internal class VoiceState : IVoiceState
{
[Flags]
private enum VoiceStates : byte
@@ -22,7 +22,7 @@ namespace Discord.WebSocket
public ulong UserId { get; }

/// <summary> Gets this user's current voice channel. </summary>
public VoiceChannel VoiceChannel { get; internal set; }
public VoiceChannel VoiceChannel { get; set; }
/// <summary> Returns true if this user has marked themselves as muted. </summary>
public bool IsSelfMuted => (_voiceStates & VoiceStates.SelfMuted) != 0;
@@ -35,13 +35,13 @@ namespace Discord.WebSocket
/// <summary> Returns true if the guild is temporarily blocking audio to/from this user. </summary>
public bool IsSuppressed => (_voiceStates & VoiceStates.Suppressed) != 0;

internal VoiceState(ulong userId, Guild guild)
public VoiceState(ulong userId, Guild guild)
{
UserId = userId;
Guild = guild;
}

internal void Update(Model model)
private void Update(Model model, UpdateSource source)
{
if (model.IsMuted == true)
_voiceStates |= VoiceStates.Muted;

+ 14
- 0
src/Discord.Net/Entities/WebSocket/Presence.cs View File

@@ -0,0 +1,14 @@
namespace Discord
{
internal struct Presence : IPresence
{
public UserStatus Status { get; }
public Game? Game { get; }
public Presence(UserStatus status, Game? game)
{
Status = status;
Game = game;
}
}
}

+ 31
- 0
src/Discord.Net/Extensions/CollectionExtensions.cs View File

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

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));
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue, TSource>(this IEnumerable<TValue> query, IReadOnlyCollection<TSource> source)
=> new ConcurrentDictionaryWrapper<TValue, TSource>(source, query);
}

internal struct ConcurrentDictionaryWrapper<TValue, TSource> : IReadOnlyCollection<TValue>
{
private readonly IReadOnlyCollection<TSource> _source;
private readonly IEnumerable<TValue> _query;

public int Count => _source.Count;
public ConcurrentDictionaryWrapper(IReadOnlyCollection<TSource> source, IEnumerable<TValue> query)
{
_source = source;
_query = query;
}

public IEnumerator<TValue> GetEnumerator() => _query.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _query.GetEnumerator();
}
}

+ 14
- 0
src/Discord.Net/Extensions/DiscordClientExtensions.cs View File

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

namespace Discord.Extensions
{
public static class DiscordClientExtensions
{
public static async Task<IVoiceRegion> GetOptimalVoiceRegion(this DiscordClient discord)
{
var regions = await discord.GetVoiceRegions().ConfigureAwait(false);
return regions.FirstOrDefault(x => x.IsOptimal);
}
}
}

src/Discord.Net/EventExtensions.cs → src/Discord.Net/Extensions/EventExtensions.cs View File

@@ -1,7 +1,7 @@
using System;
using System.Threading.Tasks;

namespace Discord
namespace Discord.Extensions
{
internal static class EventExtensions
{

+ 12
- 0
src/Discord.Net/Extensions/GuildExtensions.cs View File

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

namespace Discord.Extensions
{
public static class GuildExtensions
{
public static async Task<ITextChannel> GetTextChannel(this IGuild guild, ulong id)
=> await guild.GetChannel(id).ConfigureAwait(false) as ITextChannel;
public static async Task<IVoiceChannel> GetVoiceChannel(this IGuild guild, ulong id)
=> await guild.GetChannel(id).ConfigureAwait(false) as IVoiceChannel;
}
}

+ 6
- 11
src/Discord.Net/IDiscordClient.cs View File

@@ -1,6 +1,4 @@
using Discord.API;
using Discord.Net.Queue;
using Discord.WebSocket.Data;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
@@ -14,10 +12,7 @@ namespace Discord
ConnectionState ConnectionState { get; }

DiscordApiClient ApiClient { get; }
IRequestQueue RequestQueue { get; }
IDataStore DataStore { get; }

Task Login(string email, string password);
Task Login(TokenType tokenType, string token, bool validateToken = true);
Task Logout();

@@ -25,12 +20,12 @@ namespace Discord
Task Disconnect();

Task<IChannel> GetChannel(ulong id);
Task<IEnumerable<IDMChannel>> GetDMChannels();
Task<IReadOnlyCollection<IDMChannel>> GetDMChannels();

Task<IEnumerable<IConnection>> GetConnections();
Task<IReadOnlyCollection<IConnection>> GetConnections();

Task<IGuild> GetGuild(ulong id);
Task<IEnumerable<IUserGuild>> GetGuilds();
Task<IReadOnlyCollection<IUserGuild>> GetGuilds();
Task<IGuild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null);
Task<IInvite> GetInvite(string inviteIdOrXkcd);
@@ -38,9 +33,9 @@ namespace Discord
Task<IUser> GetUser(ulong id);
Task<IUser> GetUser(string username, ushort discriminator);
Task<ISelfUser> GetCurrentUser();
Task<IEnumerable<IUser>> QueryUsers(string query, int limit);
Task<IReadOnlyCollection<IUser>> QueryUsers(string query, int limit);

Task<IEnumerable<IVoiceRegion>> GetVoiceRegions();
Task<IReadOnlyCollection<IVoiceRegion>> GetVoiceRegions();
Task<IVoiceRegion> GetVoiceRegion(string id);
}
}

+ 4
- 3
src/Discord.Net/Logging/LogManager.cs View File

@@ -1,4 +1,5 @@
using System;
using Discord.Extensions;
using System;
using System.Threading.Tasks;

namespace Discord.Logging
@@ -9,7 +10,7 @@ namespace Discord.Logging

public event Func<LogMessage, Task> Message;

internal LogManager(LogSeverity minSeverity)
public LogManager(LogSeverity minSeverity)
{
Level = minSeverity;
}
@@ -110,6 +111,6 @@ namespace Discord.Logging
Task ILogger.Debug(Exception ex)
=> Log(LogSeverity.Debug, "Discord", ex);

internal Logger CreateLogger(string name) => new Logger(this, name);
public Logger CreateLogger(string name) => new Logger(this, name);
}
}

+ 1
- 1
src/Discord.Net/Logging/Logger.cs View File

@@ -10,7 +10,7 @@ namespace Discord.Logging
public string Name { get; }
public LogSeverity Level => _manager.Level;

internal Logger(LogManager manager, string name)
public Logger(LogManager manager, string name)
{
_manager = manager;
Name = name;


+ 17
- 7
src/Discord.Net/Net/Converters/DiscordContractResolver.cs View File

@@ -11,7 +11,8 @@ namespace Discord.Net.Converters
public class DiscordContractResolver : DefaultContractResolver
{
private static readonly TypeInfo _ienumerable = typeof(IEnumerable<ulong[]>).GetTypeInfo();

private static readonly MethodInfo _shouldSerialize = typeof(DiscordContractResolver).GetTypeInfo().GetDeclaredMethod("ShouldSerialize");
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
@@ -54,12 +55,15 @@ namespace Discord.Net.Converters
converter = ImageConverter.Instance;
else if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>))
{
var lambda = (Func<object, bool>)propInfo.GetMethod.CreateDelegate(typeof(Func<object, bool>));
/*var parentArg = Expression.Parameter(typeof(object));
var optional = Expression.Property(Expression.Convert(parentArg, property.DeclaringType), member as PropertyInfo);
var isSpecified = Expression.Property(optional, OptionalConverter.IsSpecifiedProperty);
var lambda = Expression.Lambda<Func<object, bool>>(isSpecified, parentArg).Compile();*/
property.ShouldSerialize = x => lambda(x);
var typeInput = propInfo.DeclaringType;
var typeOutput = propInfo.PropertyType;

var getter = typeof(Func<,>).MakeGenericType(typeInput, typeOutput);
var getterDelegate = propInfo.GetMethod.CreateDelegate(getter);
var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, typeOutput);
var shouldSerializeDelegate = (Func<object, Delegate, bool>)shouldSerialize.CreateDelegate(typeof(Func<object, Delegate, bool>));
property.ShouldSerialize = x => shouldSerializeDelegate(x, getterDelegate);
converter = OptionalConverter.Instance;
}
}
@@ -73,5 +77,11 @@ namespace Discord.Net.Converters

return property;
}

private static bool ShouldSerialize<TOwner, TValue>(object owner, Delegate getter)
where TValue : IOptional
{
return (getter as Func<TOwner, TValue>)((TOwner)owner).IsSpecified;
}
}
}

+ 1
- 4
src/Discord.Net/Net/Converters/OptionalConverter.cs View File

@@ -1,14 +1,11 @@
using Discord.API;
using Newtonsoft.Json;
using Newtonsoft.Json;
using System;
using System.Reflection;

namespace Discord.Net.Converters
{
public class OptionalConverter : JsonConverter
{
public static readonly OptionalConverter Instance = new OptionalConverter();
internal static readonly PropertyInfo IsSpecifiedProperty = typeof(IOptional).GetTypeInfo().GetDeclaredProperty(nameof(IOptional.IsSpecified));

public override bool CanConvert(Type objectType) => true;
public override bool CanRead => false;


+ 4
- 2
src/Discord.Net/Net/HttpException.cs View File

@@ -6,11 +6,13 @@ namespace Discord.Net
public class HttpException : Exception
{
public HttpStatusCode StatusCode { get; }
public string Reason { get; }

public HttpException(HttpStatusCode statusCode)
: base($"The server responded with error {(int)statusCode} ({statusCode})")
public HttpException(HttpStatusCode statusCode, string reason = null)
: base($"The server responded with error {(int)statusCode} ({statusCode}){(reason != null ? $": \"{reason}\"" : "")}")
{
StatusCode = statusCode;
Reason = reason;
}
}
}

+ 16
- 0
src/Discord.Net/Net/Queue/BucketDefinition.cs View File

@@ -0,0 +1,16 @@
namespace Discord.Net.Queue
{
internal struct BucketDefinition
{
public int WindowCount { get; }
public int WindowSeconds { get; }
public GlobalBucket? Parent { get; }

public BucketDefinition(int windowCount, int windowSeconds, GlobalBucket? parent = null)
{
WindowCount = windowCount;
WindowSeconds = windowSeconds;
Parent = parent;
}
}
}

+ 1
- 1
src/Discord.Net/Net/Queue/BucketGroup.cs View File

@@ -1,6 +1,6 @@
namespace Discord.Net.Queue
{
internal enum BucketGroup
public enum BucketGroup
{
Global,
Guild


+ 2
- 3
src/Discord.Net/Net/Queue/GlobalBucket.cs View File

@@ -2,11 +2,10 @@
{
public enum GlobalBucket
{
General,
Login,
GeneralRest,
DirectMessage,
SendEditMessage,
Gateway,
GeneralGateway,
UpdateStatus
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save