Browse Source

Started major API refactor

tags/docs-0.9
RogueException 9 years ago
parent
commit
dd6ceb8469
31 changed files with 838 additions and 1279 deletions
  1. +1
    -5
      src/Discord.Net.Commands/CommandsPlugin.Events.cs
  2. +3
    -3
      src/Discord.Net.Commands/CommandsPlugin.cs
  3. +9
    -12
      src/Discord.Net.Net45/Discord.Net.csproj
  4. +2
    -2
      src/Discord.Net/DiscordAPIClient.cs
  5. +8
    -37
      src/Discord.Net/DiscordClient.Bans.cs
  6. +37
    -54
      src/Discord.Net/DiscordClient.Channels.cs
  7. +20
    -38
      src/Discord.Net/DiscordClient.Invites.cs
  8. +37
    -53
      src/Discord.Net/DiscordClient.Members.cs
  9. +69
    -112
      src/Discord.Net/DiscordClient.Messages.cs
  10. +14
    -59
      src/Discord.Net/DiscordClient.Permissions.cs
  11. +14
    -35
      src/Discord.Net/DiscordClient.Roles.cs
  12. +8
    -57
      src/Discord.Net/DiscordClient.Users.cs
  13. +4
    -9
      src/Discord.Net/DiscordClient.Voice.cs
  14. +60
    -60
      src/Discord.Net/DiscordClient.cs
  15. +5
    -5
      src/Discord.Net/DiscordWSClient.Voice.cs
  16. +15
    -23
      src/Discord.Net/DiscordWSClient.cs
  17. +1
    -1
      src/Discord.Net/Helpers/AsyncCollection.cs
  18. +40
    -13
      src/Discord.Net/Helpers/Mention.cs
  19. +0
    -48
      src/Discord.Net/Helpers/MentionHelper.cs
  20. +0
    -55
      src/Discord.Net/Helpers/Shared/CollectionHelper.cs
  21. +0
    -0
      src/Discord.Net/Helpers/TaskHelper.cs
  22. +35
    -0
      src/Discord.Net/Models/CachedObject.cs
  23. +40
    -75
      src/Discord.Net/Models/Channel.cs
  24. +73
    -0
      src/Discord.Net/Models/GlobalUser.cs
  25. +19
    -24
      src/Discord.Net/Models/Invite.cs
  26. +0
    -279
      src/Discord.Net/Models/Member.cs
  27. +18
    -40
      src/Discord.Net/Models/Message.cs
  28. +18
    -26
      src/Discord.Net/Models/Role.cs
  29. +59
    -86
      src/Discord.Net/Models/Server.cs
  30. +222
    -62
      src/Discord.Net/Models/User.cs
  31. +7
    -6
      test/Discord.Net.Tests/Tests.cs

+ 1
- 5
src/Discord.Net.Commands/CommandsPlugin.Events.cs View File

@@ -11,14 +11,10 @@ namespace Discord.Commands
public string ArgText { get; } public string ArgText { get; }
public int? Permissions { get; } public int? Permissions { get; }
public string[] Args { get; } public string[] Args { get; }

public User User => Message.User;
public string UserId => Message.UserId;
public Member Member => Message.Member; public Member Member => Message.Member;
public Channel Channel => Message.Channel; public Channel Channel => Message.Channel;
public string ChannelId => Message.ChannelId;
public Server Server => Message.Channel.Server; public Server Server => Message.Channel.Server;
public string ServerId => Message.Channel.ServerId;


public CommandEventArgs(Message message, Command command, string commandText, string argText, int? permissions, string[] args) public CommandEventArgs(Message message, Command command, string commandText, string argText, int? permissions, string[] args)
{ {


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

@@ -8,7 +8,7 @@ namespace Discord.Commands
{ {
private readonly DiscordClient _client; private readonly DiscordClient _client;
private List<Command> _commands; private List<Command> _commands;
private Func<User, Server, int> _getPermissions;
private Func<Member, int> _getPermissions;


public IEnumerable<Command> Commands => _commands; public IEnumerable<Command> Commands => _commands;


@@ -17,7 +17,7 @@ namespace Discord.Commands
public bool RequireCommandCharInPublic { get; set; } public bool RequireCommandCharInPublic { get; set; }
public bool RequireCommandCharInPrivate { get; set; } public bool RequireCommandCharInPrivate { get; set; }


public CommandsPlugin(DiscordClient client, Func<User, Server, int> getPermissions = null)
public CommandsPlugin(DiscordClient client, Func<Member, int> getPermissions = null)
{ {
_client = client; _client = client;
_getPermissions = getPermissions; _getPermissions = getPermissions;
@@ -96,7 +96,7 @@ namespace Discord.Commands
argText = msg.Substring(args[cmd.Parts.Length].Index); argText = msg.Substring(args[cmd.Parts.Length].Index);


//Check Permissions //Check Permissions
int permissions = _getPermissions != null ? _getPermissions(e.Message.User, e.Message.Channel?.Server) : 0;
int permissions = _getPermissions != null ? _getPermissions(e.Message.Member) : 0;
var eventArgs = new CommandEventArgs(e.Message, cmd, msg, argText, permissions, newArgs); var eventArgs = new CommandEventArgs(e.Message, cmd, msg, argText, permissions, newArgs);
if (permissions < cmd.MinPerms) if (permissions < cmd.MinPerms)
{ {


+ 9
- 12
src/Discord.Net.Net45/Discord.Net.csproj View File

@@ -202,6 +202,9 @@
<Compile Include="..\Discord.Net\DiscordWSClientConfig.cs"> <Compile Include="..\Discord.Net\DiscordWSClientConfig.cs">
<Link>DiscordWSClientConfig.cs</Link> <Link>DiscordWSClientConfig.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net\Helpers\AsyncCollection.cs">
<Link>Helpers\AsyncCollection.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Helpers\EpochTime.cs"> <Compile Include="..\Discord.Net\Helpers\EpochTime.cs">
<Link>Helpers\EpochTime.cs</Link> <Link>Helpers\EpochTime.cs</Link>
</Compile> </Compile>
@@ -214,14 +217,8 @@
<Compile Include="..\Discord.Net\Helpers\Mention.cs"> <Compile Include="..\Discord.Net\Helpers\Mention.cs">
<Link>Helpers\Mention.cs</Link> <Link>Helpers\Mention.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net\Helpers\MentionHelper.cs">
<Link>Helpers\MentionHelper.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Helpers\Shared\CollectionHelper.cs">
<Link>Helpers\Shared\CollectionHelper.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Helpers\Shared\TaskHelper.cs"> <Compile Include="..\Discord.Net\Helpers\Shared\TaskHelper.cs">
<Link>Helpers\Shared\TaskHelper.cs</Link>
<Link>Helpers\TaskHelper.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net\Helpers\TimeoutException.cs"> <Compile Include="..\Discord.Net\Helpers\TimeoutException.cs">
<Link>Helpers\TimeoutException.cs</Link> <Link>Helpers\TimeoutException.cs</Link>
@@ -229,8 +226,8 @@
<Compile Include="..\Discord.Net\HttpException.cs"> <Compile Include="..\Discord.Net\HttpException.cs">
<Link>HttpException.cs</Link> <Link>HttpException.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net\Models\AsyncCollection.cs">
<Link>Models\AsyncCollection.cs</Link>
<Compile Include="..\Discord.Net\Models\CachedObject.cs">
<Link>Models\CachedObject.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net\Models\Channel.cs"> <Compile Include="..\Discord.Net\Models\Channel.cs">
<Link>Models\Channel.cs</Link> <Link>Models\Channel.cs</Link>
@@ -238,12 +235,12 @@
<Compile Include="..\Discord.Net\Models\Color.cs"> <Compile Include="..\Discord.Net\Models\Color.cs">
<Link>Models\Color.cs</Link> <Link>Models\Color.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net\Models\GlobalUser.cs">
<Link>Models\GlobalUser.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Models\Invite.cs"> <Compile Include="..\Discord.Net\Models\Invite.cs">
<Link>Models\Invite.cs</Link> <Link>Models\Invite.cs</Link>
</Compile> </Compile>
<Compile Include="..\Discord.Net\Models\Member.cs">
<Link>Models\Member.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Models\Message.cs"> <Compile Include="..\Discord.Net\Models\Message.cs">
<Link>Models\Message.cs</Link> <Link>Models\Message.cs</Link>
</Compile> </Compile>


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

@@ -110,11 +110,11 @@ namespace Discord
} }


//Invites //Invites
public Task<CreateInviteResponse> CreateInvite(string channelId, int maxAge, int maxUses, bool isTemporary, bool withXkcdPass)
public Task<CreateInviteResponse> CreateInvite(string channelId, int maxAge, int maxUses, bool tempMembership, bool hasXkcd)
{ {
if (channelId == null) throw new ArgumentNullException(nameof(channelId)); if (channelId == null) throw new ArgumentNullException(nameof(channelId));


var request = new CreateInviteRequest { MaxAge = maxAge, MaxUses = maxUses, IsTemporary = isTemporary, WithXkcdPass = withXkcdPass };
var request = new CreateInviteRequest { MaxAge = maxAge, MaxUses = maxUses, IsTemporary = tempMembership, WithXkcdPass = hasXkcd };
return _rest.Post<CreateInviteResponse>(Endpoints.ChannelInvites(channelId), request); return _rest.Post<CreateInviteResponse>(Endpoints.ChannelInvites(channelId), request);
} }
public Task<GetInviteResponse> GetInvite(string inviteIdOrXkcd) public Task<GetInviteResponse> GetInvite(string inviteIdOrXkcd)


+ 8
- 37
src/Discord.Net/DiscordClient.Bans.cs View File

@@ -7,14 +7,11 @@ namespace Discord
{ {
public class BanEventArgs : EventArgs public class BanEventArgs : EventArgs
{ {
public User User { get; }
public string UserId { get; } public string UserId { get; }
public Server Server { get; } public Server Server { get; }
public string ServerId => Server.Id;


internal BanEventArgs(User user, string userId, Server server)
internal BanEventArgs(string userId, Server server)
{ {
User = user;
UserId = userId; UserId = userId;
Server = server; Server = server;
} }
@@ -26,57 +23,31 @@ namespace Discord
private void RaiseBanAdded(string userId, Server server) private void RaiseBanAdded(string userId, Server server)
{ {
if (BanAdded != null) if (BanAdded != null)
RaiseEvent(nameof(BanAdded), () => BanAdded(this, new BanEventArgs(_users[userId], userId, server)));
RaiseEvent(nameof(BanAdded), () => BanAdded(this, new BanEventArgs(userId, server)));
} }
public event EventHandler<BanEventArgs> BanRemoved; public event EventHandler<BanEventArgs> BanRemoved;
private void RaiseBanRemoved(string userId, Server server) private void RaiseBanRemoved(string userId, Server server)
{ {
if (BanRemoved != null) if (BanRemoved != null)
RaiseEvent(nameof(BanRemoved), () => BanRemoved(this, new BanEventArgs(_users[userId], userId, server)));
RaiseEvent(nameof(BanRemoved), () => BanRemoved(this, new BanEventArgs(userId, server)));
} }


/// <summary> Bans a user from the provided server. </summary> /// <summary> Bans a user from the provided server. </summary>
public Task Ban(Member member) public Task Ban(Member member)
=> Ban(member?.ServerId, member?.UserId);
/// <summary> Bans a user from the provided server. </summary>
public Task Ban(Server server, User user)
=> Ban(server?.Id, user?.Id);
/// <summary> Bans a user from the provided server. </summary>
public Task Ban(Server server, string userId)
=> Ban(server?.Id, userId);
/// <summary> Bans a user from the provided server. </summary>
public Task Ban(string server, User user)
=> Ban(server, user?.Id);
/// <summary> Bans a user from the provided server. </summary>
public Task Ban(string serverId, string userId)
{ {
CheckReady(); CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (userId == null) throw new ArgumentNullException(nameof(userId));
if (member == null) throw new ArgumentNullException(nameof(member));


return _api.Ban(serverId, userId);
return _api.Ban(member.ServerId, member.Id);
} }


/// <summary> Unbans a user from the provided server. </summary> /// <summary> Unbans a user from the provided server. </summary>
public Task Unban(Member member)
=> Unban(member?.ServerId, member?.UserId);
/// <summary> Unbans a user from the provided server. </summary>
public Task Unban(Server server, User user)
=> Unban(server?.Id, user?.Id);
/// <summary> Unbans a user from the provided server. </summary>
public Task Unban(Server server, string userId)
=> Unban(server?.Id, userId);
/// <summary> Unbans a user from the provided server. </summary>
public Task Unban(string server, User user)
=> Unban(server, user?.Id);
/// <summary> Unbans a user from the provided server. </summary>
public async Task Unban(string serverId, string userId)
public async Task Unban(Member member)
{ {
CheckReady(); CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (userId == null) throw new ArgumentNullException(nameof(userId));
if (member == null) throw new ArgumentNullException(nameof(member));


try { await _api.Unban(serverId, userId).ConfigureAwait(false); }
try { await _api.Unban(member.ServerId, member.Id).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
} }
} }

+ 37
- 54
src/Discord.Net/DiscordClient.Channels.cs View File

@@ -18,9 +18,7 @@ namespace Discord
public class ChannelEventArgs : EventArgs public class ChannelEventArgs : EventArgs
{ {
public Channel Channel { get; } public Channel Channel { get; }
public string ChannelId => Channel.Id;
public Server Server => Channel.Server; public Server Server => Channel.Server;
public string ServerId => Channel.ServerId;


internal ChannelEventArgs(Channel channel) { Channel = channel; } internal ChannelEventArgs(Channel channel) { Channel = channel; }
} }
@@ -49,29 +47,30 @@ namespace Discord
RaiseEvent(nameof(ChannelUpdated), () => ChannelUpdated(this, new ChannelEventArgs(channel))); RaiseEvent(nameof(ChannelUpdated), () => ChannelUpdated(this, new ChannelEventArgs(channel)));
} }



/// <summary> Returns the channel with the specified id, or null if none was found. </summary> /// <summary> Returns the channel with the specified id, or null if none was found. </summary>
public Channel GetChannel(string id) => _channels[id];
/// <summary> Returns all channels with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and #Name. Search is case-insensitive. </remarks>
public IEnumerable<Channel> FindChannels(Server server, string name, string type = null) => FindChannels(server?.Id, name, type);
public Channel GetChannel(string id)
{
if (id == null) throw new ArgumentNullException(nameof(id));
return _channels[id];
}
/// <summary> Returns all channels with the specified server and name. </summary> /// <summary> Returns all channels with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and #Name. Search is case-insensitive. </remarks> /// <remarks> Name formats supported: Name and #Name. Search is case-insensitive. </remarks>
public IEnumerable<Channel> FindChannels(string serverId, string name, string type = null)
public IEnumerable<Channel> FindChannels(Server server, string name, string type = null)
{ {
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (server == null) throw new ArgumentNullException(nameof(server));


IEnumerable<Channel> result; IEnumerable<Channel> result;
if (name.StartsWith("#")) if (name.StartsWith("#"))
{ {
string name2 = name.Substring(1); string name2 = name.Substring(1);
result = _channels.Where(x => x.ServerId == serverId &&
result = _channels.Where(x => x.Server == server &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) ||
string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)); string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
} }
else else
{ {
result = _channels.Where(x => x.ServerId == serverId &&
result = _channels.Where(x => x.Server == server &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
} }


@@ -82,55 +81,44 @@ namespace Discord
} }


/// <summary> Creates a new channel with the provided name and type (see ChannelTypes). </summary> /// <summary> Creates a new channel with the provided name and type (see ChannelTypes). </summary>
public Task<Channel> CreateChannel(Server server, string name, string type = ChannelTypes.Text)
=> CreateChannel(server?.Id, name, type);
/// <summary> Creates a new channel with the provided name and type (see ChannelTypes). </summary>
public async Task<Channel> CreateChannel(string serverId, string name, string type = ChannelTypes.Text)
public async Task<Channel> CreateChannel(Server server, string name, string type = ChannelTypes.Text)
{ {
CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name)); if (name == null) throw new ArgumentNullException(nameof(name));
if (type == null) throw new ArgumentNullException(nameof(type)); if (type == null) throw new ArgumentNullException(nameof(type));
CheckReady();


var response = await _api.CreateChannel(serverId, name, type).ConfigureAwait(false);
var response = await _api.CreateChannel(server.Id, name, type).ConfigureAwait(false);
var channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id); var channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id);
channel.Update(response); channel.Update(response);
return channel; return channel;
} }

/// <summary> Returns the private channel with the provided user, creating one if it does not currently exist. </summary>
public Task<Channel> CreatePMChannel(string userId) => CreatePMChannel(_users[userId], userId);
/// <summary> Returns the private channel with the provided user, creating one if it does not currently exist. </summary> /// <summary> Returns the private channel with the provided user, creating one if it does not currently exist. </summary>
public Task<Channel> CreatePMChannel(User user) => CreatePMChannel(user, user?.Id);
/// <summary> Returns the private channel with the provided user, creating one if it does not currently exist. </summary>
public Task<Channel> CreatePMChannel(Member member) => CreatePMChannel(member.User, member.UserId);
private async Task<Channel> CreatePMChannel(User user, string userId)
public async Task<Channel> CreatePMChannel(Member member)
{ {
if (member == null) throw new ArgumentNullException(nameof(member));
CheckReady(); CheckReady();
if (userId == null) throw new ArgumentNullException(nameof(userId));


Channel channel = null; Channel channel = null;
if (user != null)
channel = user.PrivateChannel;
if (member != null)
channel = member.GlobalUser.PrivateChannel;
if (channel == null) if (channel == null)
{ {
var response = await _api.CreatePMChannel(CurrentUserId, userId).ConfigureAwait(false);
user = _users.GetOrAdd(response.Recipient?.Id);
user.Update(response.Recipient);
var response = await _api.CreatePMChannel(_userId, member.Id).ConfigureAwait(false);
var recipient = _members.GetOrAdd(response.Recipient?.Id, _servers.PMServer.Id);
recipient.Update(response.Recipient);
channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id); channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id);
channel.Update(response); channel.Update(response);
} }
return channel; return channel;
} }

/// <summary> Edits the provided channel, changing only non-null attributes. </summary>
public Task EditChannel(string channelId, string name = null, string topic = null, int? position = null)
=> EditChannel(_channels[channelId], name: name, topic: topic, position: position);
/// <summary> Edits the provided channel, changing only non-null attributes. </summary> /// <summary> Edits the provided channel, changing only non-null attributes. </summary>
public async Task EditChannel(Channel channel, string name = null, string topic = null, int? position = null) public async Task EditChannel(Channel channel, string name = null, string topic = null, int? position = null)
{ {
CheckReady();
if (channel == null) throw new ArgumentNullException(nameof(channel)); if (channel == null) throw new ArgumentNullException(nameof(channel));
CheckReady();


await _api.EditChannel(channel.Id, name: name, topic: topic); await _api.EditChannel(channel.Id, name: name, topic: topic);


@@ -155,34 +143,29 @@ namespace Discord
channels[i] = channels[i - 1]; channels[i] = channels[i - 1];
channels[newPos] = channel; channels[newPos] = channel;
} }
await _api.ReorderChannels(channel.ServerId, channels.Skip(minPos).Select(x => x.Id), minPos);
Channel after = minPos > 0 ? channels.Skip(minPos - 1).FirstOrDefault() : null;
await ReorderChannels(channel.Server, channels.Skip(minPos), after);
} }
} }

