| @@ -36,18 +36,20 @@ namespace Discord | |||
| public Task Ban(User member) | |||
| { | |||
| if (member == null) throw new ArgumentNullException(nameof(member)); | |||
| if (member.Server == null) throw new ArgumentException("Unable to ban a user in a private chat."); | |||
| CheckReady(); | |||
| return _api.Ban(member.ServerId, member.Id); | |||
| return _api.Ban(member.Server.Id, member.Id); | |||
| } | |||
| /// <summary> Unbans a user from the provided server. </summary> | |||
| public async Task Unban(User member) | |||
| public async Task Unban(Server server, string userId) | |||
| { | |||
| if (member == null) throw new ArgumentNullException(nameof(member)); | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (userId == null) throw new ArgumentNullException(nameof(userId)); | |||
| CheckReady(); | |||
| try { await _api.Unban(member.ServerId, member.Id).ConfigureAwait(false); } | |||
| try { await _api.Unban(server.Id, userId).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| } | |||
| @@ -10,6 +10,8 @@ namespace Discord | |||
| /// <remarks> Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode </remarks> | |||
| public async Task<Invite> GetInvite(string inviteIdOrXkcd) | |||
| { | |||
| //This doesn't work well if it's an invite to a different server! | |||
| if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd)); | |||
| CheckReady(); | |||
| @@ -22,8 +24,8 @@ namespace Discord | |||
| 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); | |||
| var invite = new Invite(this, response.Code, response.XkcdPass, response.Guild.Id, response.Inviter?.Id, response.Channel?.Id); | |||
| invite.Cache(); //Builds references | |||
| return invite; | |||
| } | |||
| @@ -53,8 +55,8 @@ namespace Discord | |||
| 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); | |||
| var invite = new Invite(this, response.Code, response.XkcdPass, response.Guild.Id, response.Inviter?.Id, response.Channel?.Id); | |||
| invite.Cache(); //Builds references | |||
| return invite; | |||
| } | |||
| @@ -121,7 +121,7 @@ namespace Discord | |||
| if (member == null) throw new ArgumentNullException(nameof(member)); | |||
| CheckReady(); | |||
| return _api.EditMember(member.ServerId, member.Id, mute: mute, deaf: deaf, roles: roles.Select(x => x.Id)); | |||
| return _api.EditMember(member.Server?.Id, member.Id, mute: mute, deaf: deaf, roles: roles.Select(x => x.Id)); | |||
| } | |||
| } | |||
| } | |||
| @@ -25,7 +25,7 @@ namespace Discord | |||
| else | |||
| { | |||
| var msg = new Message(_client, id, channelId, userId); | |||
| msg.Cache(); //Creates references to channel/server | |||
| msg.Cache(); //Builds references | |||
| return msg; | |||
| } | |||
| } | |||
| @@ -81,9 +81,17 @@ namespace Discord | |||
| if (changed) | |||
| { | |||
| if (targetType == PermissionTarget.Role) | |||
| channel.InvalidatePermissionsCache(); | |||
| { | |||
| var role = _roles[targetId]; | |||
| if (role != null) | |||
| channel.InvalidatePermissionsCache(role); | |||
| } | |||
| else if (targetType == PermissionTarget.User) | |||
| channel.InvalidatePermissionsCache(targetId); | |||
| { | |||
| var user = _users[targetId, channel.Server?.Id]; | |||
| if (user != null) | |||
| channel.InvalidatePermissionsCache(user); | |||
| } | |||
| } | |||
| } | |||
| @@ -114,9 +122,16 @@ namespace Discord | |||
| channel.PermissionOverwrites.Where(x => x.TargetType != targetType || x.TargetId != userOrRoleId).ToArray(); | |||
| if (targetType == PermissionTarget.Role) | |||
| channel.InvalidatePermissionsCache(); | |||
| { | |||
| var role = _roles[userOrRoleId]; | |||
| channel.InvalidatePermissionsCache(role); | |||
| } | |||
| else if (targetType == PermissionTarget.User) | |||
| channel.InvalidatePermissionsCache(userOrRoleId); | |||
| { | |||
| var user = _users[userOrRoleId, channel.Server?.Id]; | |||
| if (user != null) | |||
| channel.InvalidatePermissionsCache(user); | |||
| } | |||
| } | |||
| } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| @@ -7,11 +7,13 @@ namespace Discord | |||
| { | |||
| internal sealed class Roles : AsyncCollection<Role> | |||
| { | |||
| private const string VirtualEveryoneId = "[Virtual]"; | |||
| public Role VirtualEveryone { get; private set; } | |||
| public Roles(DiscordClient client, object writerLock) | |||
| : base(client, writerLock, x => x.OnCached(), x => x.OnUncached()) { } | |||
| : base(client, writerLock, x => x.OnCached(), x => x.OnUncached()) | |||
| { | |||
| VirtualEveryone = new Role(client, "Private", null); | |||
| } | |||
| public Role GetOrAdd(string id, string serverId) | |||
| => GetOrAdd(id, () => new Role(_client, id, serverId)); | |||
| @@ -57,7 +57,7 @@ namespace Discord | |||
| } | |||
| /// <summary> Returns a collection of all servers this client is a member of. </summary> | |||
| public IEnumerable<Server> AllServers => _servers.Where(x => !x.IsVirtual); | |||
| public IEnumerable<Server> AllServers => _servers; | |||
| internal Servers Servers => _servers; | |||
| private readonly Servers _servers; | |||
| @@ -59,12 +59,16 @@ namespace Discord | |||
| VoiceDisconnected += (s, e) => | |||
| { | |||
| foreach (var member in _users) | |||
| var server = _servers[e.ServerId]; | |||
| if (server != null) | |||
| { | |||
| if (member.ServerId == e.ServerId && member.IsSpeaking) | |||
| foreach (var member in server.Members) | |||
| { | |||
| member.IsSpeaking = false; | |||
| RaiseUserIsSpeaking(member, _channels[_voiceSocket.CurrentChannelId], false); | |||
| if (member.IsSpeaking) | |||
| { | |||
| member.IsSpeaking = false; | |||
| RaiseUserIsSpeaking(member, _channels[_voiceSocket.CurrentChannelId], false); | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| @@ -104,13 +108,13 @@ namespace Discord | |||
| BanRemoved += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
| $"Removed Ban: {e.Server?.Name ?? "[Private]"}/{e.UserId}"); | |||
| UserAdded += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
| $"Added Member: {e.Server?.Name ?? "[Private]"}/{e.UserId}"); | |||
| $"Added Member: {e.Server?.Name ?? "[Private]"}/{e.User.Id}"); | |||
| UserRemoved += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
| $"Removed Member: {e.Server?.Name ?? "[Private]"}/{e.UserId}"); | |||
| $"Removed Member: {e.Server?.Name ?? "[Private]"}/{e.User.Id}"); | |||
| MemberUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
| $"Updated Member: {e.Server?.Name ?? "[Private]"}/{e.UserId}"); | |||
| $"Updated Member: {e.Server?.Name ?? "[Private]"}/{e.User.Id}"); | |||
| UserVoiceStateUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
| $"Updated Member (Voice State): {e.Server?.Name ?? "[Private]"}/{e.UserId}"); | |||
| $"Updated Member (Voice State): {e.Server?.Name ?? "[Private]"}/{e.User.Id}"); | |||
| ProfileUpdated += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, | |||
| "Updated Profile"); | |||
| } | |||
| @@ -281,13 +285,12 @@ namespace Discord | |||
| { | |||
| try | |||
| { | |||
| await base.OnReceivedEvent(e); | |||
| switch (e.Type) | |||
| { | |||
| //Global | |||
| case "READY": //Resync | |||
| case "READY": //Resync | |||
| { | |||
| base.OnReceivedEvent(e).Wait(); //This cannot be an await, or we'll get later messages before we're ready | |||
| var data = e.Payload.ToObject<ReadyEvent>(_serializer); | |||
| _currentUser = _users.GetOrAdd(data.User.Id, null); | |||
| _currentUser.Update(data.User); | |||
| @@ -576,10 +579,11 @@ namespace Discord | |||
| var member = _users[data.UserId, data.GuildId]; | |||
| if (member != null) | |||
| { | |||
| if (data.ChannelId != member.VoiceChannelId && member.IsSpeaking) | |||
| var voiceChannel = member.VoiceChannel; | |||
| if (voiceChannel != null && data.ChannelId != voiceChannel.Id && member.IsSpeaking) | |||
| { | |||
| member.IsSpeaking = false; | |||
| RaiseUserIsSpeaking(member, _channels[member.VoiceChannelId], false); | |||
| RaiseUserIsSpeaking(member, _channels[voiceChannel.Id], false); | |||
| } | |||
| member.Update(data); | |||
| RaiseUserVoiceStateUpdated(member); | |||
| @@ -611,6 +615,7 @@ namespace Discord | |||
| //Internal (handled in DiscordSimpleClient) | |||
| case "VOICE_SERVER_UPDATE": | |||
| await base.OnReceivedEvent(e); | |||
| break; | |||
| //Others | |||
| @@ -90,7 +90,7 @@ namespace Discord | |||
| } | |||
| } | |||
| socket.ReceivedEvent += (s, e) => OnReceivedEvent(e); | |||
| socket.ReceivedEvent += async (s, e) => await OnReceivedEvent(e); | |||
| return socket; | |||
| } | |||
| internal virtual VoiceWebSocket CreateVoiceSocket() | |||
| @@ -292,7 +292,7 @@ namespace Discord | |||
| } | |||
| } | |||
| internal virtual Task OnReceivedEvent(WebSocketEventEventArgs e) | |||
| internal virtual async Task OnReceivedEvent(WebSocketEventEventArgs e) | |||
| { | |||
| try | |||
| { | |||
| @@ -309,7 +309,7 @@ namespace Discord | |||
| { | |||
| string token = e.Payload.Value<string>("token"); | |||
| _voiceSocket.Host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0]; | |||
| return _voiceSocket.Login(_userId, _dataSocket.SessionId, token, CancelToken); | |||
| await _voiceSocket.Login(_userId, _dataSocket.SessionId, token, CancelToken); | |||
| } | |||
| } | |||
| break; | |||
| @@ -319,7 +319,6 @@ namespace Discord | |||
| { | |||
| RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.Client, $"Error handling {e.Type} event: {ex.GetBaseException().Message}"); | |||
| } | |||
| return TaskHelper.CompletedTask; | |||
| } | |||
| } | |||
| } | |||
| @@ -53,11 +53,8 @@ namespace Discord | |||
| { | |||
| get | |||
| { | |||
| if (!_areMembersStale) | |||
| return _members.Select(x => x.Value); | |||
| _members = Server.Members.Where(x => x.GetPermissions(this)?.ReadMessages ?? false).ToDictionary(x => x.Id, x => x); | |||
| _areMembersStale = false; | |||
| if (_areMembersStale) | |||
| UpdateMembersCache(); | |||
| return _members.Select(x => x.Value); | |||
| } | |||
| } | |||
| @@ -157,22 +154,32 @@ namespace Discord | |||
| } | |||
| internal void RemoveMessage(Message message) => _messages.TryRemove(message.Id, out message); | |||
| internal void InvalidMembersCache() | |||
| internal void InvalidateMembersCache() | |||
| { | |||
| _areMembersStale = true; | |||
| } | |||
| internal void InvalidatePermissionsCache() | |||
| private void UpdateMembersCache() | |||
| { | |||
| _members = Server.Members.Where(x => x.GetPermissions(this)?.ReadMessages ?? false).ToDictionary(x => x.Id, x => x); | |||
| _areMembersStale = false; | |||
| } | |||
| internal void InvalidatePermissionsCache() | |||
| { | |||
| UpdateMembersCache(); | |||
| foreach (var member in _members) | |||
| member.Value.UpdateChannelPermissions(this); | |||
| } | |||
| internal void InvalidatePermissionsCache(Role role) | |||
| { | |||
| _areMembersStale = true; | |||
| foreach (var member in Members) | |||
| foreach (var member in role.Members) | |||
| member.UpdateChannelPermissions(this); | |||
| } | |||
| internal void InvalidatePermissionsCache(string userId) | |||
| internal void InvalidatePermissionsCache(User user) | |||
| { | |||
| _areMembersStale = true; | |||
| var user = _members[userId] | |||
| if (user != null) | |||
| user.UpdateChannelPermissions(this); | |||
| user.UpdateChannelPermissions(this); | |||
| } | |||
| } | |||
| } | |||
| @@ -34,7 +34,10 @@ namespace Discord | |||
| _users = new ConcurrentDictionary<string, User>(); | |||
| } | |||
| internal override void OnCached() { } | |||
| internal override void OnUncached() { } | |||
| internal override void OnUncached() | |||
| { | |||
| //Don't need to clean _users - they're considered owned by server | |||
| } | |||
| internal void Update(UserInfo model) | |||
| { | |||
| @@ -44,10 +47,10 @@ namespace Discord | |||
| IsVerified = model.IsVerified; | |||
| } | |||
| internal void AddUser(User user) => _users.TryAdd(user.Id, user); | |||
| internal void AddUser(User user) => _users.TryAdd(user.UniqueId, user); | |||
| internal void RemoveUser(User user) | |||
| { | |||
| if (_users.TryRemove(user.Id, out user)) | |||
| if (_users.TryRemove(user.UniqueId, out user)) | |||
| { | |||
| if (_users.Count == 0) | |||
| _client.GlobalUsers.TryRemove(Id); | |||
| @@ -5,10 +5,11 @@ using Newtonsoft.Json; | |||
| namespace Discord | |||
| { | |||
| public sealed class Invite : CachedObject | |||
| { | |||
| private readonly string _serverId; | |||
| private string _inviterId, _channelId; | |||
| { | |||
| /// <summary> Returns the unique code for this invite. </summary> | |||
| public string Code { get; private set; } | |||
| /// <summary> Returns, if enabled, an alternative human-readable code for URLs. </summary> | |||
| public string XkcdCode { get; } | |||
| /// <summary> Time (in seconds) until the invite expires. Set to 0 to never expire. </summary> | |||
| public int MaxAge { get; private set; } | |||
| /// <summary> The amount of times this invite has been used. </summary> | |||
| @@ -19,47 +20,72 @@ namespace Discord | |||
| public bool IsRevoked { get; private set; } | |||
| /// <summary> If true, a user accepting this invite will be kicked from the server after closing their client. </summary> | |||
| public bool IsTemporary { get; private set; } | |||
| /// <summary> Returns, if enabled, an alternative human-readable code for URLs. </summary> | |||
| public string XkcdPass { get; } | |||
| /// <summary> Returns a URL for this invite using XkcdPass if available or Id if not. </summary> | |||
| public string Url => API.Endpoints.InviteUrl(XkcdPass ?? Id); | |||
| /// <summary> Returns a URL for this invite using XkcdCode if available or Id if not. </summary> | |||
| public string Url => API.Endpoints.InviteUrl(XkcdCode ?? Code); | |||
| /// <summary> Returns the user that created this invite. </summary> | |||
| [JsonIgnore] | |||
| public User Inviter => _client.Users[_inviterId, _serverId]; | |||
| public User Inviter { get; private set; } | |||
| [JsonProperty("InviterId")] | |||
| private readonly string _inviterId; | |||
| /// <summary> Returns the server this invite is to. </summary> | |||
| [JsonIgnore] | |||
| public Server Server => _client.Servers[_serverId]; | |||
| public Server Server { get; private set; } | |||
| [JsonProperty("ServerId")] | |||
| private readonly string _serverId; | |||
| /// <summary> Returns the channel this invite is to. </summary> | |||
| [JsonIgnore] | |||
| public Channel Channel => _client.Channels[_channelId]; | |||
| public Channel Channel { get; private set; } | |||
| [JsonProperty("ChannelId")] | |||
| private readonly string _channelId; | |||
| internal Invite(DiscordClient client, string code, string xkcdPass, string serverId) | |||
| internal Invite(DiscordClient client, string code, string xkcdPass, string serverId, string inviterId, string channelId) | |||
| : base(client, code) | |||
| { | |||
| XkcdPass = xkcdPass; | |||
| XkcdCode = xkcdPass; | |||
| _serverId = serverId; | |||
| _inviterId = inviterId; | |||
| _channelId = channelId; | |||
| } | |||
| internal override void OnCached() { } | |||
| internal override void OnUncached() { } | |||
| public override string ToString() => XkcdPass ?? Id; | |||
| internal override void OnCached() | |||
| { | |||
| var server = _client.Servers[_serverId]; | |||
| if (server == null) | |||
| server = new Server(_client, _serverId); | |||
| Server = server; | |||
| if (_inviterId != null) | |||
| { | |||
| var inviter = _client.Users[_inviterId, _serverId]; | |||
| if (inviter == null) | |||
| inviter = new User(_client, _inviterId, _serverId); | |||
| Inviter = inviter; | |||
| } | |||
| internal void Update(InviteReference model) | |||
| if (_channelId != null) | |||
| { | |||
| var channel = _client.Channels[_channelId]; | |||
| if (channel == null) | |||
| channel = new Channel(_client, _channelId, _serverId, null); | |||
| Channel = channel; | |||
| } | |||
| } | |||
| internal override void OnUncached() | |||
| { | |||
| if (model.Channel != null) | |||
| _channelId = model.Channel.Id; | |||
| if (model.Inviter != null) | |||
| _inviterId = model.Inviter.Id; | |||
| Server = null; | |||
| Inviter = null; | |||
| Channel = null; | |||
| } | |||
| public override string ToString() => XkcdCode ?? Id; | |||
| internal void Update(InviteInfo model) | |||
| { | |||
| Update(model as InviteReference); | |||
| if (model.IsRevoked != null) | |||
| IsRevoked = model.IsRevoked.Value; | |||
| if (model.IsTemporary != null) | |||
| @@ -157,15 +157,18 @@ namespace Discord | |||
| } | |||
| internal override void OnCached() | |||
| { | |||
| //References | |||
| var channel = _client.Channels[_channelId]; | |||
| channel.AddMessage(this); | |||
| Channel = channel; | |||
| } | |||
| internal override void OnUncached() | |||
| { | |||
| //References | |||
| var channel = Channel; | |||
| if (channel != null) | |||
| channel.RemoveMessage(this); | |||
| Channel = null; | |||
| } | |||
| internal void Update(MessageInfo model) | |||
| @@ -28,10 +28,11 @@ namespace Discord | |||
| public Server Server { get; private set; } | |||
| /// <summary> Returns true if this is the role representing all users in a server. </summary> | |||
| public bool IsEveryone => Id == _serverId; | |||
| public bool IsEveryone => _serverId == null || Id == _serverId; | |||
| /// <summary> Returns a list of all members in this role. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<User> Members => IsEveryone ? Server.Members : Server.Members.Where(x => x.HasRole(this)); | |||
| //TODO: Add local members cache | |||
| internal Role(DiscordClient client, string id, string serverId) | |||
| : base(client, id) | |||
| @@ -41,18 +42,17 @@ namespace Discord | |||
| Permissions.Lock(); | |||
| Color = new Color(0); | |||
| Color.Lock(); | |||
| if (IsEveryone) | |||
| Position = int.MinValue; | |||
| } | |||
| internal override void OnCached() | |||
| { | |||
| //References | |||
| var server = _client.Servers[_serverId]; | |||
| server.AddRole(this); | |||
| Server = server; | |||
| } | |||
| internal override void OnUncached() | |||
| { | |||
| //References | |||
| var server = Server; | |||
| if (server != null) | |||
| server.RemoveRole(this); | |||
| @@ -8,21 +8,11 @@ using System.Linq; | |||
| namespace Discord | |||
| { | |||
| public sealed class Server : CachedObject | |||
| { | |||
| private readonly ConcurrentDictionary<string, bool> _bans; | |||
| private readonly ConcurrentDictionary<string, Channel> _channels; | |||
| private readonly ConcurrentDictionary<string, User> _members; | |||
| private readonly ConcurrentDictionary<string, Role> _roles; | |||
| private readonly ConcurrentDictionary<string, Invite> _invites; | |||
| private string _ownerId; | |||
| { | |||
| /// <summary> Returns the name of this channel. </summary> | |||
| public string Name { get; private set; } | |||
| /// <summary> Returns the current logged-in user's data for this server. </summary> | |||
| public User CurrentMember { get; internal set; } | |||
| /// <summary> Returns true if this is a virtual server used by Discord.Net and not a real Discord server. </summary> | |||
| public bool IsVirtual { get; internal set; } | |||
| /// <summary> Returns the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK channel (see AFKChannel). </summary> | |||
| public int AFKTimeout { get; private set; } | |||
| @@ -38,49 +28,49 @@ namespace Discord | |||
| /// <summary> Returns the user that first created this server. </summary> | |||
| [JsonIgnore] | |||
| public User Owner { get; private set; } | |||
| /// <summary> Returns the id of the AFK voice channel for this server (see AFKTimeout). </summary> | |||
| public string AFKChannelId { get; private set; } | |||
| private string _ownerId; | |||
| /// <summary> Returns the AFK voice channel for this server (see AFKTimeout). </summary> | |||
| [JsonIgnore] | |||
| public Channel AFKChannel => _client.Channels[AFKChannelId]; | |||
| /// <summary> Returns the id of the default channel for this server. </summary> | |||
| public string DefaultChannelId => Id; | |||
| public Channel AFKChannel { get; private set; } | |||
| /// <summary> Returns the default channel for this server. </summary> | |||
| [JsonIgnore] | |||
| public Channel DefaultChannel => _client.Channels[DefaultChannelId]; | |||
| public Channel DefaultChannel { get; private set; } | |||
| /// <summary> Returns a collection of the ids of all users banned on this server. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<string> Bans => _bans.Select(x => x.Key); | |||
| private ConcurrentDictionary<string, bool> _bans; | |||
| /// <summary> Returns a collection of all channels within this server. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Channel> Channels => _channels.Select(x => _client.Channels[x.Key]); | |||
| public IEnumerable<Channel> Channels => _channels.Select(x => x.Value); | |||
| /// <summary> Returns a collection of all channels within this server. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Channel> TextChannels => _channels.Select(x => _client.Channels[x.Key]).Where(x => x.Type == ChannelType.Text); | |||
| public IEnumerable<Channel> TextChannels => _channels.Select(x => x.Value).Where(x => x.Type == ChannelType.Text); | |||
| /// <summary> Returns a collection of all channels within this server. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Channel> VoiceChannels => _channels.Select(x => _client.Channels[x.Key]).Where(x => x.Type == ChannelType.Voice); | |||
| public IEnumerable<Channel> VoiceChannels => _channels.Select(x => x.Value).Where(x => x.Type == ChannelType.Voice); | |||
| private ConcurrentDictionary<string, Channel> _channels; | |||
| /// <summary> Returns a collection of all invites to this server. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Invite> Invites => _invites.Values; | |||
| private ConcurrentDictionary<string, Invite> _invites; | |||
| /// <summary> Returns a collection of all users within this server with their server-specific data. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<User> Members => _members.Select(x => _client.Users[x.Key, Id]); | |||
| public IEnumerable<User> Members => _members.Select(x => x.Value); | |||
| private ConcurrentDictionary<string, User> _members; | |||
| /// <summary> Return the id of the role representing all users in a server. </summary> | |||
| public string EveryoneRoleId => Id; | |||
| /// <summary> Return the the role representing all users in a server. </summary> | |||
| [JsonIgnore] | |||
| public Role EveryoneRole => _client.Roles[EveryoneRoleId]; | |||
| public Role EveryoneRole { get; private set; } | |||
| /// <summary> Returns a collection of all roles within this server. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Role> Roles => _roles.Select(x => _client.Roles[x.Key]); | |||
| public IEnumerable<Role> Roles => _roles.Select(x => x.Value); | |||
| private ConcurrentDictionary<string, Role> _roles; | |||
| internal Server(DiscordClient client, string id) | |||
| : base(client, id) | |||
| @@ -98,30 +88,37 @@ namespace Discord | |||
| internal override void OnUncached() | |||
| { | |||
| //Global Cache | |||
| var channels = _client.Channels; | |||
| foreach (var channel in _channels) | |||
| channels.TryRemove(channel.Key); | |||
| var members = _client.Users; | |||
| foreach (var user in _members) | |||
| members.TryRemove(user.Key, Id); | |||
| var roles = _client.Roles; | |||
| foreach (var role in _roles) | |||
| roles.TryRemove(role.Key); | |||
| var globalChannels = _client.Channels; | |||
| var channels = _channels; | |||
| foreach (var channel in channels) | |||
| globalChannels.TryRemove(channel.Key); | |||
| channels.Clear(); | |||
| var globalMembers = _client.Users; | |||
| var members = _members; | |||
| foreach (var user in members) | |||
| globalMembers.TryRemove(user.Key, Id); | |||
| members.Clear(); | |||
| var globalRoles = _client.Roles; | |||
| var roles = _roles; | |||
| foreach (var role in roles) | |||
| globalRoles.TryRemove(role.Key); | |||
| roles.Clear(); | |||
| //Local Cache | |||
| foreach (var invite in _invites) | |||
| var invites = _invites; | |||
| foreach (var invite in invites) | |||
| invite.Value.Uncache(); | |||
| _invites.Clear(); | |||
| invites.Clear(); | |||
| _bans.Clear(); | |||
| } | |||
| } | |||
| internal void Update(GuildInfo model) | |||
| { | |||
| //Can be null | |||
| AFKChannelId = model.AFKChannelId; | |||
| AFKChannel = _client.Channels[model.AFKChannelId]; | |||
| if (model.AFKTimeout != null) | |||
| AFKTimeout = model.AFKTimeout.Value; | |||
| @@ -139,18 +136,18 @@ namespace Discord | |||
| if (model.Roles != null) | |||
| { | |||
| var roles = _client.Roles; | |||
| foreach (var subModel in model.Roles) | |||
| var roleCache = _client.Roles; | |||
| foreach (var x in model.Roles) | |||
| { | |||
| var role = roles.GetOrAdd(subModel.Id, Id); | |||
| role.Update(subModel); | |||
| } | |||
| } | |||
| var role = roleCache.GetOrAdd(x.Id, Id); | |||
| role.Update(x); | |||
| } | |||
| } | |||
| } | |||
| internal void Update(ExtendedGuildInfo model) | |||
| { | |||
| Update(model as GuildInfo); | |||
| var channels = _client.Channels; | |||
| foreach (var subModel in model.Channels) | |||
| { | |||
| @@ -158,22 +155,22 @@ namespace Discord | |||
| channel.Update(subModel); | |||
| } | |||
| var users = _client.GlobalUsers; | |||
| var members = _client.Users; | |||
| var usersCache = _client.GlobalUsers; | |||
| var membersCache = _client.Users; | |||
| foreach (var subModel in model.Members) | |||
| { | |||
| var member = members.GetOrAdd(subModel.User.Id, Id); | |||
| var member = membersCache.GetOrAdd(subModel.User.Id, Id); | |||
| member.Update(subModel); | |||
| } | |||
| foreach (var subModel in model.VoiceStates) | |||
| { | |||
| var member = members[subModel.UserId, Id]; | |||
| var member = membersCache[subModel.UserId, Id]; | |||
| if (member != null) | |||
| member.Update(subModel); | |||
| } | |||
| foreach (var subModel in model.Presences) | |||
| { | |||
| var member = members[subModel.User.Id, Id]; | |||
| var member = membersCache[subModel.User.Id, Id]; | |||
| if (member != null) | |||
| member.Update(subModel); | |||
| } | |||
| @@ -193,9 +190,13 @@ namespace Discord | |||
| internal void AddChannel(Channel channel) | |||
| { | |||
| _channels.TryAdd(channel.Id, channel); | |||
| foreach (var member in Members) | |||
| member.AddChannel(channel); | |||
| if (_channels.TryAdd(channel.Id, channel)) | |||
| { | |||
| if (channel.Id == Id) | |||
| DefaultChannel = channel; | |||
| foreach (var member in Members) | |||
| member.AddChannel(channel); | |||
| } | |||
| } | |||
| internal void RemoveChannel(Channel channel) | |||
| { | |||
| @@ -213,7 +214,7 @@ namespace Discord | |||
| foreach (var channel in Channels) | |||
| { | |||
| member.AddChannel(channel); | |||
| channel.InvalidatePermissionsCache(member.Id); | |||
| channel.InvalidatePermissionsCache(member); | |||
| } | |||
| } | |||
| internal void RemoveMember(User member) | |||
| @@ -221,13 +222,27 @@ namespace Discord | |||
| foreach (var channel in Channels) | |||
| { | |||
| member.RemoveChannel(channel); | |||
| channel.InvalidatePermissionsCache(member.Id); | |||
| channel.InvalidatePermissionsCache(member); | |||
| } | |||
| _members.TryRemove(member.Id, out member); | |||
| } | |||
| internal void HasMember(User user) => _members.ContainsKey(user.Id); | |||
| internal void AddRole(Role role) => _roles.TryAdd(role.Id, role); | |||
| internal void RemoveRole(Role role) => _roles.TryRemove(role.Id, out role); | |||
| internal void AddRole(Role role) | |||
| { | |||
| if (_roles.TryAdd(role.Id, role)) | |||
| { | |||
| if (role.Id == Id) | |||
| EveryoneRole = role; | |||
| } | |||
| } | |||
| internal void RemoveRole(Role role) | |||
| { | |||
| if (_roles.TryRemove(role.Id, out role)) | |||
| { | |||
| if (role.Id == Id) | |||
| EveryoneRole = null; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -9,17 +9,14 @@ namespace Discord | |||
| { | |||
| public class User : CachedObject | |||
| { | |||
| private static readonly string[] _initialRoleIds = new string[0]; | |||
| internal static string GetId(string userId, string serverId) => (serverId ?? "Private") + '_' + userId; | |||
| private ConcurrentDictionary<string, Channel> _channels; | |||
| private ConcurrentDictionary<string, ChannelPermissions> _permissions; | |||
| private ServerPermissions _serverPermissions; | |||
| private string[] _roleIds; | |||
| /// <summary> Returns a unique identifier combining this user's id with its server's. </summary> | |||
| internal string UniqueId => GetId(Id, ServerId); | |||
| internal string UniqueId => GetId(Id, _serverId); | |||
| /// <summary> Returns the name of this user on this server. </summary> | |||
| public string Name { get; private set; } | |||
| /// <summary> Returns a by-name unique identifier separating this user from others with the same name. </summary> | |||
| @@ -52,48 +49,55 @@ namespace Discord | |||
| private DateTime _lastOnline; | |||
| [JsonIgnore] | |||
| internal GlobalUser GlobalUser => _client.GlobalUsers[Id]; | |||
| internal GlobalUser GlobalUser { get; private set; } | |||
| public string ServerId { get; } | |||
| [JsonIgnore] | |||
| public Server Server => _client.Servers[ServerId]; | |||
| public Server Server { get; private set; } | |||
| private string _serverId; | |||
| public string VoiceChannelId { get; private set; } | |||
| [JsonIgnore] | |||
| public Channel VoiceChannel => _client.Channels[VoiceChannelId]; | |||
| public Channel VoiceChannel { get; private set; } | |||
| [JsonIgnore] | |||
| public IEnumerable<Role> Roles => _roleIds.Select(x => _client.Roles[x]); | |||
| public IEnumerable<Role> Roles => _roles.Select(x => x.Value); | |||
| private Dictionary<string, Role> _roles; | |||
| /// <summary> Returns a collection of all messages this user has sent on this server that are still in cache. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Message> Messages => _client.Messages.Where(x => x.UserId == Id && x.Server.Id == ServerId); | |||
| public IEnumerable<Message> Messages => _client.Messages.Where(x => x.UserId == Id && x.Server.Id == _serverId); | |||
| /// <summary> Returns a collection of all channels this user is a member of. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Channel> Channels => _client.Channels.Where(x => x.Server.Id == ServerId && x.Members == this); | |||
| public IEnumerable<Channel> Channels => _channels.Select(x => x.Value); | |||
| internal User(DiscordClient client, string id, string serverId) | |||
| : base(client, id) | |||
| { | |||
| ServerId = serverId; | |||
| _serverId = serverId; | |||
| Status = UserStatus.Offline; | |||
| _roleIds = _initialRoleIds; | |||
| //_roles = new Dictionary<string, Role>(); | |||
| _channels = new ConcurrentDictionary<string, Channel>(); | |||
| _permissions = new ConcurrentDictionary<string, ChannelPermissions>(); | |||
| _serverPermissions = new ServerPermissions(); | |||
| } | |||
| internal override void OnCached() | |||
| { | |||
| var server = Server; | |||
| server.AddMember(this); | |||
| if (Id == _client.CurrentUserId) | |||
| server.CurrentMember = this; | |||
| var server = _client.Servers[_serverId]; | |||
| if (server != null) | |||
| { | |||
| server.AddMember(this); | |||
| if (Id == _client.CurrentUserId) | |||
| server.CurrentMember = this; | |||
| Server = server; | |||
| } | |||
| var user = GlobalUser; | |||
| if (server == null || !server.IsVirtual) | |||
| user.AddUser(this); | |||
| var user = _client.GlobalUsers.GetOrAdd(Id); | |||
| user.AddUser(this); | |||
| GlobalUser = user; | |||
| } | |||
| internal override void OnUncached() | |||
| { | |||
| //References | |||
| var server = Server; | |||
| if (server != null) | |||
| { | |||
| @@ -101,9 +105,12 @@ namespace Discord | |||
| if (Id == _client.CurrentUserId) | |||
| server.CurrentMember = null; | |||
| } | |||
| Server = null; | |||
| var globalUser = GlobalUser; | |||
| if (globalUser != null) | |||
| globalUser.RemoveUser(this); | |||
| GlobalUser = null; | |||
| } | |||
| public override string ToString() => Id; | |||
| @@ -124,7 +131,7 @@ namespace Discord | |||
| if (model.JoinedAt.HasValue) | |||
| JoinedAt = model.JoinedAt.Value; | |||
| if (model.Roles != null) | |||
| UpdateRoles(model.Roles); | |||
| UpdateRoles(model.Roles.Select(x => _client.Roles[x])); | |||
| UpdateServerPermissions(); | |||
| } | |||
| @@ -142,7 +149,7 @@ namespace Discord | |||
| Update(model.User as UserReference); | |||
| if (model.Roles != null) | |||
| UpdateRoles(model.Roles); | |||
| UpdateRoles(model.Roles.Select(x => _client.Roles[x])); | |||
| if (model.Status != null && Status != model.Status) | |||
| { | |||
| Status = UserStatus.FromString(model.Status); | |||
| @@ -165,7 +172,7 @@ namespace Discord | |||
| Token = model.Token; | |||
| if (model.ChannelId != null) | |||
| VoiceChannelId = model.ChannelId; | |||
| VoiceChannel = _client.Channels[model.ChannelId]; | |||
| if (model.IsSelfDeafened != null) | |||
| IsSelfDeafened = model.IsSelfDeafened.Value; | |||
| if (model.IsSelfMuted != null) | |||
| @@ -173,14 +180,16 @@ namespace Discord | |||
| if (model.IsServerSuppressed != null) | |||
| IsServerSuppressed = model.IsServerSuppressed.Value; | |||
| } | |||
| private void UpdateRoles(string[] roleIds) | |||
| private void UpdateRoles(IEnumerable<Role> roles) | |||
| { | |||
| //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; | |||
| var newRoles = roles.ToDictionary(x => x.Id, x => x); | |||
| Role everyone; | |||
| if (_serverId != null) | |||
| everyone = Server.EveryoneRole; | |||
| else | |||
| everyone = _client.Roles.VirtualEveryone; | |||
| newRoles.Add(everyone.Id, everyone); | |||
| _roles = newRoles; | |||
| } | |||
| internal void UpdateActivity(DateTime? activity = null) | |||
| @@ -191,7 +200,7 @@ namespace Discord | |||
| internal void UpdateChannelPermissions(Channel channel) | |||
| { | |||
| if (_roleIds == null) return; // We don't have all our data processed yet, this will be called again soon | |||
| if (_roles == 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; | |||
| @@ -225,14 +234,14 @@ namespace Discord | |||
| if (newPermissions != oldPermissions) | |||
| { | |||
| permissions.SetRawValueInternal(newPermissions); | |||
| channel.InvalidMembersCache(); | |||
| channel.InvalidateMembersCache(); | |||
| } | |||
| permissions.SetRawValueInternal(newPermissions); | |||
| } | |||
| internal void UpdateServerPermissions() | |||
| { | |||
| if (_roleIds == null) return; // We don't have all our data processed yet, this will be called again soon | |||
| if (_roles == null) return; // We don't have all our data processed yet, this will be called again soon | |||
| var server = Server; | |||
| if (server == null) return; | |||
| @@ -290,7 +299,7 @@ namespace Discord | |||
| { | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| return _roleIds.Contains(role.Id); | |||
| return _roles.ContainsKey(role.Id); | |||
| } | |||
| } | |||
| } | |||
| @@ -53,21 +53,21 @@ namespace Discord.Net.WebSockets | |||
| _connectedEvent = new ManualResetEventSlim(false); | |||
| _engine = new WebSocketSharpEngine(this, client.Config); | |||
| _engine.BinaryMessage += async (s, e) => | |||
| _engine.BinaryMessage += (s, e) => | |||
| { | |||
| using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 2)) | |||
| using (var decompressed = new MemoryStream()) | |||
| { | |||
| using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) | |||
| await zlib.CopyToAsync(decompressed); | |||
| zlib.CopyTo(decompressed); | |||
| decompressed.Position = 0; | |||
| using (var reader = new StreamReader(decompressed)) | |||
| await ProcessMessage(await reader.ReadToEndAsync()); | |||
| ProcessMessage(reader.ReadToEnd()).Wait(); | |||
| } | |||
| }; | |||
| _engine.TextMessage += async (s, e) => | |||
| _engine.TextMessage += (s, e) => | |||
| { | |||
| await ProcessMessage(e.Message); | |||
| /*await*/ ProcessMessage(e.Message).Wait(); | |||
| }; | |||
| } | |||