diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj index 8dec9397e..58aa7a35d 100644 --- a/src/Discord.Net.Net45/Discord.Net.csproj +++ b/src/Discord.Net.Net45/Discord.Net.csproj @@ -97,6 +97,9 @@ Message.cs + + PackedPermissions.cs + Regions.cs diff --git a/src/Discord.Net/API/Models/Common.cs b/src/Discord.Net/API/Models/Common.cs index af3398179..2552c5033 100644 --- a/src/Discord.Net/API/Models/Common.cs +++ b/src/Discord.Net/API/Models/Common.cs @@ -51,13 +51,52 @@ namespace Discord.API.Models [JsonProperty(PropertyName = "verified")] public bool IsVerified; } - internal class PresenceUserInfo : UserReference + internal class MemberInfo + { + [JsonProperty(PropertyName = "user_id")] + public string UserId; + [JsonProperty(PropertyName = "user")] + public UserReference User; + [JsonProperty(PropertyName = "guild_id")] + public string ServerId; + } + internal class PresenceMemberInfo : MemberInfo { [JsonProperty(PropertyName = "game_id")] public string GameId; [JsonProperty(PropertyName = "status")] public string Status; } + internal class VoiceMemberInfo : MemberInfo + { + [JsonProperty(PropertyName = "channel_id")] + public string ChannelId; + [JsonProperty(PropertyName = "suppress")] + public bool IsSuppressed; + [JsonProperty(PropertyName = "session_id")] + public string SessionId; + [JsonProperty(PropertyName = "self_mute")] + public bool IsSelfMuted; + [JsonProperty(PropertyName = "self_deaf")] + public bool IsSelfDeafened; + [JsonProperty(PropertyName = "mute")] + public bool IsMuted; + [JsonProperty(PropertyName = "deaf")] + public bool IsDeafened; + [JsonProperty(PropertyName = "token")] + public string Token; + } + internal class RoleMemberInfo : MemberInfo + { + [JsonProperty(PropertyName = "mute")] + public bool IsMuted; + [JsonProperty(PropertyName = "deaf")] + public bool IsDeafened; + [JsonProperty(PropertyName = "joined_at")] + public DateTime? JoinedAt; + [JsonProperty(PropertyName = "roles")] + public string[] Roles; + } //Channels internal class ChannelReference @@ -73,12 +112,26 @@ namespace Discord.API.Models } internal class ChannelInfo : ChannelReference { + public sealed class PermissionOverwrite + { + [JsonProperty(PropertyName = "type")] + public string Type; + [JsonProperty(PropertyName = "id")] + public string Id; + [JsonProperty(PropertyName = "deny")] + public uint Deny; + [JsonProperty(PropertyName = "allow")] + public uint Allow; + } + [JsonProperty(PropertyName = "last_message_id")] public string LastMessageId; [JsonProperty(PropertyName = "is_private")] public bool IsPrivate; + [JsonProperty(PropertyName = "position")] + public int Position; [JsonProperty(PropertyName = "permission_overwrites")] - public object[] PermissionOverwrites; + public PermissionOverwrite[] PermissionOverwrites; [JsonProperty(PropertyName = "recipient")] public UserReference Recipient; } @@ -114,28 +167,14 @@ namespace Discord.API.Models } internal class ExtendedServerInfo : ServerInfo { - public class Membership - { - [JsonProperty(PropertyName = "roles")] - public string[] Roles; - [JsonProperty(PropertyName = "mute")] - public bool IsMuted; - [JsonProperty(PropertyName = "deaf")] - public bool IsDeaf; - [JsonProperty(PropertyName = "joined_at")] - public DateTime JoinedAt; - [JsonProperty(PropertyName = "user")] - public UserReference User; - } - [JsonProperty(PropertyName = "channels")] public ChannelInfo[] Channels; [JsonProperty(PropertyName = "members")] - public Membership[] Members; - [JsonProperty(PropertyName = "presence")] - public object[] Presence; + public RoleMemberInfo[] Members; + [JsonProperty(PropertyName = "presences")] + public PresenceMemberInfo[] Presences; [JsonProperty(PropertyName = "voice_states")] - public object[] VoiceStates; + public VoiceMemberInfo[] VoiceStates; } //Messages @@ -150,7 +189,7 @@ namespace Discord.API.Models } internal class Message : MessageReference { - public class Attachment + public sealed class Attachment { [JsonProperty(PropertyName = "id")] public string Id; @@ -167,6 +206,40 @@ namespace Discord.API.Models [JsonProperty(PropertyName = "height")] public int Height; } + public sealed class Embed + { + public sealed class ProviderInfo + { + [JsonProperty(PropertyName = "url")] + public string Url; + [JsonProperty(PropertyName = "name")] + public string Name; + } + public sealed class ThumbnailInfo + { + [JsonProperty(PropertyName = "url")] + public string Url; + [JsonProperty(PropertyName = "proxy_url")] + public string ProxyUrl; + [JsonProperty(PropertyName = "width")] + public int Width; + [JsonProperty(PropertyName = "height")] + public int Height; + } + + [JsonProperty(PropertyName = "url")] + public string Url; + [JsonProperty(PropertyName = "type")] + public string Type; + [JsonProperty(PropertyName = "title")] + public string Title; + [JsonProperty(PropertyName = "description")] + public string Description; + [JsonProperty(PropertyName = "provider")] + public ProviderInfo Provider; + [JsonProperty(PropertyName = "thumbnail")] + public ThumbnailInfo Thumbnail; + } [JsonProperty(PropertyName = "tts")] public bool IsTextToSpeech; @@ -179,7 +252,7 @@ namespace Discord.API.Models [JsonProperty(PropertyName = "mentions")] public UserReference[] Mentions; [JsonProperty(PropertyName = "embeds")] - public object[] Embeds; //TODO: Parse this + public Embed[] Embeds; //TODO: Parse this [JsonProperty(PropertyName = "attachments")] public Attachment[] Attachments; [JsonProperty(PropertyName = "content")] diff --git a/src/Discord.Net/API/Models/WebSocketCommands.cs b/src/Discord.Net/API/Models/WebSocketCommands.cs index de675848d..5fc64aa52 100644 --- a/src/Discord.Net/API/Models/WebSocketCommands.cs +++ b/src/Discord.Net/API/Models/WebSocketCommands.cs @@ -22,6 +22,8 @@ namespace Discord.API.Models { [JsonProperty(PropertyName = "token")] public string Token; + [JsonProperty(PropertyName = "v")] + public int Version = 2; [JsonProperty(PropertyName = "properties")] public Dictionary Properties = new Dictionary(); } diff --git a/src/Discord.Net/API/Models/WebSocketEvents.cs b/src/Discord.Net/API/Models/WebSocketEvents.cs index 7ddab3070..5fa12b017 100644 --- a/src/Discord.Net/API/Models/WebSocketEvents.cs +++ b/src/Discord.Net/API/Models/WebSocketEvents.cs @@ -11,12 +11,24 @@ namespace Discord.API.Models { public sealed class Ready { + public sealed class ReadStateInfo + { + [JsonProperty(PropertyName = "id")] + public string ChannelId; + [JsonProperty(PropertyName = "mention_count")] + public int MentionCount; + [JsonProperty(PropertyName = "last_message_id")] + public string LastMessageId; + } + + [JsonProperty(PropertyName = "v")] + public int Version; [JsonProperty(PropertyName = "user")] public SelfUserInfo User; [JsonProperty(PropertyName = "session_id")] public string SessionId; [JsonProperty(PropertyName = "read_state")] - public object[] ReadState; + public ReadStateInfo[] ReadState; [JsonProperty(PropertyName = "guilds")] public ExtendedServerInfo[] Guilds; [JsonProperty(PropertyName = "private_channels")] @@ -36,32 +48,15 @@ namespace Discord.API.Models public sealed class ChannelUpdate : ChannelInfo { } //Memberships - public abstract class GuildMemberEvent - { - [JsonProperty(PropertyName = "user")] - public UserReference User; - [JsonProperty(PropertyName = "guild_id")] - public string GuildId; - } - public sealed class GuildMemberAdd : GuildMemberEvent - { - [JsonProperty(PropertyName = "joined_at")] - public DateTime JoinedAt; - [JsonProperty(PropertyName = "roles")] - public string[] Roles; - } - public sealed class GuildMemberUpdate : GuildMemberEvent - { - [JsonProperty(PropertyName = "roles")] - public string[] Roles; - } - public sealed class GuildMemberRemove : GuildMemberEvent { } + public sealed class GuildMemberAdd : RoleMemberInfo { } + public sealed class GuildMemberUpdate : RoleMemberInfo { } + public sealed class GuildMemberRemove : MemberInfo { } //Roles public abstract class GuildRoleEvent { [JsonProperty(PropertyName = "guild_id")] - public string GuildId; + public string ServerId; } public sealed class GuildRoleCreateUpdate : GuildRoleEvent { @@ -78,7 +73,7 @@ namespace Discord.API.Models public abstract class GuildBanEvent { [JsonProperty(PropertyName = "guild_id")] - public string GuildId; + public string ServerId; } public sealed class GuildBanAddRemove : GuildBanEvent { @@ -93,28 +88,8 @@ namespace Discord.API.Models //User public sealed class UserUpdate : SelfUserInfo { } - public sealed class PresenceUpdate : PresenceUserInfo { } - public sealed class VoiceStateUpdate - { - [JsonProperty(PropertyName = "user_id")] - public string UserId; - [JsonProperty(PropertyName = "guild_id")] - public string GuildId; - [JsonProperty(PropertyName = "channel_id")] - public string ChannelId; - [JsonProperty(PropertyName = "suppress")] - public bool IsSuppressed; - [JsonProperty(PropertyName = "session_id")] - public string SessionId; - [JsonProperty(PropertyName = "self_mute")] - public bool IsSelfMuted; - [JsonProperty(PropertyName = "self_deaf")] - public bool IsSelfDeafened; - [JsonProperty(PropertyName = "mute")] - public bool IsMuted; - [JsonProperty(PropertyName = "deaf")] - public bool IsDeafened; - } + public sealed class PresenceUpdate : PresenceMemberInfo { } + public sealed class VoiceStateUpdate : VoiceMemberInfo { } //Chat public sealed class MessageCreate : Message { } diff --git a/src/Discord.Net/Channel.cs b/src/Discord.Net/Channel.cs index 9019b4afb..45a9bbbf1 100644 --- a/src/Discord.Net/Channel.cs +++ b/src/Discord.Net/Channel.cs @@ -4,8 +4,16 @@ using System.Linq; namespace Discord { - public sealed class Channel + public sealed class Channel { + public sealed class PermissionOverwrite + { + public string Type { get; internal set; } + public string Id { get; internal set; } + public PackedPermissions Deny { get; internal set; } + public PackedPermissions Allow { get; internal set; } + } + private readonly DiscordClient _client; /// Returns the unique identifier for this channel. @@ -15,8 +23,10 @@ namespace Discord /// Returns the name of this channel. public string Name { get { return !IsPrivate ? $"#{_name}" : $"@{Recipient.Name}"; } internal set { _name = value; } } + /// Returns the position of this channel in the channel list for this server. + public int Position { get; internal 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 { get; } + public bool IsPrivate { get; } /// Returns the type of this channel (see ChannelTypes). public string Type { get; internal set; } @@ -37,9 +47,8 @@ namespace Discord [JsonIgnore] public IEnumerable Messages => _client.Messages.Where(x => x.ChannelId == Id); - //TODO: Not Implemented - /// Not implemented, stored for reference. - public object[] PermissionOverwrites { get; internal set; } + /// Returns a collection of all custom permissions used for this channel. + public PermissionOverwrite[] PermissionOverwrites { get; internal set; } internal Channel(string id, string serverId, DiscordClient client) { diff --git a/src/Discord.Net/DiscordClient.Events.cs b/src/Discord.Net/DiscordClient.Events.cs index c3bf1841a..625be27c3 100644 --- a/src/Discord.Net/DiscordClient.Events.cs +++ b/src/Discord.Net/DiscordClient.Events.cs @@ -187,19 +187,19 @@ namespace Discord } public event EventHandler MemberAdded; - private void RaiseMemberAdded(Membership membership, Server server) + private void RaiseMemberAdded(Membership membership) { if (MemberAdded != null) MemberAdded(this, new MemberEventArgs(membership)); } public event EventHandler MemberRemoved; - private void RaiseMemberRemoved(Membership membership, Server server) + private void RaiseMemberRemoved(Membership membership) { if (MemberRemoved != null) MemberRemoved(this, new MemberEventArgs(membership)); } public event EventHandler MemberUpdated; - private void RaiseMemberUpdated(Membership membership, Server server) + private void RaiseMemberUpdated(Membership membership) { if (MemberUpdated != null) MemberUpdated(this, new MemberEventArgs(membership)); @@ -217,11 +217,11 @@ namespace Discord } } - public event EventHandler PresenceUpdated; - private void RaisePresenceUpdated(User user) + public event EventHandler PresenceUpdated; + private void RaisePresenceUpdated(Membership member) { if (PresenceUpdated != null) - PresenceUpdated(this, new UserEventArgs(user)); + PresenceUpdated(this, new MemberEventArgs(member)); } public event EventHandler VoiceStateUpdated; private void RaiseVoiceStateUpdated(Membership member) diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index dc7d50205..b0a790a00 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -1,6 +1,7 @@ using Discord.API; using Discord.API.Models; using Discord.Helpers; +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; @@ -19,10 +20,13 @@ namespace Discord private ManualResetEventSlim _isStopping; private readonly Regex _userRegex, _channelRegex; private readonly MatchEvaluator _userRegexEvaluator, _channelRegexEvaluator; + private readonly JsonSerializer _serializer; /// Returns the User object for the current logged in user. public User User { get; private set; } + /// Returns the id of the current logged in user. public string UserId { get; private set; } + public string SessionId { get; private set; } /// Returns a collection of all users the client can see across all servers. /// This collection does not guarantee any ordering. @@ -64,6 +68,12 @@ namespace Discord { _isStopping = new ManualResetEventSlim(false); + _serializer = new JsonSerializer(); +#if TEST_RESPONSES + _serializer.CheckAdditionalContent = true; + _serializer.MissingMemberHandling = MissingMemberHandling.Error; +#endif + _userRegex = new Regex(@"<@\d+?>", RegexOptions.Compiled); _channelRegex = new Regex(@"<#\d+?>", RegexOptions.Compiled); _userRegexEvaluator = new MatchEvaluator(e => @@ -90,11 +100,7 @@ namespace Discord (server, model) => { server.Name = model.Name; - if (!server.Channels.Any()) //A default channel always exists with the same id as the server. - { - var defaultChannel = new ChannelReference() { Id = server.DefaultChannelId, GuildId = server.Id }; - _channels.Update(defaultChannel.Id, defaultChannel.GuildId, defaultChannel); - } + _channels.Update(server.DefaultChannelId, server.Id, null); if (model is ExtendedServerInfo) { var extendedModel = model as ExtendedServerInfo; @@ -102,9 +108,7 @@ namespace Discord server.AFKTimeout = extendedModel.AFKTimeout; server.JoinedAt = extendedModel.JoinedAt ?? DateTime.MinValue; server.OwnerId = extendedModel.OwnerId; - server.Presence = extendedModel.Presence; server.Region = extendedModel.Region; - server.VoiceStates = extendedModel.VoiceStates; foreach (var role in extendedModel.Roles) _roles.Update(role.Id, model.Id, role); @@ -113,11 +117,13 @@ namespace Discord foreach (var membership in extendedModel.Members) { _users.Update(membership.User.Id, membership.User); - var newMember = new Membership(server.Id, membership.User.Id, membership.JoinedAt, this); - newMember.Update(membership); - server.AddMember(newMember); + server.UpdateMember(membership); } - } + foreach (var membership in extendedModel.VoiceStates) + server.UpdateMember(membership); + foreach (var membership in extendedModel.Presences) + server.UpdateMember(membership); + } }, server => { } ); @@ -131,13 +137,27 @@ namespace Discord if (model is ChannelInfo) { var extendedModel = model as ChannelInfo; - channel.PermissionOverwrites = extendedModel.PermissionOverwrites; + channel.Position = extendedModel.Position; + if (extendedModel.IsPrivate) { var user = _users.Update(extendedModel.Recipient.Id, extendedModel.Recipient); - channel.RecipientId = user.Id; + channel.RecipientId = user.Id; user.PrivateChannelId = channel.Id; } + + if (extendedModel.PermissionOverwrites != null) + { + channel.PermissionOverwrites = extendedModel.PermissionOverwrites.Select(x => new Channel.PermissionOverwrite + { + Type = x.Type, + Id = x.Id, + Deny = new PackedPermissions(x.Deny), + Allow = new PackedPermissions(x.Allow) + }).ToArray(); + } + else + channel.PermissionOverwrites = null; } }, channel => @@ -170,8 +190,41 @@ namespace Discord }).ToArray(); } else - extendedModel.Attachments = null; - message.Embeds = extendedModel.Embeds; + message.Attachments = new Message.Attachment[0]; + if (extendedModel.Embeds != null) + { + message.Embeds = extendedModel.Embeds.Select(x => + { + var embed = new Message.Embed + { + Url = x.Url, + Type = x.Type, + Description = x.Description, + Title = x.Title + }; + if (x.Provider != null) + { + embed.Provider = new Message.EmbedProvider + { + Url = x.Provider.Url, + Name = x.Provider.Name + }; + } + if (x.Thumbnail != null) + { + embed.Thumbnail = new Message.File + { + Url = x.Thumbnail.Url, + ProxyUrl = x.Thumbnail.ProxyUrl, + Width = x.Thumbnail.Width, + Height = x.Thumbnail.Height + }; + } + return embed; + }).ToArray(); + } + else + message.Embeds = new Message.Embed[0]; message.IsMentioningEveryone = extendedModel.IsMentioningEveryone; message.IsTTS = extendedModel.IsTextToSpeech; message.MentionIds = extendedModel.Mentions?.Select(x => x.Id)?.ToArray() ?? new string[0]; @@ -206,12 +259,6 @@ namespace Discord var extendedModel = model as SelfUserInfo; user.Email = extendedModel.Email; user.IsVerified = extendedModel.IsVerified; - } - if (model is PresenceUserInfo) - { - var extendedModel = model as PresenceUserInfo; - user.GameId = extendedModel.GameId; - user.Status = extendedModel.Status; } }, user => { } @@ -246,101 +293,104 @@ namespace Discord //Global case "READY": //Resync { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); _servers.Clear(); _channels.Clear(); _users.Clear(); UserId = data.User.Id; + SessionId = data.SessionId; User = _users.Update(data.User.Id, data.User); foreach (var server in data.Guilds) _servers.Update(server.Id, server); foreach (var channel in data.PrivateChannels) _channels.Update(channel.Id, null, channel); - } + } break; //Servers case "GUILD_CREATE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var server = _servers.Update(data.Id, data); - RaiseServerCreated(server); + try { RaiseServerCreated(server); } catch { } } break; case "GUILD_UPDATE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var server = _servers.Update(data.Id, data); - RaiseServerUpdated(server); + try { RaiseServerUpdated(server); } catch { } } break; case "GUILD_DELETE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var server = _servers.Remove(data.Id); if (server != null) - RaiseServerDestroyed(server); + try { RaiseServerDestroyed(server); } catch { } } break; //Channels case "CHANNEL_CREATE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var channel = _channels.Update(data.Id, data.GuildId, data); - RaiseChannelCreated(channel); + try { RaiseChannelCreated(channel); } catch { } } break; case "CHANNEL_UPDATE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var channel = _channels.Update(data.Id, data.GuildId, data); - RaiseChannelUpdated(channel); + try { RaiseChannelUpdated(channel); } catch { } } break; case "CHANNEL_DELETE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var channel = _channels.Remove(data.Id); if (channel != null) - RaiseChannelDestroyed(channel); + try { RaiseChannelDestroyed(channel); } catch { } } break; //Members case "GUILD_MEMBER_ADD": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var user = _users.Update(data.User.Id, data.User); - var server = _servers[data.GuildId]; - var membership = new Membership(server.Id, data.User.Id, data.JoinedAt, this) { RoleIds = data.Roles }; - server.AddMember(membership); - RaiseMemberAdded(membership, server); + var server = _servers[data.ServerId]; + if (server != null) + { + var member = server.UpdateMember(data); + try { RaiseMemberAdded(member); } catch { } + } } break; case "GUILD_MEMBER_UPDATE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var user = _users.Update(data.User.Id, data.User); - var server = _servers[data.GuildId]; - var membership = server.GetMember(data.User.Id); - if (membership != null) - membership.RoleIds = data.Roles; - RaiseMemberUpdated(membership, server); + var server = _servers[data.ServerId]; + if (server != null) + { + var member = server.UpdateMember(data); + try { RaiseMemberUpdated(member); } catch { } + } } break; case "GUILD_MEMBER_REMOVE": { - var data = e.Event.ToObject(); - var user = _users.Update(data.User.Id, data.User); - var server = _servers[data.GuildId]; + var data = e.Event.ToObject(_serializer); + var server = _servers[data.ServerId]; if (server != null) { - var membership = server.RemoveMember(user.Id); - if (membership != null) - RaiseMemberRemoved(membership, server); + var member = server.RemoveMember(data.UserId); + if (member != null) + try { RaiseMemberRemoved(member); } catch { } } } break; @@ -348,121 +398,138 @@ namespace Discord //Roles case "GUILD_ROLE_CREATE": { - var data = e.Event.ToObject(); - var role = _roles.Update(data.Role.Id, data.GuildId, data.Role); - RaiseRoleCreated(role); + var data = e.Event.ToObject(_serializer); + var role = _roles.Update(data.Role.Id, data.ServerId, data.Role); + try { RaiseRoleCreated(role); } catch { } } break; case "GUILD_ROLE_UPDATE": { - var data = e.Event.ToObject(); - var role = _roles.Update(data.Role.Id, data.GuildId, data.Role); - RaiseRoleUpdated(role); + var data = e.Event.ToObject(_serializer); + var role = _roles.Update(data.Role.Id, data.ServerId, data.Role); + try { RaiseRoleUpdated(role); } catch { } } break; case "GUILD_ROLE_DELETE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var role = _roles.Remove(data.RoleId); if (role != null) - RaiseRoleDeleted(role); + try { RaiseRoleDeleted(role); } catch { } } break; //Bans case "GUILD_BAN_ADD": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var user = _users.Update(data.User.Id, data.User); - var server = _servers[data.GuildId]; - RaiseBanAdded(user, server); + var server = _servers[data.ServerId]; + try { RaiseBanAdded(user, server); } catch { } } break; case "GUILD_BAN_REMOVE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var user = _users.Update(data.User.Id, data.User); - var server = _servers[data.GuildId]; + var server = _servers[data.ServerId]; if (server != null && server.RemoveBan(user.Id)) - RaiseBanRemoved(user, server); + { + try { RaiseBanRemoved(user, server); } catch { } + } } break; //Messages case "MESSAGE_CREATE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var msg = _messages.Update(data.Id, data.ChannelId, data); msg.User.UpdateActivity(data.Timestamp); - RaiseMessageCreated(msg); + try { RaiseMessageCreated(msg); } catch { } } break; case "MESSAGE_UPDATE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var msg = _messages.Update(data.Id, data.ChannelId, data); - RaiseMessageUpdated(msg); + try { RaiseMessageUpdated(msg); } catch { } } break; case "MESSAGE_DELETE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var msg = GetMessage(data.MessageId); if (msg != null) + { _messages.Remove(msg.Id); + try { RaiseMessageDeleted(msg); } catch { } + } } break; case "MESSAGE_ACK": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var msg = GetMessage(data.MessageId); - RaiseMessageAcknowledged(msg); + if (msg != null) + try { RaiseMessageAcknowledged(msg); } catch { } } break; //Statuses case "PRESENCE_UPDATE": { - var data = e.Event.ToObject(); - var user = _users.Update(data.Id, data); - RaisePresenceUpdated(user); + var data = e.Event.ToObject(_serializer); + var user = _users.Update(data.User.Id, data.User); + var server = _servers[data.ServerId]; + if (server != null) + { + var member = server.UpdateMember(data); + try { RaisePresenceUpdated(member); } catch { } + } } break; case "VOICE_STATE_UPDATE": { - var data = e.Event.ToObject(); - var member = GetMember(data.GuildId, data.UserId); - if (member != null) + var data = e.Event.ToObject(_serializer); + var server = _servers[data.ServerId]; + if (server != null) { - member.Update(data); - RaiseVoiceStateUpdated(member); + var member = server.UpdateMember(data); + if (member != null) + try { RaiseVoiceStateUpdated(member); } catch { } } } break; case "TYPING_START": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var channel = _channels[data.ChannelId]; var user = _users[data.UserId]; - RaiseUserTyping(user, channel); + if (user != null) + { + user.UpdateActivity(DateTime.UtcNow); + if (channel != null) + try { RaiseUserTyping(user, channel); } catch { } + } } break; //Voice case "VOICE_SERVER_UPDATE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var server = _servers[data.ServerId]; - RaiseVoiceServerUpdated(server, data.Endpoint); + try { RaiseVoiceServerUpdated(server, data.Endpoint); } catch { } } break; //Settings case "USER_UPDATE": { - var data = e.Event.ToObject(); + var data = e.Event.ToObject(_serializer); var user = _users.Update(data.Id, data); - RaiseUserUpdated(user); + try { RaiseUserUpdated(user); } catch { } } break; case "USER_SETTINGS_UPDATE": diff --git a/src/Discord.Net/DiscordWebSocket.cs b/src/Discord.Net/DiscordWebSocket.cs index 01c6144ef..22a26ee59 100644 --- a/src/Discord.Net/DiscordWebSocket.cs +++ b/src/Discord.Net/DiscordWebSocket.cs @@ -98,7 +98,8 @@ namespace Discord { throw new InvalidOperationException("Bad Token"); } - _connectWaitOnLogin2.Wait(cancelToken); //Waiting on READY handler + try { _connectWaitOnLogin2.Wait(cancelToken); } //Waiting on READY handler + catch (OperationCanceledException) { return; } _isConnected = true; RaiseConnected(); @@ -150,11 +151,7 @@ namespace Discord QueueMessage(new WebSocketCommands.KeepAlive()); _connectWaitOnLogin.Set(); //Pre-Event } - try - { - RaiseGotEvent(msg.Type, msg.Payload as JToken); - } - catch { } //Don't allow user exceptions to affect our state + RaiseGotEvent(msg.Type, msg.Payload as JToken); if (msg.Type == "READY") _connectWaitOnLogin2.Set(); //Post-Event break; diff --git a/src/Discord.Net/Helpers/AsyncCache.cs b/src/Discord.Net/Helpers/AsyncCache.cs index 2fefbb985..570c20879 100644 --- a/src/Discord.Net/Helpers/AsyncCache.cs +++ b/src/Discord.Net/Helpers/AsyncCache.cs @@ -14,7 +14,7 @@ namespace Discord.Helpers private readonly Action _onUpdate; private readonly Action _onRemove; - public AsyncCache(Func onCreate, Action onUpdate, Action onRemove) + public AsyncCache(Func onCreate, Action onUpdate, Action onRemove = null) { _dictionary = new ConcurrentDictionary(); _onCreate = onCreate; @@ -49,7 +49,8 @@ namespace Discord.Helpers isNew = !_dictionary.TryGetValue(key, out value); if (isNew) value = _onCreate(key, parentKey); - _onUpdate(value, model); + if (model != null) + _onUpdate(value, model); if (isNew) { //If this fails, repeat as an update instead of an add @@ -68,7 +69,11 @@ namespace Discord.Helpers { TValue value = null; if (_dictionary.TryRemove(key, out value)) + { + if (_onRemove != null) + _onRemove(value); return value; + } else return null; } diff --git a/src/Discord.Net/Helpers/Http.cs b/src/Discord.Net/Helpers/Http.cs index 55c99edef..943a486d6 100644 --- a/src/Discord.Net/Helpers/Http.cs +++ b/src/Discord.Net/Helpers/Http.cs @@ -10,7 +10,6 @@ using System.Globalization; #if TEST_RESPONSES using System.Diagnostics; -using JsonErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; #endif namespace Discord.Helpers diff --git a/src/Discord.Net/Membership.cs b/src/Discord.Net/Membership.cs index 64f95f9ed..c0c5d6045 100644 --- a/src/Discord.Net/Membership.cs +++ b/src/Discord.Net/Membership.cs @@ -18,6 +18,11 @@ namespace Discord public bool IsSuppressed { get; internal set; } public string SessionId { get; internal set; } + public string Token { get; internal set; } + /// Returns the id for the game this user is currently playing. + public string GameId { get; internal set; } + /// Returns the current status for this user. + public string Status { get; internal set; } public string ServerId { get; } public Server Server => _client.GetServer(ServerId); @@ -31,29 +36,11 @@ namespace Discord public string[] RoleIds { get; internal set; } public IEnumerable Roles => RoleIds.Select(x => _client.GetRole(x)); - public Membership(string serverId, string userId, DateTime joinedAt, DiscordClient client) + public Membership(string serverId, string userId, DiscordClient client) { ServerId = serverId; UserId = userId; - JoinedAt = joinedAt; _client = client; } - - internal void Update(ExtendedServerInfo.Membership data) - { - IsDeafened = data.IsDeaf; - IsMuted = data.IsMuted; - RoleIds = data.Roles; - } - internal void Update(WebSocketEvents.VoiceStateUpdate data) - { - VoiceChannelId = data.ChannelId; - IsDeafened = data.IsDeafened; - IsMuted = data.IsMuted; - IsSelfDeafened = data.IsSelfDeafened; - IsSelfMuted = data.IsSelfMuted; - IsSuppressed = data.IsSuppressed; - SessionId = data.SessionId; - } } } diff --git a/src/Discord.Net/Message.cs b/src/Discord.Net/Message.cs index 827cdb36e..eef21ee28 100644 --- a/src/Discord.Net/Message.cs +++ b/src/Discord.Net/Message.cs @@ -7,10 +7,39 @@ namespace Discord { public sealed class Message { - public class Attachment + public sealed class Attachment : File { /// Unique identifier for this file. public string Id { get; internal set; } + /// Size, in bytes, of this file file. + public int Size { get; internal set; } + /// Filename of this file. + public string Filename { get; internal set; } + } + public sealed class Embed + { + /// URL of this embed. + public string Url { get; internal set; } + /// Type of this embed. + public string Type { get; internal set; } + /// Title for this embed. + public string Title { get; internal set; } + /// Summary of this embed. + public string Description { get; internal set; } + /// Returns information about the providing website of this embed. + public EmbedProvider Provider { get; internal set; } + /// Returns the thumbnail of this embed. + public File Thumbnail { get; internal set; } + } + public sealed class EmbedProvider + { + /// URL of this embed provider. + public string Url { get; internal set; } + /// Name of this embed provider. + public string Name { get; internal set; } + } + public class File + { /// Download url for this file. public string Url { get; internal set; } /// Preview url for this file. @@ -19,10 +48,6 @@ namespace Discord public int? Width { get; internal set; } /// Height of this file, if it is an image. public int? Height { get; internal set; } - /// Size, in bytes, of this file file. - public int Size { get; internal set; } - /// Filename of this file. - public string Filename { get; internal set; } } private readonly DiscordClient _client; @@ -49,6 +74,9 @@ namespace Discord public DateTime? EditedTimestamp { get; internal set; } /// Returns the attachments included in this message. public Attachment[] Attachments { get; internal set; } + //TODO: Not Implemented + /// Returns a collection of all embeded content in this message. + public Embed[] Embeds { get; internal set; } /// Returns a collection of all user ids mentioned in this message. public string[] MentionIds { get; internal set; } @@ -68,10 +96,6 @@ namespace Discord [JsonIgnore] public User User => _client.GetUser(UserId); - //TODO: Not Implemented - /// Not implemented, stored for reference. - public object[] Embeds { get; internal set; } - internal Message(string id, string channelId, DiscordClient client) { Id = id; diff --git a/src/Discord.Net/PackedPermissions.cs b/src/Discord.Net/PackedPermissions.cs new file mode 100644 index 000000000..3b1bd4b50 --- /dev/null +++ b/src/Discord.Net/PackedPermissions.cs @@ -0,0 +1,67 @@ +namespace Discord +{ + public sealed class PackedPermissions + { + private uint _rawValue; + internal uint RawValue { get { return _rawValue; } set { _rawValue = value; } } + + internal PackedPermissions() { } + internal PackedPermissions(uint rawValue) { _rawValue = rawValue; } + + /// If True, a user may create invites. + public bool General_CreateInstantInvite => ((_rawValue >> 0) & 0x1) == 1; + /// If True, a user may ban users from the server. + public bool General_BanMembers => ((_rawValue >> 1) & 0x1) == 1; + /// If True, a user may kick users from the server. + public bool General_KickMembers => ((_rawValue >> 2) & 0x1) == 1; + /// If True, a user adjust roles. + /// Having this permission effectively gives all the others as a user may add them to themselves. + public bool General_ManageRoles => ((_rawValue >> 3) & 0x1) == 1; + /// If True, a user may create, delete and modify channels. + public bool General_ManageChannels => ((_rawValue >> 4) & 0x1) == 1; + /// If True, a user may adjust server properties. + public bool General_ManageServer => ((_rawValue >> 5) & 0x1) == 1; + + //4 Unused + + /// If True, a user may join channels. + /// Note that without this permission, a channel is not sent by the server. + public bool Text_ReadMessages => ((_rawValue >> 10) & 0x1) == 1; + /// If True, a user may send messages. + public bool Text_SendMessages => ((_rawValue >> 11) & 0x1) == 1; + /// If True, a user may send text-to-speech messages. + public bool Text_SendTTSMessages => ((_rawValue >> 12) & 0x1) == 1; + /// If True, a user may delete messages. + public bool Text_ManageMessages => ((_rawValue >> 13) & 0x1) == 1; + /// If True, Discord will auto-embed links sent by this user. + public bool Text_EmbedLinks => ((_rawValue >> 14) & 0x1) == 1; + /// If True, a user may send files. + public bool Text_AttachFiles => ((_rawValue >> 15) & 0x1) == 1; + /// If True, a user may read previous messages. + public bool Text_ReadMessageHistory => ((_rawValue >> 16) & 0x1) == 1; + /// If True, a user may mention @everyone. + public bool Text_MentionEveryone => ((_rawValue >> 17) & 0x1) == 1; + + //2 Unused + + /// If True, a user may connect to a voice channel. + public bool Voice_Connect => ((_rawValue >> 20) & 0x1) == 1; + /// If True, a user may speak in a voice channel. + public bool Voice_Speak => ((_rawValue >> 21) & 0x1) == 1; + /// If True, a user may mute users. + public bool Voice_MuteMembers => ((_rawValue >> 22) & 0x1) == 1; + /// If True, a user may deafen users. + public bool Voice_DeafenMembers => ((_rawValue >> 23) & 0x1) == 1; + /// If True, a user may move other users between voice channels. + public bool Voice_MoveMembers => ((_rawValue >> 24) & 0x1) == 1; + /// If True, a user may use voice activation rather than push-to-talk. + public bool Voice_UseVoiceActivation => ((_rawValue >> 25) & 0x1) == 1; + + //6 Unused + + public static implicit operator uint (PackedPermissions perms) + { + return perms._rawValue; + } + } +} diff --git a/src/Discord.Net/Role.cs b/src/Discord.Net/Role.cs index cdf61c5be..515274710 100644 --- a/src/Discord.Net/Role.cs +++ b/src/Discord.Net/Role.cs @@ -4,70 +4,6 @@ namespace Discord { public sealed class Role { - public sealed class PackedPermissions - { - private uint _rawValue; - internal uint RawValue { get { return _rawValue; } set { _rawValue = value; } } - - internal PackedPermissions() { } - - /// If True, a user may create invites. - public bool General_CreateInstantInvite => ((_rawValue >> 0) & 0x1) == 1; - /// If True, a user may ban users from the server. - public bool General_BanMembers => ((_rawValue >> 1) & 0x1) == 1; - /// If True, a user may kick users from the server. - public bool General_KickMembers => ((_rawValue >> 2) & 0x1) == 1; - /// If True, a user adjust roles. - /// Having this permission effectively gives all the others as a user may add them to themselves. - public bool General_ManageRoles => ((_rawValue >> 3) & 0x1) == 1; - /// If True, a user may create, delete and modify channels. - public bool General_ManageChannels => ((_rawValue >> 4) & 0x1) == 1; - /// If True, a user may adjust server properties. - public bool General_ManageServer => ((_rawValue >> 5) & 0x1) == 1; - - //4 Unused - - /// If True, a user may join channels. - /// Note that without this permission, a channel is not sent by the server. - public bool Text_ReadMessages => ((_rawValue >> 10) & 0x1) == 1; - /// If True, a user may send messages. - public bool Text_SendMessages => ((_rawValue >> 11) & 0x1) == 1; - /// If True, a user may send text-to-speech messages. - public bool Text_SendTTSMessages => ((_rawValue >> 12) & 0x1) == 1; - /// If True, a user may delete messages. - public bool Text_ManageMessages => ((_rawValue >> 13) & 0x1) == 1; - /// If True, Discord will auto-embed links sent by this user. - public bool Text_EmbedLinks => ((_rawValue >> 14) & 0x1) == 1; - /// If True, a user may send files. - public bool Text_AttachFiles => ((_rawValue >> 15) & 0x1) == 1; - /// If True, a user may read previous messages. - public bool Text_ReadMessageHistory => ((_rawValue >> 16) & 0x1) == 1; - /// If True, a user may mention @everyone. - public bool Text_MentionEveryone => ((_rawValue >> 17) & 0x1) == 1; - - //2 Unused - - /// If True, a user may connect to a voice channel. - public bool Voice_Connect => ((_rawValue >> 20) & 0x1) == 1; - /// If True, a user may speak in a voice channel. - public bool Voice_Speak => ((_rawValue >> 21) & 0x1) == 1; - /// If True, a user may mute users. - public bool Voice_MuteMembers => ((_rawValue >> 22) & 0x1) == 1; - /// If True, a user may deafen users. - public bool Voice_DeafenMembers => ((_rawValue >> 23) & 0x1) == 1; - /// If True, a user may move other users between voice channels. - public bool Voice_MoveMembers => ((_rawValue >> 24) & 0x1) == 1; - /// If True, a user may use voice activation rather than push-to-talk. - public bool Voice_UseVoiceActivation => ((_rawValue >> 25) & 0x1) == 1; - - //6 Unused - - public static implicit operator uint(PackedPermissions perms) - { - return perms._rawValue; - } - } - private readonly DiscordClient _client; /// Returns the unique identifier for this role. diff --git a/src/Discord.Net/Server.cs b/src/Discord.Net/Server.cs index 7031bf9e4..9e2bf851e 100644 --- a/src/Discord.Net/Server.cs +++ b/src/Discord.Net/Server.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Helpers; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -38,9 +39,9 @@ namespace Discord /// Returns the default channel for this server. public Channel DefaultChannel =>_client.GetChannel(DefaultChannelId); - internal ConcurrentDictionary _members; + internal AsyncCache _members; /// Returns a collection of all channels within this server. - public IEnumerable Members => _members.Values; + public IEnumerable Members => _members; internal ConcurrentDictionary _bans; /// Returns a collection of all users banned on this server. @@ -54,38 +55,59 @@ namespace Discord /// Returns a collection of all roles within this server. public IEnumerable Roles => _client.Roles.Where(x => x.ServerId == Id); - //TODO: Not Implemented - /// Not implemented, stored for reference. - public object Presence { get; internal set; } - //TODO: Not Implemented - /// Not implemented, stored for reference. - public object[] VoiceStates { get; internal set; } - internal Server(string id, DiscordClient client) { Id = id; _client = client; - _members = new ConcurrentDictionary(); _bans = new ConcurrentDictionary(); + _members = new AsyncCache( + (key, parentKey) => new Membership(parentKey, key, _client), + (member, model) => + { + if (model is API.Models.PresenceMemberInfo) + { + var extendedModel = model as API.Models.PresenceMemberInfo; + member.Status = extendedModel.Status; + member.GameId = extendedModel.GameId; + } + if (model is API.Models.VoiceMemberInfo) + { + var extendedModel = model as API.Models.VoiceMemberInfo; + member.VoiceChannelId = extendedModel.ChannelId; + member.IsDeafened = extendedModel.IsDeafened; + member.IsMuted = extendedModel.IsMuted; + member.IsSelfDeafened = extendedModel.IsSelfDeafened; + member.IsSelfMuted = extendedModel.IsSelfMuted; + member.IsSuppressed = extendedModel.IsSuppressed; + member.SessionId = extendedModel.SessionId; + member.Token = extendedModel.Token; + } + if (model is API.Models.RoleMemberInfo) + { + var extendedModel = model as API.Models.RoleMemberInfo; + member.IsDeafened = extendedModel.IsDeafened; + member.IsMuted = extendedModel.IsMuted; + member.RoleIds = extendedModel.Roles; + if (extendedModel.JoinedAt.HasValue) + member.JoinedAt = extendedModel.JoinedAt.Value; + } + } + ); } - internal void AddMember(Membership membership) + internal Membership UpdateMember(API.Models.MemberInfo membership) { - _members[membership.UserId] = membership; + return _members.Update(membership.User?.Id ?? membership.UserId, Id, membership); } internal Membership RemoveMember(string userId) { - Membership result = null; - _members.TryRemove(userId, out result); - return result; + return _members.Remove(userId); } public Membership GetMembership(User user) => GetMember(user.Id); public Membership GetMember(string userId) { - Membership result = null; - _members.TryGetValue(userId, out result); - return result; + return _members[userId]; } internal void AddBan(string banId) diff --git a/src/Discord.Net/User.cs b/src/Discord.Net/User.cs index eee366cab..2475c1d28 100644 --- a/src/Discord.Net/User.cs +++ b/src/Discord.Net/User.cs @@ -27,10 +27,6 @@ namespace Discord /// Returns if the email for this user has been verified. /// This field is only ever populated for the current logged in user. public bool IsVerified { get; internal set; } - /// Returns the id for the game this user is currently playing. - public string GameId { get; internal set; } - /// Returns the current status for this user. - public string Status { get; internal set; } /// Returns the string "<@Id>" to be used as a shortcut when including mentions in text. public string Mention { get { return $"<@{Id}>"; } }