public Task ReorderChannels(Server server, IEnumerable<object> channels, int startPos = 0)
=> ReorderChannels(server.Id, channels, startPos);
public Task ReorderChannels(string serverId, IEnumerable<object> channels, int startPos = 0)
/// <summary> Reorders the provided channels in the server's channel list and places them after a certain channel. </summary>
public Task ReorderChannels(Server server, IEnumerable<Channel> channels, Channel after = null)
{ {
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (server == null) throw new ArgumentNullException(nameof(server));
if (channels == null) throw new ArgumentNullException(nameof(channels)); if (channels == null) throw new ArgumentNullException(nameof(channels));
if (startPos < 0) throw new ArgumentOutOfRangeException(nameof(startPos), "startPos must be a positive integer.");

var channelIds = CollectionHelper.FlattenChannels(channels);
return _api.ReorderChannels(serverId, channelIds, startPos);
return _api.ReorderChannels(server.Id, channels.Select(x => x.Id), after.Position);
} }
/// <summary> Destroys the provided channel. </summary> /// <summary> Destroys the provided channel. </summary>
public Task<Channel> DestroyChannel(Channel channel)
=> DestroyChannel(channel?.Id);
/// <summary> Destroys the provided channel. </summary>
public async Task<Channel> DestroyChannel(string channelId)
public async Task<Channel> DestroyChannel(Channel channel)
{ {
CheckReady(); CheckReady();
if (channelId == null) throw new ArgumentNullException(nameof(channelId));
if (channel == null) throw new ArgumentNullException(nameof(channel));


try { await _api.DestroyChannel(channelId).ConfigureAwait(false); }
try { await _api.DestroyChannel(channel.Id).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
return _channels.TryRemove(channelId);
return _channels.TryRemove(channel.Id);
} }
} }
} }

+ 20
- 38
src/Discord.Net/DiscordClient.Invites.cs View File

@@ -1,4 +1,3 @@
using Discord.Net;
using System; using System;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -14,6 +13,14 @@ namespace Discord
CheckReady(); CheckReady();
if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd)); if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd));


//Remove trailing slash
if (inviteIdOrXkcd.Length > 0 && inviteIdOrXkcd[inviteIdOrXkcd.Length - 1] == '/')
inviteIdOrXkcd = inviteIdOrXkcd.Substring(0, inviteIdOrXkcd.Length - 1);
//Remove leading URL
int index = inviteIdOrXkcd.LastIndexOf('/');
if (index >= 0)
inviteIdOrXkcd = inviteIdOrXkcd.Substring(index + 1);

var response = await _api.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); var response = await _api.GetInvite(inviteIdOrXkcd).ConfigureAwait(false);
var invite = new Invite(this, response.Code, response.XkcdPass, response.Guild.Id); var invite = new Invite(this, response.Code, response.XkcdPass, response.Guild.Id);
invite.Update(response); invite.Update(response);
@@ -26,44 +33,36 @@ namespace Discord
/// <param name="hasXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param> /// <param name="hasXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param>
/// <param name="maxUses"> The max amount of times this invite may be used. Set to 0 to have unlimited uses. </param> /// <param name="maxUses"> The max amount of times this invite may be used. Set to 0 to have unlimited uses. </param>
public Task<Invite> CreateInvite(Server server, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false) public Task<Invite> CreateInvite(Server server, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false)
=> CreateInvite(server?.DefaultChannelId, maxAge, maxUses, tempMembership, hasXkcd);
/// <summary> Creates a new invite to the provided channel. </summary>
/// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param>
/// <param name="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param>
/// <param name="hasXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param>
/// <param name="maxUses"> The max amount of times this invite may be used. Set to 0 to have unlimited uses. </param>
public Task<Invite> CreateInvite(Channel channel, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false)
=> CreateInvite(channel?.Id, maxAge, maxUses, tempMembership, hasXkcd);
{
if (server == null) throw new ArgumentNullException(nameof(server));
return CreateInvite(server.DefaultChannel, maxAge, maxUses, tempMembership, hasXkcd);
}
/// <summary> Creates a new invite to the provided channel. </summary> /// <summary> Creates a new invite to the provided channel. </summary>
/// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param> /// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param>
/// <param name="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param> /// <param name="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param>
/// <param name="hasXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param> /// <param name="hasXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param>
/// <param name="maxUses"> The max amount of times this invite may be used. Set to 0 to have unlimited uses. </param> /// <param name="maxUses"> The max amount of times this invite may be used. Set to 0 to have unlimited uses. </param>
public async Task<Invite> CreateInvite(string serverOrChannelId, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false)
public async Task<Invite> CreateInvite(Channel channel, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false)
{ {
CheckReady();
if (serverOrChannelId == null) throw new ArgumentNullException(nameof(serverOrChannelId));
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (maxAge <= 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); if (maxAge <= 0) throw new ArgumentOutOfRangeException(nameof(maxAge));
if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses));
CheckReady();


var response = await _api.CreateInvite(serverOrChannelId, maxAge, maxUses, tempMembership, hasXkcd).ConfigureAwait(false);
var response = await _api.CreateInvite(channel.Id, maxAge: maxAge, maxUses: maxUses,
tempMembership: tempMembership, hasXkcd: hasXkcd).ConfigureAwait(false);
var invite = new Invite(this, response.Code, response.XkcdPass, response.Guild.Id); var invite = new Invite(this, response.Code, response.XkcdPass, response.Guild.Id);
invite.Update(response); invite.Update(response);
return invite; return invite;
} }


/// <summary> Deletes the provided invite. </summary> /// <summary> Deletes the provided invite. </summary>
public async Task DestroyInvite(string inviteId)
public async Task DestroyInvite(Invite invite)
{ {
CheckReady(); CheckReady();
if (inviteId == null) throw new ArgumentNullException(nameof(inviteId));
if (invite == null) throw new ArgumentNullException(nameof(invite));


try
{
//Check if this is a human-readable link and get its ID
var response = await _api.GetInvite(inviteId).ConfigureAwait(false);
await _api.DeleteInvite(response.Code).ConfigureAwait(false);
}
try { await _api.DeleteInvite(invite.Id).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
} }
@@ -75,22 +74,5 @@ namespace Discord


return _api.AcceptInvite(invite.Id); return _api.AcceptInvite(invite.Id);
} }
/// <summary> Accepts the provided invite. </summary>
public async Task AcceptInvite(string inviteId)
{
CheckReady();
if (inviteId == null) throw new ArgumentNullException(nameof(inviteId));

//Remove trailing slash and any non-code url parts
if (inviteId.Length > 0 && inviteId[inviteId.Length - 1] == '/')
inviteId = inviteId.Substring(0, inviteId.Length - 1);
int index = inviteId.LastIndexOf('/');
if (index >= 0)
inviteId = inviteId.Substring(index + 1);

//Check if this is a human-readable link and get its ID
var invite = await GetInvite(inviteId).ConfigureAwait(false);
await _api.AcceptInvite(invite.Id).ConfigureAwait(false);
}
} }
} }

+ 37
- 53
src/Discord.Net/DiscordClient.Members.cs View File

@@ -23,8 +23,7 @@ namespace Discord
public class MemberEventArgs : EventArgs public class MemberEventArgs : EventArgs
{ {
public Member Member { get; } public Member Member { get; }
public User User => Member.User;
public string UserId => Member.UserId;
public string UserId => Member.Id;
public Server Server => Member.Server; public Server Server => Member.Server;
public string ServerId => Member.ServerId; public string ServerId => Member.ServerId;


@@ -66,80 +65,65 @@ namespace Discord
if (UserIsSpeaking != null) if (UserIsSpeaking != null)
RaiseEvent(nameof(UserIsSpeaking), () => UserIsSpeaking(this, new MemberIsSpeakingEventArgs(member, channel, isSpeaking))); RaiseEvent(nameof(UserIsSpeaking), () => UserIsSpeaking(this, new MemberIsSpeakingEventArgs(member, channel, isSpeaking)));
} }

private Member _currentUser;

internal Members Members => _members; internal Members Members => _members;
private readonly Members _members; private readonly Members _members;
/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public Member GetMember(Server server, User user) => _members[user?.Id, server?.Id];
/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public Member GetMember(Server server, string userId) => _members[userId, server?.Id];
/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public Member GetMember(string serverId, User user) => _members[user?.Id, serverId];

/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary> /// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public Member GetMember(string serverId, string userId) => _members[userId, serverId];
/// <summary> Returns the user with the specified name and discriminator, along withtheir server-specific data, or null if they couldn't be found. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public Member GetMember(Server server, string username, string discriminator)
=> GetMember(server?.Id, username, discriminator);
public Member GetMember(Server server, string userId)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (userId == null) throw new ArgumentNullException(nameof(userId));
CheckReady();

return _members[userId, server.Id];
}
/// <summary> Returns the user with the specified name and discriminator, along withtheir server-specific data, or null if they couldn't be found. </summary> /// <summary> Returns the user with the specified name and discriminator, along withtheir server-specific data, or null if they couldn't be found. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks> /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public Member GetMember(string serverId, string username, string discriminator)
public Member GetMember(Server server, string username, string discriminator)
{ {
User user = GetUser(username, discriminator);
return _members[user?.Id, serverId];
if (server == null) throw new ArgumentNullException(nameof(server));
if (username == null) throw new ArgumentNullException(nameof(username));
if (discriminator == null) throw new ArgumentNullException(nameof(discriminator));
CheckReady();

Member member = FindMembers(server, username, discriminator, true).FirstOrDefault();
return _members[member?.Id, server.Id];
} }


/// <summary> Returns all users in with the specified server and name, along with their server-specific data. </summary> /// <summary> Returns all users in with the specified server and name, along with their server-specific data. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks> /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks>
public IEnumerable<Member> FindMembers(string serverId, string name) => FindMembers(_servers[serverId], name);
/// <summary> Returns all users in with the specified server and name, along with their server-specific data. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks>
public IEnumerable<Member> FindMembers(Server server, string name)
public IEnumerable<Member> FindMembers(Server server, string name, string discriminator = null, bool exactMatch = false)
{ {
if (server == null) throw new ArgumentNullException(nameof(server)); if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name)); if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();


if (name.StartsWith("@"))
IEnumerable<Member> query;
if (!exactMatch && name.StartsWith("@"))
{ {
string name2 = name.Substring(1); string name2 = name.Substring(1);
return server.Members.Where(x =>
{
var user = x.User;
if (user == null)
return false;
return string.Equals(user.Name, name, StringComparison.OrdinalIgnoreCase) ||
string.Equals(user.Name, name2, StringComparison.OrdinalIgnoreCase);
});
query = server.Members.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) ||
string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
} }
else else
{ {
return server.Members.Where(x =>
{
var user = x.User;
if (user == null)
return false;
return string.Equals(x.User.Name, name, StringComparison.OrdinalIgnoreCase);
});
query = server.Members.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
} }
}
if (discriminator != null)
query = query.Where(x => x.Discriminator == discriminator);
return query;
}


public Task EditMember(Member member, bool? mute = null, bool? deaf = null, IEnumerable<object> roles = null)
=> EditMember(member?.ServerId, member?.UserId, mute, deaf, roles);
public Task EditMember(Server server, User user, bool? mute = null, bool? deaf = null, IEnumerable<object> roles = null)
=> EditMember(server?.Id, user?.Id, mute, deaf, roles);
public Task EditMember(Server server, string userId, bool? mute = null, bool? deaf = null, IEnumerable<string> roles = null)
=> EditMember(server?.Id, userId, mute, deaf, roles);
public Task EditMember(string serverId, User user, bool? mute = null, bool? deaf = null, IEnumerable<object> roles = null)
=> EditMember(serverId, user?.Id, mute, deaf, roles);
public Task EditMember(string serverId, string userId, bool? mute = null, bool? deaf = null, IEnumerable<object> roles = null)
public Task EditMember(Member member, bool? mute = null, bool? deaf = null, IEnumerable<Role> roles = null)
{ {
if (member == null) throw new ArgumentNullException(nameof(member));
CheckReady(); CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (userId == null) throw new ArgumentNullException(nameof(userId));

var newRoles = CollectionHelper.FlattenRoles(roles);
return _api.EditMember(serverId, userId, mute: mute, deaf: deaf, roles: newRoles);
return _api.EditMember(member.ServerId, member.Id, mute: mute, deaf: deaf, roles: roles.Select(x => x.Id));
} }
} }
} }

+ 69
- 112
src/Discord.Net/DiscordClient.Messages.cs View File

@@ -20,14 +20,9 @@ namespace Discord
public class MessageEventArgs : EventArgs public class MessageEventArgs : EventArgs
{ {
public Message Message { get; } public Message Message { get; }
public string MessageId => Message.Id;
public Member Member => Message.Member; public Member Member => Message.Member;
public Channel Channel => Message.Channel; public Channel Channel => Message.Channel;
public string ChannelId => Message.ChannelId;
public Server Server => Message.Server; public Server Server => Message.Server;
public string ServerId => Message.ServerId;
public User User => Member.User;
public string UserId => Message.UserId;


internal MessageEventArgs(Message msg) { Message = msg; } internal MessageEventArgs(Message msg) { Message = msg; }
} }
@@ -74,165 +69,127 @@ namespace Discord
public Message GetMessage(string id) => _messages[id]; public Message GetMessage(string id) => _messages[id];


/// <summary> Sends a message to the provided channel. To include a mention, see the Mention static helper class. </summary> /// <summary> Sends a message to the provided channel. To include a mention, see the Mention static helper class. </summary>
public Task<Message[]> SendMessage(Channel channel, string text)
=> SendMessage(channel, text, MentionHelper.GetUserIds(text), false);
/// <summary> Sends a message to the provided channel. To include a mention, see the Mention static helper class. </summary>
public Task<Message[]> SendMessage(string channelId, string text)
=> SendMessage(_channels[channelId], text, MentionHelper.GetUserIds(text), false);
private async Task<Message[]> SendMessage(Channel channel, string text, IEnumerable<object> mentionedUsers = null, bool isTextToSpeech = false)
public Task<Message> SendMessage(Channel channel, string text)
{ {
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (text == null) throw new ArgumentNullException(nameof(text));
CheckReady(); CheckReady();

return SendMessage(channel, text, false);
}
/// <summary> Sends a text-to-speech message to the provided channel. To include a mention, see the Mention static helper class. </summary>
public Task<Message> SendTTSMessage(Channel channel, string text)
{
if (channel == null) throw new ArgumentNullException(nameof(channel)); if (channel == null) throw new ArgumentNullException(nameof(channel));
if (text == null) throw new ArgumentNullException(nameof(text)); if (text == null) throw new ArgumentNullException(nameof(text));
var mentionedUserIds = CollectionHelper.FlattenUsers(mentionedUsers);
CheckReady();


int blockCount = (int)Math.Ceiling(text.Length / (double)MaxMessageSize);
Message[] result = new Message[blockCount];
for (int i = 0; i < blockCount; i++)
return SendMessage(channel, text, false);
}
/// <summary> Sends a private message to the provided user. </summary>
public async Task<Message> SendPrivateMessage(Member member, string text)
{
if (member == null) throw new ArgumentNullException(nameof(member));
if (text == null) throw new ArgumentNullException(nameof(text));
CheckReady();

var channel = await CreatePMChannel(member).ConfigureAwait(false);
return await SendMessage(channel, text).ConfigureAwait(false);
}
private async Task<Message> SendMessage(Channel channel, string text, bool isTextToSpeech)
{
Message msg;
var userIds = !channel.IsPrivate ? Mention.GetUserIds(text) : new string[0];
if (Config.UseMessageQueue)
{ {
int index = i * MaxMessageSize;
string blockText = text.Substring(index, Math.Min(2000, text.Length - index));
var nonce = GenerateNonce(); var nonce = GenerateNonce();
if (Config.UseMessageQueue)
{
var msg = _messages.GetOrAdd("nonce_" + nonce, channel.Id, CurrentUserId);
var currentUser = msg.User;
msg.Update(new MessageInfo
{
Content = blockText,
Timestamp = DateTime.UtcNow,
Author = new UserReference { Avatar = currentUser.AvatarId, Discriminator = currentUser.Discriminator, Id = CurrentUserId, Username = currentUser.Name },
ChannelId = channel.Id,
IsTextToSpeech = isTextToSpeech
});
msg.IsQueued = true;
msg.Nonce = nonce;
result[i] = msg;
_pendingMessages.Enqueue(msg);
}
else
msg = _messages.GetOrAdd("nonce_" + nonce, channel.Id, _userId);
var currentUser = msg.Member;
msg.Update(new MessageInfo
{ {
var model = await _api.SendMessage(channel.Id, blockText, mentionedUserIds, nonce, isTextToSpeech).ConfigureAwait(false);
var msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id);
msg.Update(model);
RaiseMessageSent(msg);
result[i] = msg;
}
await Task.Delay(1000).ConfigureAwait(false);
Content = text,
Timestamp = DateTime.UtcNow,
Author = new UserReference { Avatar = currentUser.AvatarId, Discriminator = currentUser.Discriminator, Id = _userId, Username = currentUser.Name },
ChannelId = channel.Id,
IsTextToSpeech = isTextToSpeech
});
msg.Mentions = userIds.Select(x => _members[x, channel.Server.Id]).Where(x => x != null).ToArray();
msg.IsQueued = true;
msg.Nonce = nonce;
_pendingMessages.Enqueue(msg);
} }
return result;
else
{
var model = await _api.SendMessage(channel.Id, text, userIds, null, isTextToSpeech).ConfigureAwait(false);
msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id);
msg.Update(model);
RaiseMessageSent(msg);
}
return msg;
} }


