From dd6ceb84690e36ff9ddc8799522e0d94c5c2a08d Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 24 Oct 2015 01:39:06 -0300 Subject: [PATCH] Started major API refactor --- .../CommandsPlugin.Events.cs | 6 +- src/Discord.Net.Commands/CommandsPlugin.cs | 6 +- src/Discord.Net.Net45/Discord.Net.csproj | 21 +- src/Discord.Net/DiscordAPIClient.cs | 4 +- src/Discord.Net/DiscordClient.Bans.cs | 45 +-- src/Discord.Net/DiscordClient.Channels.cs | 91 +++--- src/Discord.Net/DiscordClient.Invites.cs | 58 ++-- src/Discord.Net/DiscordClient.Members.cs | 90 +++--- src/Discord.Net/DiscordClient.Messages.cs | 181 +++++------ src/Discord.Net/DiscordClient.Permissions.cs | 73 +---- src/Discord.Net/DiscordClient.Roles.cs | 49 +-- src/Discord.Net/DiscordClient.Users.cs | 65 +--- src/Discord.Net/DiscordClient.Voice.cs | 13 +- src/Discord.Net/DiscordClient.cs | 120 ++++---- src/Discord.Net/DiscordWSClient.Voice.cs | 10 +- src/Discord.Net/DiscordWSClient.cs | 38 +-- .../{Models => Helpers}/AsyncCollection.cs | 2 +- src/Discord.Net/Helpers/Mention.cs | 53 +++- src/Discord.Net/Helpers/MentionHelper.cs | 48 --- .../Helpers/Shared/CollectionHelper.cs | 55 ---- .../Helpers/{Shared => }/TaskHelper.cs | 0 src/Discord.Net/Models/CachedObject.cs | 35 +++ src/Discord.Net/Models/Channel.cs | 115 +++---- src/Discord.Net/Models/GlobalUser.cs | 73 +++++ src/Discord.Net/Models/Invite.cs | 43 ++- src/Discord.Net/Models/Member.cs | 279 ----------------- src/Discord.Net/Models/Message.cs | 58 ++-- src/Discord.Net/Models/Role.cs | 44 ++- src/Discord.Net/Models/Server.cs | 145 ++++----- src/Discord.Net/Models/User.cs | 284 ++++++++++++++---- test/Discord.Net.Tests/Tests.cs | 13 +- 31 files changed, 838 insertions(+), 1279 deletions(-) rename src/Discord.Net/{Models => Helpers}/AsyncCollection.cs (99%) delete mode 100644 src/Discord.Net/Helpers/MentionHelper.cs delete mode 100644 src/Discord.Net/Helpers/Shared/CollectionHelper.cs rename src/Discord.Net/Helpers/{Shared => }/TaskHelper.cs (100%) create mode 100644 src/Discord.Net/Models/CachedObject.cs create mode 100644 src/Discord.Net/Models/GlobalUser.cs delete mode 100644 src/Discord.Net/Models/Member.cs diff --git a/src/Discord.Net.Commands/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandsPlugin.Events.cs index c33f751ad..30d6c8e1c 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.Events.cs @@ -11,14 +11,10 @@ namespace Discord.Commands public string ArgText { get; } public int? Permissions { get; } public string[] Args { get; } - - public User User => Message.User; - public string UserId => Message.UserId; + public Member Member => Message.Member; public Channel Channel => Message.Channel; - public string ChannelId => Message.ChannelId; 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) { diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 478a584db..0da770ab2 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -8,7 +8,7 @@ namespace Discord.Commands { private readonly DiscordClient _client; private List _commands; - private Func _getPermissions; + private Func _getPermissions; public IEnumerable Commands => _commands; @@ -17,7 +17,7 @@ namespace Discord.Commands public bool RequireCommandCharInPublic { get; set; } public bool RequireCommandCharInPrivate { get; set; } - public CommandsPlugin(DiscordClient client, Func getPermissions = null) + public CommandsPlugin(DiscordClient client, Func getPermissions = null) { _client = client; _getPermissions = getPermissions; @@ -96,7 +96,7 @@ namespace Discord.Commands argText = msg.Substring(args[cmd.Parts.Length].Index); //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); if (permissions < cmd.MinPerms) { diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj index 954c2e920..86b223f46 100644 --- a/src/Discord.Net.Net45/Discord.Net.csproj +++ b/src/Discord.Net.Net45/Discord.Net.csproj @@ -202,6 +202,9 @@ DiscordWSClientConfig.cs + + Helpers\AsyncCollection.cs + Helpers\EpochTime.cs @@ -214,14 +217,8 @@ Helpers\Mention.cs - - Helpers\MentionHelper.cs - - - Helpers\Shared\CollectionHelper.cs - - Helpers\Shared\TaskHelper.cs + Helpers\TaskHelper.cs Helpers\TimeoutException.cs @@ -229,8 +226,8 @@ HttpException.cs - - Models\AsyncCollection.cs + + Models\CachedObject.cs Models\Channel.cs @@ -238,12 +235,12 @@ Models\Color.cs + + Models\GlobalUser.cs + Models\Invite.cs - - Models\Member.cs - Models\Message.cs diff --git a/src/Discord.Net/DiscordAPIClient.cs b/src/Discord.Net/DiscordAPIClient.cs index 344013019..29bd540b0 100644 --- a/src/Discord.Net/DiscordAPIClient.cs +++ b/src/Discord.Net/DiscordAPIClient.cs @@ -110,11 +110,11 @@ namespace Discord } //Invites - public Task CreateInvite(string channelId, int maxAge, int maxUses, bool isTemporary, bool withXkcdPass) + public Task CreateInvite(string channelId, int maxAge, int maxUses, bool tempMembership, bool hasXkcd) { 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(Endpoints.ChannelInvites(channelId), request); } public Task GetInvite(string inviteIdOrXkcd) diff --git a/src/Discord.Net/DiscordClient.Bans.cs b/src/Discord.Net/DiscordClient.Bans.cs index 4c7563447..3d1d631b3 100644 --- a/src/Discord.Net/DiscordClient.Bans.cs +++ b/src/Discord.Net/DiscordClient.Bans.cs @@ -7,14 +7,11 @@ namespace Discord { public class BanEventArgs : EventArgs { - public User User { get; } public string UserId { 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; Server = server; } @@ -26,57 +23,31 @@ namespace Discord private void RaiseBanAdded(string userId, Server server) { 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 BanRemoved; private void RaiseBanRemoved(string userId, Server server) { if (BanRemoved != null) - RaiseEvent(nameof(BanRemoved), () => BanRemoved(this, new BanEventArgs(_users[userId], userId, server))); + RaiseEvent(nameof(BanRemoved), () => BanRemoved(this, new BanEventArgs(userId, server))); } /// Bans a user from the provided server. public Task Ban(Member member) - => Ban(member?.ServerId, member?.UserId); - /// Bans a user from the provided server. - public Task Ban(Server server, User user) - => Ban(server?.Id, user?.Id); - /// Bans a user from the provided server. - public Task Ban(Server server, string userId) - => Ban(server?.Id, userId); - /// Bans a user from the provided server. - public Task Ban(string server, User user) - => Ban(server, user?.Id); - /// Bans a user from the provided server. - public Task Ban(string serverId, string userId) { 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); } /// Unbans a user from the provided server. - public Task Unban(Member member) - => Unban(member?.ServerId, member?.UserId); - /// Unbans a user from the provided server. - public Task Unban(Server server, User user) - => Unban(server?.Id, user?.Id); - /// Unbans a user from the provided server. - public Task Unban(Server server, string userId) - => Unban(server?.Id, userId); - /// Unbans a user from the provided server. - public Task Unban(string server, User user) - => Unban(server, user?.Id); - /// Unbans a user from the provided server. - public async Task Unban(string serverId, string userId) + public async Task Unban(Member member) { 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) { } } } diff --git a/src/Discord.Net/DiscordClient.Channels.cs b/src/Discord.Net/DiscordClient.Channels.cs index 764ed0351..0d69cdb63 100644 --- a/src/Discord.Net/DiscordClient.Channels.cs +++ b/src/Discord.Net/DiscordClient.Channels.cs @@ -18,9 +18,7 @@ namespace Discord public class ChannelEventArgs : EventArgs { public Channel Channel { get; } - public string ChannelId => Channel.Id; public Server Server => Channel.Server; - public string ServerId => Channel.ServerId; internal ChannelEventArgs(Channel channel) { Channel = channel; } } @@ -49,29 +47,30 @@ namespace Discord RaiseEvent(nameof(ChannelUpdated), () => ChannelUpdated(this, new ChannelEventArgs(channel))); } - /// Returns the channel with the specified id, or null if none was found. - public Channel GetChannel(string id) => _channels[id]; - /// Returns all channels with the specified server and name. - /// Name formats supported: Name and #Name. Search is case-insensitive. - public IEnumerable 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]; + } + /// Returns all channels with the specified server and name. /// Name formats supported: Name and #Name. Search is case-insensitive. - public IEnumerable FindChannels(string serverId, string name, string type = null) + public IEnumerable 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 result; if (name.StartsWith("#")) { 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, name2, StringComparison.OrdinalIgnoreCase)); } else { - result = _channels.Where(x => x.ServerId == serverId && + result = _channels.Where(x => x.Server == server && string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); } @@ -82,55 +81,44 @@ namespace Discord } /// Creates a new channel with the provided name and type (see ChannelTypes). - public Task CreateChannel(Server server, string name, string type = ChannelTypes.Text) - => CreateChannel(server?.Id, name, type); - /// Creates a new channel with the provided name and type (see ChannelTypes). - public async Task CreateChannel(string serverId, string name, string type = ChannelTypes.Text) + public async Task 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 (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); channel.Update(response); return channel; } - - /// Returns the private channel with the provided user, creating one if it does not currently exist. - public Task CreatePMChannel(string userId) => CreatePMChannel(_users[userId], userId); + /// Returns the private channel with the provided user, creating one if it does not currently exist. - public Task CreatePMChannel(User user) => CreatePMChannel(user, user?.Id); - /// Returns the private channel with the provided user, creating one if it does not currently exist. - public Task CreatePMChannel(Member member) => CreatePMChannel(member.User, member.UserId); - private async Task CreatePMChannel(User user, string userId) + public async Task CreatePMChannel(Member member) { + if (member == null) throw new ArgumentNullException(nameof(member)); CheckReady(); - if (userId == null) throw new ArgumentNullException(nameof(userId)); Channel channel = null; - if (user != null) - channel = user.PrivateChannel; + if (member != null) + channel = member.GlobalUser.PrivateChannel; 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.Update(response); } return channel; } - - /// Edits the provided channel, changing only non-null attributes. - public Task EditChannel(string channelId, string name = null, string topic = null, int? position = null) - => EditChannel(_channels[channelId], name: name, topic: topic, position: position); + /// Edits the provided channel, changing only non-null attributes. public async Task EditChannel(Channel channel, string name = null, string topic = null, int? position = null) { - CheckReady(); if (channel == null) throw new ArgumentNullException(nameof(channel)); + CheckReady(); await _api.EditChannel(channel.Id, name: name, topic: topic); @@ -155,34 +143,29 @@ namespace Discord channels[i] = channels[i - 1]; 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 channels, int startPos = 0) - => ReorderChannels(server.Id, channels, startPos); - public Task ReorderChannels(string serverId, IEnumerable channels, int startPos = 0) + + /// Reorders the provided channels in the server's channel list and places them after a certain channel. + public Task ReorderChannels(Server server, IEnumerable 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 (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); } - + /// Destroys the provided channel. - public Task DestroyChannel(Channel channel) - => DestroyChannel(channel?.Id); - /// Destroys the provided channel. - public async Task DestroyChannel(string channelId) + public async Task DestroyChannel(Channel channel) { 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) { } - return _channels.TryRemove(channelId); + return _channels.TryRemove(channel.Id); } } } \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.Invites.cs b/src/Discord.Net/DiscordClient.Invites.cs index cda080e19..d2aed4501 100644 --- a/src/Discord.Net/DiscordClient.Invites.cs +++ b/src/Discord.Net/DiscordClient.Invites.cs @@ -1,4 +1,3 @@ -using Discord.Net; using System; using System.Net; using System.Threading.Tasks; @@ -14,6 +13,14 @@ namespace Discord CheckReady(); 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 invite = new Invite(this, response.Code, response.XkcdPass, response.Guild.Id); invite.Update(response); @@ -26,44 +33,36 @@ namespace Discord /// If true, creates a human-readable link. Not supported if maxAge is set to 0. /// The max amount of times this invite may be used. Set to 0 to have unlimited uses. public Task CreateInvite(Server server, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false) - => CreateInvite(server?.DefaultChannelId, maxAge, maxUses, tempMembership, hasXkcd); - /// Creates a new invite to the provided channel. - /// Time (in seconds) until the invite expires. Set to 0 to never expire. - /// If true, a user accepting this invite will be kicked from the server after closing their client. - /// If true, creates a human-readable link. Not supported if maxAge is set to 0. - /// The max amount of times this invite may be used. Set to 0 to have unlimited uses. - public Task 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); + } /// Creates a new invite to the provided channel. /// Time (in seconds) until the invite expires. Set to 0 to never expire. /// If true, a user accepting this invite will be kicked from the server after closing their client. /// If true, creates a human-readable link. Not supported if maxAge is set to 0. /// The max amount of times this invite may be used. Set to 0 to have unlimited uses. - public async Task CreateInvite(string serverOrChannelId, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false) + public async Task 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 (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); invite.Update(response); return invite; } /// Deletes the provided invite. - public async Task DestroyInvite(string inviteId) + public async Task DestroyInvite(Invite invite) { 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) { } } @@ -75,22 +74,5 @@ namespace Discord return _api.AcceptInvite(invite.Id); } - /// Accepts the provided invite. - 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); - } } } \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.Members.cs b/src/Discord.Net/DiscordClient.Members.cs index f92591a00..66edc1c3b 100644 --- a/src/Discord.Net/DiscordClient.Members.cs +++ b/src/Discord.Net/DiscordClient.Members.cs @@ -23,8 +23,7 @@ namespace Discord public class MemberEventArgs : EventArgs { 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 string ServerId => Member.ServerId; @@ -66,80 +65,65 @@ namespace Discord if (UserIsSpeaking != null) RaiseEvent(nameof(UserIsSpeaking), () => UserIsSpeaking(this, new MemberIsSpeakingEventArgs(member, channel, isSpeaking))); } - + + private Member _currentUser; + internal Members Members => _members; private readonly Members _members; - - /// Returns the user with the specified id, along with their server-specific data, or null if none was found. - public Member GetMember(Server server, User user) => _members[user?.Id, server?.Id]; - /// Returns the user with the specified id, along with their server-specific data, or null if none was found. - public Member GetMember(Server server, string userId) => _members[userId, server?.Id]; - /// Returns the user with the specified id, along with their server-specific data, or null if none was found. - public Member GetMember(string serverId, User user) => _members[user?.Id, serverId]; + /// Returns the user with the specified id, along with their server-specific data, or null if none was found. - public Member GetMember(string serverId, string userId) => _members[userId, serverId]; - /// Returns the user with the specified name and discriminator, along withtheir server-specific data, or null if they couldn't be found. - /// Name formats supported: Name and @Name. Search is case-insensitive. - 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]; + } /// Returns the user with the specified name and discriminator, along withtheir server-specific data, or null if they couldn't be found. /// Name formats supported: Name and @Name. Search is case-insensitive. - 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]; } /// Returns all users in with the specified server and name, along with their server-specific data. /// Name formats supported: Name and @Name. Search is case-insensitive. - public IEnumerable FindMembers(string serverId, string name) => FindMembers(_servers[serverId], name); - /// Returns all users in with the specified server and name, along with their server-specific data. - /// Name formats supported: Name and @Name. Search is case-insensitive. - public IEnumerable FindMembers(Server server, string name) + public IEnumerable FindMembers(Server server, string name, string discriminator = null, bool exactMatch = false) { if (server == null) throw new ArgumentNullException(nameof(server)); if (name == null) throw new ArgumentNullException(nameof(name)); + CheckReady(); - if (name.StartsWith("@")) + IEnumerable query; + if (!exactMatch && name.StartsWith("@")) { 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 { - 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 roles = null) - => EditMember(member?.ServerId, member?.UserId, mute, deaf, roles); - public Task EditMember(Server server, User user, bool? mute = null, bool? deaf = null, IEnumerable roles = null) - => EditMember(server?.Id, user?.Id, mute, deaf, roles); - public Task EditMember(Server server, string userId, bool? mute = null, bool? deaf = null, IEnumerable roles = null) - => EditMember(server?.Id, userId, mute, deaf, roles); - public Task EditMember(string serverId, User user, bool? mute = null, bool? deaf = null, IEnumerable roles = null) - => EditMember(serverId, user?.Id, mute, deaf, roles); - public Task EditMember(string serverId, string userId, bool? mute = null, bool? deaf = null, IEnumerable roles = null) + public Task EditMember(Member member, bool? mute = null, bool? deaf = null, IEnumerable roles = null) { + if (member == null) throw new ArgumentNullException(nameof(member)); 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)); } } } \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.Messages.cs b/src/Discord.Net/DiscordClient.Messages.cs index 9c395f894..4c59b374c 100644 --- a/src/Discord.Net/DiscordClient.Messages.cs +++ b/src/Discord.Net/DiscordClient.Messages.cs @@ -20,14 +20,9 @@ namespace Discord public class MessageEventArgs : EventArgs { public Message Message { get; } - public string MessageId => Message.Id; public Member Member => Message.Member; public Channel Channel => Message.Channel; - public string ChannelId => Message.ChannelId; 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; } } @@ -74,165 +69,127 @@ namespace Discord public Message GetMessage(string id) => _messages[id]; /// Sends a message to the provided channel. To include a mention, see the Mention static helper class. - public Task SendMessage(Channel channel, string text) - => SendMessage(channel, text, MentionHelper.GetUserIds(text), false); - /// Sends a message to the provided channel. To include a mention, see the Mention static helper class. - public Task SendMessage(string channelId, string text) - => SendMessage(_channels[channelId], text, MentionHelper.GetUserIds(text), false); - private async Task SendMessage(Channel channel, string text, IEnumerable mentionedUsers = null, bool isTextToSpeech = false) + public Task SendMessage(Channel channel, string text) { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (text == null) throw new ArgumentNullException(nameof(text)); CheckReady(); + + return SendMessage(channel, text, false); + } + /// Sends a text-to-speech message to the provided channel. To include a mention, see the Mention static helper class. + public Task SendTTSMessage(Channel channel, string text) + { if (channel == null) throw new ArgumentNullException(nameof(channel)); 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); + } + /// Sends a private message to the provided user. + public async Task 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 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(); - 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; } - /// Sends a private message to the provided user. - public Task SendPrivateMessage(Member member, string text) - => SendPrivateMessage(member?.UserId, text); - /// Sends a private message to the provided user. - public Task SendPrivateMessage(User user, string text) - => SendPrivateMessage(user?.Id, text); - /// Sends a private message to the provided user. - public async Task SendPrivateMessage(string userId, string text) - { - var channel = await CreatePMChannel(userId).ConfigureAwait(false); - return await SendMessage(channel, text, new string[0]).ConfigureAwait(false); - } /// Sends a file to the provided channel. public Task SendFile(Channel channel, string filePath) - => SendFile(channel?.Id, filePath); - /// Sends a file to the provided channel. - 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)); + CheckReady(); - return _api.SendFile(channelId, filePath); + return _api.SendFile(channel.Id, filePath); } /// Edits the provided message, changing only non-null attributes. /// While not required, it is recommended to include a mention reference in the text (see Mention.User). - public Task EditMessage(Message message, string text = null, IEnumerable mentionedUsers = null) - => EditMessage(message?.ChannelId, message?.Id, text, mentionedUsers); - /// Edits the provided message, changing only non-null attributes. - /// While not required, it is recommended to include a mention reference in the text (see Mention.User). - public Task EditMessage(Channel channel, string messageId, string text = null, IEnumerable mentionedUsers = null) - => EditMessage(channel?.Id, messageId, text, mentionedUsers); - /// Edits the provided message, changing only non-null attributes. - /// While not required, it is recommended to include a mention reference in the text (see Mention.User). - public async Task EditMessage(string channelId, string messageId, string text = null, IEnumerable mentionedUsers = null) + public async Task EditMessage(Message message, string text) { + if (message == null) throw new ArgumentNullException(nameof(message)); 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) 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); } /// Deletes the provided message. - public Task DeleteMessage(Message msg) - => DeleteMessage(msg?.ChannelId, msg?.Id); - /// Deletes the provided message. - public async Task DeleteMessage(string channelId, string msgId) + public async Task DeleteMessage(Message message) { + if (message == null) throw new ArgumentNullException(nameof(message)); CheckReady(); - if (channelId == null) throw new ArgumentNullException(nameof(channelId)); - if (msgId == null) throw new ArgumentNullException(nameof(msgId)); 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) { } } - public async Task DeleteMessages(IEnumerable 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 msgIds) + public async Task DeleteMessages(IEnumerable messages) { + if (messages == null) throw new ArgumentNullException(nameof(messages)); CheckReady(); - if (msgIds == null) throw new ArgumentNullException(nameof(msgIds)); - foreach (var msgId in msgIds) + foreach (var message in messages) { 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) { } } } /// Downloads last count messages from the server, starting at beforeMessageId if it's provided. - public Task DownloadMessages(Channel channel, int count, string beforeMessageId = null, bool cache = true) - => DownloadMessages(channel.Id, count, beforeMessageId, cache); - /// Downloads last count messages from the server, starting at beforeMessageId if it's provided. - public async Task DownloadMessages(string channelId, int count, string beforeMessageId = null, bool cache = true) + public async Task 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) return new Message[0]; + CheckReady(); - Channel channel = _channels[channelId]; + if (count == 0) return new Message[0]; if (channel != null && channel.Type == ChannelTypes.Text) { try @@ -283,7 +240,7 @@ namespace Discord SendMessageResponse response = null; 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 (HttpException) { hasFailed = true; } diff --git a/src/Discord.Net/DiscordClient.Permissions.cs b/src/Discord.Net/DiscordClient.Permissions.cs index f8ea29353..b67bacac3 100644 --- a/src/Discord.Net/DiscordClient.Permissions.cs +++ b/src/Discord.Net/DiscordClient.Permissions.cs @@ -8,47 +8,13 @@ namespace Discord public partial class DiscordClient { 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) - => 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) => 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) => 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) { CheckReady(); @@ -103,34 +69,23 @@ namespace Discord } 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) - => 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 (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 { var perms = channel.PermissionOverwrites.Where(x => x.TargetType != idType || x.TargetId != userOrRoleId).FirstOrDefault(); diff --git a/src/Discord.Net/DiscordClient.Roles.cs b/src/Discord.Net/DiscordClient.Roles.cs index 0d8fcf1ec..2248baed9 100644 --- a/src/Discord.Net/DiscordClient.Roles.cs +++ b/src/Discord.Net/DiscordClient.Roles.cs @@ -33,9 +33,7 @@ namespace Discord public class RoleEventArgs : EventArgs { public Role Role { get; } - public string RoleId => Role.Id; public Server Server => Role.Server; - public string ServerId => Role.ServerId; internal RoleEventArgs(Role role) { Role = role; } } @@ -68,23 +66,20 @@ namespace Discord public Role GetRole(string id) => _roles[id]; /// Returns all roles with the specified server and name. /// Name formats supported: Name and @Name. Search is case-insensitive. - public IEnumerable FindRoles(Server server, string name) => FindRoles(server?.Id, name); - /// Returns all roles with the specified server and name. - /// Name formats supported: Name and @Name. Search is case-insensitive. - public IEnumerable FindRoles(string serverId, string name) + public IEnumerable 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.StartsWith("@")) { 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)); } else { - return _roles.Where(x => x.ServerId == serverId && + return _roles.Where(x => x.Server.Id == server.Id && string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); } } @@ -106,16 +101,14 @@ namespace Discord 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) { - CheckReady(); if (role == null) throw new ArgumentNullException(nameof(role)); + CheckReady(); //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, permissions: permissions?.RawValue ?? role.Permissions.RawValue, color: color?.RawValue, @@ -142,40 +135,26 @@ namespace Discord roles[i] = roles[i - 1]; 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) - => DeleteRole(role?.ServerId, role?.Id); - public Task DeleteRole(string serverId, string roleId) { + if (role == null) throw new ArgumentNullException(nameof(role)); 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 roles, int startPos = 0) - => ReorderChannels(server.Id, roles, startPos); - public Task ReorderRoles(string serverId, IEnumerable roles, int startPos = 0) + public Task ReorderRoles(Server server, IEnumerable 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 (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); } } } \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.Users.cs b/src/Discord.Net/DiscordClient.Users.cs index 21b432beb..6b4bdcf66 100644 --- a/src/Discord.Net/DiscordClient.Users.cs +++ b/src/Discord.Net/DiscordClient.Users.cs @@ -6,20 +6,12 @@ using System.Threading.Tasks; namespace Discord { - internal sealed class Users : AsyncCollection + internal sealed class Users : AsyncCollection { public Users(DiscordClient client, object writerLock) : 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 @@ -36,11 +28,11 @@ namespace Discord if (UserRemoved != null) RaiseEvent(nameof(UserRemoved), () => UserRemoved(this, new MemberEventArgs(member))); } - public event EventHandler 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 MemberUpdated; private void RaiseMemberUpdated(Member member) @@ -65,55 +57,14 @@ namespace Discord internal Users Users => _users; private readonly Users _users; - /// Returns the current logged-in user. - public User CurrentUser => _currentUser; - private User _currentUser; - - /// Returns the user with the specified id, or null if none was found. - public User GetUser(string id) => _users[id]; - /// Returns the user with the specified name and discriminator, or null if none was found. - /// Name formats supported: Name and @Name. Search is case-insensitive. - 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(); - } - - /// Returns all users with the specified name across all servers. - /// Name formats supported: Name and @Name. Search is case-insensitive. - public IEnumerable 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 EditProfile(string currentPassword = "", string username = null, string email = null, string password = null, ImageType avatarType = ImageType.Png, byte[] avatar = null) { 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); } diff --git a/src/Discord.Net/DiscordClient.Voice.cs b/src/Discord.Net/DiscordClient.Voice.cs index 56f4e2635..6562a9461 100644 --- a/src/Discord.Net/DiscordClient.Voice.cs +++ b/src/Discord.Net/DiscordClient.Voice.cs @@ -47,18 +47,13 @@ namespace Discord return client; } - public Task JoinVoiceServer(Channel channel) - => JoinVoiceServer(channel?.ServerId, channel?.Id); - public Task JoinVoiceServer(Server server, string channelId) - => JoinVoiceServer(server?.Id, channelId); - public async Task JoinVoiceServer(string serverId, string channelId) + public async Task JoinVoiceServer(Channel channel) { + if (channel == null) throw new ArgumentNullException(nameof(channel)); 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; } diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 819e16421..bdc6e5172 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -10,9 +10,9 @@ using System.Threading.Tasks; namespace Discord { /// Provides a connection to the DiscordApp service. - 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 JsonSerializer _serializer; private readonly ConcurrentQueue _pendingMessages; @@ -24,6 +24,9 @@ namespace Discord public new DiscordClientConfig Config => _config as DiscordClientConfig; + /// Returns the current logged-in user. + public Member CurrentUser => _currentUser; + /// Initializes a new instance of the DiscordClient class. public DiscordClient(DiscordClientConfig config = null) : 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) { 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, - $"Destroyed Server: {e.Server?.Name ?? e.ServerId}"); + $"Destroyed Server: {e.Server?.Name}"); 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, - $"Server Available: {e.Server?.Name ?? e.ServerId}"); + $"Server Available: {e.Server?.Name}"); 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, - $"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, - $"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, - $"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, - $"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, - $"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, - $"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, - $"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, - $"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, - $"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, - $"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, - $"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, - $"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, - $"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, - $"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, - $"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) { 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, - $"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, - $"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, - $"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) => { - 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})"); else 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) { - _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"); - _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"); - _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"); - _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"); _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}"); @@ -295,7 +293,7 @@ namespace Discord case "READY": //Resync { var data = e.Payload.ToObject(_serializer); - _currentUser = _users.GetOrAdd(data.User.Id); + _currentUser = _members.GetOrAdd(data.User.Id, _servers.PMServer.Id); _currentUser.Update(data.User); foreach (var model in data.Guilds) { @@ -307,7 +305,7 @@ namespace Discord } 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); var channel = _channels.GetOrAdd(model.Id, null, user.Id); channel.Update(model); @@ -362,9 +360,9 @@ namespace Discord Channel channel; 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 channel = _channels.GetOrAdd(data.Id, data.GuildId, null); @@ -396,8 +394,6 @@ namespace Discord case "GUILD_MEMBER_ADD": { var data = e.Payload.ToObject(_serializer); - var user = _users.GetOrAdd(data.User.Id); - user.Update(data.User); var member = _members.GetOrAdd(data.User.Id, data.GuildId); member.Update(data); if (Config.TrackActivity) @@ -433,7 +429,7 @@ namespace Discord role.Update(data.Data); var server = _servers[data.GuildId]; if (server != null) - server.AddRole(data.Data.Id); + server.AddRole(role); RaiseRoleUpdated(role); } break; @@ -442,19 +438,23 @@ namespace Discord var data = e.Payload.ToObject(_serializer); var role = _roles[data.Data.Id]; if (role != null) + { role.Update(data.Data); - RaiseRoleUpdated(role); + RaiseRoleUpdated(role); + } } break; case "GUILD_ROLE_DELETE": { var data = e.Payload.ToObject(_serializer); - var server = _servers[data.GuildId]; - if (server != null) - server.RemoveRole(data.RoleId); var role = _roles.TryRemove(data.RoleId); if (role != null) + { RaiseRoleDeleted(role); + var server = _servers[data.GuildId]; + if (server != null) + server.RemoveRole(role); + } } break; @@ -485,7 +485,7 @@ namespace Discord var data = e.Payload.ToObject(_serializer); Message msg = null; - bool isAuthor = data.Author.Id == CurrentUserId; + bool isAuthor = data.Author.Id == _userId; bool hasFinishedSending = false; if (Config.UseMessageQueue && isAuthor && data.Nonce != null) { @@ -566,7 +566,7 @@ namespace Discord var channel = _channels[data.ChannelId]; if (channel != null) { - var user = _members[data.UserId, channel.ServerId]; + var user = _members[data.UserId, channel.Server.Id]; if (user != null) { @@ -577,7 +577,7 @@ namespace Discord { if (!channel.IsPrivate) { - var member = _members[data.UserId, channel.ServerId]; + var member = _members[data.UserId, channel.Server.Id]; if (member != null) member.UpdateActivity(); } @@ -612,7 +612,7 @@ namespace Discord if (user != null) { user.Update(data); - RaiseUserUpdated(user); + RaiseProfileUpdated(user); } } break; diff --git a/src/Discord.Net/DiscordWSClient.Voice.cs b/src/Discord.Net/DiscordWSClient.Voice.cs index 0d1a7ca4b..e04caa8a1 100644 --- a/src/Discord.Net/DiscordWSClient.Voice.cs +++ b/src/Discord.Net/DiscordWSClient.Voice.cs @@ -17,7 +17,7 @@ namespace Discord await _voiceSocket.SetChannel(_voiceServerId, channelId).ConfigureAwait(false); _dataSocket.SendJoinVoice(_voiceServerId, channelId); - await _voiceSocket.WaitForConnection(_config.ConnectionTimeout); + await _voiceSocket.WaitForConnection(_config.ConnectionTimeout).ConfigureAwait(false); } /// Sends a PCM frame to the voice server. Will block until space frees up in the outgoing buffer. @@ -25,12 +25,12 @@ namespace Discord /// Number of bytes in this frame. void IDiscordVoiceClient.SendVoicePCM(byte[] data, int count) { - CheckReady(checkVoice: true); if (data == null) throw new ArgumentException(nameof(data)); 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); } /// Clears the PCM buffer. void IDiscordVoiceClient.ClearVoicePCM() diff --git a/src/Discord.Net/DiscordWSClient.cs b/src/Discord.Net/DiscordWSClient.cs index ba24c0271..f9f6d621e 100644 --- a/src/Discord.Net/DiscordWSClient.cs +++ b/src/Discord.Net/DiscordWSClient.cs @@ -20,27 +20,21 @@ namespace Discord /// Provides a minimalistic websocket connection to the Discord service. public partial class DiscordWSClient { - internal readonly DataWebSocket _dataSocket; - internal readonly VoiceWebSocket _voiceSocket; + protected readonly DiscordWSClientConfig _config; protected readonly ManualResetEvent _disconnectedEvent; 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 _voiceServerId; + protected string _userId, _voiceServerId; private Task _runTask; - - protected ExceptionDispatchInfo _disconnectReason; private bool _wasDisconnectUnexpected; - /// 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. - public DiscordWSClientConfig Config => _config; - protected readonly DiscordWSClientConfig _config; + public string CurrentUserId => _userId; - /// Returns the id of the current logged-in user. - public string CurrentUserId => _currentUserId; - private string _currentUserId; - /*/// Returns the server this user is currently connected to for voice. - public string CurrentVoiceServerId => _voiceSocket.CurrentServerId;*/ + /// 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. + public DiscordWSClientConfig Config => _config; /// Returns the current connection state of this client. public DiscordClientState State => (DiscordClientState)_state; @@ -56,15 +50,13 @@ namespace Discord _config = config ?? new DiscordWSClientConfig(); _config.Lock(); - _enableVoice = _config.EnableVoice; - _state = (int)DiscordClientState.Disconnected; _cancelToken = new CancellationToken(true); _disconnectedEvent = new ManualResetEvent(true); _connectedEvent = new ManualResetEventSlim(false); _dataSocket = CreateDataSocket(); - if (_enableVoice) + if (_config.EnableVoice) _voiceSocket = CreateVoiceSocket(); } internal DiscordWSClient(DiscordWSClientConfig config = null, string voiceServerId = null) @@ -247,7 +239,7 @@ namespace Discord protected virtual async Task Cleanup() { - if (_enableVoice) + if (_config.EnableVoice) { string voiceServerId = _voiceSocket.CurrentServerId; if (voiceServerId != null) @@ -256,7 +248,7 @@ namespace Discord } await _dataSocket.Disconnect().ConfigureAwait(false); - _currentUserId = null; + _userId = null; _gateway = null; _token = null; } @@ -286,7 +278,7 @@ namespace Discord throw new InvalidOperationException("The client is connecting."); } - if (checkVoice && !_enableVoice) + if (checkVoice && !_config.EnableVoice) throw new InvalidOperationException("Voice is not enabled for this client."); } protected void RaiseEvent(string name, Action action) @@ -307,17 +299,17 @@ namespace Discord switch (e.Type) { case "READY": - _currentUserId = e.Payload["user"].Value("id"); + _userId = e.Payload["user"].Value("id"); break; case "VOICE_SERVER_UPDATE": { string guildId = e.Payload.Value("guild_id"); - if (_enableVoice && guildId == _voiceSocket.CurrentServerId) + if (_config.EnableVoice && guildId == _voiceSocket.CurrentServerId) { string token = e.Payload.Value("token"); _voiceSocket.Host = "wss://" + e.Payload.Value("endpoint").Split(':')[0]; - return _voiceSocket.Login(_currentUserId, _dataSocket.SessionId, token, CancelToken); + return _voiceSocket.Login(_userId, _dataSocket.SessionId, token, CancelToken); } } break; diff --git a/src/Discord.Net/Models/AsyncCollection.cs b/src/Discord.Net/Helpers/AsyncCollection.cs similarity index 99% rename from src/Discord.Net/Models/AsyncCollection.cs rename to src/Discord.Net/Helpers/AsyncCollection.cs index 3ae26a0a5..cc3cc9413 100644 --- a/src/Discord.Net/Models/AsyncCollection.cs +++ b/src/Discord.Net/Helpers/AsyncCollection.cs @@ -7,7 +7,7 @@ using System.Linq; namespace Discord { internal abstract class AsyncCollection : IEnumerable - where TValue : class + where TValue : CachedObject { private readonly object _writerLock; diff --git a/src/Discord.Net/Helpers/Mention.cs b/src/Discord.Net/Helpers/Mention.cs index 29c3bde54..6288ab2f0 100644 --- a/src/Discord.Net/Helpers/Mention.cs +++ b/src/Discord.Net/Helpers/Mention.cs @@ -1,26 +1,53 @@ -namespace Discord +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Discord { public static class Mention { - /// Returns the string used to create a user mention. - 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); + /// Returns the string used to create a user mention. public static string User(Member member) - => $"<@{member.UserId}>"; - /// Returns the string used to create a user mention. - public static string User(string userId) - => $"<@{userId}>"; - + => $"<@{member.Id}>"; /// Returns the string used to create a channel mention. public static string Channel(Channel channel) => $"<#{channel.Id}>"; /// Returns the string used to create a channel mention. - public static string Channel(string channelId) - => $"<#{channelId}>"; - - /// Returns the string used to create a channel mention. public static string 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 GetUserIds(string text) + { + return _userRegex.Matches(text) + .OfType() + .Select(x => x.Groups[1].Value) + .Where(x => x != null); + } } } diff --git a/src/Discord.Net/Helpers/MentionHelper.cs b/src/Discord.Net/Helpers/MentionHelper.cs deleted file mode 100644 index b630c61a3..000000000 --- a/src/Discord.Net/Helpers/MentionHelper.cs +++ /dev/null @@ -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 GetUserIds(string text) - { - return _userRegex.Matches(text) - .OfType() - .Select(x => x.Groups[1].Value) - .Where(x => x != null); - } - } -} diff --git a/src/Discord.Net/Helpers/Shared/CollectionHelper.cs b/src/Discord.Net/Helpers/Shared/CollectionHelper.cs deleted file mode 100644 index 127ea1da2..000000000 --- a/src/Discord.Net/Helpers/Shared/CollectionHelper.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Discord -{ - internal static class CollectionHelper - { - public static IEnumerable FlattenChannels(IEnumerable 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 FlattenUsers(IEnumerable 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 FlattenRoles(IEnumerable 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)); - }); - } - } -} diff --git a/src/Discord.Net/Helpers/Shared/TaskHelper.cs b/src/Discord.Net/Helpers/TaskHelper.cs similarity index 100% rename from src/Discord.Net/Helpers/Shared/TaskHelper.cs rename to src/Discord.Net/Helpers/TaskHelper.cs diff --git a/src/Discord.Net/Models/CachedObject.cs b/src/Discord.Net/Models/CachedObject.cs new file mode 100644 index 000000000..ff5413002 --- /dev/null +++ b/src/Discord.Net/Models/CachedObject.cs @@ -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; + } + + /// Returns the unique identifier for this object. + 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(); + } +} diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs index 534eff650..3044507cf 100644 --- a/src/Discord.Net/Models/Channel.cs +++ b/src/Discord.Net/Models/Channel.cs @@ -7,7 +7,7 @@ using System.Linq; namespace Discord { - public sealed class Channel + public sealed class Channel : CachedObject { public sealed class PermissionOverwrite { @@ -21,44 +21,37 @@ namespace Discord TargetType = targetType; TargetId = targetId; Allow = new ChannelPermissions(allow); - Deny = new ChannelPermissions(deny); Allow.Lock(); + Deny = new ChannelPermissions(deny); Deny.Lock(); } } - - private readonly DiscordClient _client; + private readonly ConcurrentDictionary _messages; private bool _areMembersStale; - private bool _hasRef; - /// Returns the unique identifier for this channel. - public string Id { get; } - - private string _name; + private readonly string _serverId, _recipientId; + private Server _server; + private Member _recipient; + /// Returns the name of this channel. - public string Name { get { return !IsPrivate ? $"{_name}" : $"@{Recipient.Name}"; } internal set { _name = value; } } - + public string Name { get; private set; } /// Returns the topic associated with this channel. public string Topic { get; private set; } /// Returns the position of this channel in the channel list for this server. public int Position { get; private set; } /// Returns false is this is a public chat and true if this is a private chat with another user (see Recipient). - public bool IsPrivate => RecipientId != null; + public bool IsPrivate => _recipientId != null; /// Returns the type of this channel (see ChannelTypes). public string Type { get; private set; } - /// Returns the id of the server containing this channel. - public string ServerId { get; } /// Returns the server containing this channel. [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. [JsonIgnore] - public User Recipient => _client.Users[RecipientId]; + public Member Recipient => _client.Members[_recipientId, _serverId]; /// Returns a collection of the IDs of all users with read access to this channel. public IEnumerable UserIds @@ -68,7 +61,7 @@ namespace Discord if (!_areMembersStale) 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; return _userIds; } @@ -76,10 +69,7 @@ namespace Discord private string[] _userIds; /// Returns a collection of all users with read access to this channel. [JsonIgnore] - public IEnumerable Members => UserIds.Select(x => _client.Members[x, ServerId]); - /// Returns a collection of all users with read access to this channel. - [JsonIgnore] - public IEnumerable Users => UserIds.Select(x => _client.Users[x]); + public IEnumerable Members => UserIds.Select(x => _client.Members[x, _serverId]); /// Returns a collection of the ids of all messages the client has seen posted in this channel. This collection does not guarantee any ordering. [JsonIgnore] @@ -94,64 +84,39 @@ namespace Discord public IEnumerable PermissionOverwrites => _permissionOverwrites; 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(); + _serverId = serverId ?? _client.Servers.PMServer.Id; + _recipientId = recipientId; _permissionOverwrites = _initialPermissionsOverwrites; _areMembersStale = true; + + //Local Cache + _messages = new ConcurrentDictionary(); } - internal void OnCached() + internal override void OnCached() { - var server = Server; - if (server != null) - server.AddChannel(Id); + 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) @@ -199,14 +164,14 @@ namespace Discord { _areMembersStale = true; foreach (var member in Members) - member.UpdatePermissions(Id); + member.UpdatePermissions(this); } internal void InvalidatePermissionsCache(string userId) { _areMembersStale = true; - var member = _client.Members[userId, ServerId]; + var member = _client.Members[userId, _serverId]; if (member != null) - member.UpdatePermissions(Id); + member.UpdatePermissions(this); } } } diff --git a/src/Discord.Net/Models/GlobalUser.cs b/src/Discord.Net/Models/GlobalUser.cs new file mode 100644 index 000000000..f025fe85c --- /dev/null +++ b/src/Discord.Net/Models/GlobalUser.cs @@ -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 _servers; + private int _refCount; + + /// Returns the email for this user. + /// This field is only ever populated for the current logged in user. + [JsonIgnore] + public string Email { get; private set; } + /// Returns if the email for this user has been verified. + /// This field is only ever populated for the current logged in user. + [JsonIgnore] + public bool? IsVerified { get; private set; } + + /// Returns the private messaging channel with this user, if one exists. + [JsonIgnore] + public Channel PrivateChannel { get; internal set; } + + /// Returns a collection of all server-specific data for every server this user is a member of. + [JsonIgnore] + public IEnumerable Memberships => _servers.Select(x => _client.Members[Id, x.Key]); + /// Returns a collection of all servers this user is a member of. + [JsonIgnore] + public IEnumerable Servers => _servers.Select(x => _client.Servers[x.Key]); + + internal GlobalUser(DiscordClient client, string id) + : base(client, id) + { + _servers = new ConcurrentDictionary(); + } + 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; + } +} diff --git a/src/Discord.Net/Models/Invite.cs b/src/Discord.Net/Models/Invite.cs index bdf1e6518..27c1817c2 100644 --- a/src/Discord.Net/Models/Invite.cs +++ b/src/Discord.Net/Models/Invite.cs @@ -1,15 +1,14 @@ -using Discord.API; +using System; +using Discord.API; using Newtonsoft.Json; namespace Discord { - public sealed class Invite + public sealed class Invite : CachedObject { - private readonly DiscordClient _client; - - /// Returns the unique identifier for this invite. - public string Id { get; } - + private readonly string _serverId; + private string _inviterId, _channelId; + /// Time (in seconds) until the invite expires. Set to 0 to never expire. public int MaxAge { get; private set; } /// The amount of times this invite has been used. @@ -25,41 +24,37 @@ namespace Discord /// Returns a URL for this invite using XkcdPass if available or Id if not. public string Url => API.Endpoints.InviteUrl(XkcdPass ?? Id); - - /// Returns the id of the user that created this invite. - public string InviterId { get; private set; } + /// Returns the user that created this invite. [JsonIgnore] - public User Inviter => _client.Users[InviterId]; - - /// Returns the id of the server this invite is to. - public string ServerId { get; } + public Member Inviter => _client.Members[_inviterId, _serverId]; + /// Returns the server this invite is to. [JsonIgnore] - public Server Server => _client.Servers[ServerId]; - - /// Returns the id of the channel this invite is to. - public string ChannelId { get; private set; } + public Server Server => _client.Servers[_serverId]; + /// Returns the channel this invite is to. [JsonIgnore] - public Channel Channel => _client.Channels[ChannelId]; + public Channel Channel => _client.Channels[_channelId]; internal Invite(DiscordClient client, string code, string xkcdPass, string serverId) + : base(client, code) { - _client = client; - Id = code; XkcdPass = xkcdPass; - ServerId = serverId; + _serverId = serverId; } + internal override void OnCached() { } + internal override void OnUncached() { } public override string ToString() => XkcdPass ?? Id; + internal void Update(InviteReference model) { if (model.Channel != null) - ChannelId = model.Channel.Id; + _channelId = model.Channel.Id; if (model.Inviter != null) - InviterId = model.Inviter.Id; + _inviterId = model.Inviter.Id; } internal void Update(InviteInfo model) diff --git a/src/Discord.Net/Models/Member.cs b/src/Discord.Net/Models/Member.cs deleted file mode 100644 index 6088cfc4e..000000000 --- a/src/Discord.Net/Models/Member.cs +++ /dev/null @@ -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 _permissions; - private bool _hasRef; - - /// Returns the name of this user on this server. - public string Name { get; private set; } - /// Returns a by-name unique identifier separating this user from others with the same name. - public string Discriminator { get; private set; } - /// Returns the unique identifier for this user's current avatar. - public string AvatarId { get; private set; } - /// Returns the URL to this user's current avatar. - public string AvatarUrl => API.Endpoints.UserAvatar(UserId, AvatarId); - /// Returns the datetime that this user joined this server. - 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; } - - /// Returns the id for the game this user is currently playing. - public string GameId { get; private set; } - /// Returns the current status for this user. - public string Status { get; private set; } - /// Returns the time this user last sent/edited a message, started typing or sent voice data in this server. - public DateTime? LastActivityAt { get; private set; } - /// Returns the time this user was last seen online in this server. - 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 Roles => RoleIds.Select(x => _client.Roles[x]); - - /// Returns a collection of all messages this user has sent on this server that are still in cache. - [JsonIgnore] - public IEnumerable Messages => _client.Messages.Where(x => x.UserId == UserId && x.ServerId == ServerId); - - /// Returns a collection of all channels this user is a member of. - [JsonIgnore] - public IEnumerable 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(); - } - 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); - } -} \ No newline at end of file diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index a7370ac16..158278885 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -6,7 +6,7 @@ using System.Linq; namespace Discord { - public sealed class Message + public sealed class Message : CachedObject { public sealed class Attachment : File { @@ -91,12 +91,9 @@ namespace Discord } } - private readonly DiscordClient _client; private string _cleanText; - private bool _gotRef; - - /// Returns the global unique identifier for this message. - public string Id { get; internal set; } + private string _channelId, _userId; + /// Returns the local unique identifier for this message. public string Nonce { get; internal set; } @@ -115,7 +112,7 @@ namespace Discord public string RawText { get; private set; } /// Returns the content of this message with any special references such as mentions converted. /// This value is lazy loaded and only processed on first request. Each subsequent request will pull from cache. - public string Text => _cleanText != null ? _cleanText : (_cleanText = MentionHelper.ConvertToNames(_client, RawText)); + public string Text => _cleanText != null ? _cleanText : (_cleanText = Mention.ConvertToNames(_client, Server, RawText)); /// Returns the timestamp for when this message was sent. public DateTime Timestamp { get; private set; } /// Returns the timestamp for when this message was last edited. @@ -132,19 +129,14 @@ namespace Discord public string[] MentionIds { get; private set; } /// Returns a collection of all users mentioned in this message. [JsonIgnore] - public IEnumerable Mentions => MentionIds.Select(x => _client.Users[x]).Where(x => x != null); - - /// Returns the id of the server containing the channel this message was sent to. - public string ServerId => Channel.ServerId; + public IEnumerable Mentions { get; internal set; } + /// Returns the server containing the channel this message was sent to. [JsonIgnore] - public Server Server => _client.Servers[Channel.ServerId]; - - /// Returns the id of the channel this message was sent to. - public string ChannelId { get; } + public Server Server => Channel.Server; /// Returns the channel this message was sent to. [JsonIgnore] - public Channel Channel => _client.Channels[ChannelId]; + public Channel Channel { get; private set; } /// Returns true if the current user created this message. public bool IsAuthor => _client.CurrentUserId == UserId; @@ -152,42 +144,28 @@ namespace Discord public string UserId { get; } /// Returns the author of this message. [JsonIgnore] - public User User => _client.Users[UserId]; - /// Returns the author of this message. - [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) + : base(client, id) { - _client = client; - Id = id; - ChannelId = channelId; - UserId = userId; + _channelId = channelId; + _userId = userId; Attachments = _initialAttachments; Embeds = _initialEmbeds; 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; if (channel != null) channel.RemoveMessage(Id); - var user = User; - if (user != null && _gotRef) - user.RemoveRef(); - _gotRef = false; } 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; } } diff --git a/src/Discord.Net/Models/Role.cs b/src/Discord.Net/Models/Role.cs index 95099a029..bad0e3503 100644 --- a/src/Discord.Net/Models/Role.cs +++ b/src/Discord.Net/Models/Role.cs @@ -5,12 +5,11 @@ using System.Linq; namespace Discord { - public sealed class Role + public sealed class Role : CachedObject { - private readonly DiscordClient _client; - - /// Returns the unique identifier for this role. - public string Id { get; } + private readonly string _serverId; + private Server _server; + /// Returns the name of this role. public string Name { get; private set; } /// If true, this role is displayed isolated from other users. @@ -24,27 +23,21 @@ namespace Discord /// Returns the the permissions contained by this role. public ServerPermissions Permissions { get; } - - /// Returns the id of the server this role is a member of. - public string ServerId { get; } + /// Returns the server this role is a member of. [JsonIgnore] - public Server Server => _client.Servers[ServerId]; + public Server Server => _server; /// Returns true if this is the role representing all users in a server. - public bool IsEveryone => Id == ServerId; - /// Returns a list of the ids of all members in this role. - [JsonIgnore] - public IEnumerable MemberIds => IsEveryone ? Server.UserIds : Server.Members.Where(x => x.RoleIds.Contains(Id)).Select(x => x.UserId); + public bool IsEveryone => Id == _serverId; /// Returns a list of all members in this role. [JsonIgnore] - public IEnumerable Members => IsEveryone ? Server.Members : Server.Members.Where(x => x.RoleIds.Contains(Id)); + public IEnumerable Members => IsEveryone ? Server.Members : Server.Members.Where(x => x.HasRole(this)); internal Role(DiscordClient client, string id, string serverId) + : base(client, id) { - _client = client; - Id = id; - ServerId = serverId; + _serverId = serverId; Permissions = new ServerPermissions(0); Permissions.Lock(); Color = new Color(0); @@ -53,18 +46,17 @@ namespace Discord if (IsEveryone) 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) { diff --git a/src/Discord.Net/Models/Server.cs b/src/Discord.Net/Models/Server.cs index e47bd6fef..606e4df01 100644 --- a/src/Discord.Net/Models/Server.cs +++ b/src/Discord.Net/Models/Server.cs @@ -7,13 +7,16 @@ using System.Linq; namespace Discord { - public sealed class Server + public sealed class Server : CachedObject { - private readonly DiscordClient _client; - private readonly ConcurrentDictionary _bans, _channels, _invites, _members, _roles; + private readonly ConcurrentDictionary _bans; + private readonly ConcurrentDictionary _channels; + private readonly ConcurrentDictionary _members; + private readonly ConcurrentDictionary _roles; + private readonly ConcurrentDictionary _invites; - /// Returns the unique identifier for this server. - public string Id { get; } + private string _ownerId; + /// Returns the name of this channel. public string Name { get; private set; } /// Returns the current logged-in user's data for this server. @@ -31,12 +34,10 @@ namespace Discord internal string VoiceServer { get; set; }*/ /// Returns true if the current user created this server. - public bool IsOwner => _client.CurrentUserId == OwnerId; - /// Returns the id of the user that first created this server. - public string OwnerId { get; private set; } + public bool IsOwner => _client.CurrentUserId == _ownerId; /// Returns the user that first created this server. [JsonIgnore] - public User Owner => _client.Users[OwnerId]; + public Member Owner => _client.Members[_ownerId, Id]; /// Returns the id of the AFK voice channel for this server (see AFKTimeout). public string AFKChannelId { get; private set; } @@ -52,11 +53,8 @@ namespace Discord /// Returns a collection of the ids of all users banned on this server. [JsonIgnore] - public IEnumerable BanIds => _bans.Select(x => x.Key); - - /// Returns a collection of the ids of all channels within this server. - [JsonIgnore] - public IEnumerable ChannelIds => _channels.Select(x => x.Key); + public IEnumerable Bans => _bans.Select(x => x.Key); + /// Returns a collection of all channels within this server. [JsonIgnore] public IEnumerable Channels => _channels.Select(x => _client.Channels[x.Key]); @@ -67,62 +65,58 @@ namespace Discord [JsonIgnore] public IEnumerable VoiceChannels => _channels.Select(x => _client.Channels[x.Key]).Where(x => x.Type == ChannelTypes.Voice); - /// Returns a collection of all invite codes to this server. + /// Returns a collection of all invites to this server. [JsonIgnore] - public IEnumerable InviteCodes => _invites.Select(x => x.Key); - /*/// Returns a collection of all invites to this server. - [JsonIgnore] - public IEnumerable Invites => _invites.Select(x => _client.Invites[x.Key]);*/ - + public IEnumerable Invites => _invites.Values; + /// Returns a collection of all users within this server with their server-specific data. [JsonIgnore] public IEnumerable Members => _members.Select(x => _client.Members[x.Key, Id]); - /// Returns a collection of the ids of all users within this server. - [JsonIgnore] - public IEnumerable UserIds => _members.Select(x => x.Key); - /// Returns a collection of all users within this server. - [JsonIgnore] - public IEnumerable Users => _members.Select(x => _client.Users[x.Key]); /// Return the id of the role representing all users in a server. public string EveryoneRoleId => Id; /// Return the the role representing all users in a server. [JsonIgnore] public Role EveryoneRole => _client.Roles[EveryoneRoleId]; - /// Returns a collection of the ids of all roles within this server. - [JsonIgnore] - public IEnumerable RoleIds => _roles.Select(x => x.Key); /// Returns a collection of all roles within this server. [JsonIgnore] public IEnumerable Roles => _roles.Select(x => _client.Roles[x.Key]); internal Server(DiscordClient client, string id) + : base(client, id) { - _client = client; - Id = id; + //Global Cache + _channels = new ConcurrentDictionary(); + _members = new ConcurrentDictionary(); + _roles = new ConcurrentDictionary(); + + //Local Cache _bans = new ConcurrentDictionary(); - _channels = new ConcurrentDictionary(); - _invites = new ConcurrentDictionary(); - _members = new ConcurrentDictionary(); - _roles = new ConcurrentDictionary(); - } - internal void OnCached() - { + _invites = new ConcurrentDictionary(); } - internal void OnUncached() + internal override void OnCached() { } + internal override void OnUncached() { + //Global Cache 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; - foreach (var userId in UserIds) - members.TryRemove(userId, Id); + foreach (var user in _members) + members.TryRemove(user.Key, Id); 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) { @@ -136,7 +130,7 @@ namespace Discord if (model.Name != null) Name = model.Name; if (model.OwnerId != null) - OwnerId = model.OwnerId; + _ownerId = model.OwnerId; if (model.Region != null) Region = model.Region; @@ -165,8 +159,6 @@ namespace Discord var members = _client.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); member.Update(subModel); } @@ -196,62 +188,43 @@ namespace Discord 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) - member.AddChannel(channelId); + member.AddChannel(channel); } - internal bool RemoveChannel(string channelId) + internal void RemoveChannel(Channel channel) { - bool ignored; 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) { - _members.TryAdd(member.UserId, true); + _members.TryAdd(member.Id, member); 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) { - 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); } } diff --git a/src/Discord.Net/Models/User.cs b/src/Discord.Net/Models/User.cs index 867db23a0..cef5a07af 100644 --- a/src/Discord.Net/Models/User.cs +++ b/src/Discord.Net/Models/User.cs @@ -1,20 +1,21 @@ using Discord.API; using Newtonsoft.Json; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Threading; namespace Discord { - public sealed class User + public class Member : CachedObject { - private readonly DiscordClient _client; - private readonly ConcurrentDictionary _servers; - private int _refCount; + private static readonly string[] _initialRoleIds = new string[0]; + + private ConcurrentDictionary _channels; + private ConcurrentDictionary _permissions; + private bool _hasRef; + private string[] _roleIds; - /// Returns the unique identifier for this user. - public string Id { get; } /// Returns the name of this user on this server. public string Name { get; private set; } /// Returns a by-name unique identifier separating this user from others with the same name. @@ -23,59 +24,97 @@ namespace Discord public string AvatarId { get; private set; } /// Returns the URL to this user's current avatar. public string AvatarUrl => API.Endpoints.UserAvatar(Id, AvatarId); + /// Returns the datetime that this user joined this server. + 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; } + + /// Returns the id for the game this user is currently playing. + public string GameId { get; private set; } + /// Returns the current status for this user. + public string Status { get; private set; } + /// Returns the time this user last sent/edited a message, started typing or sent voice data in this server. + public DateTime? LastActivityAt { get; private set; } + /// Returns the time this user was last seen online in this server. + public DateTime LastOnlineAt => Status != UserStatus.Offline ? DateTime.UtcNow : _lastOnline; + private DateTime _lastOnline; - /// Returns the email for this user. - /// This field is only ever populated for the current logged in user. - [JsonIgnore] - public string Email { get; private set; } - /// Returns if the email for this user has been verified. - /// This field is only ever populated for the current logged in user. [JsonIgnore] - public bool? IsVerified { get; private set; } + internal GlobalUser GlobalUser => _client.Users[Id]; - /// Returns the Id of the private messaging channel with this user, if one exists. - public string PrivateChannelId { get; internal set; } - /// Returns the private messaging channel with this user, if one exists. + public string ServerId { get; } [JsonIgnore] - public Channel PrivateChannel => _client.Channels[PrivateChannelId]; + public Server Server => _client.Servers[ServerId]; - /// Returns a collection of all server-specific data for every server this user is a member of. + public string VoiceChannelId { get; private set; } [JsonIgnore] - public IEnumerable Memberships => _servers.Select(x => _client.GetMember(x.Key, Id)); - /// Returns a collection of all servers this user is a member of. + public Channel VoiceChannel => _client.Channels[VoiceChannelId]; + [JsonIgnore] - public IEnumerable Servers => _servers.Select(x => _client.GetServer(x.Key)); - /// Returns a collection of the ids of all servers this user is a member of. + public IEnumerable Roles => _roleIds.Select(x => _client.Roles[x]); + /// Returns a collection of all messages this user has sent on this server that are still in cache. [JsonIgnore] - public IEnumerable ServersIds => _servers.Select(x => x.Key); - /// Returns a collection of all messages this user has sent that are still in cache. + public IEnumerable Messages => _client.Messages.Where(x => x.UserId == Id && x.Server.Id == ServerId); + /// Returns a collection of all channels this user is a member of. [JsonIgnore] - public IEnumerable Messages => _client.Messages.Where(x => x.UserId == Id); + public IEnumerable Channels => _client.Channels.Where(x => x.Server.Id == ServerId && x.UserIds.Contains(Id)); - /// Returns the id for the game this user is currently playing. - /*public string GameId => Memberships.Where(x => x.GameId != null).Select(x => x.GameId).FirstOrDefault(); - /// Returns the current status for this user. - public string Status => Memberships.OrderByDescending(x => x.StatusSince).Select(x => x.Status).FirstOrDefault(); - /// Returns the time this user's status was last changed. - public DateTime StatusSince => Memberships.OrderByDescending(x => x.StatusSince).Select(x => x.StatusSince).First(); - /// Returns the time this user last sent/edited a message, started typing or sent voice data. - public DateTime? LastActivity => Memberships.OrderByDescending(x => x.LastActivity).Select(x => x.LastActivity).FirstOrDefault(); - /// Returns the time this user was last seen online. - 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(); + 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(); + _permissions = new ConcurrentDictionary(); } - 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) { if (model.Avatar != null) @@ -85,36 +124,157 @@ namespace Discord if (model.Username != null) 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); } } -} +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Tests.cs b/test/Discord.Net.Tests/Tests.cs index 97f6b3445..81515ffd9 100644 --- a/test/Discord.Net.Tests/Tests.cs +++ b/test/Discord.Net.Tests/Tests.cs @@ -59,14 +59,14 @@ namespace Discord.Tests string name = $"#test_{_random.Next()}"; AssertEvent( "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, (s, e) => e.Channel.Name == name); AssertEvent( "ChannelDestroyed event never received", - () => _hostClient.DestroyChannel(channel), + async () => await _hostClient.DestroyChannel(channel), x => _targetBot.ChannelDestroyed += x, x => _targetBot.ChannelDestroyed -= x, (s, e) => e.Channel.Name == name); @@ -120,15 +120,15 @@ namespace Discord.Tests _observerBot.Disconnect()); } - private static void AssertEvent(string msg, Action action, Action> addEvent, Action> removeEvent, Func test = null) + private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) { AssertEvent(msg, action, addEvent, removeEvent, test, true); } - private static void AssertNoEvent(string msg, Action action, Action> addEvent, Action> removeEvent, Func test = null) + private static void AssertNoEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) { AssertEvent(msg, action, addEvent, removeEvent, test, false); } - private static void AssertEvent(string msg, Action action, Action> addEvent, Action> removeEvent, Func test, bool assertTrue) + private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test, bool assertTrue) { ManualResetEventSlim trigger = new ManualResetEventSlim(false); bool result = false; @@ -145,8 +145,9 @@ namespace Discord.Tests }; addEvent(handler); - action(); + var task = action(); trigger.Wait(EventTimeout); + task.Wait(); removeEvent(handler); Assert.AreEqual(assertTrue, result, msg);