/// <summary> Sends a private message to the provided user. </summary>
public Task<Message[]> SendPrivateMessage(Member member, string text)
=> SendPrivateMessage(member?.UserId, text);
/// <summary> Sends a private message to the provided user. </summary>
public Task<Message[]> SendPrivateMessage(User user, string text)
=> SendPrivateMessage(user?.Id, text);
/// <summary> Sends a private message to the provided user. </summary>
public async Task<Message[]> SendPrivateMessage(string userId, string text)
{
var channel = await CreatePMChannel(userId).ConfigureAwait(false);
return await SendMessage(channel, text, new string[0]).ConfigureAwait(false);
}


/// <summary> Sends a file to the provided channel. </summary> /// <summary> Sends a file to the provided channel. </summary>
public Task SendFile(Channel channel, string filePath) public Task SendFile(Channel channel, string filePath)
=> SendFile(channel?.Id, filePath);
/// <summary> Sends a file to the provided channel. </summary>
public Task SendFile(string channelId, string filePath)
{ {
CheckReady();
if (channelId == null) throw new ArgumentNullException(nameof(channelId));
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (filePath == null) throw new ArgumentNullException(nameof(filePath)); if (filePath == null) throw new ArgumentNullException(nameof(filePath));
CheckReady();


return _api.SendFile(channelId, filePath);
return _api.SendFile(channel.Id, filePath);
} }


/// <summary> Edits the provided message, changing only non-null attributes. </summary> /// <summary> Edits the provided message, changing only non-null attributes. </summary>
/// <remarks> While not required, it is recommended to include a mention reference in the text (see Mention.User). </remarks> /// <remarks> While not required, it is recommended to include a mention reference in the text (see Mention.User). </remarks>
public Task EditMessage(Message message, string text = null, IEnumerable<object> mentionedUsers = null)
=> EditMessage(message?.ChannelId, message?.Id, text, mentionedUsers);
/// <summary> Edits the provided message, changing only non-null attributes. </summary>
/// <remarks> While not required, it is recommended to include a mention reference in the text (see Mention.User). </remarks>
public Task EditMessage(Channel channel, string messageId, string text = null, IEnumerable<object> mentionedUsers = null)
=> EditMessage(channel?.Id, messageId, text, mentionedUsers);
/// <summary> Edits the provided message, changing only non-null attributes. </summary>
/// <remarks> While not required, it is recommended to include a mention reference in the text (see Mention.User). </remarks>
public async Task EditMessage(string channelId, string messageId, string text = null, IEnumerable<object> mentionedUsers = null)
public async Task EditMessage(Message message, string text)
{ {
if (message == null) throw new ArgumentNullException(nameof(message));
CheckReady(); CheckReady();
if (channelId == null) throw new ArgumentNullException(nameof(channelId));
if (messageId == null) throw new ArgumentNullException(nameof(messageId));
var mentionedUserIds = CollectionHelper.FlattenUsers(mentionedUsers);


if (text != null && text.Length > MaxMessageSize) if (text != null && text.Length > MaxMessageSize)
text = text.Substring(0, MaxMessageSize); text = text.Substring(0, MaxMessageSize);


var model = await _api.EditMessage(messageId, channelId, text, mentionedUserIds).ConfigureAwait(false);
var msg = _messages[messageId];
if (msg != null)
msg.Update(model);
var model = await _api.EditMessage(message.Id, message.Channel.Id, text, Mention.GetUserIds(text)).ConfigureAwait(false);
message.Update(model);
} }


/// <summary> Deletes the provided message. </summary> /// <summary> Deletes the provided message. </summary>
public Task DeleteMessage(Message msg)
=> DeleteMessage(msg?.ChannelId, msg?.Id);
/// <summary> Deletes the provided message. </summary>
public async Task DeleteMessage(string channelId, string msgId)
public async Task DeleteMessage(Message message)
{ {
if (message == null) throw new ArgumentNullException(nameof(message));
CheckReady(); CheckReady();
if (channelId == null) throw new ArgumentNullException(nameof(channelId));
if (msgId == null) throw new ArgumentNullException(nameof(msgId));


try try
{ {
await _api.DeleteMessage(msgId, channelId).ConfigureAwait(false);
_messages.TryRemove(msgId);
await _api.DeleteMessage(message.Id, message.Channel.Id).ConfigureAwait(false);
_messages.TryRemove(message.Id);
} }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
} }
public async Task DeleteMessages(IEnumerable<Message> msgs)
{
CheckReady();
if (msgs == null) throw new ArgumentNullException(nameof(msgs));

foreach (var msg in msgs)
{
try
{
await _api.DeleteMessage(msg.Id, msg.ChannelId).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}
}
public async Task DeleteMessages(string channelId, IEnumerable<string> msgIds)
public async Task DeleteMessages(IEnumerable<Message> messages)
{ {
if (messages == null) throw new ArgumentNullException(nameof(messages));
CheckReady(); CheckReady();
if (msgIds == null) throw new ArgumentNullException(nameof(msgIds));


foreach (var msgId in msgIds)
foreach (var message in messages)
{ {
try try
{ {
await _api.DeleteMessage(msgId, channelId).ConfigureAwait(false);
await _api.DeleteMessage(message.Id, message.Channel.Id).ConfigureAwait(false);
_messages.TryRemove(message.Id);
} }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
} }
} }


/// <summary> Downloads last count messages from the server, starting at beforeMessageId if it's provided. </summary> /// <summary> Downloads last count messages from the server, starting at beforeMessageId if it's provided. </summary>
public Task<Message[]> DownloadMessages(Channel channel, int count, string beforeMessageId = null, bool cache = true)
=> DownloadMessages(channel.Id, count, beforeMessageId, cache);
/// <summary> Downloads last count messages from the server, starting at beforeMessageId if it's provided. </summary>
public async Task<Message[]> DownloadMessages(string channelId, int count, string beforeMessageId = null, bool cache = true)
public async Task<Message[]> DownloadMessages(Channel channel, int count, string beforeMessageId = null, bool cache = true)
{ {
CheckReady();
if (channelId == null) throw new ArgumentNullException(nameof(channelId));
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (count < 0) throw new ArgumentNullException(nameof(count)); if (count < 0) throw new ArgumentNullException(nameof(count));
if (count == 0) return new Message[0];
CheckReady();


Channel channel = _channels[channelId];
if (count == 0) return new Message[0];
if (channel != null && channel.Type == ChannelTypes.Text) if (channel != null && channel.Type == ChannelTypes.Text)
{ {
try try
@@ -283,7 +240,7 @@ namespace Discord
SendMessageResponse response = null; SendMessageResponse response = null;
try try
{ {
response = await _api.SendMessage(msg.ChannelId, msg.RawText, msg.MentionIds, msg.Nonce, msg.IsTTS).ConfigureAwait(false);
response = await _api.SendMessage(msg.Channel.Id, msg.RawText, msg.MentionIds, msg.Nonce, msg.IsTTS).ConfigureAwait(false);
} }
catch (WebException) { break; } catch (WebException) { break; }
catch (HttpException) { hasFailed = true; } catch (HttpException) { hasFailed = true; }


+ 14
- 59
src/Discord.Net/DiscordClient.Permissions.cs View File

@@ -8,47 +8,13 @@ namespace Discord
public partial class DiscordClient public partial class DiscordClient
{ {
public Task SetChannelUserPermissions(Channel channel, Member member, ChannelPermissions allow = null, ChannelPermissions deny = null) public Task SetChannelUserPermissions(Channel channel, Member member, ChannelPermissions allow = null, ChannelPermissions deny = null)
=> SetChannelPermissions(channel, member?.UserId, PermissionTarget.Member, allow, deny);
public Task SetChannelUserPermissions(string channelId, Member member, ChannelPermissions allow = null, ChannelPermissions deny = null)
=> SetChannelPermissions(_channels[channelId], member?.UserId, PermissionTarget.Member, allow, deny);
public Task SetChannelUserPermissions(Channel channel, User user, ChannelPermissions allow = null, ChannelPermissions deny = null)
=> SetChannelPermissions(channel, user?.Id, PermissionTarget.Member, allow, deny);
public Task SetChannelUserPermissions(string channelId, User user, ChannelPermissions allow = null, ChannelPermissions deny = null)
=> SetChannelPermissions(_channels[channelId], user?.Id, PermissionTarget.Member, allow, deny);
public Task SetChannelUserPermissions(Channel channel, string userId, ChannelPermissions allow = null, ChannelPermissions deny = null)
=> SetChannelPermissions(channel, userId, PermissionTarget.Member, allow, deny);
public Task SetChannelUserPermissions(string channelId, string userId, ChannelPermissions allow = null, ChannelPermissions deny = null)
=> SetChannelPermissions(_channels[channelId], userId, PermissionTarget.Member, allow, deny);
=> SetChannelPermissions(channel, member?.Id, PermissionTarget.Member, allow, deny);
public Task SetChannelUserPermissions(Channel channel, Member member, DualChannelPermissions permissions = null) public Task SetChannelUserPermissions(Channel channel, Member member, DualChannelPermissions permissions = null)
=> SetChannelPermissions(channel, member?.UserId, PermissionTarget.Member, permissions?.Allow, permissions?.Deny);
public Task SetChannelUserPermissions(string channelId, Member member, DualChannelPermissions permissions = null)
=> SetChannelPermissions(_channels[channelId], member?.UserId, PermissionTarget.Member, permissions?.Allow, permissions?.Deny);
public Task SetChannelUserPermissions(Channel channel, User user, DualChannelPermissions permissions = null)
=> SetChannelPermissions(channel, user?.Id, PermissionTarget.Member, permissions?.Allow, permissions?.Deny);
public Task SetChannelUserPermissions(string channelId, User user, DualChannelPermissions permissions = null)
=> SetChannelPermissions(_channels[channelId], user?.Id, PermissionTarget.Member, permissions?.Allow, permissions?.Deny);
public Task SetChannelUserPermissions(Channel channel, string userId, DualChannelPermissions permissions = null)
=> SetChannelPermissions(channel, userId, PermissionTarget.Member, permissions?.Allow, permissions?.Deny);
public Task SetChannelUserPermissions(string channelId, string userId, DualChannelPermissions permissions = null)
=> SetChannelPermissions(_channels[channelId], userId, PermissionTarget.Member, permissions?.Allow, permissions?.Deny);

=> SetChannelPermissions(channel, member?.Id, PermissionTarget.Member, permissions?.Allow, permissions?.Deny);
public Task SetChannelRolePermissions(Channel channel, Role role, ChannelPermissions allow = null, ChannelPermissions deny = null) public Task SetChannelRolePermissions(Channel channel, Role role, ChannelPermissions allow = null, ChannelPermissions deny = null)
=> SetChannelPermissions(channel, role?.Id, PermissionTarget.Role, allow, deny); => SetChannelPermissions(channel, role?.Id, PermissionTarget.Role, allow, deny);
public Task SetChannelRolePermissions(string channelId, Role role, ChannelPermissions allow = null, ChannelPermissions deny = null)
=> SetChannelPermissions(_channels[channelId], role?.Id, PermissionTarget.Role, allow, deny);
public Task SetChannelRolePermissions(Channel channel, string userId, ChannelPermissions allow = null, ChannelPermissions deny = null)
=> SetChannelPermissions(channel, userId, PermissionTarget.Role, allow, deny);
public Task SetChannelRolePermissions(string channelId, string userId, ChannelPermissions allow = null, ChannelPermissions deny = null)
=> SetChannelPermissions(_channels[channelId], userId, PermissionTarget.Role, allow, deny);
public Task SetChannelRolePermissions(Channel channel, Role role, DualChannelPermissions permissions = null) public Task SetChannelRolePermissions(Channel channel, Role role, DualChannelPermissions permissions = null)
=> SetChannelPermissions(channel, role?.Id, PermissionTarget.Role, permissions?.Allow, permissions?.Deny); => SetChannelPermissions(channel, role?.Id, PermissionTarget.Role, permissions?.Allow, permissions?.Deny);
public Task SetChannelRolePermissions(string channelId, Role role, DualChannelPermissions permissions = null)
=> SetChannelPermissions(_channels[channelId], role?.Id, PermissionTarget.Role, permissions?.Allow, permissions?.Deny);
public Task SetChannelRolePermissions(Channel channel, string userId, DualChannelPermissions permissions = null)
=> SetChannelPermissions(channel, userId, PermissionTarget.Role, permissions?.Allow, permissions?.Deny);
public Task SetChannelRolePermissions(string channelId, string userId, DualChannelPermissions permissions = null)
=> SetChannelPermissions(_channels[channelId], userId, PermissionTarget.Role, permissions?.Allow, permissions?.Deny);

private async Task SetChannelPermissions(Channel channel, string targetId, string targetType, ChannelPermissions allow = null, ChannelPermissions deny = null) private async Task SetChannelPermissions(Channel channel, string targetId, string targetType, ChannelPermissions allow = null, ChannelPermissions deny = null)
{ {
CheckReady(); CheckReady();
@@ -103,34 +69,23 @@ namespace Discord
} }


public Task RemoveChannelUserPermissions(Channel channel, Member member) public Task RemoveChannelUserPermissions(Channel channel, Member member)
=> RemoveChannelPermissions(channel, member?.UserId, PermissionTarget.Member);
public Task RemoveChannelUserPermissions(string channelId, Member member)
=> RemoveChannelPermissions(_channels[channelId], member?.UserId, PermissionTarget.Member);
public Task RemoveChannelUserPermissions(Channel channel, User user)
=> RemoveChannelPermissions(channel, user?.Id, PermissionTarget.Member);
public Task RemoveChannelUserPermissions(string channelId, User user)
=> RemoveChannelPermissions(_channels[channelId], user?.Id, PermissionTarget.Member);
public Task RemoveChannelUserPermissions(Channel channel, string userId)
=> RemoveChannelPermissions(channel, userId, PermissionTarget.Member);
public Task RemoveChannelUserPermissions(string channelId, string userId)
=> RemoveChannelPermissions(_channels[channelId], userId, PermissionTarget.Member);
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (member == null) throw new ArgumentNullException(nameof(member));
CheckReady();


return RemoveChannelPermissions(channel, member?.Id, PermissionTarget.Member);
}
public Task RemoveChannelRolePermissions(Channel channel, Role role) public Task RemoveChannelRolePermissions(Channel channel, Role role)
=> RemoveChannelPermissions(channel, role?.Id, PermissionTarget.Role);
public Task RemoveChannelRolePermissions(string channelId, Role role)
=> RemoveChannelPermissions(_channels[channelId], role?.Id, PermissionTarget.Role);
public Task RemoveChannelRolePermissions(Channel channel, string roleId)
=> RemoveChannelPermissions(channel, roleId, PermissionTarget.Role);
public Task RemoveChannelRolePermissions(string channelId, string roleId)
=> RemoveChannelPermissions(_channels[channelId], roleId, PermissionTarget.Role);

private async Task RemoveChannelPermissions(Channel channel, string userOrRoleId, string idType)
{ {
CheckReady();
if (channel == null) throw new ArgumentNullException(nameof(channel)); if (channel == null) throw new ArgumentNullException(nameof(channel));
if (userOrRoleId == null) throw new ArgumentNullException(nameof(userOrRoleId));
if (idType == null) throw new ArgumentNullException(nameof(idType));
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();


return RemoveChannelPermissions(channel, role?.Id, PermissionTarget.Role);
}
private async Task RemoveChannelPermissions(Channel channel, string userOrRoleId, string idType)
{
try try
{ {
var perms = channel.PermissionOverwrites.Where(x => x.TargetType != idType || x.TargetId != userOrRoleId).FirstOrDefault(); var perms = channel.PermissionOverwrites.Where(x => x.TargetType != idType || x.TargetId != userOrRoleId).FirstOrDefault();


+ 14
- 35
src/Discord.Net/DiscordClient.Roles.cs View File

@@ -33,9 +33,7 @@ namespace Discord
public class RoleEventArgs : EventArgs public class RoleEventArgs : EventArgs
{ {
public Role Role { get; } public Role Role { get; }
public string RoleId => Role.Id;
public Server Server => Role.Server; public Server Server => Role.Server;
public string ServerId => Role.ServerId;


internal RoleEventArgs(Role role) { Role = role; } internal RoleEventArgs(Role role) { Role = role; }
} }
@@ -68,23 +66,20 @@ namespace Discord
public Role GetRole(string id) => _roles[id]; public Role GetRole(string id) => _roles[id];
/// <summary> Returns all roles with the specified server and name. </summary> /// <summary> Returns all roles with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks> /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public IEnumerable<Role> FindRoles(Server server, string name) => FindRoles(server?.Id, name);
/// <summary> Returns all roles with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public IEnumerable<Role> FindRoles(string serverId, string name)
public IEnumerable<Role> FindRoles(Server server, string name)
{ {
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name)); if (name == null) throw new ArgumentNullException(nameof(name));


if (name.StartsWith("@")) if (name.StartsWith("@"))
{ {
string name2 = name.Substring(1); string name2 = name.Substring(1);
return _roles.Where(x => x.ServerId == serverId &&
return _roles.Where(x => x.Server.Id == server.Id &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)); string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
} }
else else
{ {
return _roles.Where(x => x.ServerId == serverId &&
return _roles.Where(x => x.Server.Id == server.Id &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
} }
} }
@@ -106,16 +101,14 @@ namespace Discord


return role; return role;
} }

public Task EditRole(string roleId, string name = null, ServerPermissions permissions = null, Color color = null, bool? hoist = null, int? position = null)
=> EditRole(_roles[roleId], name: name, permissions: permissions, color: color, hoist: hoist, position: position);
public async Task EditRole(Role role, string name = null, ServerPermissions permissions = null, Color color = null, bool? hoist = null, int? position = null) public async Task EditRole(Role role, string name = null, ServerPermissions permissions = null, Color color = null, bool? hoist = null, int? position = null)
{ {
CheckReady();
if (role == null) throw new ArgumentNullException(nameof(role)); if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();


//TODO: check this null workaround later, should be fixed on Discord's end soon //TODO: check this null workaround later, should be fixed on Discord's end soon
var response = await _api.EditRole(role.ServerId, role.Id,
var response = await _api.EditRole(role.Server.Id, role.Id,
name: name ?? role.Name, name: name ?? role.Name,
permissions: permissions?.RawValue ?? role.Permissions.RawValue, permissions: permissions?.RawValue ?? role.Permissions.RawValue,
color: color?.RawValue, color: color?.RawValue,
@@ -142,40 +135,26 @@ namespace Discord
roles[i] = roles[i - 1]; roles[i] = roles[i - 1];
roles[newPos] = role; roles[newPos] = role;
} }
await _api.ReorderRoles(role.ServerId, roles.Skip(minPos).Select(x => x.Id), minPos);
await _api.ReorderRoles(role.Server.Id, roles.Skip(minPos).Select(x => x.Id), minPos);
} }
} }


public Task DeleteRole(Role role) public Task DeleteRole(Role role)
=> DeleteRole(role?.ServerId, role?.Id);
public Task DeleteRole(string serverId, string roleId)
{ {
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady(); CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (roleId == null) throw new ArgumentNullException(nameof(roleId));


return _api.DeleteRole(serverId, roleId);
return _api.DeleteRole(role.Server.Id, role.Id);
} }


public Task ReorderRoles(Server server, IEnumerable<object> roles, int startPos = 0)
=> ReorderChannels(server.Id, roles, startPos);
public Task ReorderRoles(string serverId, IEnumerable<object> roles, int startPos = 0)
public Task ReorderRoles(Server server, IEnumerable<Role> roles, int startPos = 0)
{ {
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (server == null) throw new ArgumentNullException(nameof(server));
if (roles == null) throw new ArgumentNullException(nameof(roles)); if (roles == null) throw new ArgumentNullException(nameof(roles));
if (startPos < 0) throw new ArgumentOutOfRangeException(nameof(startPos), "startPos must be a positive integer."); if (startPos < 0) throw new ArgumentOutOfRangeException(nameof(startPos), "startPos must be a positive integer.");
CheckReady();


var roleIds = roles.Select(x =>
{
if (x is string)
return x as string;
else if (x is Role)
return (x as Role).Id;
else
throw new ArgumentException("Channels must be a collection of string or Role.", nameof(roles));
});

return _api.ReorderRoles(serverId, roleIds, startPos);
return _api.ReorderRoles(server.Id, roles.Select(x => x.Id), startPos);
} }
} }
} }

+ 8
- 57
src/Discord.Net/DiscordClient.Users.cs View File

@@ -6,20 +6,12 @@ using System.Threading.Tasks;


namespace Discord namespace Discord
{ {
internal sealed class Users : AsyncCollection<User>
internal sealed class Users : AsyncCollection<GlobalUser>
{ {
public Users(DiscordClient client, object writerLock) public Users(DiscordClient client, object writerLock)
: base(client, writerLock, x => x.OnCached(), x => x.OnUncached()) { } : base(client, writerLock, x => x.OnCached(), x => x.OnUncached()) { }


public User GetOrAdd(string id) => GetOrAdd(id, () => new User(_client, id));
}

public sealed class UserEventArgs : EventArgs
{
public User User { get; }
public string UserId => User.Id;

internal UserEventArgs(User user) { User = user; }
public GlobalUser GetOrAdd(string id) => GetOrAdd(id, () => new GlobalUser(_client, id));
} }


public partial class DiscordClient public partial class DiscordClient
@@ -36,11 +28,11 @@ namespace Discord
if (UserRemoved != null) if (UserRemoved != null)
RaiseEvent(nameof(UserRemoved), () => UserRemoved(this, new MemberEventArgs(member))); RaiseEvent(nameof(UserRemoved), () => UserRemoved(this, new MemberEventArgs(member)));
} }
public event EventHandler<UserEventArgs> UserUpdated;
private void RaiseUserUpdated(User user)
public event EventHandler ProfileUpdated;
private void RaiseProfileUpdated(GlobalUser user)
{ {
if (UserUpdated != null)
RaiseEvent(nameof(UserUpdated), () => UserUpdated(this, new UserEventArgs(user)));
if (ProfileUpdated != null)
RaiseEvent(nameof(ProfileUpdated), () => ProfileUpdated(this, EventArgs.Empty));
} }
public event EventHandler<MemberEventArgs> MemberUpdated; public event EventHandler<MemberEventArgs> MemberUpdated;
private void RaiseMemberUpdated(Member member) private void RaiseMemberUpdated(Member member)
@@ -65,55 +57,14 @@ namespace Discord
internal Users Users => _users; internal Users Users => _users;
private readonly Users _users; private readonly Users _users;


/// <summary> Returns the current logged-in user. </summary>
public User CurrentUser => _currentUser;
private User _currentUser;

/// <summary> Returns the user with the specified id, or null if none was found. </summary>
public User GetUser(string id) => _users[id];
/// <summary> Returns the user with the specified name and discriminator, or null if none was found. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public User GetUser(string username, string discriminator)
{
if (username == null) throw new ArgumentNullException(nameof(username));
if (discriminator == null) throw new ArgumentNullException(nameof(discriminator));

if (username.StartsWith("@"))
username = username.Substring(1);

return _users.Where(x =>
string.Equals(x.Name, username, StringComparison.OrdinalIgnoreCase) &&
x.Discriminator == discriminator
)
.FirstOrDefault();
}

/// <summary> Returns all users with the specified name across all servers. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public IEnumerable<User> FindUsers(string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));

if (name.StartsWith("@"))
{
string name2 = name.Substring(1);
return _users.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
}
else
{
return _users.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}
}

public Task<EditUserResponse> EditProfile(string currentPassword = "", public Task<EditUserResponse> EditProfile(string currentPassword = "",
string username = null, string email = null, string password = null, string username = null, string email = null, string password = null,
ImageType avatarType = ImageType.Png, byte[] avatar = null) ImageType avatarType = ImageType.Png, byte[] avatar = null)
{ {
if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword));


return _api.EditUser(currentPassword: currentPassword, username: username ?? _currentUser?.Name, email: email ?? _currentUser?.Email, password: password,
return _api.EditUser(currentPassword: currentPassword,
username: username ?? _currentUser?.Name, email: email ?? _currentUser?.GlobalUser.Email, password: password,
avatarType: avatarType, avatar: avatar); avatarType: avatarType, avatar: avatar);
} }




+ 4
- 9
src/Discord.Net/DiscordClient.Voice.cs View File

@@ -47,18 +47,13 @@ namespace Discord
return client; return client;
} }


public Task<IDiscordVoiceClient> JoinVoiceServer(Channel channel)
=> JoinVoiceServer(channel?.ServerId, channel?.Id);
public Task<IDiscordVoiceClient> JoinVoiceServer(Server server, string channelId)
=> JoinVoiceServer(server?.Id, channelId);
public async Task<IDiscordVoiceClient> JoinVoiceServer(string serverId, string channelId)
public async Task<IDiscordVoiceClient> JoinVoiceServer(Channel channel)
{ {
if (channel == null) throw new ArgumentNullException(nameof(channel));
CheckReady(); //checkVoice is done inside the voice client CheckReady(); //checkVoice is done inside the voice client
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (channelId == null) throw new ArgumentNullException(nameof(channelId));


var client = await CreateVoiceClient(serverId).ConfigureAwait(false);
await client.JoinChannel(channelId).ConfigureAwait(false);
var client = await CreateVoiceClient(channel.Server.Id).ConfigureAwait(false);
await client.JoinChannel(channel.Id).ConfigureAwait(false);
return client; return client;
} }




+ 60
- 60
src/Discord.Net/DiscordClient.cs View File

@@ -10,9 +10,9 @@ using System.Threading.Tasks;
namespace Discord namespace Discord
{ {
/// <summary> Provides a connection to the DiscordApp service. </summary> /// <summary> Provides a connection to the DiscordApp service. </summary>
public partial class DiscordClient : DiscordWSClient
public sealed partial class DiscordClient : DiscordWSClient
{ {
protected readonly DiscordAPIClient _api;
private readonly DiscordAPIClient _api;
private readonly Random _rand; private readonly Random _rand;
private readonly JsonSerializer _serializer; private readonly JsonSerializer _serializer;
private readonly ConcurrentQueue<Message> _pendingMessages; private readonly ConcurrentQueue<Message> _pendingMessages;
@@ -24,6 +24,9 @@ namespace Discord


public new DiscordClientConfig Config => _config as DiscordClientConfig; public new DiscordClientConfig Config => _config as DiscordClientConfig;


/// <summary> Returns the current logged-in user. </summary>
public Member CurrentUser => _currentUser;

/// <summary> Initializes a new instance of the DiscordClient class. </summary> /// <summary> Initializes a new instance of the DiscordClient class. </summary>
public DiscordClient(DiscordClientConfig config = null) public DiscordClient(DiscordClientConfig config = null)
: base(config ?? new DiscordClientConfig()) : base(config ?? new DiscordClientConfig())
@@ -69,71 +72,66 @@ namespace Discord
} }
} }
}; };

bool showIDs = _config.LogLevel > LogMessageSeverity.Debug; //Hide this for now
if (_config.LogLevel >= LogMessageSeverity.Info) if (_config.LogLevel >= LogMessageSeverity.Info)
{ {
ServerCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, ServerCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Created Server: {e.Server?.Name ?? e.ServerId}");
$"Created Server: {e.Server?.Name}");
ServerDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, ServerDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Destroyed Server: {e.Server?.Name ?? e.ServerId}");
$"Destroyed Server: {e.Server?.Name}");
ServerUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, ServerUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Updated Server: {e.Server?.Name ?? e.ServerId}");
$"Updated Server: {e.Server?.Name}");
ServerAvailable += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, ServerAvailable += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Server Available: {e.Server?.Name ?? e.ServerId}");
$"Server Available: {e.Server?.Name}");
ServerUnavailable += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, ServerUnavailable += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Server Unavailable: {e.Server?.Name ?? e.ServerId}");
$"Server Unavailable: {e.Server?.Name}");
ChannelCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, ChannelCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Created Channel: {e.Server?.Name ?? e.ServerId}/{e.Channel?.Name ?? e.ChannelId}");
$"Created Channel: {e.Server?.Name}/{e.Channel?.Name}");
ChannelDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, ChannelDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Destroyed Channel: {e.Server?.Name ?? e.ServerId}/{e.Channel?.Name ?? e.ChannelId}");
$"Destroyed Channel: {e.Server?.Name}/{e.Channel?.Name}");
ChannelUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, ChannelUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Updated Channel: {e.Server?.Name ?? e.ServerId}/{e.Channel?.Name ?? e.ChannelId}");
$"Updated Channel: {e.Server?.Name}/{e.Channel?.Name}");
MessageCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, MessageCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Created Message: {e.Server?.Name ?? e.ServerId}/{e.Channel?.Name ?? e.ChannelId}/{e.MessageId}");
$"Created Message: {e.Server?.Name}/{e.Channel?.Name}/{e.Message?.Id}");
MessageDeleted += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, MessageDeleted += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Deleted Message: {e.Server?.Name ?? e.ServerId}/{e.Channel?.Name ?? e.ChannelId}/{e.MessageId}");
$"Deleted Message: {e.Server?.Name}/{e.Channel?.Name}/{e.Message?.Id}");
MessageUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, MessageUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Updated Message: {e.Server?.Name ?? e.ServerId}/{e.Channel?.Name ?? e.ChannelId}/{e.MessageId}");
$"Updated Message: {e.Server?.Name}/{e.Channel?.Name}/{e.Message?.Id}");
RoleCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, RoleCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Created Role: {e.Server?.Name ?? e.ServerId}/{e.Role?.Name ?? e.RoleId}");
$"Created Role: {e.Server?.Name}/{e.Role?.Name}");
RoleUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, RoleUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Updated Role: {e.Server?.Name ?? e.ServerId}/{e.Role?.Name ?? e.RoleId}");
$"Updated Role: {e.Server?.Name}/{e.Role?.Name}");
RoleDeleted += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, RoleDeleted += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Deleted Role: {e.Server?.Name ?? e.ServerId}/{e.Role?.Name ?? e.RoleId}");
$"Deleted Role: {e.Server?.Name}/{e.Role?.Name}");
BanAdded += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, BanAdded += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Added Ban: {e.Server?.Name ?? e.ServerId}/{e.User?.Name ?? e.UserId}");
$"Added Ban: {e.Server?.Name }/{e.UserId}");
BanRemoved += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, BanRemoved += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Removed Ban: {e.Server?.Name ?? e.ServerId}/{e.User?.Name ?? e.UserId}");
$"Removed Ban: {e.Server?.Name}/{e.UserId}");
UserAdded += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, UserAdded += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Added Member: {e.Server?.Name ?? e.ServerId}/{e.User?.Name ?? e.UserId}");
$"Added Member: {e.Server?.Name}/{e.UserId}");
UserRemoved += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, UserRemoved += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Removed Member: {e.Server?.Name ?? e.ServerId}/{e.User?.Name ?? e.UserId}");
$"Removed Member: {e.Server?.Name}/{e.UserId}");
MemberUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, MemberUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Updated Member: {e.Server?.Name ?? e.ServerId}/{e.User?.Name ?? e.UserId}");
$"Updated Member: {e.Server?.Name}/{e.UserId}");
UserVoiceStateUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, UserVoiceStateUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Updated Member (Voice State): {e.Server?.Name ?? e.ServerId}/{e.User?.Name ?? e.UserId}");
UserUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
$"Updated User: {e.User.Name}");
$"Updated Member (Voice State): {e.Server?.Name}/{e.UserId}");
ProfileUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client,
"Updated Profile");
} }
if (_config.LogLevel >= LogMessageSeverity.Verbose) if (_config.LogLevel >= LogMessageSeverity.Verbose)
{ {
UserIsTyping += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, UserIsTyping += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client,
$"Updated User (Is Typing): {e.Server?.Name ?? e.ServerId}/{e.Channel?.Name ?? e.ChannelId}/{e.User?.Name ?? e.UserId}" +
(showIDs ? $" ({e.ServerId}/{e.ChannelId}/{e.UserId})" : ""));
$"Updated User (Is Typing): {e.Server?.Name}/{e.Channel?.Name}/{e.Member?.Name}");
MessageReadRemotely += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, MessageReadRemotely += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client,
$"Read Message (Remotely): {e.Server?.Name ?? e.ServerId}/{e.Channel?.Name ?? e.ChannelId}/{e.MessageId}" +
(showIDs ? $" ({e.ServerId}/{e.ChannelId}/{e.MessageId})" : ""));
$"Read Message (Remotely): {e.Server?.Name}/{e.Channel?.Name}/{e.Message?.Id}");
MessageSent += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, MessageSent += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client,
$"Sent Message: {e.Server?.Name ?? e.ServerId}/{e.Channel?.Name ?? e.ChannelId}/{e.MessageId}" +
(showIDs ? $" ({e.ServerId}/{e.ChannelId}/{e.MessageId})" : ""));
$"Sent Message: {e.Server?.Name}/{e.Channel?.Name}/{e.Message?.Id}");
UserPresenceUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client, UserPresenceUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Client,
$"Updated Member (Presence): {e.Server?.Name ?? e.ServerId}/{e.User?.Name ?? e.UserId}" +
(showIDs ? $" ({e.ServerId}/{e.UserId})" : ""));
$"Updated Member (Presence): {e.Server?.Name}/{e.Member?.Name}");
_api.RestClient.OnRequest += (s, e) => _api.RestClient.OnRequest += (s, e) =>
{ {
if (e.Payload != null)
if (e.Payload != null)
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})"); RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})");
else else
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms"); RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Rest, $"{e.Method.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms");
@@ -141,18 +139,18 @@ namespace Discord
} }
if (_config.LogLevel >= LogMessageSeverity.Debug) if (_config.LogLevel >= LogMessageSeverity.Debug)
{ {
_channels.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Channel {e.Item.ServerId}/{e.Item.Id}");
_channels.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Channel {e.Item.ServerId}/{e.Item.Id}");
_channels.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Channel {e.Item.Server.Id}/{e.Item.Id}");
_channels.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Channel {e.Item.Server.Id}/{e.Item.Id}");
_channels.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Channels"); _channels.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Channels");
_members.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Member {e.Item.ServerId}/{e.Item.UserId}");
_members.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Member {e.Item.ServerId}/{e.Item.UserId}");
_members.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Member {e.Item.Server.Id}/{e.Item.Id}");
_members.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Member {e.Item.Server.Id}/{e.Item.Id}");
_members.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Members"); _members.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Members");
_messages.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Message {e.Item.ServerId}/{e.Item.ChannelId}/{e.Item.Id}");
_messages.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Message {e.Item.ServerId}/{e.Item.ChannelId}/{e.Item.Id}");
_messages.ItemRemapped += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Remapped Message {e.Item.ServerId}/{e.Item.ChannelId}/[{e.OldId} -> {e.NewId}]");
_messages.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Message {e.Item.Server.Id}/{e.Item.Channel.Id}/{e.Item.Id}");
_messages.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Message {e.Item.Server.Id}/{e.Item.Channel.Id}/{e.Item.Id}");
_messages.ItemRemapped += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Remapped Message {e.Item.Server.Id}/{e.Item.Channel.Id}/[{e.OldId} -> {e.NewId}]");
_messages.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Messages"); _messages.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Messages");
_roles.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Role {e.Item.ServerId}/{e.Item.Id}");
_roles.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Role {e.Item.ServerId}/{e.Item.Id}");
_roles.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Role {e.Item.Server.Id}/{e.Item.Id}");
_roles.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Role {e.Item.Server.Id}/{e.Item.Id}");
_roles.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Roles"); _roles.Cleared += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Cleared Roles");
_servers.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Server {e.Item.Id}"); _servers.ItemCreated += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Created Server {e.Item.Id}");
_servers.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Server {e.Item.Id}"); _servers.ItemDestroyed += (s, e) => RaiseOnLog(LogMessageSeverity.Debug, LogMessageSource.Cache, $"Destroyed Server {e.Item.Id}");
@@ -295,7 +293,7 @@ namespace Discord
case "READY": //Resync case "READY": //Resync
{ {
var data = e.Payload.ToObject<ReadyEvent>(_serializer); var data = e.Payload.ToObject<ReadyEvent>(_serializer);
_currentUser = _users.GetOrAdd(data.User.Id);
_currentUser = _members.GetOrAdd(data.User.Id, _servers.PMServer.Id);
_currentUser.Update(data.User); _currentUser.Update(data.User);
foreach (var model in data.Guilds) foreach (var model in data.Guilds)
{ {
@@ -307,7 +305,7 @@ namespace Discord
} }
foreach (var model in data.PrivateChannels) foreach (var model in data.PrivateChannels)
{ {
var user = _users.GetOrAdd(model.Recipient.Id);
var user = _members.GetOrAdd(model.Recipient.Id, _servers.PMServer.Id);
user.Update(model.Recipient); user.Update(model.Recipient);
var channel = _channels.GetOrAdd(model.Id, null, user.Id); var channel = _channels.GetOrAdd(model.Id, null, user.Id);
channel.Update(model); channel.Update(model);
@@ -362,9 +360,9 @@ namespace Discord
Channel channel; Channel channel;
if (data.IsPrivate) if (data.IsPrivate)
{ {
var user = _users.GetOrAdd(data.Recipient.Id);
user.Update(data.Recipient);
channel = _channels.GetOrAdd(data.Id, null, user.Id);
var member = _members.GetOrAdd(data.Recipient.Id, _servers.PMServer.Id);
member.Update(data.Recipient);
channel = _channels.GetOrAdd(data.Id, null, member.Id);
} }
else else
channel = _channels.GetOrAdd(data.Id, data.GuildId, null); channel = _channels.GetOrAdd(data.Id, data.GuildId, null);
@@ -396,8 +394,6 @@ namespace Discord
case "GUILD_MEMBER_ADD": case "GUILD_MEMBER_ADD":
{ {
var data = e.Payload.ToObject<MemberAddEvent>(_serializer); var data = e.Payload.ToObject<MemberAddEvent>(_serializer);
var user = _users.GetOrAdd(data.User.Id);
user.Update(data.User);
var member = _members.GetOrAdd(data.User.Id, data.GuildId); var member = _members.GetOrAdd(data.User.Id, data.GuildId);
member.Update(data); member.Update(data);
if (Config.TrackActivity) if (Config.TrackActivity)
@@ -433,7 +429,7 @@ namespace Discord
role.Update(data.Data); role.Update(data.Data);
var server = _servers[data.GuildId]; var server = _servers[data.GuildId];
if (server != null) if (server != null)
server.AddRole(data.Data.Id);
server.AddRole(role);
RaiseRoleUpdated(role); RaiseRoleUpdated(role);
} }
break; break;
@@ -442,19 +438,23 @@ namespace Discord
var data = e.Payload.ToObject<RoleUpdateEvent>(_serializer); var data = e.Payload.ToObject<RoleUpdateEvent>(_serializer);
var role = _roles[data.Data.Id]; var role = _roles[data.Data.Id];
if (role != null) if (role != null)
{
role.Update(data.Data); role.Update(data.Data);
RaiseRoleUpdated(role);
RaiseRoleUpdated(role);
}
} }
break; break;
case "GUILD_ROLE_DELETE": case "GUILD_ROLE_DELETE":
{ {
var data = e.Payload.ToObject<RoleDeleteEvent>(_serializer); var data = e.Payload.ToObject<RoleDeleteEvent>(_serializer);
var server = _servers[data.GuildId];
if (server != null)
server.RemoveRole(data.RoleId);
var role = _roles.TryRemove(data.RoleId); var role = _roles.TryRemove(data.RoleId);
if (role != null) if (role != null)
{
RaiseRoleDeleted(role); RaiseRoleDeleted(role);
var server = _servers[data.GuildId];
if (server != null)
server.RemoveRole(role);
}
} }
break; break;


@@ -485,7 +485,7 @@ namespace Discord
var data = e.Payload.ToObject<MessageCreateEvent>(_serializer); var data = e.Payload.ToObject<MessageCreateEvent>(_serializer);
Message msg = null; Message msg = null;


bool isAuthor = data.Author.Id == CurrentUserId;
bool isAuthor = data.Author.Id == _userId;
bool hasFinishedSending = false; bool hasFinishedSending = false;
if (Config.UseMessageQueue && isAuthor && data.Nonce != null) if (Config.UseMessageQueue && isAuthor && data.Nonce != null)
{ {
@@ -566,7 +566,7 @@ namespace Discord
var channel = _channels[data.ChannelId]; var channel = _channels[data.ChannelId];
if (channel != null) if (channel != null)
{ {
var user = _members[data.UserId, channel.ServerId];
var user = _members[data.UserId, channel.Server.Id];


if (user != null) if (user != null)
{ {
@@ -577,7 +577,7 @@ namespace Discord
{ {
if (!channel.IsPrivate) if (!channel.IsPrivate)
{ {
var member = _members[data.UserId, channel.ServerId];
var member = _members[data.UserId, channel.Server.Id];
if (member != null) if (member != null)
member.UpdateActivity(); member.UpdateActivity();
} }
@@ -612,7 +612,7 @@ namespace Discord
if (user != null) if (user != null)
{ {
user.Update(data); user.Update(data);
RaiseUserUpdated(user);
RaiseProfileUpdated(user);
} }
} }
break; break;


+ 5
- 5
src/Discord.Net/DiscordWSClient.Voice.cs View File

@@ -17,7 +17,7 @@ namespace Discord
await _voiceSocket.SetChannel(_voiceServerId, channelId).ConfigureAwait(false); await _voiceSocket.SetChannel(_voiceServerId, channelId).ConfigureAwait(false);
_dataSocket.SendJoinVoice(_voiceServerId, channelId); _dataSocket.SendJoinVoice(_voiceServerId, channelId);
await _voiceSocket.WaitForConnection(_config.ConnectionTimeout);
await _voiceSocket.WaitForConnection(_config.ConnectionTimeout).ConfigureAwait(false);
} }


/// <summary> Sends a PCM frame to the voice server. Will block until space frees up in the outgoing buffer. </summary> /// <summary> Sends a PCM frame to the voice server. Will block until space frees up in the outgoing buffer. </summary>
@@ -25,12 +25,12 @@ namespace Discord
/// <param name="count">Number of bytes in this frame. </param> /// <param name="count">Number of bytes in this frame. </param>
void IDiscordVoiceClient.SendVoicePCM(byte[] data, int count) void IDiscordVoiceClient.SendVoicePCM(byte[] data, int count)
{ {
CheckReady(checkVoice: true);
if (data == null) throw new ArgumentException(nameof(data)); if (data == null) throw new ArgumentException(nameof(data));
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
if (count == 0) return;
_voiceSocket.SendPCMFrames(data, count);
CheckReady(checkVoice: true);

if (count != 0)
_voiceSocket.SendPCMFrames(data, count);
} }
/// <summary> Clears the PCM buffer. </summary> /// <summary> Clears the PCM buffer. </summary>
void IDiscordVoiceClient.ClearVoicePCM() void IDiscordVoiceClient.ClearVoicePCM()


+ 15
- 23
src/Discord.Net/DiscordWSClient.cs View File

@@ -20,27 +20,21 @@ namespace Discord
/// <summary> Provides a minimalistic websocket connection to the Discord service. </summary> /// <summary> Provides a minimalistic websocket connection to the Discord service. </summary>
public partial class DiscordWSClient public partial class DiscordWSClient
{ {
internal readonly DataWebSocket _dataSocket;
internal readonly VoiceWebSocket _voiceSocket;
protected readonly DiscordWSClientConfig _config;
protected readonly ManualResetEvent _disconnectedEvent; protected readonly ManualResetEvent _disconnectedEvent;
protected readonly ManualResetEventSlim _connectedEvent; protected readonly ManualResetEventSlim _connectedEvent;
protected readonly bool _enableVoice;
internal readonly DataWebSocket _dataSocket;
internal readonly VoiceWebSocket _voiceSocket;
protected ExceptionDispatchInfo _disconnectReason;
protected string _gateway, _token; protected string _gateway, _token;
protected string _voiceServerId;
protected string _userId, _voiceServerId;
private Task _runTask; private Task _runTask;

protected ExceptionDispatchInfo _disconnectReason;
private bool _wasDisconnectUnexpected; private bool _wasDisconnectUnexpected;


/// <summary> Returns the configuration object used to make this client. Note that this object cannot be edited directly - to change the configuration of this client, use the DiscordClient(DiscordClientConfig config) constructor. </summary>
public DiscordWSClientConfig Config => _config;
protected readonly DiscordWSClientConfig _config;
public string CurrentUserId => _userId;


/// <summary> Returns the id of the current logged-in user. </summary>
public string CurrentUserId => _currentUserId;
private string _currentUserId;
/*/// <summary> Returns the server this user is currently connected to for voice. </summary>
public string CurrentVoiceServerId => _voiceSocket.CurrentServerId;*/
/// <summary> Returns the configuration object used to make this client. Note that this object cannot be edited directly - to change the configuration of this client, use the DiscordClient(DiscordClientConfig config) constructor. </summary>
public DiscordWSClientConfig Config => _config;


/// <summary> Returns the current connection state of this client. </summary> /// <summary> Returns the current connection state of this client. </summary>
public DiscordClientState State => (DiscordClientState)_state; public DiscordClientState State => (DiscordClientState)_state;
@@ -56,15 +50,13 @@ namespace Discord
_config = config ?? new DiscordWSClientConfig(); _config = config ?? new DiscordWSClientConfig();
_config.Lock(); _config.Lock();


_enableVoice = _config.EnableVoice;

_state = (int)DiscordClientState.Disconnected; _state = (int)DiscordClientState.Disconnected;
_cancelToken = new CancellationToken(true); _cancelToken = new CancellationToken(true);
_disconnectedEvent = new ManualResetEvent(true); _disconnectedEvent = new ManualResetEvent(true);
_connectedEvent = new ManualResetEventSlim(false); _connectedEvent = new ManualResetEventSlim(false);


_dataSocket = CreateDataSocket(); _dataSocket = CreateDataSocket();
if (_enableVoice)
if (_config.EnableVoice)
_voiceSocket = CreateVoiceSocket(); _voiceSocket = CreateVoiceSocket();
} }
internal DiscordWSClient(DiscordWSClientConfig config = null, string voiceServerId = null) internal DiscordWSClient(DiscordWSClientConfig config = null, string voiceServerId = null)
@@ -247,7 +239,7 @@ namespace Discord


protected virtual async Task Cleanup() protected virtual async Task Cleanup()
{ {
if (_enableVoice)
if (_config.EnableVoice)
{ {
string voiceServerId = _voiceSocket.CurrentServerId; string voiceServerId = _voiceSocket.CurrentServerId;
if (voiceServerId != null) if (voiceServerId != null)
@@ -256,7 +248,7 @@ namespace Discord
} }
await _dataSocket.Disconnect().ConfigureAwait(false); await _dataSocket.Disconnect().ConfigureAwait(false);


_currentUserId = null;
_userId = null;
_gateway = null; _gateway = null;
_token = null; _token = null;
} }
@@ -286,7 +278,7 @@ namespace Discord
throw new InvalidOperationException("The client is connecting."); throw new InvalidOperationException("The client is connecting.");
} }
if (checkVoice && !_enableVoice)
if (checkVoice && !_config.EnableVoice)
throw new InvalidOperationException("Voice is not enabled for this client."); throw new InvalidOperationException("Voice is not enabled for this client.");
} }
protected void RaiseEvent(string name, Action action) protected void RaiseEvent(string name, Action action)
@@ -307,17 +299,17 @@ namespace Discord
switch (e.Type) switch (e.Type)
{ {
case "READY": case "READY":
_currentUserId = e.Payload["user"].Value<string>("id");
_userId = e.Payload["user"].Value<string>("id");
break; break;
case "VOICE_SERVER_UPDATE": case "VOICE_SERVER_UPDATE":
{ {
string guildId = e.Payload.Value<string>("guild_id"); string guildId = e.Payload.Value<string>("guild_id");


if (_enableVoice && guildId == _voiceSocket.CurrentServerId)
if (_config.EnableVoice && guildId == _voiceSocket.CurrentServerId)
{ {
string token = e.Payload.Value<string>("token"); string token = e.Payload.Value<string>("token");
_voiceSocket.Host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0]; _voiceSocket.Host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0];
return _voiceSocket.Login(_currentUserId, _dataSocket.SessionId, token, CancelToken);
return _voiceSocket.Login(_userId, _dataSocket.SessionId, token, CancelToken);
} }
} }
break; break;


src/Discord.Net/Models/AsyncCollection.cs → src/Discord.Net/Helpers/AsyncCollection.cs View File

@@ -7,7 +7,7 @@ using System.Linq;
namespace Discord namespace Discord
{ {
internal abstract class AsyncCollection<TValue> : IEnumerable<TValue> internal abstract class AsyncCollection<TValue> : IEnumerable<TValue>
where TValue : class
where TValue : CachedObject
{ {
private readonly object _writerLock; private readonly object _writerLock;



+ 40
- 13
src/Discord.Net/Helpers/Mention.cs View File

@@ -1,26 +1,53 @@
namespace Discord
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Discord
{ {
public static class Mention public static class Mention
{ {
/// <summary> Returns the string used to create a user mention. </summary>
public static string User(User user)
=> $"<@{user.Id}>";
private static readonly Regex _userRegex = new Regex(@"<@(\d+?)>", RegexOptions.Compiled);
private static readonly Regex _channelRegex = new Regex(@"<#(\d+?)>", RegexOptions.Compiled);
/// <summary> Returns the string used to create a user mention. </summary> /// <summary> Returns the string used to create a user mention. </summary>
public static string User(Member member) public static string User(Member member)
=> $"<@{member.UserId}>";
/// <summary> Returns the string used to create a user mention. </summary>
public static string User(string userId)
=> $"<@{userId}>";

=> $"<@{member.Id}>";
/// <summary> Returns the string used to create a channel mention. </summary> /// <summary> Returns the string used to create a channel mention. </summary>
public static string Channel(Channel channel) public static string Channel(Channel channel)
=> $"<#{channel.Id}>"; => $"<#{channel.Id}>";
/// <summary> Returns the string used to create a channel mention. </summary> /// <summary> Returns the string used to create a channel mention. </summary>
public static string Channel(string channelId)
=> $"<#{channelId}>";

/// <summary> Returns the string used to create a channel mention. </summary>
public static string Everyone() public static string Everyone()
=> $"@everyone"; => $"@everyone";

internal static string ConvertToNames(DiscordClient client, Server server, string text)
{
text = _userRegex.Replace(text, new MatchEvaluator(e =>
{
string id = e.Value.Substring(2, e.Value.Length - 3);
var user = client.Members[id, server.Id];
if (user != null)
return '@' + user.Name;
else //User not found
return e.Value;
}));
text = _channelRegex.Replace(text, new MatchEvaluator(e =>
{
string id = e.Value.Substring(2, e.Value.Length - 3);
var channel = client.Channels[id];
if (channel != null && channel.Server.Id == server.Id)
return channel.Name;
else //Channel not found
return e.Value;
}));
return text;
}

internal static IEnumerable<string> GetUserIds(string text)
{
return _userRegex.Matches(text)
.OfType<Match>()
.Select(x => x.Groups[1].Value)
.Where(x => x != null);
}
} }
} }

+ 0
- 48
src/Discord.Net/Helpers/MentionHelper.cs View File

@@ -1,48 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Discord
{
internal static class MentionHelper
{
private static readonly Regex _userRegex, _channelRegex;

static MentionHelper()
{
_userRegex = new Regex(@"<@(\d+?)>", RegexOptions.Compiled);
_channelRegex = new Regex(@"<#(\d+?)>", RegexOptions.Compiled);
}

public static string ConvertToNames(DiscordClient client, string text)
{
text = _userRegex.Replace(text, new MatchEvaluator(e =>
{
string id = e.Value.Substring(2, e.Value.Length - 3);
var user = client.Users[id];
if (user != null)
return '@' + user.Name;
else //User not found
return e.Value;
}));
text = _channelRegex.Replace(text, new MatchEvaluator(e =>
{
string id = e.Value.Substring(2, e.Value.Length - 3);
var channel = client.Channels[id];
if (channel != null)
return channel.Name;
else //Channel not found
return e.Value;
}));
return text;
}

public static IEnumerable<string> GetUserIds(string text)
{
return _userRegex.Matches(text)
.OfType<Match>()
.Select(x => x.Groups[1].Value)
.Where(x => x != null);
}
}
}

+ 0
- 55
src/Discord.Net/Helpers/Shared/CollectionHelper.cs View File

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

namespace Discord
{
internal static class CollectionHelper
{
public static IEnumerable<string> FlattenChannels(IEnumerable<object> channels)
{
if (channels == null)
return new string[0];

return channels.Select(x =>
{
if (x is string)
return x as string;
else if (x is Channel)
return (x as Channel).Id;
else
throw new ArgumentException("Collection may only contain string or Channel.", nameof(channels));
});
}
public static IEnumerable<string> FlattenUsers(IEnumerable<object> users)
{
if (users == null)
return new string[0];

return users.Select(x =>
{
if (x is string)
return x as string;
else if (x is User)
return (x as User).Id;
else
throw new ArgumentException("Collection may only contain string or User.", nameof(users));
});
}
public static IEnumerable<string> FlattenRoles(IEnumerable<object> roles)
{
if (roles == null)
return new string[0];

return roles.Select(x =>
{
if (x is string)
return x as string;
else if (x is Role)
return (x as Role).Id;
else
throw new ArgumentException("Collection may only contain string or Role.", nameof(roles));
});
}
}
}

src/Discord.Net/Helpers/Shared/TaskHelper.cs → src/Discord.Net/Helpers/TaskHelper.cs View File


+ 35
- 0
src/Discord.Net/Models/CachedObject.cs View File

@@ -0,0 +1,35 @@
namespace Discord
{
public abstract class CachedObject
{
protected readonly DiscordClient _client;
private bool _isCached;

internal CachedObject(DiscordClient client, string id)
{
_client = client;
Id = id;
}

/// <summary> Returns the unique identifier for this object. </summary>
public string Id { get; internal set; }

public override string ToString() => $"{this.GetType().Name} {Id}";

internal void Cache()
{
OnCached();
_isCached = true;
}
internal void Uncache()
{
if (_isCached)
{
OnUncached();
_isCached = false;
}
}
internal abstract void OnCached();
internal abstract void OnUncached();
}
}

+ 40
- 75
src/Discord.Net/Models/Channel.cs View File

@@ -7,7 +7,7 @@ using System.Linq;


namespace Discord namespace Discord
{ {
public sealed class Channel
public sealed class Channel : CachedObject
{ {
public sealed class PermissionOverwrite public sealed class PermissionOverwrite
{ {
@@ -21,44 +21,37 @@ namespace Discord
TargetType = targetType; TargetType = targetType;
TargetId = targetId; TargetId = targetId;
Allow = new ChannelPermissions(allow); Allow = new ChannelPermissions(allow);
Deny = new ChannelPermissions(deny);
Allow.Lock(); Allow.Lock();
Deny = new ChannelPermissions(deny);
Deny.Lock(); Deny.Lock();
} }
} }

private readonly DiscordClient _client;
private readonly ConcurrentDictionary<string, bool> _messages; private readonly ConcurrentDictionary<string, bool> _messages;
private bool _areMembersStale; private bool _areMembersStale;
private bool _hasRef;


/// <summary> Returns the unique identifier for this channel. </summary>
public string Id { get; }
private string _name;
private readonly string _serverId, _recipientId;
private Server _server;
private Member _recipient;
/// <summary> Returns the name of this channel. </summary> /// <summary> Returns the name of this channel. </summary>
public string Name { get { return !IsPrivate ? $"{_name}" : $"@{Recipient.Name}"; } internal set { _name = value; } }

public string Name { get; private set; }
/// <summary> Returns the topic associated with this channel. </summary> /// <summary> Returns the topic associated with this channel. </summary>
public string Topic { get; private set; } public string Topic { get; private set; }
/// <summary> Returns the position of this channel in the channel list for this server. </summary> /// <summary> Returns the position of this channel in the channel list for this server. </summary>
public int Position { get; private set; } public int Position { get; private set; }
/// <summary> Returns false is this is a public chat and true if this is a private chat with another user (see Recipient). </summary> /// <summary> Returns false is this is a public chat and true if this is a private chat with another user (see Recipient). </summary>
public bool IsPrivate => RecipientId != null;
public bool IsPrivate => _recipientId != null;
/// <summary> Returns the type of this channel (see ChannelTypes). </summary> /// <summary> Returns the type of this channel (see ChannelTypes). </summary>
public string Type { get; private set; } public string Type { get; private set; }


/// <summary> Returns the id of the server containing this channel. </summary>
public string ServerId { get; }
/// <summary> Returns the server containing this channel. </summary> /// <summary> Returns the server containing this channel. </summary>
[JsonIgnore] [JsonIgnore]
public Server Server => _client.Servers[ServerId];

/// For private chats, returns the Id of the target user, otherwise null.
public string RecipientId { get; set; }
public Server Server => _client.Servers[_serverId];
/// For private chats, returns the target user, otherwise null. /// For private chats, returns the target user, otherwise null.
[JsonIgnore] [JsonIgnore]
public User Recipient => _client.Users[RecipientId];
public Member Recipient => _client.Members[_recipientId, _serverId];


/// <summary> Returns a collection of the IDs of all users with read access to this channel. </summary> /// <summary> Returns a collection of the IDs of all users with read access to this channel. </summary>
public IEnumerable<string> UserIds public IEnumerable<string> UserIds
@@ -68,7 +61,7 @@ namespace Discord
if (!_areMembersStale) if (!_areMembersStale)
return _userIds; return _userIds;
_userIds = Server.Members.Where(x => x.GetPermissions(Id)?.ReadMessages ?? false).Select(x => x.UserId).ToArray();
_userIds = Server.Members.Where(x => x.GetPermissions(this)?.ReadMessages ?? false).Select(x => x.Id).ToArray();
_areMembersStale = false; _areMembersStale = false;
return _userIds; return _userIds;
} }
@@ -76,10 +69,7 @@ namespace Discord
private string[] _userIds; private string[] _userIds;
/// <summary> Returns a collection of all users with read access to this channel. </summary> /// <summary> Returns a collection of all users with read access to this channel. </summary>
[JsonIgnore] [JsonIgnore]
public IEnumerable<Member> Members => UserIds.Select(x => _client.Members[x, ServerId]);
/// <summary> Returns a collection of all users with read access to this channel. </summary>
[JsonIgnore]
public IEnumerable<User> Users => UserIds.Select(x => _client.Users[x]);
public IEnumerable<Member> Members => UserIds.Select(x => _client.Members[x, _serverId]);


/// <summary> Returns a collection of the ids of all messages the client has seen posted in this channel. This collection does not guarantee any ordering. </summary> /// <summary> Returns a collection of the ids of all messages the client has seen posted in this channel. This collection does not guarantee any ordering. </summary>
[JsonIgnore] [JsonIgnore]
@@ -94,64 +84,39 @@ namespace Discord
public IEnumerable<PermissionOverwrite> PermissionOverwrites => _permissionOverwrites; public IEnumerable<PermissionOverwrite> PermissionOverwrites => _permissionOverwrites;


internal Channel(DiscordClient client, string id, string serverId, string recipientId) internal Channel(DiscordClient client, string id, string serverId, string recipientId)
: base(client, id)
{ {
_client = client;
Id = id;
ServerId = serverId ?? _client.Servers.PMServer.Id;
RecipientId = recipientId;
_messages = new ConcurrentDictionary<string, bool>();
_serverId = serverId ?? _client.Servers.PMServer.Id;
_recipientId = recipientId;
_permissionOverwrites = _initialPermissionsOverwrites; _permissionOverwrites = _initialPermissionsOverwrites;
_areMembersStale = true; _areMembersStale = true;

//Local Cache
_messages = new ConcurrentDictionary<string, bool>();
} }
internal void OnCached()
internal override void OnCached()
{ {
var server = Server;
if (server != null)
server.AddChannel(Id);

if (IsPrivate) if (IsPrivate)
{ {
var user = Recipient;
if (user != null)
{
Name = "@" + user.Name;
user.PrivateChannelId = Id;
user.AddRef();
_hasRef = true;
}
else
Name = "@" + RecipientId;
var member = _client.Members.GetOrAdd(RecipientId, ServerId);
member.Update(new ExtendedMemberInfo
{
GuildId = ServerId,
UserId = RecipientId,
JoinedAt = DateTime.UtcNow,
Roles = new string[0]
});
_permissionOverwrites = new PermissionOverwrite[]
{
new PermissionOverwrite(PermissionTarget.Member, _client.CurrentUserId, ChannelPermissions.PrivateOnly.RawValue, 0),
new PermissionOverwrite(PermissionTarget.Member, RecipientId, ChannelPermissions.PrivateOnly.RawValue, 0)
};
_recipient = _client.Members[_recipientId, _serverId];
Name = "@" + _recipient.Name;
}
else
{
_server = _client.Servers[_serverId];
_server.AddChannel(this);
} }
} }
internal void OnUncached()
internal override void OnUncached()
{ {
var server = Server;
if (server != null)
server.RemoveChannel(Id);
if (IsPrivate)
{
var user = Recipient;
if (user != null)
{
user.PrivateChannelId = null;
if (_hasRef)
user.RemoveRef();
}
_client.Members.TryRemove(RecipientId, ServerId);
}
_hasRef = false;
if (_server != null)
_server.RemoveChannel(this);
_server = null;

if (_recipient != null)
_recipient.GlobalUser.PrivateChannel = null;
_recipient = null;
} }


internal void Update(ChannelReference model) internal void Update(ChannelReference model)
@@ -199,14 +164,14 @@ namespace Discord
{ {
_areMembersStale = true; _areMembersStale = true;
foreach (var member in Members) foreach (var member in Members)
member.UpdatePermissions(Id);
member.UpdatePermissions(this);
} }
internal void InvalidatePermissionsCache(string userId) internal void InvalidatePermissionsCache(string userId)
{ {
_areMembersStale = true; _areMembersStale = true;
var member = _client.Members[userId, ServerId];
var member = _client.Members[userId, _serverId];
if (member != null) if (member != null)
member.UpdatePermissions(Id);
member.UpdatePermissions(this);
} }
} }
} }

+ 73
- 0
src/Discord.Net/Models/GlobalUser.cs View File

@@ -0,0 +1,73 @@
using Discord.API;
using Newtonsoft.Json;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace Discord
{
internal sealed class GlobalUser : CachedObject
{
private readonly ConcurrentDictionary<string, bool> _servers;
private int _refCount;
/// <summary> Returns the email for this user. </summary>
/// <remarks> This field is only ever populated for the current logged in user. </remarks>
[JsonIgnore]
public string Email { get; private set; }
/// <summary> Returns if the email for this user has been verified. </summary>
/// <remarks> This field is only ever populated for the current logged in user. </remarks>
[JsonIgnore]
public bool? IsVerified { get; private set; }

/// <summary> Returns the private messaging channel with this user, if one exists. </summary>
[JsonIgnore]
public Channel PrivateChannel { get; internal set; }

/// <summary> Returns a collection of all server-specific data for every server this user is a member of. </summary>
[JsonIgnore]
public IEnumerable<Member> Memberships => _servers.Select(x => _client.Members[Id, x.Key]);
/// <summary> Returns a collection of all servers this user is a member of. </summary>
[JsonIgnore]
public IEnumerable<Server> Servers => _servers.Select(x => _client.Servers[x.Key]);

internal GlobalUser(DiscordClient client, string id)
: base(client, id)
{
_servers = new ConcurrentDictionary<string, bool>();
}
internal override void OnCached() { }
internal override void OnUncached() { }
internal void Update(UserInfo model)
{
if (model.Email != null)
Email = model.Email;
if (model.IsVerified != null)
IsVerified = model.IsVerified;
}

internal void AddServer(string serverId)
{
_servers.TryAdd(serverId, true);
}
internal bool RemoveServer(string serverId)
{
bool ignored;
return _servers.TryRemove(serverId, out ignored);
}

internal void AddRef()
{
Interlocked.Increment(ref _refCount);
}
internal void RemoveRef()
{
if (Interlocked.Decrement(ref _refCount) == 0)
_client.Users.TryRemove(Id);
}

public override string ToString() => Id;
}
}

+ 19
- 24
src/Discord.Net/Models/Invite.cs View File

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


namespace Discord namespace Discord
{ {
public sealed class Invite
public sealed class Invite : CachedObject
{ {
private readonly DiscordClient _client;

/// <summary> Returns the unique identifier for this invite. </summary>
public string Id { get; }

private readonly string _serverId;
private string _inviterId, _channelId;
/// <summary> Time (in seconds) until the invite expires. Set to 0 to never expire. </summary> /// <summary> Time (in seconds) until the invite expires. Set to 0 to never expire. </summary>
public int MaxAge { get; private set; } public int MaxAge { get; private set; }
/// <summary> The amount of times this invite has been used. </summary> /// <summary> The amount of times this invite has been used. </summary>
@@ -25,41 +24,37 @@ namespace Discord


/// <summary> Returns a URL for this invite using XkcdPass if available or Id if not. </summary> /// <summary> Returns a URL for this invite using XkcdPass if available or Id if not. </summary>
public string Url => API.Endpoints.InviteUrl(XkcdPass ?? Id); public string Url => API.Endpoints.InviteUrl(XkcdPass ?? Id);

/// <summary> Returns the id of the user that created this invite. </summary>
public string InviterId { get; private set; }
/// <summary> Returns the user that created this invite. </summary> /// <summary> Returns the user that created this invite. </summary>
[JsonIgnore] [JsonIgnore]
public User Inviter => _client.Users[InviterId];

/// <summary> Returns the id of the server this invite is to. </summary>
public string ServerId { get; }
public Member Inviter => _client.Members[_inviterId, _serverId];
/// <summary> Returns the server this invite is to. </summary> /// <summary> Returns the server this invite is to. </summary>
[JsonIgnore] [JsonIgnore]
public Server Server => _client.Servers[ServerId];

/// <summary> Returns the id of the channel this invite is to. </summary>
public string ChannelId { get; private set; }
public Server Server => _client.Servers[_serverId];
/// <summary> Returns the channel this invite is to. </summary> /// <summary> Returns the channel this invite is to. </summary>
[JsonIgnore] [JsonIgnore]
public Channel Channel => _client.Channels[ChannelId];
public Channel Channel => _client.Channels[_channelId];


internal Invite(DiscordClient client, string code, string xkcdPass, string serverId) internal Invite(DiscordClient client, string code, string xkcdPass, string serverId)
: base(client, code)
{ {
_client = client;
Id = code;
XkcdPass = xkcdPass; XkcdPass = xkcdPass;
ServerId = serverId;
_serverId = serverId;
} }
internal override void OnCached() { }
internal override void OnUncached() { }


public override string ToString() => XkcdPass ?? Id; public override string ToString() => XkcdPass ?? Id;



internal void Update(InviteReference model) internal void Update(InviteReference model)
{ {
if (model.Channel != null) if (model.Channel != null)
ChannelId = model.Channel.Id;
_channelId = model.Channel.Id;
if (model.Inviter != null) if (model.Inviter != null)
InviterId = model.Inviter.Id;
_inviterId = model.Inviter.Id;
} }


internal void Update(InviteInfo model) internal void Update(InviteInfo model)


+ 0
- 279
src/Discord.Net/Models/Member.cs View File

@@ -1,279 +0,0 @@
using Discord.API;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Discord
{
public class Member
{
private readonly DiscordClient _client;
private ConcurrentDictionary<string, ChannelPermissions> _permissions;
private bool _hasRef;

/// <summary> Returns the name of this user on this server. </summary>
public string Name { get; private set; }
/// <summary> Returns a by-name unique identifier separating this user from others with the same name. </summary>
public string Discriminator { get; private set; }
/// <summary> Returns the unique identifier for this user's current avatar. </summary>
public string AvatarId { get; private set; }
/// <summary> Returns the URL to this user's current avatar. </summary>
public string AvatarUrl => API.Endpoints.UserAvatar(UserId, AvatarId);
/// <summary> Returns the datetime that this user joined this server. </summary>
public DateTime JoinedAt { get; private set; }

public bool IsSelfMuted { get; private set; }
public bool IsSelfDeafened { get; private set; }
public bool IsServerMuted { get; private set; }
public bool IsServerDeafened { get; private set; }
public bool IsServerSuppressed { get; private set; }
public bool IsSpeaking { get; internal set; }

public string SessionId { get; private set; }
public string Token { get; private set; }

/// <summary> Returns the id for the game this user is currently playing. </summary>
public string GameId { get; private set; }
/// <summary> Returns the current status for this user. </summary>
public string Status { get; private set; }
/// <summary> Returns the time this user last sent/edited a message, started typing or sent voice data in this server. </summary>
public DateTime? LastActivityAt { get; private set; }
/// <summary> Returns the time this user was last seen online in this server. </summary>
public DateTime LastOnlineAt => Status != UserStatus.Offline ? DateTime.UtcNow : _lastOnline;
private DateTime _lastOnline;

public string UserId { get; }
[JsonIgnore]
public User User => _client.Users[UserId];

public string ServerId { get; }
[JsonIgnore]
public Server Server => _client.Servers[ServerId];

public string VoiceChannelId { get; private set; }
[JsonIgnore]
public Channel VoiceChannel => _client.Channels[VoiceChannelId];

private static readonly string[] _initialRoleIds = new string[0];
public string[] RoleIds { get; private set; }
[JsonIgnore]
public IEnumerable<Role> Roles => RoleIds.Select(x => _client.Roles[x]);

/// <summary> Returns a collection of all messages this user has sent on this server that are still in cache. </summary>
[JsonIgnore]
public IEnumerable<Message> Messages => _client.Messages.Where(x => x.UserId == UserId && x.ServerId == ServerId);

/// <summary> Returns a collection of all channels this user is a member of. </summary>
[JsonIgnore]
public IEnumerable<Channel> Channels => _client.Channels.Where(x => x.ServerId == ServerId && x.UserIds.Contains(UserId));

internal Member(DiscordClient client, string userId, string serverId)
{
_client = client;
UserId = userId;
ServerId = serverId ?? _client.Servers.PMServer.Id;
Status = UserStatus.Offline;
RoleIds = _initialRoleIds;
_permissions = new ConcurrentDictionary<string, ChannelPermissions>();
}
internal void OnCached()
{
var server = Server;
if (server != null)
{
server.AddMember(this);
if (UserId == _client.CurrentUserId)
server.CurrentMember = this;
}
var user = User;
if (user != null)
{
if (server == null || !server.IsVirtual)
user.AddServer(ServerId);
user.AddRef();
_hasRef = true;
}
}
internal void OnUncached()
{
var server = Server;
if (server != null)
{
server.RemoveMember(this);
if (UserId == _client.CurrentUserId)
server.CurrentMember = null;
}
var user = User;
if (user != null)
{
user.RemoveServer(ServerId);
if (_hasRef)
user.RemoveRef();
}
_hasRef = false;
}

public override string ToString() => UserId;

internal void Update(UserReference model)
{
if (model.Avatar != null)
AvatarId = model.Avatar;
if (model.Discriminator != null)
Discriminator = model.Discriminator;
if (model.Username != null)
Name = model.Username;
}
internal void Update(MemberInfo model)
{
if (model.User != null)
Update(model.User);
if (model.JoinedAt.HasValue)
JoinedAt = model.JoinedAt.Value;
if (model.Roles != null)
UpdateRoles(model.Roles);

UpdatePermissions();
}
internal void Update(ExtendedMemberInfo model)
{
Update(model as API.MemberInfo);
if (model.IsServerDeafened != null)
IsServerDeafened = model.IsServerDeafened.Value;
if (model.IsServerMuted != null)
IsServerMuted = model.IsServerMuted.Value;
}
internal void Update(PresenceInfo model)
{
if (model.User != null)
Update(model.User as UserReference);

if (model.Roles != null)
UpdateRoles(model.Roles);
if (model.Status != null && Status != model.Status)
{
Status = model.Status;
if (Status == UserStatus.Offline)
_lastOnline = DateTime.UtcNow;
}

//Allows null
GameId = model.GameId;
}
internal void Update(VoiceMemberInfo model)
{
if (model.IsServerDeafened != null)
IsServerDeafened = model.IsServerDeafened.Value;
if (model.IsServerMuted != null)
IsServerMuted = model.IsServerMuted.Value;
if (model.SessionId != null)
SessionId = model.SessionId;
if (model.Token != null)
Token = model.Token;

if (model.ChannelId != null)
VoiceChannelId = model.ChannelId;
if (model.IsSelfDeafened != null)
IsSelfDeafened = model.IsSelfDeafened.Value;
if (model.IsSelfMuted != null)
IsSelfMuted = model.IsSelfMuted.Value;
if (model.IsServerSuppressed != null)
IsServerSuppressed = model.IsServerSuppressed.Value;
}
private void UpdateRoles(string[] roleIds)
{
//Set roles, with the everyone role added too
string[] newRoles = new string[roleIds.Length + 1];
newRoles[0] = ServerId; //Everyone
for (int i = 0; i < roleIds.Length; i++)
newRoles[i + 1] = roleIds[i];
RoleIds = newRoles;
}

internal void UpdateActivity(DateTime? activity = null)
{
if (LastActivityAt == null || activity > LastActivityAt.Value)
LastActivityAt = activity ?? DateTime.UtcNow;
}

internal void UpdatePermissions()
{
foreach (var channel in _permissions)
UpdatePermissions(channel.Key);
}
internal void UpdatePermissions(string channelId)
{
if (RoleIds == null) return; // We don't have all our data processed yet, this will be called again soon

var server = Server;
if (server == null) return;
var channel = _client.Channels[channelId];

ChannelPermissions permissions;
if (!_permissions.TryGetValue(channelId, out permissions)) return;
uint newPermissions = 0x0;
uint oldPermissions = permissions.RawValue;
if (UserId == server.OwnerId)
newPermissions = ChannelPermissions.All(channel).RawValue;
else
{
if (channel == null) return;
var channelOverwrites = channel.PermissionOverwrites;

//var roles = Roles.OrderBy(x => x.Id);
var roles = Roles;
foreach (var serverRole in roles)
newPermissions |= serverRole.Permissions.RawValue;
foreach (var denyRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Deny.RawValue != 0 && roles.Any(y => y.Id == x.TargetId)))
newPermissions &= ~denyRole.Deny.RawValue;
foreach (var allowRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Allow.RawValue != 0 && roles.Any(y => y.Id == x.TargetId)))
newPermissions |= allowRole.Allow.RawValue;
foreach (var denyMembers in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Member && x.TargetId == UserId && x.Deny.RawValue != 0))
newPermissions &= ~denyMembers.Deny.RawValue;
foreach (var allowMembers in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Member && x.TargetId == UserId && x.Allow.RawValue != 0))
newPermissions |= allowMembers.Allow.RawValue;
}

permissions.SetRawValueInternal(newPermissions);

if (permissions.ManagePermissions)
permissions.SetRawValueInternal(ChannelPermissions.All(channel).RawValue);
/*else if (server.DefaultChannelId == channelId)
permissions.SetBitInternal(PackedPermissions.Text_ReadMessagesBit, true);*/

if (permissions.RawValue != oldPermissions)
channel.InvalidMembersCache();
}
//TODO: Add GetServerPermissions
public ChannelPermissions GetPermissions(Channel channel)
=> GetPermissions(channel?.Id);
public ChannelPermissions GetPermissions(string channelId)
{
if (channelId == null) throw new ArgumentNullException(nameof(channelId));

ChannelPermissions perms;
if (_permissions.TryGetValue(channelId, out perms))
return perms;
return null;
}

internal void AddChannel(string channelId)
{
var perms = new ChannelPermissions();
perms.Lock();
_permissions.TryAdd(channelId, perms);
UpdatePermissions(channelId);
}
internal bool RemoveChannel(string channelId)
{
ChannelPermissions ignored;
return _permissions.TryRemove(channelId, out ignored);
}

public bool HasRole(Role role) => RoleIds.Contains(role?.Id);
public bool HasRole(string roleId) => RoleIds.Contains(roleId);
}
}

+ 18
- 40
src/Discord.Net/Models/Message.cs View File

@@ -6,7 +6,7 @@ using System.Linq;


namespace Discord namespace Discord
{ {
public sealed class Message
public sealed class Message : CachedObject
{ {
public sealed class Attachment : File public sealed class Attachment : File
{ {
@@ -91,12 +91,9 @@ namespace Discord
} }
} }


private readonly DiscordClient _client;
private string _cleanText; private string _cleanText;
private bool _gotRef;

/// <summary> Returns the global unique identifier for this message. </summary>
public string Id { get; internal set; }
private string _channelId, _userId;
/// <summary> Returns the local unique identifier for this message. </summary> /// <summary> Returns the local unique identifier for this message. </summary>
public string Nonce { get; internal set; } public string Nonce { get; internal set; }


@@ -115,7 +112,7 @@ namespace Discord
public string RawText { get; private set; } public string RawText { get; private set; }
/// <summary> Returns the content of this message with any special references such as mentions converted. </summary> /// <summary> Returns the content of this message with any special references such as mentions converted. </summary>
/// <remarks> This value is lazy loaded and only processed on first request. Each subsequent request will pull from cache. </remarks> /// <remarks> This value is lazy loaded and only processed on first request. Each subsequent request will pull from cache. </remarks>
public string Text => _cleanText != null ? _cleanText : (_cleanText = MentionHelper.ConvertToNames(_client, RawText));
public string Text => _cleanText != null ? _cleanText : (_cleanText = Mention.ConvertToNames(_client, Server, RawText));
/// <summary> Returns the timestamp for when this message was sent. </summary> /// <summary> Returns the timestamp for when this message was sent. </summary>
public DateTime Timestamp { get; private set; } public DateTime Timestamp { get; private set; }
/// <summary> Returns the timestamp for when this message was last edited. </summary> /// <summary> Returns the timestamp for when this message was last edited. </summary>
@@ -132,19 +129,14 @@ namespace Discord
public string[] MentionIds { get; private set; } public string[] MentionIds { get; private set; }
/// <summary> Returns a collection of all users mentioned in this message. </summary> /// <summary> Returns a collection of all users mentioned in this message. </summary>
[JsonIgnore] [JsonIgnore]
public IEnumerable<User> Mentions => MentionIds.Select(x => _client.Users[x]).Where(x => x != null);

/// <summary> Returns the id of the server containing the channel this message was sent to. </summary>
public string ServerId => Channel.ServerId;
public IEnumerable<Member> Mentions { get; internal set; }
/// <summary> Returns the server containing the channel this message was sent to. </summary> /// <summary> Returns the server containing the channel this message was sent to. </summary>
[JsonIgnore] [JsonIgnore]
public Server Server => _client.Servers[Channel.ServerId];

/// <summary> Returns the id of the channel this message was sent to. </summary>
public string ChannelId { get; }
public Server Server => Channel.Server;
/// <summary> Returns the channel this message was sent to. </summary> /// <summary> Returns the channel this message was sent to. </summary>
[JsonIgnore] [JsonIgnore]
public Channel Channel => _client.Channels[ChannelId];
public Channel Channel { get; private set; }


/// <summary> Returns true if the current user created this message. </summary> /// <summary> Returns true if the current user created this message. </summary>
public bool IsAuthor => _client.CurrentUserId == UserId; public bool IsAuthor => _client.CurrentUserId == UserId;
@@ -152,42 +144,28 @@ namespace Discord
public string UserId { get; } public string UserId { get; }
/// <summary> Returns the author of this message. </summary> /// <summary> Returns the author of this message. </summary>
[JsonIgnore] [JsonIgnore]
public User User => _client.Users[UserId];
/// <summary> Returns the author of this message. </summary>
[JsonIgnore]
public Member Member => _client.Members[UserId, ServerId];
public Member Member => _client.Members[_userId, Channel.Server.Id];


internal Message(DiscordClient client, string id, string channelId, string userId) internal Message(DiscordClient client, string id, string channelId, string userId)
: base(client, id)
{ {
_client = client;
Id = id;
ChannelId = channelId;
UserId = userId;
_channelId = channelId;
_userId = userId;
Attachments = _initialAttachments; Attachments = _initialAttachments;
Embeds = _initialEmbeds; Embeds = _initialEmbeds;
MentionIds = _initialMentions; MentionIds = _initialMentions;
} }
internal void OnCached()
internal override void OnCached()
{ {
var channel = Channel;
if (channel != null)
channel.AddMessage(Id);
var user = User;
if (user != null)
{
user.AddRef();
_gotRef = true;
}
var channel = _client.Channels[_channelId];
channel.AddMessage(Id);
Channel = channel;
} }
internal void OnUncached()
internal override void OnUncached()
{ {
var channel = Channel; var channel = Channel;
if (channel != null) if (channel != null)
channel.RemoveMessage(Id); channel.RemoveMessage(Id);
var user = User;
if (user != null && _gotRef)
user.RemoveRef();
_gotRef = false;
} }


internal void Update(MessageInfo model) internal void Update(MessageInfo model)
@@ -236,6 +214,6 @@ namespace Discord
} }
} }


public override string ToString() => User.ToString() + ": " + RawText;
public override string ToString() => Member.Name + ": " + RawText;
} }
} }

+ 18
- 26
src/Discord.Net/Models/Role.cs View File

@@ -5,12 +5,11 @@ using System.Linq;


namespace Discord namespace Discord
{ {
public sealed class Role
public sealed class Role : CachedObject
{ {
private readonly DiscordClient _client;

/// <summary> Returns the unique identifier for this role. </summary>
public string Id { get; }
private readonly string _serverId;
private Server _server;
/// <summary> Returns the name of this role. </summary> /// <summary> Returns the name of this role. </summary>
public string Name { get; private set; } public string Name { get; private set; }
/// <summary> If true, this role is displayed isolated from other users. </summary> /// <summary> If true, this role is displayed isolated from other users. </summary>
@@ -24,27 +23,21 @@ namespace Discord


/// <summary> Returns the the permissions contained by this role. </summary> /// <summary> Returns the the permissions contained by this role. </summary>
public ServerPermissions Permissions { get; } public ServerPermissions Permissions { get; }

/// <summary> Returns the id of the server this role is a member of. </summary>
public string ServerId { get; }
/// <summary> Returns the server this role is a member of. </summary> /// <summary> Returns the server this role is a member of. </summary>
[JsonIgnore] [JsonIgnore]
public Server Server => _client.Servers[ServerId];
public Server Server => _server;


/// <summary> Returns true if this is the role representing all users in a server. </summary> /// <summary> Returns true if this is the role representing all users in a server. </summary>
public bool IsEveryone => Id == ServerId;
/// <summary> Returns a list of the ids of all members in this role. </summary>
[JsonIgnore]
public IEnumerable<string> MemberIds => IsEveryone ? Server.UserIds : Server.Members.Where(x => x.RoleIds.Contains(Id)).Select(x => x.UserId);
public bool IsEveryone => Id == _serverId;
/// <summary> Returns a list of all members in this role. </summary> /// <summary> Returns a list of all members in this role. </summary>
[JsonIgnore] [JsonIgnore]
public IEnumerable<Member> Members => IsEveryone ? Server.Members : Server.Members.Where(x => x.RoleIds.Contains(Id));
public IEnumerable<Member> Members => IsEveryone ? Server.Members : Server.Members.Where(x => x.HasRole(this));


internal Role(DiscordClient client, string id, string serverId) internal Role(DiscordClient client, string id, string serverId)
: base(client, id)
{ {
_client = client;
Id = id;
ServerId = serverId;
_serverId = serverId;
Permissions = new ServerPermissions(0); Permissions = new ServerPermissions(0);
Permissions.Lock(); Permissions.Lock();
Color = new Color(0); Color = new Color(0);
@@ -53,18 +46,17 @@ namespace Discord
if (IsEveryone) if (IsEveryone)
Position = int.MinValue; Position = int.MinValue;
} }
internal void OnCached()
internal override void OnCached()
{ {
var server = Server;
if (server != null)
server.AddRole(Id);
_server = _client.Servers[_serverId];
_server.AddRole(this);
} }
internal void OnUncached()
internal override void OnUncached()
{ {
var server = Server;
if (server != null)
server.RemoveRole(Id);
}
if (_server != null)
_server.RemoveRole(this);
_server = null;
}


internal void Update(RoleInfo model) internal void Update(RoleInfo model)
{ {


+ 59
- 86
src/Discord.Net/Models/Server.cs View File

@@ -7,13 +7,16 @@ using System.Linq;


namespace Discord namespace Discord
{ {
public sealed class Server
public sealed class Server : CachedObject
{ {
private readonly DiscordClient _client;
private readonly ConcurrentDictionary<string, bool> _bans, _channels, _invites, _members, _roles;
private readonly ConcurrentDictionary<string, bool> _bans;
private readonly ConcurrentDictionary<string, Channel> _channels;
private readonly ConcurrentDictionary<string, Member> _members;
private readonly ConcurrentDictionary<string, Role> _roles;
private readonly ConcurrentDictionary<string, Invite> _invites;


/// <summary> Returns the unique identifier for this server. </summary>
public string Id { get; }
private string _ownerId;
/// <summary> Returns the name of this channel. </summary> /// <summary> Returns the name of this channel. </summary>
public string Name { get; private set; } public string Name { get; private set; }
/// <summary> Returns the current logged-in user's data for this server. </summary> /// <summary> Returns the current logged-in user's data for this server. </summary>
@@ -31,12 +34,10 @@ namespace Discord
internal string VoiceServer { get; set; }*/ internal string VoiceServer { get; set; }*/


/// <summary> Returns true if the current user created this server. </summary> /// <summary> Returns true if the current user created this server. </summary>
public bool IsOwner => _client.CurrentUserId == OwnerId;
/// <summary> Returns the id of the user that first created this server. </summary>
public string OwnerId { get; private set; }
public bool IsOwner => _client.CurrentUserId == _ownerId;
/// <summary> Returns the user that first created this server. </summary> /// <summary> Returns the user that first created this server. </summary>
[JsonIgnore] [JsonIgnore]
public User Owner => _client.Users[OwnerId];
public Member Owner => _client.Members[_ownerId, Id];


/// <summary> Returns the id of the AFK voice channel for this server (see AFKTimeout). </summary> /// <summary> Returns the id of the AFK voice channel for this server (see AFKTimeout). </summary>
public string AFKChannelId { get; private set; } public string AFKChannelId { get; private set; }
@@ -52,11 +53,8 @@ namespace Discord
/// <summary> Returns a collection of the ids of all users banned on this server. </summary> /// <summary> Returns a collection of the ids of all users banned on this server. </summary>
[JsonIgnore] [JsonIgnore]
public IEnumerable<string> BanIds => _bans.Select(x => x.Key);

/// <summary> Returns a collection of the ids of all channels within this server. </summary>
[JsonIgnore]
public IEnumerable<string> ChannelIds => _channels.Select(x => x.Key);
public IEnumerable<string> Bans => _bans.Select(x => x.Key);
/// <summary> Returns a collection of all channels within this server. </summary> /// <summary> Returns a collection of all channels within this server. </summary>
[JsonIgnore] [JsonIgnore]
public IEnumerable<Channel> Channels => _channels.Select(x => _client.Channels[x.Key]); public IEnumerable<Channel> Channels => _channels.Select(x => _client.Channels[x.Key]);
@@ -67,62 +65,58 @@ namespace Discord
[JsonIgnore] [JsonIgnore]
public IEnumerable<Channel> VoiceChannels => _channels.Select(x => _client.Channels[x.Key]).Where(x => x.Type == ChannelTypes.Voice); public IEnumerable<Channel> VoiceChannels => _channels.Select(x => _client.Channels[x.Key]).Where(x => x.Type == ChannelTypes.Voice);
/// <summary> Returns a collection of all invite codes to this server. </summary>
/// <summary> Returns a collection of all invites to this server. </summary>
[JsonIgnore] [JsonIgnore]
public IEnumerable<string> InviteCodes => _invites.Select(x => x.Key);
/*/// <summary> Returns a collection of all invites to this server. </summary>
[JsonIgnore]
public IEnumerable<Invite> Invites => _invites.Select(x => _client.Invites[x.Key]);*/

public IEnumerable<Invite> Invites => _invites.Values;
/// <summary> Returns a collection of all users within this server with their server-specific data. </summary> /// <summary> Returns a collection of all users within this server with their server-specific data. </summary>
[JsonIgnore] [JsonIgnore]
public IEnumerable<Member> Members => _members.Select(x => _client.Members[x.Key, Id]); public IEnumerable<Member> Members => _members.Select(x => _client.Members[x.Key, Id]);
/// <summary> Returns a collection of the ids of all users within this server. </summary>
[JsonIgnore]
public IEnumerable<string> UserIds => _members.Select(x => x.Key);
/// <summary> Returns a collection of all users within this server. </summary>
[JsonIgnore]
public IEnumerable<User> Users => _members.Select(x => _client.Users[x.Key]);


/// <summary> Return the id of the role representing all users in a server. </summary> /// <summary> Return the id of the role representing all users in a server. </summary>
public string EveryoneRoleId => Id; public string EveryoneRoleId => Id;
/// <summary> Return the the role representing all users in a server. </summary> /// <summary> Return the the role representing all users in a server. </summary>
[JsonIgnore] [JsonIgnore]
public Role EveryoneRole => _client.Roles[EveryoneRoleId]; public Role EveryoneRole => _client.Roles[EveryoneRoleId];
/// <summary> Returns a collection of the ids of all roles within this server. </summary>
[JsonIgnore]
public IEnumerable<string> RoleIds => _roles.Select(x => x.Key);
/// <summary> Returns a collection of all roles within this server. </summary> /// <summary> Returns a collection of all roles within this server. </summary>
[JsonIgnore] [JsonIgnore]
public IEnumerable<Role> Roles => _roles.Select(x => _client.Roles[x.Key]); public IEnumerable<Role> Roles => _roles.Select(x => _client.Roles[x.Key]);


internal Server(DiscordClient client, string id) internal Server(DiscordClient client, string id)
: base(client, id)
{ {
_client = client;
Id = id;
//Global Cache
_channels = new ConcurrentDictionary<string, Channel>();
_members = new ConcurrentDictionary<string, Member>();
_roles = new ConcurrentDictionary<string, Role>();

//Local Cache
_bans = new ConcurrentDictionary<string, bool>(); _bans = new ConcurrentDictionary<string, bool>();
_channels = new ConcurrentDictionary<string, bool>();
_invites = new ConcurrentDictionary<string, bool>();
_members = new ConcurrentDictionary<string, bool>();
_roles = new ConcurrentDictionary<string, bool>();
}
internal void OnCached()
{
_invites = new ConcurrentDictionary<string, Invite>();
} }
internal void OnUncached()
internal override void OnCached() { }
internal override void OnUncached()
{ {
//Global Cache
var channels = _client.Channels; var channels = _client.Channels;
foreach (var channelId in ChannelIds)
channels.TryRemove(channelId);
foreach (var channel in _channels)
channels.TryRemove(channel.Key);


var members = _client.Members; var members = _client.Members;
foreach (var userId in UserIds)
members.TryRemove(userId, Id);
foreach (var user in _members)
members.TryRemove(user.Key, Id);


var roles = _client.Roles; var roles = _client.Roles;
foreach (var roleId in RoleIds)
roles.TryRemove(roleId);
}
foreach (var role in _roles)
roles.TryRemove(role.Key);

//Local Cache
foreach (var invite in _invites)
invite.Value.Uncache();
_invites.Clear();

_bans.Clear();
}


internal void Update(GuildInfo model) internal void Update(GuildInfo model)
{ {
@@ -136,7 +130,7 @@ namespace Discord
if (model.Name != null) if (model.Name != null)
Name = model.Name; Name = model.Name;
if (model.OwnerId != null) if (model.OwnerId != null)
OwnerId = model.OwnerId;
_ownerId = model.OwnerId;
if (model.Region != null) if (model.Region != null)
Region = model.Region; Region = model.Region;


@@ -165,8 +159,6 @@ namespace Discord
var members = _client.Members; var members = _client.Members;
foreach (var subModel in model.Members) foreach (var subModel in model.Members)
{ {
var user = users.GetOrAdd(subModel.User.Id);
user.Update(subModel.User);
var member = members.GetOrAdd(subModel.User.Id, Id); var member = members.GetOrAdd(subModel.User.Id, Id);
member.Update(subModel); member.Update(subModel);
} }
@@ -196,62 +188,43 @@ namespace Discord
return _bans.TryRemove(banId, out ignored); return _bans.TryRemove(banId, out ignored);
} }


internal void AddChannel(string channelId)
internal void AddChannel(Channel channel)
{ {
_channels.TryAdd(channelId, true);
_channels.TryAdd(channel.Id, channel);
foreach (var member in Members) foreach (var member in Members)
member.AddChannel(channelId);
member.AddChannel(channel);
} }
internal bool RemoveChannel(string channelId)
internal void RemoveChannel(Channel channel)
{ {
bool ignored;
foreach (var member in Members) foreach (var member in Members)
member.RemoveChannel(channelId);
return _channels.TryRemove(channelId, out ignored);
member.RemoveChannel(channel);
_channels.TryRemove(channel.Id, out channel);
} }


internal void AddInvite(string inviteId)
{
_invites.TryAdd(inviteId, true);
}
internal bool RemoveInvite(string inviteId)
{
bool ignored;
return _invites.TryRemove(inviteId, out ignored);
}
internal void AddInvite(Invite invite) => _invites.TryAdd(invite.Id, invite);
internal void RemoveInvite(Invite invite) => _invites.TryRemove(invite.Id, out invite);


internal void AddMember(Member member) internal void AddMember(Member member)
{ {
_members.TryAdd(member.UserId, true);
_members.TryAdd(member.Id, member);
foreach (var channel in Channels) foreach (var channel in Channels)
{ {
member.AddChannel(channel.Id);
channel.InvalidatePermissionsCache(member.UserId);
member.AddChannel(channel);
channel.InvalidatePermissionsCache(member.Id);
} }
} }
internal bool RemoveMember(Member member)
internal void RemoveMember(Member member)
{ {
bool ignored;
foreach (var channel in Channels) foreach (var channel in Channels)
{ {
member.RemoveChannel(channel.Id);
channel.InvalidatePermissionsCache(member.UserId);
member.RemoveChannel(channel);
channel.InvalidatePermissionsCache(member.Id);
} }
return _members.TryRemove(member.UserId, out ignored);
}
internal bool HasMember(string userId)
{
return _members.ContainsKey(userId);
_members.TryRemove(member.Id, out member);
} }
internal void HasMember(Member user) => _members.ContainsKey(user.Id);


internal void AddRole(string roleId)
{
_roles.TryAdd(roleId, true);
}
internal bool RemoveRole(string roleId)
{
bool ignored;
return _roles.TryRemove(roleId, out ignored);
}
internal void AddRole(Role role) => _roles.TryAdd(role.Id, role);
internal void RemoveRole(Role role) => _roles.TryRemove(role.Id, out role);
} }
} }

+ 222
- 62
src/Discord.Net/Models/User.cs View File

@@ -1,20 +1,21 @@
using Discord.API; using Discord.API;
using Newtonsoft.Json; using Newtonsoft.Json;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;


namespace Discord namespace Discord
{ {
public sealed class User
public class Member : CachedObject
{ {
private readonly DiscordClient _client;
private readonly ConcurrentDictionary<string, bool> _servers;
private int _refCount;
private static readonly string[] _initialRoleIds = new string[0];

private ConcurrentDictionary<string, Channel> _channels;
private ConcurrentDictionary<string, ChannelPermissions> _permissions;
private bool _hasRef;
private string[] _roleIds;


/// <summary> Returns the unique identifier for this user. </summary>
public string Id { get; }
/// <summary> Returns the name of this user on this server. </summary> /// <summary> Returns the name of this user on this server. </summary>
public string Name { get; private set; } public string Name { get; private set; }
/// <summary> Returns a by-name unique identifier separating this user from others with the same name. </summary> /// <summary> Returns a by-name unique identifier separating this user from others with the same name. </summary>
@@ -23,59 +24,97 @@ namespace Discord
public string AvatarId { get; private set; } public string AvatarId { get; private set; }
/// <summary> Returns the URL to this user's current avatar. </summary> /// <summary> Returns the URL to this user's current avatar. </summary>
public string AvatarUrl => API.Endpoints.UserAvatar(Id, AvatarId); public string AvatarUrl => API.Endpoints.UserAvatar(Id, AvatarId);
/// <summary> Returns the datetime that this user joined this server. </summary>
public DateTime JoinedAt { get; private set; }

public bool IsSelfMuted { get; private set; }
public bool IsSelfDeafened { get; private set; }
public bool IsServerMuted { get; private set; }
public bool IsServerDeafened { get; private set; }
public bool IsServerSuppressed { get; private set; }
public bool IsSpeaking { get; internal set; }

public string SessionId { get; private set; }
public string Token { get; private set; }

/// <summary> Returns the id for the game this user is currently playing. </summary>
public string GameId { get; private set; }
/// <summary> Returns the current status for this user. </summary>
public string Status { get; private set; }
/// <summary> Returns the time this user last sent/edited a message, started typing or sent voice data in this server. </summary>
public DateTime? LastActivityAt { get; private set; }
/// <summary> Returns the time this user was last seen online in this server. </summary>
public DateTime LastOnlineAt => Status != UserStatus.Offline ? DateTime.UtcNow : _lastOnline;
private DateTime _lastOnline;


/// <summary> Returns the email for this user. </summary>
/// <remarks> This field is only ever populated for the current logged in user. </remarks>
[JsonIgnore]
public string Email { get; private set; }
/// <summary> Returns if the email for this user has been verified. </summary>
/// <remarks> This field is only ever populated for the current logged in user. </remarks>
[JsonIgnore] [JsonIgnore]
public bool? IsVerified { get; private set; }
internal GlobalUser GlobalUser => _client.Users[Id];


/// <summary> Returns the Id of the private messaging channel with this user, if one exists. </summary>
public string PrivateChannelId { get; internal set; }
/// <summary> Returns the private messaging channel with this user, if one exists. </summary>
public string ServerId { get; }
[JsonIgnore] [JsonIgnore]
public Channel PrivateChannel => _client.Channels[PrivateChannelId];
public Server Server => _client.Servers[ServerId];


/// <summary> Returns a collection of all server-specific data for every server this user is a member of. </summary>
public string VoiceChannelId { get; private set; }
[JsonIgnore] [JsonIgnore]
public IEnumerable<Member> Memberships => _servers.Select(x => _client.GetMember(x.Key, Id));
/// <summary> Returns a collection of all servers this user is a member of. </summary>
public Channel VoiceChannel => _client.Channels[VoiceChannelId];
[JsonIgnore] [JsonIgnore]
public IEnumerable<Server> Servers => _servers.Select(x => _client.GetServer(x.Key));
/// <summary> Returns a collection of the ids of all servers this user is a member of. </summary>
public IEnumerable<Role> Roles => _roleIds.Select(x => _client.Roles[x]);
/// <summary> Returns a collection of all messages this user has sent on this server that are still in cache. </summary>
[JsonIgnore] [JsonIgnore]
public IEnumerable<string> ServersIds => _servers.Select(x => x.Key);
/// <summary> Returns a collection of all messages this user has sent that are still in cache. </summary>
public IEnumerable<Message> Messages => _client.Messages.Where(x => x.UserId == Id && x.Server.Id == ServerId);
/// <summary> Returns a collection of all channels this user is a member of. </summary>
[JsonIgnore] [JsonIgnore]
public IEnumerable<Message> Messages => _client.Messages.Where(x => x.UserId == Id);
public IEnumerable<Channel> Channels => _client.Channels.Where(x => x.Server.Id == ServerId && x.UserIds.Contains(Id));


/// <summary> Returns the id for the game this user is currently playing. </summary>
/*public string GameId => Memberships.Where(x => x.GameId != null).Select(x => x.GameId).FirstOrDefault();
/// <summary> Returns the current status for this user. </summary>
public string Status => Memberships.OrderByDescending(x => x.StatusSince).Select(x => x.Status).FirstOrDefault();
/// <summary> Returns the time this user's status was last changed. </summary>
public DateTime StatusSince => Memberships.OrderByDescending(x => x.StatusSince).Select(x => x.StatusSince).First();
/// <summary> Returns the time this user last sent/edited a message, started typing or sent voice data. </summary>
public DateTime? LastActivity => Memberships.OrderByDescending(x => x.LastActivity).Select(x => x.LastActivity).FirstOrDefault();
/// <summary> Returns the time this user was last seen online. </summary>
public DateTime? LastOnline => Memberships.OrderByDescending(x => x.LastOnline).Select(x => x.LastOnline).FirstOrDefault();*/

internal User(DiscordClient client, string id)
{
_client = client;
Id = id;
_servers = new ConcurrentDictionary<string, bool>();
internal Member(DiscordClient client, string id, string serverId)
: base(client, id)
{
ServerId = serverId ?? _client.Servers.PMServer.Id;
Status = UserStatus.Offline;
_roleIds = _initialRoleIds;
_channels = new ConcurrentDictionary<string, Channel>();
_permissions = new ConcurrentDictionary<string, ChannelPermissions>();
} }
internal void OnCached()
internal override void OnCached()
{ {
var server = Server;
if (server != null)
{
server.AddMember(this);
if (Id == _client.CurrentUserId)
server.CurrentMember = this;
}
var user = GlobalUser;
if (user != null)
{
if (server == null || !server.IsVirtual)
user.AddServer(ServerId);
user.AddRef();
_hasRef = true;
}
} }
internal void OnUncached()
internal override void OnUncached()
{ {
var server = Server;
if (server != null)
{
server.RemoveMember(this);
if (Id == _client.CurrentUserId)
server.CurrentMember = null;
}
var user = GlobalUser;
if (user != null)
{
user.RemoveServer(ServerId);
if (_hasRef)
user.RemoveRef();
}
_hasRef = false;
} }


public override string ToString() => Id;

internal void Update(UserReference model) internal void Update(UserReference model)
{ {
if (model.Avatar != null) if (model.Avatar != null)
@@ -85,36 +124,157 @@ namespace Discord
if (model.Username != null) if (model.Username != null)
Name = model.Username; Name = model.Username;
} }
internal void Update(UserInfo model)
internal void Update(MemberInfo model)
{
if (model.User != null)
Update(model.User);
if (model.JoinedAt.HasValue)
JoinedAt = model.JoinedAt.Value;
if (model.Roles != null)
UpdateRoles(model.Roles);

UpdatePermissions();
}
internal void Update(ExtendedMemberInfo model)
{
Update(model as API.MemberInfo);
if (model.IsServerDeafened != null)
IsServerDeafened = model.IsServerDeafened.Value;
if (model.IsServerMuted != null)
IsServerMuted = model.IsServerMuted.Value;
}
internal void Update(PresenceInfo model)
{
if (model.User != null)
Update(model.User as UserReference);

if (model.Roles != null)
UpdateRoles(model.Roles);
if (model.Status != null && Status != model.Status)
{
Status = model.Status;
if (Status == UserStatus.Offline)
_lastOnline = DateTime.UtcNow;
}

//Allows null
GameId = model.GameId;
}
internal void Update(VoiceMemberInfo model)
{ {
Update(model as UserReference);
if (model.IsServerDeafened != null)
IsServerDeafened = model.IsServerDeafened.Value;
if (model.IsServerMuted != null)
IsServerMuted = model.IsServerMuted.Value;
if (model.SessionId != null)
SessionId = model.SessionId;
if (model.Token != null)
Token = model.Token;


if (model.Email != null)
Email = model.Email;
if (model.IsVerified != null)
IsVerified = model.IsVerified;
if (model.ChannelId != null)
VoiceChannelId = model.ChannelId;
if (model.IsSelfDeafened != null)
IsSelfDeafened = model.IsSelfDeafened.Value;
if (model.IsSelfMuted != null)
IsSelfMuted = model.IsSelfMuted.Value;
if (model.IsServerSuppressed != null)
IsServerSuppressed = model.IsServerSuppressed.Value;
}
private void UpdateRoles(string[] roleIds)
{
//Set roles, with the everyone role added too
string[] newRoles = new string[roleIds.Length + 1];
newRoles[0] = ServerId; //Everyone
for (int i = 0; i < roleIds.Length; i++)
newRoles[i + 1] = roleIds[i];
_roleIds = newRoles;
} }


public override string ToString() => Name;
internal void UpdateActivity(DateTime? activity = null)
{
if (LastActivityAt == null || activity > LastActivityAt.Value)
LastActivityAt = activity ?? DateTime.UtcNow;
}


internal void AddServer(string serverId)
internal void UpdatePermissions()
{
foreach (var channel in _channels)
UpdatePermissions(channel.Value);
}
internal void UpdatePermissions(Channel channel)
{ {
_servers.TryAdd(serverId, true);
if (_roleIds == null) return; // We don't have all our data processed yet, this will be called again soon

var server = Server;
if (server == null || channel.Server != server) return;
ChannelPermissions permissions;
if (!_permissions.TryGetValue(channel.Id, out permissions)) return;
uint newPermissions = 0x0;
uint oldPermissions = permissions.RawValue;
if (Id == server.Owner.Id)
newPermissions = ChannelPermissions.All(channel).RawValue;
else
{
if (channel == null) return;
var channelOverwrites = channel.PermissionOverwrites;

//var roles = Roles.OrderBy(x => x.Id);
var roles = Roles;
foreach (var serverRole in roles)
newPermissions |= serverRole.Permissions.RawValue;
foreach (var denyRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Deny.RawValue != 0 && roles.Any(y => y.Id == x.TargetId)))
newPermissions &= ~denyRole.Deny.RawValue;
foreach (var allowRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Allow.RawValue != 0 && roles.Any(y => y.Id == x.TargetId)))
newPermissions |= allowRole.Allow.RawValue;
foreach (var denyMembers in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Member && x.TargetId == Id && x.Deny.RawValue != 0))
newPermissions &= ~denyMembers.Deny.RawValue;
foreach (var allowMembers in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Member && x.TargetId == Id && x.Allow.RawValue != 0))
newPermissions |= allowMembers.Allow.RawValue;
}

permissions.SetRawValueInternal(newPermissions);

if (permissions.ManagePermissions)
permissions.SetRawValueInternal(ChannelPermissions.All(channel).RawValue);
/*else if (server.DefaultChannelId == channelId)
permissions.SetBitInternal(PackedPermissions.Text_ReadMessagesBit, true);*/

if (permissions.RawValue != oldPermissions)
channel.InvalidMembersCache();
} }
internal bool RemoveServer(string serverId)
//TODO: Add GetServerPermissions
public ChannelPermissions GetPermissions(Channel channel)
{ {
bool ignored;
return _servers.TryRemove(serverId, out ignored);
if (channel == null) throw new ArgumentNullException(nameof(channel));

ChannelPermissions perms;
if (_permissions.TryGetValue(channel.Id, out perms))
return perms;
return null;
} }


internal void AddRef()
internal void AddChannel(Channel channel)
{ {
Interlocked.Increment(ref _refCount);
var perms = new ChannelPermissions();
perms.Lock();
_channels.TryAdd(channel.Id, channel);
_permissions.TryAdd(channel.Id, perms);
UpdatePermissions(channel);
} }
internal void RemoveRef()
internal void RemoveChannel(Channel channel)
{ {
if (Interlocked.Decrement(ref _refCount) == 0)
_client.Users.TryRemove(Id);
ChannelPermissions ignored;
_channels.TryRemove(channel.Id, out channel);
_permissions.TryRemove(channel.Id, out ignored);
}

public bool HasRole(Role role)
{
if (role == null) throw new ArgumentNullException(nameof(role));

return _roleIds.Contains(role.Id);
} }
} }
}
}

+ 7
- 6
test/Discord.Net.Tests/Tests.cs View File

@@ -59,14 +59,14 @@ namespace Discord.Tests
string name = $"#test_{_random.Next()}"; string name = $"#test_{_random.Next()}";
AssertEvent<ChannelEventArgs>( AssertEvent<ChannelEventArgs>(
"ChannelCreated event never received", "ChannelCreated event never received",
() => channel = _hostClient.CreateChannel(_testServer, name.Substring(1), type).Result,
async () => channel = await _hostClient.CreateChannel(_testServer, name.Substring(1), type),
x => _targetBot.ChannelCreated += x, x => _targetBot.ChannelCreated += x,
x => _targetBot.ChannelCreated -= x, x => _targetBot.ChannelCreated -= x,
(s, e) => e.Channel.Name == name); (s, e) => e.Channel.Name == name);


AssertEvent<ChannelEventArgs>( AssertEvent<ChannelEventArgs>(
"ChannelDestroyed event never received", "ChannelDestroyed event never received",
() => _hostClient.DestroyChannel(channel),
async () => await _hostClient.DestroyChannel(channel),
x => _targetBot.ChannelDestroyed += x, x => _targetBot.ChannelDestroyed += x,
x => _targetBot.ChannelDestroyed -= x, x => _targetBot.ChannelDestroyed -= x,
(s, e) => e.Channel.Name == name); (s, e) => e.Channel.Name == name);
@@ -120,15 +120,15 @@ namespace Discord.Tests
_observerBot.Disconnect()); _observerBot.Disconnect());
} }


private static void AssertEvent<TArgs>(string msg, Action action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test = null)
private static void AssertEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test = null)
{ {
AssertEvent(msg, action, addEvent, removeEvent, test, true); AssertEvent(msg, action, addEvent, removeEvent, test, true);
} }
private static void AssertNoEvent<TArgs>(string msg, Action action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test = null)
private static void AssertNoEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test = null)
{ {
AssertEvent(msg, action, addEvent, removeEvent, test, false); AssertEvent(msg, action, addEvent, removeEvent, test, false);
} }
private static void AssertEvent<TArgs>(string msg, Action action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test, bool assertTrue)
private static void AssertEvent<TArgs>(string msg, Func<Task> action, Action<EventHandler<TArgs>> addEvent, Action<EventHandler<TArgs>> removeEvent, Func<object, TArgs, bool> test, bool assertTrue)
{ {
ManualResetEventSlim trigger = new ManualResetEventSlim(false); ManualResetEventSlim trigger = new ManualResetEventSlim(false);
bool result = false; bool result = false;
@@ -145,8 +145,9 @@ namespace Discord.Tests
}; };


addEvent(handler); addEvent(handler);
action();
var task = action();
trigger.Wait(EventTimeout); trigger.Wait(EventTimeout);
task.Wait();
removeEvent(handler); removeEvent(handler);
Assert.AreEqual(assertTrue, result, msg); Assert.AreEqual(assertTrue, result, msg);


Loading…
Cancel
Save