diff --git a/src/Discord.Net.Modules/ModuleManager.cs b/src/Discord.Net.Modules/ModuleManager.cs index 25692705d..e279e89d4 100644 --- a/src/Discord.Net.Modules/ModuleManager.cs +++ b/src/Discord.Net.Modules/ModuleManager.cs @@ -34,7 +34,7 @@ namespace Discord.Modules public event EventHandler UserUpdated; public event EventHandler UserPresenceUpdated; public event EventHandler UserVoiceStateUpdated; - public event EventHandler UserIsTypingUpdated; + public event EventHandler UserIsTypingUpdated; public event EventHandler MessageReceived; public event EventHandler MessageSent; diff --git a/src/Discord.Net.Shared/EventHelper.cs b/src/Discord.Net.Shared/EventHelper.cs deleted file mode 100644 index 264930199..000000000 --- a/src/Discord.Net.Shared/EventHelper.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Discord -{ - internal static class EventHelper - { - public static void Raise(Logger logger, string name, Action action) - { - try { action(); } - catch (Exception ex) - { - var ex2 = ex.GetBaseException(); - logger.Error($"{name}'s handler raised {ex2.GetType().Name}: ${ex2.Message}", ex); - } - } - } -} diff --git a/src/Discord.Net.Shared/IdConvert.cs b/src/Discord.Net.Shared/IdConvert.cs deleted file mode 100644 index e4d67b063..000000000 --- a/src/Discord.Net.Shared/IdConvert.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Globalization; - -namespace Discord -{ - internal static class IdConvert - { - internal static readonly IFormatProvider _format = CultureInfo.InvariantCulture; - - public static ulong ToLong(string value) - => ulong.Parse(value, NumberStyles.None, _format); - public static ulong? ToNullableLong(string value) - => value == null ? (ulong?)null : ulong.Parse(value, NumberStyles.None, _format); - - public static string ToString(ulong value) - => value.ToString(_format); - public static string ToString(ulong? value) - => value?.ToString(_format); - } -} diff --git a/src/Discord.Net/API/Client/Common/Channel.cs b/src/Discord.Net/API/Client/Common/Channel.cs index 475abd5c7..fd879c680 100644 --- a/src/Discord.Net/API/Client/Common/Channel.cs +++ b/src/Discord.Net/API/Client/Common/Channel.cs @@ -21,9 +21,9 @@ namespace Discord.API.Client [JsonProperty("last_message_id"), JsonConverter(typeof(NullableLongStringConverter))] public ulong? LastMessageId { get; set; } [JsonProperty("is_private")] - public bool IsPrivate { get; set; } + public bool? IsPrivate { get; set; } [JsonProperty("position")] - public int Position { get; set; } + public int? Position { get; set; } [JsonProperty("topic")] public string Topic { get; set; } [JsonProperty("permission_overwrites")] diff --git a/src/Discord.Net/API/Client/Common/MemberPresence.cs b/src/Discord.Net/API/Client/Common/MemberPresence.cs index 52a445190..87e488c2d 100644 --- a/src/Discord.Net/API/Client/Common/MemberPresence.cs +++ b/src/Discord.Net/API/Client/Common/MemberPresence.cs @@ -6,7 +6,7 @@ namespace Discord.API.Client public class MemberPresence : MemberReference { [JsonProperty("game_id")] - public int? GameId { get; set; } + public string GameId { get; set; } [JsonProperty("status")] public string Status { get; set; } [JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] diff --git a/src/Discord.Net/API/Client/Common/MemberReference.cs b/src/Discord.Net/API/Client/Common/MemberReference.cs index e064921ee..2db5c30a2 100644 --- a/src/Discord.Net/API/Client/Common/MemberReference.cs +++ b/src/Discord.Net/API/Client/Common/MemberReference.cs @@ -6,7 +6,7 @@ namespace Discord.API.Client public class MemberReference { [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] - public ulong GuildId { get; set; } + public ulong? GuildId { get; set; } [JsonProperty("user")] public UserReference User { get; set; } } diff --git a/src/Discord.Net/API/Client/Rest/GetMessages.cs b/src/Discord.Net/API/Client/Rest/GetMessages.cs index 5e58c6488..d4dc89a94 100644 --- a/src/Discord.Net/API/Client/Rest/GetMessages.cs +++ b/src/Discord.Net/API/Client/Rest/GetMessages.cs @@ -14,7 +14,7 @@ namespace Discord.API.Client.Rest StringBuilder query = new StringBuilder(); this.AddQueryParam(query, "limit", Limit.ToString()); if (RelativeDir != null) - this.AddQueryParam(query, RelativeDir, RelativeId.Value.ToString()); + this.AddQueryParam(query, RelativeDir, RelativeId.ToString()); return $"channels/{ChannelId}/messages{query}"; } } @@ -25,7 +25,7 @@ namespace Discord.API.Client.Rest public int Limit { get; set; } = 100; public string RelativeDir { get; set; } = null; - public ulong? RelativeId { get; set; } = 0; + public ulong RelativeId { get; set; } = 0; public GetMessagesRequest(ulong channelId) { diff --git a/src/Discord.Net/API/Converters.cs b/src/Discord.Net/API/Converters.cs index 53e25f4d7..91ea53575 100644 --- a/src/Discord.Net/API/Converters.cs +++ b/src/Discord.Net/API/Converters.cs @@ -9,9 +9,9 @@ namespace Discord.API.Converters public override bool CanConvert(Type objectType) => objectType == typeof(ulong); public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - => IdConvert.ToLong((string)reader.Value); + => ((string)reader.Value).ToId(); public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - => writer.WriteValue(IdConvert.ToString((ulong)value)); + => writer.WriteValue(((ulong)value).ToIdString()); } public class NullableLongStringConverter : JsonConverter @@ -19,9 +19,9 @@ namespace Discord.API.Converters public override bool CanConvert(Type objectType) => objectType == typeof(ulong?); public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - => IdConvert.ToNullableLong((string)reader.Value); + => ((string)reader.Value).ToNullableId(); public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - => writer.WriteValue(IdConvert.ToString((ulong?)value)); + => writer.WriteValue(((ulong?)value).ToIdString()); } /*public class LongStringEnumerableConverter : JsonConverter @@ -66,7 +66,7 @@ namespace Discord.API.Converters reader.Read(); while (reader.TokenType != JsonToken.EndArray) { - result.Add(IdConvert.ToLong((string)reader.Value)); + result.Add(((string)reader.Value).ToId()); reader.Read(); } } @@ -81,7 +81,7 @@ namespace Discord.API.Converters writer.WriteStartArray(); var a = (ulong[])value; for (int i = 0; i < a.Length; i++) - writer.WriteValue(IdConvert.ToString(a[i])); + writer.WriteValue(a[i].ToIdString()); writer.WriteEndArray(); } } diff --git a/src/Discord.Net/DiscordClient.Channels.cs b/src/Discord.Net/DiscordClient.Channels.cs deleted file mode 100644 index dcecf3e91..000000000 --- a/src/Discord.Net/DiscordClient.Channels.cs +++ /dev/null @@ -1,223 +0,0 @@ -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - internal sealed class Channels : AsyncCollection - { - public IEnumerable PrivateChannels => _privateChannels.Select(x => x.Value); - private ConcurrentDictionary _privateChannels; - - public Channels(DiscordClient client, object writerLock) - : base(client, writerLock) - { - _privateChannels = new ConcurrentDictionary(); - ItemCreated += (s, e) => - { - if (e.Item.IsPrivate) - _privateChannels.TryAdd(e.Item.Id, e.Item); - }; - ItemDestroyed += (s, e) => - { - if (e.Item.IsPrivate) - { - Channel ignored; - _privateChannels.TryRemove(e.Item.Id, out ignored); - } - }; - Cleared += (s, e) => _privateChannels.Clear(); - } - - public Channel GetOrAdd(ulong id, ulong? serverId, ulong? recipientId = null) - => GetOrAdd(id, () => new Channel(_client, id, serverId, recipientId)); - } - - public class ChannelEventArgs : EventArgs - { - public Channel Channel { get; } - public Server Server => Channel.Server; - - public ChannelEventArgs(Channel channel) { Channel = channel; } - } - - public partial class DiscordClient - { - public event EventHandler ChannelCreated; - private void RaiseChannelCreated(Channel channel) - { - if (ChannelCreated != null) - EventHelper.Raise(_logger, nameof(ChannelCreated), () => ChannelCreated(this, new ChannelEventArgs(channel))); - } - public event EventHandler ChannelDestroyed; - private void RaiseChannelDestroyed(Channel channel) - { - if (ChannelDestroyed != null) - EventHelper.Raise(_logger, nameof(ChannelDestroyed), () => ChannelDestroyed(this, new ChannelEventArgs(channel))); - } - public event EventHandler ChannelUpdated; - private void RaiseChannelUpdated(Channel channel) - { - if (ChannelUpdated != null) - EventHelper.Raise(_logger, nameof(ChannelUpdated), () => ChannelUpdated(this, new ChannelEventArgs(channel))); - } - - /// Returns a collection of all servers this client is a member of. - public IEnumerable PrivateChannels { get { CheckReady(); return _channels.PrivateChannels; } } - internal Channels Channels => _channels; - private readonly Channels _channels; - - /// Returns the channel with the specified id, or null if none was found. - public Channel GetChannel(ulong id) - { - CheckReady(); - - return _channels[id]; - } - - /// Returns all channels with the specified server and name. - /// Name formats supported: Name, #Name and <#Id>. Search is case-insensitive if exactMatch is false. - public IEnumerable FindChannels(Server server, string name, ChannelType type = null, bool exactMatch = false) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (name == null) throw new ArgumentNullException(nameof(name)); - CheckReady(); - - var query = server.Channels.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); - - if (!exactMatch && name.Length >= 2) - { - if (name[0] == '<' && name[1] == '#' && name[name.Length - 1] == '>') //Parse mention - { - var id = IdConvert.ToLong(name.Substring(2, name.Length - 3)); - var channel = _channels[id]; - if (channel != null) - query = query.Concat(new Channel[] { channel }); - } - else if (name[0] == '#' && (type == null || type == ChannelType.Text)) //If we somehow get text starting with # but isn't a mention - { - string name2 = name.Substring(1); - query = query.Concat(server.TextChannels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); - } - } - - if (type != null) - query = query.Where(x => x.Type == type); - return query; - } - - /// Creates a new channel with the provided name and type. - public async Task CreateChannel(Server server, string name, ChannelType type) - { - 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 request = new CreateChannelRequest(server.Id) { Name = name, Type = type.Value }; - var response = await _clientRest.Send(request).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 async Task CreatePMChannel(User user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - CheckReady(); - - Channel channel = null; - if (user != null) - channel = user.Global.PrivateChannel; - if (channel == null) - { - var request = new CreatePrivateChannelRequest() { RecipientId = user.Id }; - var response = await _clientRest.Send(request).ConfigureAwait(false); - - var recipient = _users.GetOrAdd(response.Recipient.Id, null); - 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 async Task EditChannel(Channel channel, string name = null, string topic = null, int? position = null) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - CheckReady(); - - if (name != null || topic != null) - { - var request = new UpdateChannelRequest(channel.Id) - { - Name = name ?? channel.Name, - Topic = topic ?? channel.Topic, - Position = channel.Position - }; - await _clientRest.Send(request).ConfigureAwait(false); - } - - if (position != null) - { - Channel[] channels = channel.Server.Channels.Where(x => x.Type == channel.Type).OrderBy(x => x.Position).ToArray(); - int oldPos = Array.IndexOf(channels, channel); - var newPosChannel = channels.Where(x => x.Position > position).FirstOrDefault(); - int newPos = (newPosChannel != null ? Array.IndexOf(channels, newPosChannel) : channels.Length) - 1; - if (newPos < 0) - newPos = 0; - int minPos; - - if (oldPos < newPos) //Moving Down - { - minPos = oldPos; - for (int i = oldPos; i < newPos; i++) - channels[i] = channels[i + 1]; - channels[newPos] = channel; - } - else //(oldPos > newPos) Moving Up - { - minPos = newPos; - for (int i = oldPos; i > newPos; i--) - channels[i] = channels[i - 1]; - channels[newPos] = channel; - } - Channel after = minPos > 0 ? channels.Skip(minPos - 1).FirstOrDefault() : null; - await ReorderChannels(channel.Server, channels.Skip(minPos), after).ConfigureAwait(false); - } - } - - /// 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 (server == null) throw new ArgumentNullException(nameof(server)); - if (channels == null) throw new ArgumentNullException(nameof(channels)); - CheckReady(); - - var request = new ReorderChannelsRequest(server.Id) - { - ChannelIds = channels.Select(x => x.Id).ToArray(), - StartPos = after != null ? after.Position + 1 : channels.Min(x => x.Position) - }; - return _clientRest.Send(request); - } - - /// Destroys the provided channel. - public async Task DeleteChannel(Channel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - CheckReady(); - - try { await _clientRest.Send(new DeleteChannelRequest(channel.Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - } -} \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.Events.cs b/src/Discord.Net/DiscordClient.Events.cs new file mode 100644 index 000000000..b3c2e41ab --- /dev/null +++ b/src/Discord.Net/DiscordClient.Events.cs @@ -0,0 +1,114 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Discord +{ + public partial class DiscordClient + { + public event EventHandler Connected = delegate { }; + public event EventHandler Disconnected = delegate { }; + public event EventHandler ChannelCreated = delegate { }; + public event EventHandler ChannelDestroyed = delegate { }; + public event EventHandler ChannelUpdated = delegate { }; + public event EventHandler MessageAcknowledged = delegate { }; + public event EventHandler MessageDeleted = delegate { }; + public event EventHandler MessageReceived = delegate { }; + public event EventHandler MessageSent = delegate { }; + public event EventHandler MessageUpdated = delegate { }; + public event EventHandler ProfileUpdated = delegate { }; + public event EventHandler RoleCreated = delegate { }; + public event EventHandler RoleUpdated = delegate { }; + public event EventHandler RoleDeleted = delegate { }; + public event EventHandler JoinedServer = delegate { }; + public event EventHandler LeftServer = delegate { }; + public event EventHandler ServerAvailable = delegate { }; + public event EventHandler ServerUpdated = delegate { }; + public event EventHandler ServerUnavailable = delegate { }; + public event EventHandler UserBanned = delegate { }; + public event EventHandler UserIsTypingUpdated = delegate { }; + public event EventHandler UserJoined = delegate { }; + public event EventHandler UserLeft = delegate { }; + public event EventHandler UserPresenceUpdated = delegate { }; + public event EventHandler UserUpdated = delegate { }; + public event EventHandler UserUnbanned = delegate { }; + public event EventHandler UserVoiceStateUpdated = delegate { }; + + private void OnConnected() + => OnEvent(Connected); + private void OnDisconnected(bool wasUnexpected, Exception ex) + => OnEvent(Disconnected, new DisconnectedEventArgs(wasUnexpected, ex)); + + private void OnChannelCreated(Channel channel) + => OnEvent(ChannelCreated, new ChannelEventArgs(channel)); + private void OnChannelDestroyed(Channel channel) + => OnEvent(ChannelDestroyed, new ChannelEventArgs(channel)); + private void OnChannelUpdated(Channel channel) + => OnEvent(ChannelUpdated, new ChannelEventArgs(channel)); + + private void OnMessageAcknowledged(Message msg) + => OnEvent(MessageAcknowledged, new MessageEventArgs(msg)); + private void OnMessageDeleted(Message msg) + => OnEvent(MessageDeleted, new MessageEventArgs(msg)); + private void OnMessageReceived(Message msg) + => OnEvent(MessageReceived, new MessageEventArgs(msg)); + /*private void OnMessageSent(Message msg) + => OnEvent(MessageSent, new MessageEventArgs(msg));*/ + private void OnMessageUpdated(Message msg) + => OnEvent(MessageUpdated, new MessageEventArgs(msg)); + + private void OnProfileUpdated(Profile profile) + => OnEvent(ProfileUpdated, new ProfileEventArgs(profile)); + + private void OnRoleCreated(Role role) + => OnEvent(RoleCreated, new RoleEventArgs(role)); + private void OnRoleDeleted(Role role) + => OnEvent(RoleDeleted, new RoleEventArgs(role)); + private void OnRoleUpdated(Role role) + => OnEvent(RoleUpdated, new RoleEventArgs(role)); + + private void OnJoinedServer(Server server) + => OnEvent(JoinedServer, new ServerEventArgs(server)); + private void OnLeftServer(Server server) + => OnEvent(LeftServer, new ServerEventArgs(server)); + private void OnServerAvailable(Server server) + => OnEvent(ServerAvailable, new ServerEventArgs(server)); + private void OnServerUpdated(Server server) + => OnEvent(ServerUpdated, new ServerEventArgs(server)); + private void OnServerUnavailable(Server server) + => OnEvent(ServerUnavailable, new ServerEventArgs(server)); + + private void OnUserBanned(Server server, ulong userId) + => OnEvent(UserBanned, new BanEventArgs(server, userId)); + private void OnUserIsTypingUpdated(Channel channel, User user) + => OnEvent(UserIsTypingUpdated, new ChannelUserEventArgs(channel, user)); + private void OnUserJoined(User user) + => OnEvent(UserJoined, new UserEventArgs(user)); + private void OnUserLeft(User user) + => OnEvent(UserLeft, new UserEventArgs(user)); + private void OnUserPresenceUpdated(User user) + => OnEvent(UserPresenceUpdated, new UserEventArgs(user)); + private void OnUserUnbanned(Server server, ulong userId) + => OnEvent(UserUnbanned, new BanEventArgs(server, userId)); + private void OnUserUpdated(User user) + => OnEvent(UserUpdated, new UserEventArgs(user)); + private void OnUserVoiceStateUpdated(User user) + => OnEvent(UserVoiceStateUpdated, new UserEventArgs(user)); + + private void OnEvent(EventHandler handler, T eventArgs, [CallerMemberName] string callerName = null) + { + try { handler(this, eventArgs); } + catch (Exception ex) + { + Logger.Error($"{callerName.Substring(2)}'s handler encountered error {ex.GetType().Name}: ${ex.Message}", ex); + } + } + private void OnEvent(EventHandler handler, [CallerMemberName] string callerName = null) + { + try { handler(this, EventArgs.Empty); } + catch (Exception ex) + { + Logger.Error($"{callerName.Substring(2)}'s handler encountered error {ex.GetType().Name}: ${ex.Message}", ex); + } + } + } +} diff --git a/src/Discord.Net/DiscordClient.Invites.cs b/src/Discord.Net/DiscordClient.Invites.cs deleted file mode 100644 index 02dcfb3d3..000000000 --- a/src/Discord.Net/DiscordClient.Invites.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Linq; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - public partial class DiscordClient - { - /// Gets more info about the provided invite code. - /// Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode - public async Task GetInvite(string inviteIdOrXkcd) - { - if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd)); - CheckReady(); - - //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 _clientRest.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false); - var invite = new Invite(response.Code, response.XkcdPass); - invite.Update(response); - return invite; - } - - /// Gets all active (non-expired) invites to a provided server. - public async Task GetInvites(Server server) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - CheckReady(); - - var response = await _clientRest.Send(new GetInvitesRequest(server.Id)).ConfigureAwait(false); - return response.Select(x => - { - var invite = new Invite(x.Code, x.XkcdPass); - invite.Update(x); - return invite; - }).ToArray(); - } - - /// Creates a new invite to the default channel of the provided server. - /// 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(Server server, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - CheckReady(); - - 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(Channel channel, int maxAge = 1800, int maxUses = 0, bool isTemporary = false, bool withXkcd = false) - { - 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 request = new CreateInviteRequest(channel.Id) - { - MaxAge = maxAge, - MaxUses = maxUses, - IsTemporary = isTemporary, - WithXkcdPass = withXkcd - }; - - var response = await _clientRest.Send(request).ConfigureAwait(false); - var invite = new Invite(response.Code, response.XkcdPass); - return invite; - } - - /// Deletes the provided invite. - public async Task DeleteInvite(Invite invite) - { - if (invite == null) throw new ArgumentNullException(nameof(invite)); - CheckReady(); - - try { await _clientRest.Send(new DeleteInviteRequest(invite.Code)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - - /// Accepts the provided invite. - public Task AcceptInvite(Invite invite) - { - if (invite == null) throw new ArgumentNullException(nameof(invite)); - CheckReady(); - - return _clientRest.Send(new AcceptInviteRequest(invite.Code)); - } - } -} \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.Messages.cs b/src/Discord.Net/DiscordClient.Messages.cs deleted file mode 100644 index 513ce5d5d..000000000 --- a/src/Discord.Net/DiscordClient.Messages.cs +++ /dev/null @@ -1,429 +0,0 @@ -using Discord.API; -using Discord.API.Client.Rest; -using Discord.Net; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using APIMessage = Discord.API.Client.Message; -using APIUser = Discord.API.Client.User; - -namespace Discord -{ - public enum RelativeDirection { Before, After} - internal sealed class Messages : AsyncCollection - { - private bool _isEnabled; - - public Messages(DiscordClient client, object writerLock, bool isEnabled) - : base(client, writerLock) - { - _isEnabled = isEnabled; - } - - public Message GetOrAdd(ulong id, ulong channelId, ulong userId) - { - if (_isEnabled) - return GetOrAdd(id, () => new Message(_client, id, channelId, userId)); - else - { - var msg = new Message(_client, id, channelId, userId); - msg.Cache(); //Builds references - return msg; - } - } - public void Import(Dictionary messages) - => base.Import(messages); - } - - internal class MessageQueueItem - { - public readonly Message Message; - public readonly string Text; - public readonly ulong[] MentionedUsers; - public MessageQueueItem(Message msg, string text, ulong[] userIds) - { - Message = msg; - Text = text; - MentionedUsers = userIds; - } - } - - public class MessageEventArgs : EventArgs - { - public Message Message { get; } - public User User => Message.User; - public Channel Channel => Message.Channel; - public Server Server => Message.Server; - - public MessageEventArgs(Message msg) { Message = msg; } - } - - public partial class DiscordClient - { - public const int MaxMessageSize = 2000; - - public event EventHandler MessageReceived; - private void RaiseMessageReceived(Message msg) - { - if (MessageReceived != null) - EventHelper.Raise(_logger, nameof(MessageReceived), () => MessageReceived(this, new MessageEventArgs(msg))); - } - public event EventHandler MessageSent; - private void RaiseMessageSent(Message msg) - { - if (MessageSent != null) - EventHelper.Raise(_logger, nameof(MessageSent), () => MessageSent(this, new MessageEventArgs(msg))); - } - public event EventHandler MessageDeleted; - private void RaiseMessageDeleted(Message msg) - { - if (MessageDeleted != null) - EventHelper.Raise(_logger, nameof(MessageDeleted), () => MessageDeleted(this, new MessageEventArgs(msg))); - } - public event EventHandler MessageUpdated; - private void RaiseMessageUpdated(Message msg) - { - if (MessageUpdated != null) - EventHelper.Raise(_logger, nameof(MessageUpdated), () => MessageUpdated(this, new MessageEventArgs(msg))); - } - public event EventHandler MessageAcknowledged; - private void RaiseMessageAcknowledged(Message msg) - { - if (MessageAcknowledged != null) - EventHelper.Raise(_logger, nameof(MessageAcknowledged), () => MessageAcknowledged(this, new MessageEventArgs(msg))); - } - - internal Messages Messages => _messages; - private readonly Random _nonceRand; - private readonly Messages _messages; - private readonly JsonSerializer _messageImporter; - private readonly ConcurrentQueue _pendingMessages; - - /// Returns the message with the specified id, or null if none was found. - public Message GetMessage(ulong id) - { - if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id)); - CheckReady(); - - return _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) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (text == null) throw new ArgumentNullException(nameof(text)); - CheckReady(); - - return SendMessageInternal(channel, text, false); - } - /// Sends a private message to the provided user. - public async Task SendMessage(User user, string text) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (text == null) throw new ArgumentNullException(nameof(text)); - CheckReady(); - - var channel = await CreatePMChannel(user).ConfigureAwait(false); - return await SendMessageInternal(channel, text, false).ConfigureAwait(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)); - CheckReady(); - - return SendMessageInternal(channel, text, true); - } - /// Sends a file to the provided channel. - public Task SendFile(Channel channel, string filePath) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (filePath == null) throw new ArgumentNullException(nameof(filePath)); - CheckReady(); - - return SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath)); - } - /// Sends a file to the provided channel. - public async Task SendFile(Channel channel, string filename, Stream stream) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (filename == null) throw new ArgumentNullException(nameof(filename)); - if (stream == null) throw new ArgumentNullException(nameof(stream)); - CheckReady(); - - var request = new SendFileRequest(channel.Id) - { - Filename = filename, - Stream = stream - }; - var model = await _clientRest.Send(request).ConfigureAwait(false); - - var msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); - msg.Update(model); - RaiseMessageSent(msg); - return msg; - } - /// Sends a file to the provided channel. - public async Task SendFile(User user, string filePath) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (filePath == null) throw new ArgumentNullException(nameof(filePath)); - CheckReady(); - - var channel = await CreatePMChannel(user).ConfigureAwait(false); - return await SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath)).ConfigureAwait(false); - } - /// Sends a file to the provided channel. - public async Task SendFile(User user, string filename, Stream stream) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (filename == null) throw new ArgumentNullException(nameof(filename)); - if (stream == null) throw new ArgumentNullException(nameof(stream)); - CheckReady(); - - var channel = await CreatePMChannel(user).ConfigureAwait(false); - return await SendFile(channel, filename, stream).ConfigureAwait(false); - } - private async Task SendMessageInternal(Channel channel, string text, bool isTextToSpeech) - { - Message msg; - var server = channel.Server; - - var mentionedUsers = new List(); - text = Mention.CleanUserMentions(this, server, text, mentionedUsers); - if (text.Length > MaxMessageSize) - throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less."); - - if (Config.UseMessageQueue) - { - var nonce = GenerateNonce(); - msg = new Message(this, 0, channel.Id, _currentUser.Id); //_messages.GetOrAdd(nonce, channel.Id, _privateUser.Id); - var currentUser = msg.User; - msg.Update(new APIMessage - { - Content = text, - Timestamp = DateTime.UtcNow, - Author = new APIUser { Avatar = currentUser.AvatarId, Discriminator = currentUser.Discriminator, Id = _currentUser.Id, Username = currentUser.Name }, - ChannelId = channel.Id, - Nonce = IdConvert.ToString(nonce), - IsTextToSpeech = isTextToSpeech - }); - msg.State = MessageState.Queued; - - _pendingMessages.Enqueue(new MessageQueueItem(msg, text, mentionedUsers.Select(x => x.Id).ToArray())); - } - else - { - var request = new SendMessageRequest(channel.Id) - { - Content = text, - MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray(), - Nonce = null, - IsTTS = isTextToSpeech - }; - var model = await _clientRest.Send(request).ConfigureAwait(false); - msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); - msg.Update(model); - RaiseMessageSent(msg); - } - return msg; - } - - /// 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(Message message, string text) - { - if (message == null) throw new ArgumentNullException(nameof(message)); - if (text == null) throw new ArgumentNullException(nameof(text)); - CheckReady(); - - var channel = message.Channel; - var mentionedUsers = new List(); - if (!channel.IsPrivate) - text = Mention.CleanUserMentions(this, channel.Server, text, mentionedUsers); - - if (text.Length > MaxMessageSize) - throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less."); - - if (Config.UseMessageQueue) - _pendingMessages.Enqueue(new MessageQueueItem(message, text, mentionedUsers.Select(x => x.Id).ToArray())); - else - { - var request = new UpdateMessageRequest(message.Channel.Id, message.Id) - { - Content = text, - MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray() - }; - await _clientRest.Send(request).ConfigureAwait(false); - } - } - - /// Deletes the provided message. - public async Task DeleteMessage(Message message) - { - if (message == null) throw new ArgumentNullException(nameof(message)); - CheckReady(); - - var request = new DeleteMessageRequest(message.Id, message.Channel.Id); - try { await _clientRest.Send(request).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - public async Task DeleteMessages(IEnumerable messages) - { - if (messages == null) throw new ArgumentNullException(nameof(messages)); - CheckReady(); - - foreach (var message in messages) - { - var request = new DeleteMessageRequest(message.Id, message.Channel.Id); - try { await _clientRest.Send(request).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - } - - /// Downloads messages from the server, returning all messages before or after relativeMessageId, if it's provided. - public async Task DownloadMessages(Channel channel, int limit = 100, ulong? relativeMessageId = null, RelativeDirection relativeDir = RelativeDirection.Before, bool useCache = true) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (limit < 0) throw new ArgumentNullException(nameof(limit)); - CheckReady(); - - if (limit == 0) return new Message[0]; - if (channel != null && channel.Type == ChannelType.Text) - { - try - { - var request = new GetMessagesRequest(channel.Id) - { - Limit = limit, - RelativeDir = relativeDir == RelativeDirection.Before ? "before" : "after", - RelativeId = relativeMessageId - }; - var msgs = await _clientRest.Send(request).ConfigureAwait(false); - return msgs.Select(x => - { - Message msg = null; - if (useCache) - { - msg = _messages.GetOrAdd(x.Id, x.ChannelId, x.Author.Id); - var user = msg.User; - if (user != null) - user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp); - } - else - msg = /*_messages[x.Id] ??*/ new Message(this, x.Id, x.ChannelId, x.Author.Id); - msg.Update(x); - return msg; - }) - .ToArray(); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Forbidden){ } //Bad Permissions - } - return new Message[0]; - } - - /// Marks a given message as read. - public void AckMessage(Message message) - { - if (message == null) throw new ArgumentNullException(nameof(message)); - - if (!message.IsAuthor) - _clientRest.Send(new AckMessageRequest(message.Id, message.Channel.Id)); - } - - /// Deserializes messages from JSON format and imports them into the message cache. - public IEnumerable ImportMessages(Channel channel, string json) - { - if (json == null) throw new ArgumentNullException(nameof(json)); - - var dic = JArray.Parse(json) - .Select(x => - { - var msg = new Message(this, - x["Id"].Value(), - channel.Id, - x["UserId"].Value()); - - var reader = x.CreateReader(); - _messageImporter.Populate(reader, msg); - msg.Text = Mention.Resolve(msg, msg.RawText); - return msg; - }) - .ToDictionary(x => x.Id); - _messages.Import(dic); - foreach (var msg in dic.Values) - { - var user = msg.User; - if (user != null) - user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp); - } - return dic.Values; - } - - /// Serializes the message cache for a given channel to JSON. - public string ExportMessages(Channel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - - return JsonConvert.SerializeObject(channel.Messages); - } - - private Task MessageQueueAsync() - { - var cancelToken = _cancelToken; - int interval = Config.MessageQueueInterval; - - return Task.Run(async () => - { - MessageQueueItem queuedMessage; - - while (!cancelToken.IsCancellationRequested) - { - while (_pendingMessages.TryDequeue(out queuedMessage)) - { - var msg = queuedMessage.Message; - try - { - if (msg.Id == 0) - { - var request = new SendMessageRequest(msg.Channel.Id) - { - Content = queuedMessage.Text, - MentionedUserIds = queuedMessage.MentionedUsers, - Nonce = IdConvert.ToString(msg.Id), //Nonce - IsTTS = msg.IsTTS - }; - await _clientRest.Send(request).ConfigureAwait(false); - } - else - { - var request = new UpdateMessageRequest(msg.Channel.Id, msg.Id) - { - Content = queuedMessage.Text, - MentionedUserIds = queuedMessage.MentionedUsers - }; - await _clientRest.Send(request).ConfigureAwait(false); - } - } - catch (WebException) { break; } - catch (HttpException) { msg.State = MessageState.Failed; } - } - await Task.Delay(interval).ConfigureAwait(false); - } - }); - } - private ulong GenerateNonce() - { - lock (_nonceRand) - return (ulong)_nonceRand.Next(1, int.MaxValue); - } - } -} \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.Obsolete.cs b/src/Discord.Net/DiscordClient.Obsolete.cs new file mode 100644 index 000000000..c855afdca --- /dev/null +++ b/src/Discord.Net/DiscordClient.Obsolete.cs @@ -0,0 +1,972 @@ +namespace Discord +{ + /*public enum RelativeDirection { Before, After } + public partial class DiscordClient + { + /// Returns the channel with the specified id, or null if none was found. + public Channel GetChannel(ulong id) + { + CheckReady(); + + return _channels[id]; + } + + /// Returns all channels with the specified server and name. + /// Name formats supported: Name, #Name and <#Id>. Search is case-insensitive if exactMatch is false. + public IEnumerable FindChannels(Server server, string name, ChannelType type = null, bool exactMatch = false) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (name == null) throw new ArgumentNullException(nameof(name)); + CheckReady(); + + var query = server.Channels.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); + + if (!exactMatch && name.Length >= 2) + { + if (name[0] == '<' && name[1] == '#' && name[name.Length - 1] == '>') //Parse mention + { + var id = IdConvert.ToLong(name.Substring(2, name.Length - 3)); + var channel = _channels[id]; + if (channel != null) + query = query.Concat(new Channel[] { channel }); + } + else if (name[0] == '#' && (type == null || type == ChannelType.Text)) //If we somehow get text starting with # but isn't a mention + { + string name2 = name.Substring(1); + query = query.Concat(server.TextChannels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); + } + } + + if (type != null) + query = query.Where(x => x.Type == type); + return query; + } + + /// Creates a new channel with the provided name and type. + public async Task CreateChannel(Server server, string name, ChannelType type) + { + 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 request = new CreateChannelRequest(server.Id) { Name = name, Type = type.Value }; + var response = await _clientRest.Send(request).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 async Task CreatePMChannel(User user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + CheckReady(); + + Channel channel = null; + if (user != null) + channel = user.Global.PrivateChannel; + if (channel == null) + { + var request = new CreatePrivateChannelRequest() { RecipientId = user.Id }; + var response = await _clientRest.Send(request).ConfigureAwait(false); + + var recipient = _users.GetOrAdd(response.Recipient.Id, null); + 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 async Task EditChannel(Channel channel, string name = null, string topic = null, int? position = null) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + CheckReady(); + + if (name != null || topic != null) + { + var request = new UpdateChannelRequest(channel.Id) + { + Name = name ?? channel.Name, + Topic = topic ?? channel.Topic, + Position = channel.Position + }; + await _clientRest.Send(request).ConfigureAwait(false); + } + + if (position != null) + { + Channel[] channels = channel.Server.Channels.Where(x => x.Type == channel.Type).OrderBy(x => x.Position).ToArray(); + int oldPos = Array.IndexOf(channels, channel); + var newPosChannel = channels.Where(x => x.Position > position).FirstOrDefault(); + int newPos = (newPosChannel != null ? Array.IndexOf(channels, newPosChannel) : channels.Length) - 1; + if (newPos < 0) + newPos = 0; + int minPos; + + if (oldPos < newPos) //Moving Down + { + minPos = oldPos; + for (int i = oldPos; i < newPos; i++) + channels[i] = channels[i + 1]; + channels[newPos] = channel; + } + else //(oldPos > newPos) Moving Up + { + minPos = newPos; + for (int i = oldPos; i > newPos; i--) + channels[i] = channels[i - 1]; + channels[newPos] = channel; + } + Channel after = minPos > 0 ? channels.Skip(minPos - 1).FirstOrDefault() : null; + await ReorderChannels(channel.Server, channels.Skip(minPos), after).ConfigureAwait(false); + } + } + + /// 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 (server == null) throw new ArgumentNullException(nameof(server)); + if (channels == null) throw new ArgumentNullException(nameof(channels)); + CheckReady(); + + var request = new ReorderChannelsRequest(server.Id) + { + ChannelIds = channels.Select(x => x.Id).ToArray(), + StartPos = after != null ? after.Position + 1 : channels.Min(x => x.Position) + }; + return _clientRest.Send(request); + } + + /// Destroys the provided channel. + public async Task DeleteChannel(Channel channel) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + CheckReady(); + + try { await _clientRest.Send(new DeleteChannelRequest(channel.Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + /// Gets more info about the provided invite code. + /// Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode + public async Task GetInvite(string inviteIdOrXkcd) + { + if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd)); + CheckReady(); + + //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 _clientRest.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false); + var invite = new Invite(response.Code, response.XkcdPass); + invite.Update(response); + return invite; + } + + /// Gets all active (non-expired) invites to a provided server. + public async Task GetInvites(Server server) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + CheckReady(); + + var response = await _clientRest.Send(new GetInvitesRequest(server.Id)).ConfigureAwait(false); + return response.Select(x => + { + var invite = new Invite(x.Code, x.XkcdPass); + invite.Update(x); + return invite; + }).ToArray(); + } + + /// Creates a new invite to the default channel of the provided server. + /// 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(Server server, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + CheckReady(); + + 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(Channel channel, int maxAge = 1800, int maxUses = 0, bool isTemporary = false, bool withXkcd = false) + { + 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 request = new CreateInviteRequest(channel.Id) + { + MaxAge = maxAge, + MaxUses = maxUses, + IsTemporary = isTemporary, + WithXkcdPass = withXkcd + }; + + var response = await _clientRest.Send(request).ConfigureAwait(false); + var invite = new Invite(response.Code, response.XkcdPass); + return invite; + } + + /// Deletes the provided invite. + public async Task DeleteInvite(Invite invite) + { + if (invite == null) throw new ArgumentNullException(nameof(invite)); + CheckReady(); + + try { await _clientRest.Send(new DeleteInviteRequest(invite.Code)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + /// Accepts the provided invite. + public Task AcceptInvite(Invite invite) + { + if (invite == null) throw new ArgumentNullException(nameof(invite)); + CheckReady(); + + return _clientRest.Send(new AcceptInviteRequest(invite.Code)); + } + + + /// Returns the message with the specified id, or null if none was found. + public Message GetMessage(ulong id) + { + if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id)); + CheckReady(); + + return _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) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (text == null) throw new ArgumentNullException(nameof(text)); + CheckReady(); + + return SendMessageInternal(channel, text, false); + } + /// Sends a private message to the provided user. + public async Task SendMessage(User user, string text) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + if (text == null) throw new ArgumentNullException(nameof(text)); + CheckReady(); + + var channel = await CreatePMChannel(user).ConfigureAwait(false); + return await SendMessageInternal(channel, text, false).ConfigureAwait(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)); + CheckReady(); + + return SendMessageInternal(channel, text, true); + } + /// Sends a file to the provided channel. + public Task SendFile(Channel channel, string filePath) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (filePath == null) throw new ArgumentNullException(nameof(filePath)); + CheckReady(); + + return SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath)); + } + /// Sends a file to the provided channel. + public async Task SendFile(Channel channel, string filename, Stream stream) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (filename == null) throw new ArgumentNullException(nameof(filename)); + if (stream == null) throw new ArgumentNullException(nameof(stream)); + CheckReady(); + + var request = new SendFileRequest(channel.Id) + { + Filename = filename, + Stream = stream + }; + var model = await _clientRest.Send(request).ConfigureAwait(false); + + var msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); + msg.Update(model); + RaiseMessageSent(msg); + return msg; + } + /// Sends a file to the provided channel. + public async Task SendFile(User user, string filePath) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + if (filePath == null) throw new ArgumentNullException(nameof(filePath)); + CheckReady(); + + var channel = await CreatePMChannel(user).ConfigureAwait(false); + return await SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath)).ConfigureAwait(false); + } + /// Sends a file to the provided channel. + public async Task SendFile(User user, string filename, Stream stream) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + if (filename == null) throw new ArgumentNullException(nameof(filename)); + if (stream == null) throw new ArgumentNullException(nameof(stream)); + CheckReady(); + + var channel = await CreatePMChannel(user).ConfigureAwait(false); + return await SendFile(channel, filename, stream).ConfigureAwait(false); + } + private async Task SendMessageInternal(Channel channel, string text, bool isTextToSpeech) + { + Message msg; + var server = channel.Server; + + var mentionedUsers = new List(); + text = Mention.CleanUserMentions(this, server, text, mentionedUsers); + if (text.Length > MaxMessageSize) + throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less."); + + if (Config.UseMessageQueue) + { + var nonce = GenerateNonce(); + msg = new Message(this, 0, channel.Id, _currentUser.Id); //_messages.GetOrAdd(nonce, channel.Id, _privateUser.Id); + var currentUser = msg.User; + msg.Update(new APIMessage + { + Content = text, + Timestamp = DateTime.UtcNow, + Author = new APIUser { Avatar = currentUser.AvatarId, Discriminator = currentUser.Discriminator, Id = _currentUser.Id, Username = currentUser.Name }, + ChannelId = channel.Id, + Nonce = IdConvert.ToString(nonce), + IsTextToSpeech = isTextToSpeech + }); + msg.State = MessageState.Queued; + + _pendingMessages.Enqueue(new MessageQueueItem(msg, text, mentionedUsers.Select(x => x.Id).ToArray())); + } + else + { + var request = new SendMessageRequest(channel.Id) + { + Content = text, + MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray(), + Nonce = null, + IsTTS = isTextToSpeech + }; + var model = await _clientRest.Send(request).ConfigureAwait(false); + msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); + msg.Update(model); + RaiseMessageSent(msg); + } + return msg; + } + + /// 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(Message message, string text) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + if (text == null) throw new ArgumentNullException(nameof(text)); + CheckReady(); + + var channel = message.Channel; + var mentionedUsers = new List(); + if (!channel.IsPrivate) + text = Mention.CleanUserMentions(this, channel.Server, text, mentionedUsers); + + if (text.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); + + if (Config.UseMessageQueue) + _pendingMessages.Enqueue(new MessageQueueItem(message, text, mentionedUsers.Select(x => x.Id).ToArray())); + else + { + var request = new UpdateMessageRequest(message.Channel.Id, message.Id) + { + Content = text, + MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray() + }; + await _clientRest.Send(request).ConfigureAwait(false); + } + } + + /// Deletes the provided message. + public async Task DeleteMessage(Message message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + CheckReady(); + + var request = new DeleteMessageRequest(message.Id, message.Channel.Id); + try { await _clientRest.Send(request).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + public async Task DeleteMessages(IEnumerable messages) + { + if (messages == null) throw new ArgumentNullException(nameof(messages)); + CheckReady(); + + foreach (var message in messages) + { + var request = new DeleteMessageRequest(message.Id, message.Channel.Id); + try { await _clientRest.Send(request).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + } + + /// Downloads messages from the server, returning all messages before or after relativeMessageId, if it's provided. + public async Task DownloadMessages(Channel channel, int limit = 100, ulong? relativeMessageId = null, RelativeDirection relativeDir = RelativeDirection.Before, bool useCache = true) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (limit < 0) throw new ArgumentNullException(nameof(limit)); + CheckReady(); + + if (limit == 0) return new Message[0]; + if (channel != null && channel.Type == ChannelType.Text) + { + try + { + var request = new GetMessagesRequest(channel.Id) + { + Limit = limit, + RelativeDir = relativeDir == RelativeDirection.Before ? "before" : "after", + RelativeId = relativeMessageId + }; + var msgs = await _clientRest.Send(request).ConfigureAwait(false); + return msgs.Select(x => + { + Message msg = null; + if (useCache) + { + msg = _messages.GetOrAdd(x.Id, x.ChannelId, x.Author.Id); + var user = msg.User; + if (user != null) + user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp); + } + else + msg = new Message(this, x.Id, x.ChannelId, x.Author.Id); + msg.Update(x); + return msg; + }) + .ToArray(); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) { } //Bad Permissions + } + return new Message[0]; + } + + /// Marks a given message as read. + public void AckMessage(Message message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + + if (!message.IsAuthor) + _clientRest.Send(new AckMessageRequest(message.Id, message.Channel.Id)); + } + + /// Deserializes messages from JSON format and imports them into the message cache. + public IEnumerable ImportMessages(Channel channel, string json) + { + if (json == null) throw new ArgumentNullException(nameof(json)); + + var dic = JArray.Parse(json) + .Select(x => + { + var msg = new Message(this, + x["Id"].Value(), + channel.Id, + x["UserId"].Value()); + + var reader = x.CreateReader(); + _messageImporter.Populate(reader, msg); + msg.Text = Mention.Resolve(msg, msg.RawText); + return msg; + }) + .ToDictionary(x => x.Id); + _messages.Import(dic); + foreach (var msg in dic.Values) + { + var user = msg.User; + if (user != null) + user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp); + } + return dic.Values; + } + + /// Serializes the message cache for a given channel to JSON. + public string ExportMessages(Channel channel) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + + return JsonConvert.SerializeObject(channel.Messages); + } + + /// Returns the user with the specified id, along with their server-specific data, or null if none was found. + public User GetUser(Server server, ulong userId) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + CheckReady(); + + return _users[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. + public User GetUser(Server server, string username, ushort discriminator) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (username == null) throw new ArgumentNullException(nameof(username)); + CheckReady(); + + return FindUsers(server.Members, server.Id, username, discriminator, true).FirstOrDefault(); + } + + /// Returns all users with the specified server and name, along with their server-specific data. + /// Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false. + public IEnumerable FindUsers(Server server, string name, bool exactMatch = false) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (name == null) throw new ArgumentNullException(nameof(name)); + CheckReady(); + + return FindUsers(server.Members, server.Id, name, exactMatch: exactMatch); + } + /// Returns all users with the specified channel and name, along with their server-specific data. + /// Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false. + public IEnumerable FindUsers(Channel channel, string name, bool exactMatch = false) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (name == null) throw new ArgumentNullException(nameof(name)); + CheckReady(); + + return FindUsers(channel.Members, channel.IsPrivate ? (ulong?)null : channel.Server.Id, name, exactMatch: exactMatch); + } + + private IEnumerable FindUsers(IEnumerable users, ulong? serverId, string name, ushort? discriminator = null, bool exactMatch = false) + { + var query = users.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); + + if (!exactMatch && name.Length >= 2) + { + if (name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Parse mention + { + ulong id = IdConvert.ToLong(name.Substring(2, name.Length - 3)); + var user = _users[id, serverId]; + if (user != null) + query = query.Concat(new User[] { user }); + } + else if (name[0] == '@') //If we somehow get text starting with @ but isn't a mention + { + string name2 = name.Substring(1); + query = query.Concat(users.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); + } + } + + if (discriminator != null) + query = query.Where(x => x.Discriminator == discriminator.Value); + return query; + } + + public Task EditUser(User user, bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable roles = null) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + if (user.IsPrivate) throw new InvalidOperationException("Unable to edit users in a private channel"); + CheckReady(); + + //Modify the roles collection and filter out the everyone role + var roleIds = roles == null ? null : user.Roles.Where(x => !x.IsEveryone).Select(x => x.Id); + + var request = new UpdateMemberRequest(user.Server.Id, user.Id) + { + IsMuted = isMuted ?? user.IsServerMuted, + IsDeafened = isDeafened ?? user.IsServerDeafened, + VoiceChannelId = voiceChannel?.Id, + RoleIds = roleIds.ToArray() + }; + return _clientRest.Send(request); + } + + public Task KickUser(User user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + if (user.IsPrivate) throw new InvalidOperationException("Unable to kick users from a private channel"); + CheckReady(); + + var request = new KickMemberRequest(user.Server.Id, user.Id); + return _clientRest.Send(request); + } + public Task BanUser(User user, int pruneDays = 0) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + if (user.IsPrivate) throw new InvalidOperationException("Unable to ban users from a private channel"); + CheckReady(); + + var request = new AddGuildBanRequest(user.Server.Id, user.Id); + request.PruneDays = pruneDays; + return _clientRest.Send(request); + } + public async Task UnbanUser(Server server, ulong userId) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (userId <= 0) throw new ArgumentOutOfRangeException(nameof(userId)); + CheckReady(); + + try { await _clientRest.Send(new RemoveGuildBanRequest(server.Id, userId)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + public async Task PruneUsers(Server server, int days, bool simulate = false) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days)); + CheckReady(); + + var request = new PruneMembersRequest(server.Id) + { + Days = days, + IsSimulation = simulate + }; + var response = await _clientRest.Send(request).ConfigureAwait(false); + return response.Pruned; + } + + /// When Config.UseLargeThreshold is enabled, running this command will request the Discord server to provide you with all offline users for a particular server. + public void RequestOfflineUsers(Server server) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + + _webSocket.SendRequestMembers(server.Id, "", 0); + } + + public async Task EditProfile(string currentPassword = "", + string username = null, string email = null, string password = null, + Stream avatar = null, ImageType avatarType = ImageType.Png) + { + if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); + CheckReady(); + + var request = new UpdateProfileRequest() + { + CurrentPassword = currentPassword, + Email = email ?? _currentUser?.Email, + Password = password, + Username = username ?? _privateUser?.Name, + AvatarBase64 = Base64Image(avatarType, avatar, _privateUser?.AvatarId) + }; + + await _clientRest.Send(request).ConfigureAwait(false); + + if (password != null) + { + var loginRequest = new LoginRequest() + { + Email = _currentUser.Email, + Password = password + }; + var loginResponse = await _clientRest.Send(loginRequest).ConfigureAwait(false); + _clientRest.SetToken(loginResponse.Token); + } + } + + /// Returns the role with the specified id, or null if none was found. + public Role GetRole(ulong id) + { + CheckReady(); + + return _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) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (name == null) throw new ArgumentNullException(nameof(name)); + CheckReady(); + + // if (name.StartsWith("@")) + // { + // string name2 = name.Substring(1); + // 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.Server.Id == server.Id && + string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); + // } + } + + /// Note: due to current API limitations, the created role cannot be returned. + public async Task CreateRole(Server server, string name, ServerPermissions permissions = null, Color color = null, bool isHoisted = false) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (name == null) throw new ArgumentNullException(nameof(name)); + CheckReady(); + + var request1 = new CreateRoleRequest(server.Id); + var response1 = await _clientRest.Send(request1).ConfigureAwait(false); + var role = _roles.GetOrAdd(response1.Id, server.Id); + role.Update(response1); + + var request2 = new UpdateRoleRequest(role.Server.Id, role.Id) + { + Name = name, + Permissions = (permissions ?? role.Permissions).RawValue, + Color = (color ?? Color.Default).RawValue, + IsHoisted = isHoisted + }; + var response2 = await _clientRest.Send(request2).ConfigureAwait(false); + role.Update(response2); + + return role; + } + + public async Task EditRole(Role role, string name = null, ServerPermissions permissions = null, Color color = null, bool? isHoisted = null, int? position = null) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + CheckReady(); + + var request1 = new UpdateRoleRequest(role.Server.Id, role.Id) + { + Name = name ?? role.Name, + Permissions = (permissions ?? role.Permissions).RawValue, + Color = (color ?? role.Color).RawValue, + IsHoisted = isHoisted ?? role.IsHoisted + }; + + var response = await _clientRest.Send(request1).ConfigureAwait(false); + + if (position != null) + { + int oldPos = role.Position; + int newPos = position.Value; + int minPos; + Role[] roles = role.Server.Roles.OrderBy(x => x.Position).ToArray(); + + if (oldPos < newPos) //Moving Down + { + minPos = oldPos; + for (int i = oldPos; i < newPos; i++) + roles[i] = roles[i + 1]; + roles[newPos] = role; + } + else //(oldPos > newPos) Moving Up + { + minPos = newPos; + for (int i = oldPos; i > newPos; i--) + roles[i] = roles[i - 1]; + roles[newPos] = role; + } + + var request2 = new ReorderRolesRequest(role.Server.Id) + { + RoleIds = roles.Skip(minPos).Select(x => x.Id).ToArray(), + StartPos = minPos + }; + await _clientRest.Send(request2).ConfigureAwait(false); + } + } + + public async Task DeleteRole(Role role) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + CheckReady(); + + try { await _clientRest.Send(new DeleteRoleRequest(role.Server.Id, role.Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + public Task ReorderRoles(Server server, IEnumerable roles, int startPos = 0) + { + 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(); + + return _clientRest.Send(new ReorderRolesRequest(server.Id) + { + RoleIds = roles.Select(x => x.Id).ToArray(), + StartPos = startPos + }); + } + + /// Returns the server with the specified id, or null if none was found. + public Server GetServer(ulong id) + { + CheckReady(); + + return _servers[id]; + } + + /// Returns all servers with the specified name. + /// Search is case-insensitive. + public IEnumerable FindServers(string name) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + CheckReady(); + + return _servers.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); + } + + /// Creates a new server with the provided name and region (see Regions). + public async Task CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (region == null) throw new ArgumentNullException(nameof(region)); + CheckReady(); + + var request = new CreateGuildRequest() + { + Name = name, + Region = region.Id, + IconBase64 = Base64Image(iconType, icon, null) + }; + var response = await _clientRest.Send(request).ConfigureAwait(false); + + var server = _servers.GetOrAdd(response.Id); + server.Update(response); + return server; + } + + /// Edits the provided server, changing only non-null attributes. + public async Task EditServer(Server server, string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + CheckReady(); + + var request = new UpdateGuildRequest(server.Id) + { + Name = name ?? server.Name, + Region = region ?? server.Region, + IconBase64 = Base64Image(iconType, icon, server.IconId), + AFKChannelId = server.AFKChannel?.Id, + AFKTimeout = server.AFKTimeout + }; + var response = await _clientRest.Send(request).ConfigureAwait(false); + server.Update(response); + } + + /// Leaves the provided server, destroying it if you are the owner. + public async Task LeaveServer(Server server) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + CheckReady(); + + try { await _clientRest.Send(new LeaveGuildRequest(server.Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + public async Task> GetVoiceRegions() + { + CheckReady(); + + var regions = await _clientRest.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false); + return regions.Select(x => new Region(x.Id, x.Name, x.Hostname, x.Port)); + } + public DualChannelPermissions GetChannelPermissions(Channel channel, User user) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (user == null) throw new ArgumentNullException(nameof(user)); + CheckReady(); + + return channel.PermissionOverwrites + .Where(x => x.TargetType == PermissionTarget.User && x.TargetId == user.Id) + .Select(x => x.Permissions) + .FirstOrDefault(); + } + public DualChannelPermissions GetChannelPermissions(Channel channel, Role role) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (role == null) throw new ArgumentNullException(nameof(role)); + CheckReady(); + + return channel.PermissionOverwrites + .Where(x => x.TargetType == PermissionTarget.Role && x.TargetId == role.Id) + .Select(x => x.Permissions) + .FirstOrDefault(); + } + + public Task SetChannelPermissions(Channel channel, User user, ChannelPermissions allow = null, ChannelPermissions deny = null) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (user == null) throw new ArgumentNullException(nameof(user)); + CheckReady(); + + return SetChannelPermissions(channel, user.Id, PermissionTarget.User, allow, deny); + } + public Task SetChannelPermissions(Channel channel, User user, DualChannelPermissions permissions = null) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (user == null) throw new ArgumentNullException(nameof(user)); + CheckReady(); + + return SetChannelPermissions(channel, user.Id, PermissionTarget.User, permissions?.Allow, permissions?.Deny); + } + public Task SetChannelPermissions(Channel channel, Role role, ChannelPermissions allow = null, ChannelPermissions deny = null) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (role == null) throw new ArgumentNullException(nameof(role)); + CheckReady(); + + return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, allow, deny); + } + public Task SetChannelPermissions(Channel channel, Role role, DualChannelPermissions permissions = null) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (role == null) throw new ArgumentNullException(nameof(role)); + CheckReady(); + + return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, permissions?.Allow, permissions?.Deny); + } + private Task SetChannelPermissions(Channel channel, ulong targetId, PermissionTarget targetType, ChannelPermissions allow = null, ChannelPermissions deny = null) + { + var request = new AddChannelPermissionsRequest(channel.Id) + { + TargetId = targetId, + TargetType = targetType.Value, + Allow = allow?.RawValue ?? 0, + Deny = deny?.RawValue ?? 0 + }; + return _clientRest.Send(request); + } + + public Task RemoveChannelPermissions(Channel channel, User user) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (user == null) throw new ArgumentNullException(nameof(user)); + CheckReady(); + + return RemoveChannelPermissions(channel, user.Id, PermissionTarget.User); + } + public Task RemoveChannelPermissions(Channel channel, Role role) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (role == null) throw new ArgumentNullException(nameof(role)); + CheckReady(); + + return RemoveChannelPermissions(channel, role.Id, PermissionTarget.Role); + } + private async Task RemoveChannelPermissions(Channel channel, ulong userOrRoleId, PermissionTarget targetType) + { + try + { + var perms = channel.PermissionOverwrites.Where(x => x.TargetType != targetType || x.TargetId != userOrRoleId).FirstOrDefault(); + await _clientRest.Send(new RemoveChannelPermissionsRequest(channel.Id, userOrRoleId)).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + }*/ +} diff --git a/src/Discord.Net/DiscordClient.Permissions.cs b/src/Discord.Net/DiscordClient.Permissions.cs deleted file mode 100644 index 5b90e1cbb..000000000 --- a/src/Discord.Net/DiscordClient.Permissions.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Linq; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - public partial class DiscordClient - { - public DualChannelPermissions GetChannelPermissions(Channel channel, User user) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (user == null) throw new ArgumentNullException(nameof(user)); - CheckReady(); - - return channel.PermissionOverwrites - .Where(x => x.TargetType == PermissionTarget.User && x.TargetId == user.Id) - .Select(x => x.Permissions) - .FirstOrDefault(); - } - public DualChannelPermissions GetChannelPermissions(Channel channel, Role role) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (role == null) throw new ArgumentNullException(nameof(role)); - CheckReady(); - - return channel.PermissionOverwrites - .Where(x => x.TargetType == PermissionTarget.Role && x.TargetId == role.Id) - .Select(x => x.Permissions) - .FirstOrDefault(); - } - - public Task SetChannelPermissions(Channel channel, User user, ChannelPermissions allow = null, ChannelPermissions deny = null) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (user == null) throw new ArgumentNullException(nameof(user)); - CheckReady(); - - return SetChannelPermissions(channel, user.Id, PermissionTarget.User, allow, deny); - } - public Task SetChannelPermissions(Channel channel, User user, DualChannelPermissions permissions = null) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (user == null) throw new ArgumentNullException(nameof(user)); - CheckReady(); - - return SetChannelPermissions(channel, user.Id, PermissionTarget.User, permissions?.Allow, permissions?.Deny); - } - public Task SetChannelPermissions(Channel channel, Role role, ChannelPermissions allow = null, ChannelPermissions deny = null) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (role == null) throw new ArgumentNullException(nameof(role)); - CheckReady(); - - return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, allow, deny); - } - public Task SetChannelPermissions(Channel channel, Role role, DualChannelPermissions permissions = null) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (role == null) throw new ArgumentNullException(nameof(role)); - CheckReady(); - - return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, permissions?.Allow, permissions?.Deny); - } - private Task SetChannelPermissions(Channel channel, ulong targetId, PermissionTarget targetType, ChannelPermissions allow = null, ChannelPermissions deny = null) - { - var request = new AddChannelPermissionsRequest(channel.Id) - { - TargetId = targetId, - TargetType = targetType.Value, - Allow = allow?.RawValue ?? 0, - Deny = deny?.RawValue ?? 0 - }; - return _clientRest.Send(request); - } - - public Task RemoveChannelPermissions(Channel channel, User user) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (user == null) throw new ArgumentNullException(nameof(user)); - CheckReady(); - - return RemoveChannelPermissions(channel, user.Id, PermissionTarget.User); - } - public Task RemoveChannelPermissions(Channel channel, Role role) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (role == null) throw new ArgumentNullException(nameof(role)); - CheckReady(); - - return RemoveChannelPermissions(channel, role.Id, PermissionTarget.Role); - } - private async Task RemoveChannelPermissions(Channel channel, ulong userOrRoleId, PermissionTarget targetType) - { - try - { - var perms = channel.PermissionOverwrites.Where(x => x.TargetType != targetType || x.TargetId != userOrRoleId).FirstOrDefault(); - await _clientRest.Send(new RemoveChannelPermissionsRequest(channel.Id, userOrRoleId)).ConfigureAwait(false); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - } -} \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.Roles.cs b/src/Discord.Net/DiscordClient.Roles.cs deleted file mode 100644 index 46879927e..000000000 --- a/src/Discord.Net/DiscordClient.Roles.cs +++ /dev/null @@ -1,176 +0,0 @@ -using Discord.API; -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - internal sealed class Roles : AsyncCollection - { - public Roles(DiscordClient client, object writerLock) - : base(client, writerLock) { } - - public Role GetOrAdd(ulong id, ulong serverId) - => GetOrAdd(id, () => new Role(_client, id, serverId)); - } - - public class RoleEventArgs : EventArgs - { - public Role Role { get; } - public Server Server => Role.Server; - - public RoleEventArgs(Role role) { Role = role; } - } - - public partial class DiscordClient - { - public event EventHandler RoleCreated; - private void RaiseRoleCreated(Role role) - { - if (RoleCreated != null) - EventHelper.Raise(_logger, nameof(RoleCreated), () => RoleCreated(this, new RoleEventArgs(role))); - } - public event EventHandler RoleUpdated; - private void RaiseRoleDeleted(Role role) - { - if (RoleDeleted != null) - EventHelper.Raise(_logger, nameof(RoleDeleted), () => RoleDeleted(this, new RoleEventArgs(role))); - } - public event EventHandler RoleDeleted; - private void RaiseRoleUpdated(Role role) - { - if (RoleUpdated != null) - EventHelper.Raise(_logger, nameof(RoleUpdated), () => RoleUpdated(this, new RoleEventArgs(role))); - } - - internal Roles Roles => _roles; - private readonly Roles _roles; - - /// Returns the role with the specified id, or null if none was found. - public Role GetRole(ulong id) - { - CheckReady(); - - return _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) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (name == null) throw new ArgumentNullException(nameof(name)); - CheckReady(); - - /*if (name.StartsWith("@")) - { - string name2 = name.Substring(1); - 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.Server.Id == server.Id && - string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); - //} - } - - /// Note: due to current API limitations, the created role cannot be returned. - public async Task CreateRole(Server server, string name, ServerPermissions permissions = null, Color color = null, bool isHoisted = false) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (name == null) throw new ArgumentNullException(nameof(name)); - CheckReady(); - - var request1 = new CreateRoleRequest(server.Id); - var response1 = await _clientRest.Send(request1).ConfigureAwait(false); - var role = _roles.GetOrAdd(response1.Id, server.Id); - role.Update(response1); - - var request2 = new UpdateRoleRequest(role.Server.Id, role.Id) - { - Name = name, - Permissions = (permissions ?? role.Permissions).RawValue, - Color = (color ?? Color.Default).RawValue, - IsHoisted = isHoisted - }; - var response2 = await _clientRest.Send(request2).ConfigureAwait(false); - role.Update(response2); - - return role; - } - - public async Task EditRole(Role role, string name = null, ServerPermissions permissions = null, Color color = null, bool? isHoisted = null, int? position = null) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - CheckReady(); - - var request1 = new UpdateRoleRequest(role.Server.Id, role.Id) - { - Name = name ?? role.Name, - Permissions = (permissions ?? role.Permissions).RawValue, - Color = (color ?? role.Color).RawValue, - IsHoisted = isHoisted ?? role.IsHoisted - }; - - var response = await _clientRest.Send(request1).ConfigureAwait(false); - - if (position != null) - { - int oldPos = role.Position; - int newPos = position.Value; - int minPos; - Role[] roles = role.Server.Roles.OrderBy(x => x.Position).ToArray(); - - if (oldPos < newPos) //Moving Down - { - minPos = oldPos; - for (int i = oldPos; i < newPos; i++) - roles[i] = roles[i + 1]; - roles[newPos] = role; - } - else //(oldPos > newPos) Moving Up - { - minPos = newPos; - for (int i = oldPos; i > newPos; i--) - roles[i] = roles[i - 1]; - roles[newPos] = role; - } - - var request2 = new ReorderRolesRequest(role.Server.Id) - { - RoleIds = roles.Skip(minPos).Select(x => x.Id).ToArray(), - StartPos = minPos - }; - await _clientRest.Send(request2).ConfigureAwait(false); - } - } - - public async Task DeleteRole(Role role) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - CheckReady(); - - try { await _clientRest.Send(new DeleteRoleRequest(role.Server.Id, role.Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - - public Task ReorderRoles(Server server, IEnumerable roles, int startPos = 0) - { - 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(); - - return _clientRest.Send(new ReorderRolesRequest(server.Id) - { - RoleIds = roles.Select(x => x.Id).ToArray(), - StartPos = startPos - }); - } - } -} \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.Servers.cs b/src/Discord.Net/DiscordClient.Servers.cs deleted file mode 100644 index ff9beca83..000000000 --- a/src/Discord.Net/DiscordClient.Servers.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - internal sealed class Servers : AsyncCollection - { - public Servers(DiscordClient client, object writerLock) - : base(client, writerLock) { } - - public Server GetOrAdd(ulong id) - => GetOrAdd(id, () => new Server(_client, id)); - } - - public class ServerEventArgs : EventArgs - { - public Server Server { get; } - - public ServerEventArgs(Server server) { Server = server; } - } - - public partial class DiscordClient - { - public event EventHandler JoinedServer; - private void RaiseJoinedServer(Server server) - { - if (JoinedServer != null) - EventHelper.Raise(_logger, nameof(JoinedServer), () => JoinedServer(this, new ServerEventArgs(server))); - } - public event EventHandler LeftServer; - private void RaiseLeftServer(Server server) - { - if (LeftServer != null) - EventHelper.Raise(_logger, nameof(LeftServer), () => LeftServer(this, new ServerEventArgs(server))); - } - public event EventHandler ServerUpdated; - private void RaiseServerUpdated(Server server) - { - if (ServerUpdated != null) - EventHelper.Raise(_logger, nameof(ServerUpdated), () => ServerUpdated(this, new ServerEventArgs(server))); - } - public event EventHandler ServerUnavailable; - private void RaiseServerUnavailable(Server server) - { - if (ServerUnavailable != null) - EventHelper.Raise(_logger, nameof(ServerUnavailable), () => ServerUnavailable(this, new ServerEventArgs(server))); - } - public event EventHandler ServerAvailable; - private void RaiseServerAvailable(Server server) - { - if (ServerAvailable != null) - EventHelper.Raise(_logger, nameof(ServerAvailable), () => ServerAvailable(this, new ServerEventArgs(server))); - } - - /// Returns a collection of all servers this client is a member of. - public IEnumerable AllServers { get { CheckReady(); return _servers; } } - internal Servers Servers => _servers; - private readonly Servers _servers; - - /// Returns the server with the specified id, or null if none was found. - public Server GetServer(ulong id) - { - CheckReady(); - - return _servers[id]; - } - - /// Returns all servers with the specified name. - /// Search is case-insensitive. - public IEnumerable FindServers(string name) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - CheckReady(); - - return _servers.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); - } - - /// Creates a new server with the provided name and region (see Regions). - public async Task CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (region == null) throw new ArgumentNullException(nameof(region)); - CheckReady(); - - var request = new CreateGuildRequest() - { - Name = name, - Region = region.Id, - IconBase64 = Base64Image(iconType, icon, null) - }; - var response = await _clientRest.Send(request).ConfigureAwait(false); - - var server = _servers.GetOrAdd(response.Id); - server.Update(response); - return server; - } - - /// Edits the provided server, changing only non-null attributes. - public async Task EditServer(Server server, string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - CheckReady(); - - var request = new UpdateGuildRequest(server.Id) - { - Name = name ?? server.Name, - Region = region ?? server.Region, - IconBase64 = Base64Image(iconType, icon, server.IconId), - AFKChannelId = server.AFKChannel?.Id, - AFKTimeout = server.AFKTimeout - }; - var response = await _clientRest.Send(request).ConfigureAwait(false); - server.Update(response); - } - - /// Leaves the provided server, destroying it if you are the owner. - public async Task LeaveServer(Server server) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - CheckReady(); - - try { await _clientRest.Send(new LeaveGuildRequest(server.Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - - public async Task> GetVoiceRegions() - { - CheckReady(); - - var regions = await _clientRest.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false); - return regions.Select(x => new Region(x.Id, x.Name, x.Hostname, x.Port)); - } - } -} \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.Users.cs b/src/Discord.Net/DiscordClient.Users.cs deleted file mode 100644 index 5800fbbe1..000000000 --- a/src/Discord.Net/DiscordClient.Users.cs +++ /dev/null @@ -1,329 +0,0 @@ -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - internal sealed class GlobalUsers : AsyncCollection - { - public GlobalUsers(DiscordClient client, object writerLock) - : base(client, writerLock) { } - - public GlobalUser GetOrAdd(ulong id) => GetOrAdd(id, () => new GlobalUser(_client, id)); - } - internal sealed class Users : AsyncCollection - { - public Users(DiscordClient client, object writerLock) - : base(client, writerLock) - { } - - public User this[ulong userId, ulong? serverId] - => base[new User.CompositeKey(userId, serverId)]; - public User GetOrAdd(ulong userId, ulong? serverId) - => GetOrAdd(new User.CompositeKey(userId, serverId), () => new User(_client, userId, serverId)); - public User TryRemove(ulong userId, ulong? serverId) - => TryRemove(new User.CompositeKey(userId, serverId)); - } - - public class UserEventArgs : EventArgs - { - public User User { get; } - public Server Server => User.Server; - - public UserEventArgs(User user) { User = user; } - } - public class UserChannelEventArgs : UserEventArgs - { - public Channel Channel { get; } - - public UserChannelEventArgs(User user, Channel channel) - : base(user) - { - Channel = channel; - } - } - public class BanEventArgs : EventArgs - { - public ulong UserId { get; } - public Server Server { get; } - - public BanEventArgs(ulong userId, Server server) - { - UserId = userId; - Server = server; - } - } - - public partial class DiscordClient : IDisposable - { - public event EventHandler UserJoined; - private void RaiseUserJoined(User user) - { - if (UserJoined != null) - EventHelper.Raise(_logger, nameof(UserJoined), () => UserJoined(this, new UserEventArgs(user))); - } - public event EventHandler UserLeft; - private void RaiseUserLeft(User user) - { - if (UserLeft != null) - EventHelper.Raise(_logger, nameof(UserLeft), () => UserLeft(this, new UserEventArgs(user))); - } - public event EventHandler UserUpdated; - private void RaiseUserUpdated(User user) - { - if (UserUpdated != null) - EventHelper.Raise(_logger, nameof(UserUpdated), () => UserUpdated(this, new UserEventArgs(user))); - } - public event EventHandler UserPresenceUpdated; - private void RaiseUserPresenceUpdated(User user) - { - if (UserPresenceUpdated != null) - EventHelper.Raise(_logger, nameof(UserPresenceUpdated), () => UserPresenceUpdated(this, new UserEventArgs(user))); - } - public event EventHandler UserVoiceStateUpdated; - private void RaiseUserVoiceStateUpdated(User user) - { - if (UserVoiceStateUpdated != null) - EventHelper.Raise(_logger, nameof(UserVoiceStateUpdated), () => UserVoiceStateUpdated(this, new UserEventArgs(user))); - } - public event EventHandler UserIsTypingUpdated; - private void RaiseUserIsTyping(User user, Channel channel) - { - if (UserIsTypingUpdated != null) - EventHelper.Raise(_logger, nameof(UserIsTypingUpdated), () => UserIsTypingUpdated(this, new UserChannelEventArgs(user, channel))); - } - public event EventHandler ProfileUpdated; - private void RaiseProfileUpdated() - { - if (ProfileUpdated != null) - EventHelper.Raise(_logger, nameof(ProfileUpdated), () => ProfileUpdated(this, EventArgs.Empty)); - } - public event EventHandler UserBanned; - private void RaiseUserBanned(ulong userId, Server server) - { - if (UserBanned != null) - EventHelper.Raise(_logger, nameof(UserBanned), () => UserBanned(this, new BanEventArgs(userId, server))); - } - public event EventHandler UserUnbanned; - private void RaiseUserUnbanned(ulong userId, Server server) - { - if (UserUnbanned != null) - EventHelper.Raise(_logger, nameof(UserUnbanned), () => UserUnbanned(this, new BanEventArgs(userId, server))); - } - - /// Returns the current logged-in user used in private channels. - internal User PrivateUser => _privateUser; - private User _privateUser; - - /// Returns information about the currently logged-in account. - public GlobalUser CurrentUser => _currentUser; - private GlobalUser _currentUser; - - /// Returns a collection of all unique users this client can currently see. - public IEnumerable AllUsers { get { CheckReady(); return _globalUsers; } } - internal GlobalUsers GlobalUsers => _globalUsers; - private readonly GlobalUsers _globalUsers; - - internal Users Users => _users; - private readonly Users _users; - - public GlobalUser GetUser(ulong userId) - { - CheckReady(); - - return _globalUsers[userId]; - } - /// Returns the user with the specified id, along with their server-specific data, or null if none was found. - public User GetUser(Server server, ulong userId) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - CheckReady(); - - return _users[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. - public User GetUser(Server server, string username, ushort discriminator) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (username == null) throw new ArgumentNullException(nameof(username)); - CheckReady(); - - return FindUsers(server.Members, server.Id, username, discriminator, true).FirstOrDefault(); - } - - /// Returns all users with the specified server and name, along with their server-specific data. - /// Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false. - public IEnumerable FindUsers(Server server, string name, bool exactMatch = false) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (name == null) throw new ArgumentNullException(nameof(name)); - CheckReady(); - - return FindUsers(server.Members, server.Id, name, exactMatch: exactMatch); - } - /// Returns all users with the specified channel and name, along with their server-specific data. - /// Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false. - public IEnumerable FindUsers(Channel channel, string name, bool exactMatch = false) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (name == null) throw new ArgumentNullException(nameof(name)); - CheckReady(); - - return FindUsers(channel.Members, channel.IsPrivate ? (ulong?)null : channel.Server.Id, name, exactMatch: exactMatch); - } - - private IEnumerable FindUsers(IEnumerable users, ulong? serverId, string name, ushort? discriminator = null, bool exactMatch = false) - { - var query = users.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); - - if (!exactMatch && name.Length >= 2) - { - if (name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Parse mention - { - ulong id = IdConvert.ToLong(name.Substring(2, name.Length - 3)); - var user = _users[id, serverId]; - if (user != null) - query = query.Concat(new User[] { user }); - } - else if (name[0] == '@') //If we somehow get text starting with @ but isn't a mention - { - string name2 = name.Substring(1); - query = query.Concat(users.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); - } - } - - if (discriminator != null) - query = query.Where(x => x.Discriminator == discriminator.Value); - return query; - } - - public Task EditUser(User user, bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable roles = null) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (user.IsPrivate) throw new InvalidOperationException("Unable to edit users in a private channel"); - CheckReady(); - - //Modify the roles collection and filter out the everyone role - var roleIds = roles == null ? null : user.Roles.Where(x => !x.IsEveryone) .Select(x => x.Id); - - var request = new UpdateMemberRequest(user.Server.Id, user.Id) - { - IsMuted = isMuted ?? user.IsServerMuted, - IsDeafened = isDeafened ?? user.IsServerDeafened, - VoiceChannelId = voiceChannel?.Id, - RoleIds = roleIds.ToArray() - }; - return _clientRest.Send(request); - } - - public Task KickUser(User user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (user.IsPrivate) throw new InvalidOperationException("Unable to kick users from a private channel"); - CheckReady(); - - var request = new KickMemberRequest(user.Server.Id, user.Id); - return _clientRest.Send(request); - } - public Task BanUser(User user, int pruneDays = 0) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (user.IsPrivate) throw new InvalidOperationException("Unable to ban users from a private channel"); - CheckReady(); - - var request = new AddGuildBanRequest(user.Server.Id, user.Id); - request.PruneDays = pruneDays; - return _clientRest.Send(request); - } - public async Task UnbanUser(Server server, ulong userId) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (userId <= 0) throw new ArgumentOutOfRangeException(nameof(userId)); - CheckReady(); - - try { await _clientRest.Send(new RemoveGuildBanRequest(server.Id, userId)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - - public async Task PruneUsers(Server server, int days, bool simulate = false) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days)); - CheckReady(); - - var request = new PruneMembersRequest(server.Id) - { - Days = days, - IsSimulation = simulate - }; - var response = await _clientRest.Send(request).ConfigureAwait(false); - return response.Pruned; - } - - /// When Config.UseLargeThreshold is enabled, running this command will request the Discord server to provide you with all offline users for a particular server. - public void RequestOfflineUsers(Server server) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - _webSocket.SendRequestMembers(server.Id, "", 0); - } - - public async Task EditProfile(string currentPassword = "", - string username = null, string email = null, string password = null, - Stream avatar = null, ImageType avatarType = ImageType.Png) - { - if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); - CheckReady(); - - var request = new UpdateProfileRequest() - { - CurrentPassword = currentPassword, - Email = email ?? _currentUser?.Email, - Password = password, - Username = username ?? _privateUser?.Name, - AvatarBase64 = Base64Image(avatarType, avatar, _privateUser?.AvatarId) - }; - - await _clientRest.Send(request).ConfigureAwait(false); - - if (password != null) - { - var loginRequest = new LoginRequest() - { - Email = _currentUser.Email, - Password = password - }; - var loginResponse = await _clientRest.Send(loginRequest).ConfigureAwait(false); - _clientRest.SetToken(loginResponse.Token); - } - } - - public Task SetStatus(UserStatus status) - { - if (status == null) throw new ArgumentNullException(nameof(status)); - if (status != UserStatus.Online && status != UserStatus.Idle) - throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status)); - CheckReady(); - - _status = status; - return SendStatus(); - } - public Task SetGame(int? gameId) - { - CheckReady(); - - _gameId = gameId; - return SendStatus(); - } - private Task SendStatus() - { - _webSocket.SendUpdateStatus(_status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, _gameId); - return TaskHelper.CompletedTask; - } - } -} \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index c6fa25240..e57031f0e 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -1,5 +1,6 @@ using Discord.API.Client.GatewaySocket; using Discord.API.Client.Rest; +using Discord.Logging; using Discord.Net; using Discord.Net.Rest; using Discord.Net.WebSockets; @@ -8,6 +9,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading; @@ -15,239 +17,130 @@ using System.Threading.Tasks; namespace Discord { - public enum ConnectionState : byte + /// Provides a connection to the DiscordApp service. + public partial class DiscordClient { - Disconnected, - Connecting, - Connected, - Disconnecting - } - - public class DisconnectedEventArgs : EventArgs - { - public readonly bool WasUnexpected; - public readonly Exception Error; - - public DisconnectedEventArgs(bool wasUnexpected, Exception error) - { - WasUnexpected = wasUnexpected; - Error = error; - } - } - public sealed class LogMessageEventArgs : EventArgs - { - public LogSeverity Severity { get; } - public string Source { get; } - public string Message { get; } - public Exception Exception { get; } - - public LogMessageEventArgs(LogSeverity severity, string source, string msg, Exception exception) - { - Severity = severity; - Source = source; - Message = msg; - Exception = exception; - } - } - - /// Provides a connection to the DiscordApp service. - public partial class DiscordClient - { - private readonly LogService _log; - private readonly Logger _logger, _restLogger, _cacheLogger, _webSocketLogger; - private readonly Dictionary _singletons; - private readonly object _cacheLock; - private readonly Semaphore _lock; + private readonly Semaphore _connectionLock; private readonly ManualResetEvent _disconnectedEvent; - private readonly ManualResetEventSlim _connectedEvent; - private readonly TaskManager _taskManager; - private UserStatus _status; - private int? _gameId; - - /// 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 DiscordConfig Config => _config; - private readonly DiscordConfig _config; - - /// Returns the current connection state of this client. - public ConnectionState State => _state; - private ConnectionState _state; - - /// Gives direct access to the underlying DiscordAPIClient. This can be used to modify objects not in cache. - public RestClient ClientAPI => _clientRest; - public RestClient StatusAPI => _statusRest; - private readonly RestClient _clientRest, _statusRest; - - /// Returns the internal websocket object. - public GatewaySocket WebSocket => _webSocket; - private readonly GatewaySocket _webSocket; - - public string GatewayUrl => _gateway; - private string _gateway; + private readonly ManualResetEventSlim _connectedEvent; + private readonly TaskManager _taskManager; + private readonly ConcurrentDictionary _servers; + private readonly ConcurrentDictionary _channels; + private readonly ConcurrentDictionary _privateChannels; //Key = RecipientId + private readonly JsonSerializer _serializer; + private readonly Region _unknownRegion; + private Dictionary _regions; + private CancellationTokenSource _cancelTokenSource; + + /// Gets the configuration object used to make this client. + public DiscordConfig Config { get; } + /// Gets the log manager. + public LogManager Log { get; } + /// Gets the internal RestClient for the Client API endpoint. + public RestClient ClientAPI { get; } + /// Gets the internal RestClient for the Status API endpoint. + public RestClient StatusAPI { get; } + /// Gets the internal WebSocket for the Gateway event stream. + public GatewaySocket GatewaySocket { get; } + /// Gets the queue used for outgoing messages, if enabled. + internal MessageQueue MessageQueue { get; } + /// Gets the logger used for this client. + internal Logger Logger { get; } + + /// Gets the current connection state of this client. + public ConnectionState State { get; private set; } + /// Gets a cancellation token that triggers when the client is manually disconnected. + public CancellationToken CancelToken { get; private set; } + /// Gets the current logged-in user used in private channels. + internal User PrivateUser { get; private set; } + /// Gets information about the current logged-in account. + public Profile CurrentUser { get; private set; } + /// Gets the status of the current user. + public UserStatus Status { get; private set; } + /// Gets the game this current user is reported as playing. + public int? CurrentGameId { get; private set; } + + /// Gets a collection of all servers this client is a member of. + public IEnumerable Servers => _servers.Select(x => x.Value); + // /// Gets a collection of all channels this client is a member of. + // public IEnumerable Channels => _servers.Select(x => x.Value); + /// Gets a collection of all private channels this client is a member of. + public IEnumerable PrivateChannels => _channels.Select(x => x.Value); - public string Token => _token; - private string _token; - - public string SessionId => _sessionId; - private string _sessionId; - - public ulong? UserId => _currentUser?.Id; - - /// Returns a cancellation token that triggers when the client is manually disconnected. - public CancellationToken CancelToken => _cancelToken; - private CancellationTokenSource _cancelTokenSource; - private CancellationToken _cancelToken; - - public event EventHandler Connected; - private void RaiseConnected() - { - if (Connected != null) - EventHelper.Raise(_logger, nameof(Connected), () => Connected(this, EventArgs.Empty)); - } - public event EventHandler Disconnected; - private void RaiseDisconnected(DisconnectedEventArgs e) - { - if (Disconnected != null) - EventHelper.Raise(_logger, nameof(Disconnected), () => Disconnected(this, e)); - } - /// Initializes a new instance of the DiscordClient class. public DiscordClient(DiscordConfig config = null) { - _config = config ?? new DiscordConfig(); - _config.Lock(); - - _nonceRand = new Random(); - _state = (int)ConnectionState.Disconnected; - _status = UserStatus.Online; + Config = config ?? new DiscordConfig(); + Config.Lock(); + + State = (int)ConnectionState.Disconnected; + Status = UserStatus.Online; //Services - _singletons = new Dictionary(); - _log = AddService(new LogService()); - _logger = _log.CreateLogger("Client"); - _cacheLogger = _log.CreateLogger("Cache"); - _restLogger = _log.CreateLogger("Rest"); - _webSocketLogger = _log.CreateLogger("WebSocket"); + Log = new LogManager(this); + Logger = Log.CreateLogger("Discord"); //Async - _lock = new Semaphore(1, 1); _taskManager = new TaskManager(Cleanup); - _cancelToken = new CancellationToken(true); + _connectionLock = new Semaphore(1, 1); _disconnectedEvent = new ManualResetEvent(true); _connectedEvent = new ManualResetEventSlim(false); + CancelToken = new CancellationToken(true); //Cache - _cacheLock = new object(); - _channels = new Channels(this, _cacheLock); - _users = new Users(this, _cacheLock); - _messages = new Messages(this, _cacheLock, Config.MessageCacheSize > 0); - _roles = new Roles(this, _cacheLock); - _servers = new Servers(this, _cacheLock); - _globalUsers = new GlobalUsers(this, _cacheLock); + _servers = new ConcurrentDictionary(); + _channels = new ConcurrentDictionary(); + _privateChannels = new ConcurrentDictionary(); + _unknownRegion = new Region("", "Unknown", "", 0); + + //Serialization + _serializer = new JsonSerializer(); + _serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; +#if TEST_RESPONSES + _serializer.CheckAdditionalContent = true; + _serializer.MissingMemberHandling = MissingMemberHandling.Error; +#else + _serializer.Error += (s, e) => + { + e.ErrorContext.Handled = true; + Logger.Error("Serialization Failed", e.ErrorContext.Error); + }; +#endif //Networking - _clientRest = new RestClient(_config, _restLogger, DiscordConfig.ClientAPIUrl); - _statusRest = new RestClient(_config, _restLogger, DiscordConfig.StatusAPIUrl); - _webSocket = new GatewaySocket(this, _webSocketLogger); - _webSocket.Connected += (s, e) => + ClientAPI = new RestClient(Config, DiscordConfig.ClientAPIUrl, Log.CreateLogger("ClientAPI")); + StatusAPI = new RestClient(Config, DiscordConfig.StatusAPIUrl, Log.CreateLogger("StatusAPI")); + GatewaySocket = new GatewaySocket(this, _serializer, Log.CreateLogger("GatewaySocket")); + GatewaySocket.Connected += (s, e) => { - if (_state == ConnectionState.Connecting) + if (State == ConnectionState.Connecting) EndConnect(); }; - _webSocket.Disconnected += (s, e) => - { - RaiseDisconnected(e); - }; - _webSocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); + GatewaySocket.Disconnected += (s, e) => OnDisconnected(e.WasUnexpected, e.Exception); + GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); if (Config.UseMessageQueue) - _pendingMessages = new ConcurrentQueue(); + MessageQueue = new MessageQueue(this); Connected += async (s, e) => { - _clientRest.SetCancelToken(_cancelToken); - await SendStatus().ConfigureAwait(false); + ClientAPI.CancelToken = CancelToken; + await SendStatus(); }; - - //Import/Export - _messageImporter = new JsonSerializer(); - _messageImporter.ContractResolver = new Message.ImportResolver(); - - //Logging - if (_log.Level >= LogSeverity.Info) - { - JoinedServer += (s, e) => _logger.Info($"Server Created: {e.Server?.Name ?? "[Private]"}"); - LeftServer += (s, e) => _logger.Info($"Server Destroyed: {e.Server?.Name ?? "[Private]"}"); - ServerUpdated += (s, e) => _logger.Info($"Server Updated: {e.Server?.Name ?? "[Private]"}"); - ServerAvailable += (s, e) => _logger.Info($"Server Available: {e.Server?.Name ?? "[Private]"}"); - ServerUnavailable += (s, e) => _logger.Info($"Server Unavailable: {e.Server?.Name ?? "[Private]"}"); - ChannelCreated += (s, e) => _logger.Info($"Channel Created: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); - ChannelDestroyed += (s, e) => _logger.Info($"Channel Destroyed: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); - ChannelUpdated += (s, e) => _logger.Info($"Channel Updated: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}"); - MessageReceived += (s, e) => _logger.Info($"Message Received: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); - MessageDeleted += (s, e) => _logger.Info($"Message Deleted: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); - MessageUpdated += (s, e) => _logger.Info($"Message Update: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); - RoleCreated += (s, e) => _logger.Info($"Role Created: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); - RoleUpdated += (s, e) => _logger.Info($"Role Updated: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); - RoleDeleted += (s, e) => _logger.Info($"Role Deleted: {e.Server?.Name ?? "[Private]"}/{e.Role?.Name}"); - UserBanned += (s, e) => _logger.Info($"Banned User: {e.Server?.Name ?? "[Private]" }/{e.UserId}"); - UserUnbanned += (s, e) => _logger.Info($"Unbanned User: {e.Server?.Name ?? "[Private]"}/{e.UserId}"); - UserJoined += (s, e) => _logger.Info($"User Joined: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); - UserLeft += (s, e) => _logger.Info($"User Left: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); - UserUpdated += (s, e) => _logger.Info($"User Updated: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); - UserVoiceStateUpdated += (s, e) => _logger.Info($"Voice Updated: {e.Server?.Name ?? "[Private]"}/{e.User.Name}"); - ProfileUpdated += (s, e) => _logger.Info("Profile Updated"); - } - if (_log.Level >= LogSeverity.Verbose) - { - UserIsTypingUpdated += (s, e) => _logger.Verbose($"Is Typing: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.User?.Name}"); - MessageAcknowledged += (s, e) => _logger.Verbose($"Ack Message: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); - MessageSent += (s, e) => _logger.Verbose($"Sent Message: {e.Server?.Name ?? "[Private]"}/{e.Channel?.Name}/{e.Message?.Id}"); - UserPresenceUpdated += (s, e) => _logger.Verbose($"Presence Updated: {e.Server?.Name ?? "[Private]"}/{e.User?.Name}"); - } - if (_log.Level >= LogSeverity.Verbose) - { - _clientRest.OnRequest += (s, e) => - { - if (e.Payload != null) - _restLogger.Verbose($"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})"); - else - _restLogger.Verbose($"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms"); - }; - } - - if (_log.Level >= LogSeverity.Debug) - { - _channels.ItemCreated += (s, e) => _cacheLogger.Debug($"Created Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); - _channels.ItemDestroyed += (s, e) => _cacheLogger.Debug($"Destroyed Channel {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); - _channels.Cleared += (s, e) => _cacheLogger.Debug($"Cleared Channels"); - _users.ItemCreated += (s, e) => _cacheLogger.Debug($"Created User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); - _users.ItemDestroyed += (s, e) => _cacheLogger.Debug($"Destroyed User {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); - _users.Cleared += (s, e) => _cacheLogger.Debug($"Cleared Users"); - _messages.ItemCreated += (s, e) => _cacheLogger.Debug($"Created Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); - _messages.ItemDestroyed += (s, e) => _cacheLogger.Debug($"Destroyed Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/{e.Item.Id}"); - _messages.ItemRemapped += (s, e) => _cacheLogger.Debug($"Remapped Message {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Channel.Id}/[{e.OldId} -> {e.NewId}]"); - _messages.Cleared += (s, e) => _cacheLogger.Debug($"Cleared Messages"); - _roles.ItemCreated += (s, e) => _cacheLogger.Debug($"Created Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); - _roles.ItemDestroyed += (s, e) => _cacheLogger.Debug($"Destroyed Role {IdConvert.ToString(e.Item.Server?.Id) ?? "[Private]"}/{e.Item.Id}"); - _roles.Cleared += (s, e) => _cacheLogger.Debug($"Cleared Roles"); - _servers.ItemCreated += (s, e) => _cacheLogger.Debug($"Created Server {e.Item.Id}"); - _servers.ItemDestroyed += (s, e) => _cacheLogger.Debug($"Destroyed Server {e.Item.Id}"); - _servers.Cleared += (s, e) => _cacheLogger.Debug($"Cleared Servers"); - _globalUsers.ItemCreated += (s, e) => _cacheLogger.Debug($"Created User {e.Item.Id}"); - _globalUsers.ItemDestroyed += (s, e) => _cacheLogger.Debug($"Destroyed User {e.Item.Id}"); - _globalUsers.Cleared += (s, e) => _cacheLogger.Debug($"Cleared Users"); - } + //Import/Export + //_messageImporter = new JsonSerializer(); + //_messageImporter.ContractResolver = new Message.ImportResolver(); } /// Connects to the Discord server with the provided email and password. - /// Returns a token for future connections. + /// Returns a token that can be optionally stored for future connections. public async Task Connect(string email, string password) { if (email == null) throw new ArgumentNullException(email); if (password == null) throw new ArgumentNullException(password); await BeginConnect(email, password, null).ConfigureAwait(false); - return _token; + return ClientAPI.Token; } /// Connects to the Discord server with the provided token. public async Task Connect(string token) @@ -261,47 +154,32 @@ namespace Discord { try { - _lock.WaitOne(); + _connectionLock.WaitOne(); try { if (State != ConnectionState.Disconnected) await Disconnect().ConfigureAwait(false); await _taskManager.Stop().ConfigureAwait(false); _taskManager.ClearException(); - _state = ConnectionState.Connecting; + State = ConnectionState.Connecting; _disconnectedEvent.Reset(); + + await Login(email, password, token).ConfigureAwait(false); - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = _cancelTokenSource.Token; - - await Login(email, password, token); - - _webSocket.Host = _gateway; - _webSocket.ParentCancelToken = _cancelToken; - await _webSocket.Connect().ConfigureAwait(false); + ClientAPI.Token = token; + await GatewaySocket.Connect(token).ConfigureAwait(false); List tasks = new List(); - tasks.Add(_cancelToken.Wait()); - if (_config.UseMessageQueue) - tasks.Add(MessageQueueAsync()); + tasks.Add(CancelToken.Wait()); + if (Config.UseMessageQueue) + tasks.Add(MessageQueue.Run(CancelToken, Config.MessageQueueInterval)); await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); - - try - { - //Cancel if either Disconnect is called, data socket errors or timeout is reached - var cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, _webSocket.CancelToken).Token; - _connectedEvent.Wait(cancelToken); - } - catch (OperationCanceledException) - { - _webSocket.TaskManager.ThrowException(); //Throws data socket's internal error if any occured - throw; - } + GatewaySocket.WaitForConnection(CancelToken); } finally { - _lock.Release(); + _connectionLock.Release(); } } catch (Exception ex) @@ -312,7 +190,11 @@ namespace Discord } private async Task Login(string email, string password, string token) { - bool useCache = _config.CacheToken; + _cancelTokenSource = new CancellationTokenSource(); + CancelToken = _cancelTokenSource.Token; + GatewaySocket.ParentCancelToken = CancelToken; + + bool useCache = Config.CacheToken; while (true) { //Get Token @@ -329,7 +211,7 @@ namespace Discord if (token == null) { var request = new LoginRequest() { Email = email, Password = password }; - var response = await _clientRest.Send(request).ConfigureAwait(false); + var response = await ClientAPI.Send(request).ConfigureAwait(false); token = response.Token; SaveToken(tokenPath, key, token); useCache = false; @@ -338,21 +220,19 @@ namespace Discord else { var request = new LoginRequest() { Email = email, Password = password }; - var response = await _clientRest.Send(request).ConfigureAwait(false); + var response = await ClientAPI.Send(request).ConfigureAwait(false); token = response.Token; } } - _token = token; - _clientRest.SetToken(token); //Get gateway and check token try { - var gatewayResponse = await _clientRest.Send(new GatewayRequest()).ConfigureAwait(false); + var gatewayResponse = await ClientAPI.Send(new GatewayRequest()).ConfigureAwait(false); var gateway = gatewayResponse.Url; - _gateway = gateway; - if (_config.LogLevel >= LogSeverity.Verbose) - _logger.Verbose($"Login successful, gateway: {gateway}"); + GatewaySocket.Host = gateway; + if (Config.LogLevel >= LogSeverity.Verbose) + Logger.Verbose($"Login successful, gateway: {gateway}"); } catch (HttpException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized && useCache) { @@ -360,42 +240,39 @@ namespace Discord token = null; continue; } + + //Cache other stuff + var regionsResponse = (await ClientAPI.Send(new GetVoiceRegionsRequest())); + _regions = regionsResponse.Select(x => new Region(x.Id, x.Name, x.Hostname, x.Port)) + .ToDictionary(x => x.Id); break; } } private void EndConnect() { - _state = ConnectionState.Connected; + State = ConnectionState.Connected; _connectedEvent.Set(); - RaiseConnected(); - } + OnConnected(); + } /// Disconnects from the Discord server, canceling any pending requests. public Task Disconnect() => _taskManager.Stop(); - private async Task Cleanup() { - _state = ConnectionState.Disconnecting; + State = ConnectionState.Disconnecting; if (Config.UseMessageQueue) - { - MessageQueueItem ignored; - while (_pendingMessages.TryDequeue(out ignored)) { } - } + MessageQueue.Clear(); - await _clientRest.Send(new LogoutRequest()).ConfigureAwait(false); + await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); - _channels.Clear(); - _users.Clear(); - _messages.Clear(); - _roles.Clear(); _servers.Clear(); - _globalUsers.Clear(); + _channels.Clear(); + _privateChannels.Clear(); - _currentUser = null; - _gateway = null; - _token = null; + PrivateUser = null; + CurrentUser = null; - _state = (int)ConnectionState.Disconnected; + State = (int)ConnectionState.Disconnected; _connectedEvent.Reset(); _disconnectedEvent.Set(); } @@ -409,25 +286,22 @@ namespace Discord //Global case "READY": //Resync { - var data = e.Payload.ToObject(_webSocket.Serializer); - _sessionId = data.SessionId; - _privateUser = _users.GetOrAdd(data.User.Id, null); - _privateUser.Update(data.User); - _currentUser = _privateUser.Global; - _currentUser.Update(data.User); + var data = e.Payload.ToObject(_serializer); + //SessionId = data.SessionId; + PrivateUser = new User(data.User.Id, null); + PrivateUser.Update(data.User); + CurrentUser.Update(data.User); foreach (var model in data.Guilds) { if (model.Unavailable != true) { - var server = _servers.GetOrAdd(model.Id); + var server = AddServer(model.Id); server.Update(model); } } foreach (var model in data.PrivateChannels) - { - var user = _users.GetOrAdd(model.Recipient.Id, null); - user.Update(model.Recipient); - var channel = _channels.GetOrAdd(model.Id, null, user.Id); + { + var channel = AddChannel(model.Id, null, model.Recipient.Id); channel.Update(model); } } @@ -436,176 +310,218 @@ namespace Discord //Servers case "GUILD_CREATE": { - var data = e.Payload.ToObject(_webSocket.Serializer); + var data = e.Payload.ToObject(_serializer); if (data.Unavailable != true) { - var server = _servers.GetOrAdd(data.Id); + var server = AddServer(data.Id); server.Update(data); if (data.Unavailable != false) - RaiseJoinedServer(server); - RaiseServerAvailable(server); + { + Logger.Info($"Server Created: {server.Name}"); + OnJoinedServer(server); + } + else + Logger.Info($"Server Available: {server.Name}"); + OnServerAvailable(server); } } break; case "GUILD_UPDATE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var server = _servers[data.Id]; - if (server != null) + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.Id); + if (server != null) { server.Update(data); - RaiseServerUpdated(server); + Logger.Info($"Server Updated: {server.Name}"); + OnServerUpdated(server); } } break; case "GUILD_DELETE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var server = _servers.TryRemove(data.Id); + var data = e.Payload.ToObject(_serializer); + Server server = RemoveServer(data.Id); if (server != null) { - RaiseServerUnavailable(server); if (data.Unavailable != true) - RaiseLeftServer(server); - } + Logger.Info($"Server Destroyed: {server.Name}"); + else + Logger.Info($"Server Unavailable: {server.Name}"); + + OnServerUnavailable(server); + if (data.Unavailable != true) + OnLeftServer(server); + } } break; //Channels case "CHANNEL_CREATE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - Channel channel; - if (data.IsPrivate) - { - var user = _users.GetOrAdd(data.Recipient.Id, null); - user.Update(data.Recipient); - channel = _channels.GetOrAdd(data.Id, null, user.Id); - } - else - channel = _channels.GetOrAdd(data.Id, data.GuildId, null); + var data = e.Payload.ToObject(_serializer); + Channel channel = AddChannel(data.Id, data.GuildId, data.Recipient.Id); channel.Update(data); - RaiseChannelCreated(channel); + Logger.Info($"Channel Created: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); + OnChannelCreated(channel); } break; case "CHANNEL_UPDATE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var channel = _channels[data.Id]; + var data = e.Payload.ToObject(_serializer); + var channel = GetChannel(data.Id); if (channel != null) { channel.Update(data); - RaiseChannelUpdated(channel); + Logger.Info($"Channel Updated: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); + OnChannelUpdated(channel); } } break; case "CHANNEL_DELETE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var channel = _channels.TryRemove(data.Id); - if (channel != null) - RaiseChannelDestroyed(channel); + var data = e.Payload.ToObject(_serializer); + var channel = RemoveChannel(data.Id); + if (channel != null) + { + Logger.Info($"Channel Destroyed: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); + OnChannelDestroyed(channel); + } } break; //Members case "GUILD_MEMBER_ADD": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var user = _users.GetOrAdd(data.User.Id, data.GuildId); - user.Update(data); - user.UpdateActivity(); - RaiseUserJoined(user); + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = server.AddMember(data.User.Id); + user.Update(data); + user.UpdateActivity(); + Logger.Info($"User Joined: {server.Name}/{user.Name}"); + OnUserJoined(user); + } } break; case "GUILD_MEMBER_UPDATE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var user = _users[data.User.Id, data.GuildId]; - if (user != null) - { - user.Update(data); - RaiseUserUpdated(user); - } + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = server.GetUser(data.User.Id); + if (user != null) + { + user.Update(data); + Logger.Info($"User Updated: {server.Name}/{user.Name}"); + OnUserUpdated(user); + } + } } break; case "GUILD_MEMBER_REMOVE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var user = _users.TryRemove(data.User.Id, data.GuildId); - if (user != null) - RaiseUserLeft(user); + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = server.RemoveMember(data.User.Id); + if (user != null) + { + Logger.Info($"User Left: {server.Name}/{user.Name}"); + OnUserLeft(user); + } + } } break; case "GUILD_MEMBERS_CHUNK": { - var data = e.Payload.ToObject(_webSocket.Serializer); - foreach (var memberData in data.Members) - { - var user = _users.GetOrAdd(memberData.User.Id, memberData.GuildId); - user.Update(memberData); - //RaiseUserAdded(user); - } - } + var data = e.Payload.ToObject(_serializer); + foreach (var memberData in data.Members) + { + var server = GetServer(memberData.GuildId.Value); + if (server != null) + { + var user = server.AddMember(memberData.User.Id); + user.Update(memberData); + //OnUserAdded(user); + } + } + } break; //Roles case "GUILD_ROLE_CREATE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var role = _roles.GetOrAdd(data.Data.Id, data.GuildId); - role.Update(data.Data); - var server = _servers[data.GuildId]; - if (server != null) - server.AddRole(role); - RaiseRoleUpdated(role); + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + var role = server.AddRole(data.Data.Id); + role.Update(data.Data); + Logger.Info($"Role Created: {server.Name}/{role.Name}"); + OnRoleUpdated(role); + } } break; case "GUILD_ROLE_UPDATE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var role = _roles[data.Data.Id]; - if (role != null) - { - role.Update(data.Data); - RaiseRoleUpdated(role); - } + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + var role = server.GetRole(data.Data.Id); + if (role != null) + { + role.Update(data.Data); + Logger.Info($"Role Updated: {server.Name}/{role.Name}"); + OnRoleUpdated(role); + } + } } break; case "GUILD_ROLE_DELETE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var role = _roles.TryRemove(data.RoleId); - if (role != null) - { - RaiseRoleDeleted(role); - var server = _servers[data.GuildId]; - if (server != null) - server.RemoveRole(role); - } + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + var role = server.RemoveRole(data.RoleId); + if (role != null) + { + Logger.Info($"Role Deleted: {server.Name}/{role.Name}"); + OnRoleDeleted(role); + } + } } break; //Bans case "GUILD_BAN_ADD": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var server = _servers[data.GuildId]; - if (server != null) + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.GuildId); + if (server != null) { server.AddBan(data.UserId); - RaiseUserBanned(data.UserId, server); + Logger.Info($"User Banned: {server.Name}/{data.UserId}"); + OnUserBanned(server, data.UserId); } } break; case "GUILD_BAN_REMOVE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var server = _servers[data.GuildId]; - if (server != null) + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.GuildId); + if (server != null) { - if (server.RemoveBan(data.UserId)) - RaiseUserUnbanned(data.UserId, server); + if (server.RemoveBan(data.UserId)) + { + Logger.Info($"User Unbanned: {server.Name}/{data.UserId}"); + OnUserUnbanned(server, data.UserId); + } } } break; @@ -613,92 +529,138 @@ namespace Discord //Messages case "MESSAGE_CREATE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - Message msg = null; - - bool isAuthor = data.Author.Id == _currentUser.Id; - //ulong nonce = 0; + var data = e.Payload.ToObject(_serializer); - /*if (data.Author.Id == _privateUser.Id && Config.UseMessageQueue) + Channel channel = GetChannel(data.ChannelId); + if (channel != null) { - if (data.Nonce != null && ulong.TryParse(data.Nonce, out nonce)) - msg = _messages[nonce]; - }*/ - if (msg == null) - { - msg = _messages.GetOrAdd(data.Id, data.ChannelId, data.Author.Id); - //nonce = 0; - } - - msg.Update(data); - var user = msg.User; - if (user != null) - user.UpdateActivity();// data.Timestamp); - - //Remapped queued message - /*if (nonce != 0) - { - msg = _messages.Remap(nonce, data.Id); - msg.Id = data.Id; - RaiseMessageSent(msg); - }*/ + Message msg = null; + bool isAuthor = data.Author.Id == CurrentUser.Id; + //ulong nonce = 0; + + /*if (data.Author.Id == _privateUser.Id && Config.UseMessageQueue) + { + if (data.Nonce != null && ulong.TryParse(data.Nonce, out nonce)) + msg = _messages[nonce]; + }*/ + if (msg == null) + { + msg = channel.AddMessage(data.Id, data.Author.Id, data.Timestamp.Value); + //nonce = 0; + } + + msg.Update(data); + var user = msg.User; + if (user != null) + user.UpdateActivity();// data.Timestamp); + + //Remapped queued message + /*if (nonce != 0) + { + msg = _messages.Remap(nonce, data.Id); + msg.Id = data.Id; + RaiseMessageSent(msg); + }*/ - msg.State = MessageState.Normal; - RaiseMessageReceived(msg); + msg.State = MessageState.Normal; + Logger.Info($"Message Received: {channel.Server?.Name ?? "[Private]"}/{channel.Name}/{msg.Id}"); + OnMessageReceived(msg); + } } break; case "MESSAGE_UPDATE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var msg = _messages[data.Id]; - if (msg != null) + var data = e.Payload.ToObject(_serializer); + var channel = GetChannel(data.ChannelId); + if (channel != null) { - msg.Update(data); - msg.State = MessageState.Normal; - RaiseMessageUpdated(msg); - } + var msg = channel.GetMessage(data.Id); + if (msg != null) + { + msg.Update(data); + msg.State = MessageState.Normal; + Logger.Info($"Message Update: {channel.Server?.Name ?? "[Private]"}/{channel.Name}/{msg.Id}"); + OnMessageUpdated(msg); + } + } } break; case "MESSAGE_DELETE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var msg = _messages.TryRemove(data.Id); - if (msg != null) - RaiseMessageDeleted(msg); + var data = e.Payload.ToObject(_serializer); + var channel = GetChannel(data.ChannelId); + if (channel != null) + { + var msg = channel.RemoveMessage(data.Id); + if (msg != null) + { + Logger.Info($"Message Deleted: {channel.Server?.Name ?? "[Private]"}/{channel.Name}/{msg.Id}"); + OnMessageDeleted(msg); + } + } } break; case "MESSAGE_ACK": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var msg = _messages[data.MessageId]; - if (msg != null) - RaiseMessageAcknowledged(msg); + var data = e.Payload.ToObject(_serializer); + var channel = GetChannel(data.ChannelId); + if (channel != null) + { + var msg = channel.GetMessage(data.MessageId); + if (msg != null) + { + Logger.Verbose($"Message Ack: {channel.Server?.Name ?? "[Private]"}/{channel.Name}/{msg.Id}"); + OnMessageAcknowledged(msg); + } + } } break; //Statuses case "PRESENCE_UPDATE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var user = _users.GetOrAdd(data.User.Id, data.GuildId); + var data = e.Payload.ToObject(_serializer); + User user; + Server server; + if (data.GuildId == null) + { + server = null; + user = GetPrivateChannel(data.User.Id)?.Recipient; + } + else + { + server = GetServer(data.GuildId.Value); + user = server?.GetUser(data.User.Id); + } + if (user != null) { user.Update(data); - RaiseUserPresenceUpdated(user); + Logger.Verbose($"Presence Updated: {server.Name}/{user.Name}"); + OnUserPresenceUpdated(user); } } break; case "TYPING_START": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var channel = _channels[data.ChannelId]; + var data = e.Payload.ToObject(_serializer); + var channel = GetChannel(data.ChannelId); if (channel != null) { - var user = _users[data.UserId, channel.Server?.Id]; + User user; + if (channel.IsPrivate) + { + if (channel.Recipient.Id == data.UserId) + user = channel.Recipient; + else + return; ; + } + else + user = channel.Server.GetUser(data.UserId); if (user != null) - { - if (channel != null) - RaiseUserIsTyping(user, channel); + { + Logger.Verbose($"Is Typing: {channel.Server?.Name ?? "[Private]"}/{channel.Name}/{user.Name}"); + OnUserIsTypingUpdated(channel, user); user.UpdateActivity(); } } @@ -708,33 +670,33 @@ namespace Discord //Voice case "VOICE_STATE_UPDATE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var user = _users[data.UserId, data.GuildId]; - if (user != null) - { - /*var voiceChannel = user.VoiceChannel; - if (voiceChannel != null && data.ChannelId != voiceChannel.Id && user.IsSpeaking) - { - user.IsSpeaking = false; - RaiseUserIsSpeaking(user, _channels[voiceChannel.Id], false); - }*/ - user.Update(data); - RaiseUserVoiceStateUpdated(user); - } + var data = e.Payload.ToObject(_serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + var user = server.GetUser(data.UserId); + if (user != null) + { + user.Update(data); + Logger.Info($"Voice Updated: {server.Name}/{user.Name}"); + OnUserVoiceStateUpdated(user); + } + } } break; //Settings case "USER_UPDATE": { - var data = e.Payload.ToObject(_webSocket.Serializer); - var globalUser = _globalUsers[data.Id]; - if (globalUser != null) - { - globalUser.Update(data); - foreach (var user in globalUser.Memberships) - user.Update(data); - RaiseProfileUpdated(); + var data = e.Payload.ToObject(_serializer); + if (data.Id == CurrentUser.Id) + { + CurrentUser.Update(data); + PrivateUser.Update(data); + foreach (var server in _servers) + server.Value.CurrentUser.Update(data); + Logger.Info("Profile Updated"); + OnProfileUpdated(CurrentUser); } } break; @@ -750,16 +712,96 @@ namespace Discord //Others default: - _webSocket.Logger.Log(LogSeverity.Warning, $"Unknown message type: {e.Type}"); + Logger.Warning($"Unknown message type: {e.Type}"); break; } } catch (Exception ex) { - _logger.Log(LogSeverity.Error, $"Error handling {e.Type} event", ex); + Logger.Error($"Error handling {e.Type} event", ex); } } + public Task SetStatus(UserStatus status) + { + if (status == null) throw new ArgumentNullException(nameof(status)); + if (status != UserStatus.Online && status != UserStatus.Idle) + throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status)); + CheckReady(); + + Status = status; + return SendStatus(); + } + public Task SetGame(int? gameId) + { + CheckReady(); + + CurrentGameId = gameId; + return SendStatus(); + } + private Task SendStatus() + { + GatewaySocket.SendUpdateStatus(Status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, CurrentGameId); + return TaskHelper.CompletedTask; + } + + private Server AddServer(ulong id) + => _servers.GetOrAdd(id, x => new Server(this, x)); + private Server RemoveServer(ulong id) + { + Server server; + _servers.TryRemove(id, out server); + return server; + } + public Server GetServer(ulong id) + { + Server server; + _servers.TryGetValue(id, out server); + return server; + } + + private Channel AddChannel(ulong id, ulong? guildId, ulong? recipientId) + { + Channel channel; + if (recipientId != null) + { + channel = _privateChannels.GetOrAdd(recipientId.Value, + x => new Channel(this, x, new User(recipientId.Value, null))); + } + else + { + var server = GetServer(guildId.Value); + channel = server.AddChannel(id); + } + _channels[channel.Id] = channel; + return channel; + } + private Channel RemoveChannel(ulong id) + { + Channel channel; + if (_channels.TryRemove(id, out channel)) + { + if (channel.IsPrivate) + _privateChannels.TryRemove(channel.Recipient.Id, out channel); + else + channel.Server.RemoveChannel(id); + } + return channel; + } + internal Channel GetChannel(ulong id) + { + Channel channel; + _channels.TryGetValue(id, out channel); + return channel; + } + internal Channel GetPrivateChannel(ulong recipientId) + { + Channel channel; + _privateChannels.TryGetValue(recipientId, out channel); + return channel; + } + + #region Async Wrapper /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. public void Run(Func asyncAction) @@ -778,37 +820,6 @@ namespace Discord } #endregion - #region Services - public T AddSingleton(T obj) - where T : class - { - _singletons.Add(typeof(T), obj); - return obj; - } - public T GetSingleton(bool required = true) - where T : class - { - object singleton; - T singletonT = null; - if (_singletons.TryGetValue(typeof(T), out singleton)) - singletonT = singleton as T; - - if (singletonT == null && required) - throw new InvalidOperationException($"This operation requires {typeof(T).Name} to be added to {nameof(DiscordClient)}."); - return singletonT; - } - public T AddService(T obj) - where T : class, IService - { - AddSingleton(obj); - obj.Install(this); - return obj; - } - public T GetService(bool required = true) - where T : class, IService - => GetSingleton(required); - #endregion - #region IDisposable private bool _isDisposed = false; @@ -834,7 +845,7 @@ namespace Discord //Helpers private void CheckReady() { - switch (_state) + switch (State) { case ConnectionState.Disconnecting: throw new InvalidOperationException("The client is disconnecting."); @@ -845,16 +856,6 @@ namespace Discord } } - public void GetCacheStats(out int serverCount, out int channelCount, out int userCount, out int uniqueUserCount, out int messageCount, out int roleCount) - { - serverCount = _servers.Count; - channelCount = _channels.Count; - userCount = _users.Count; - uniqueUserCount = _globalUsers.Count; - messageCount = _messages.Count; - roleCount = _roles.Count; - } - private string GetTokenCachePath(string email) { using (var md5 = MD5.Create()) @@ -863,7 +864,7 @@ namespace Discord StringBuilder filenameBuilder = new StringBuilder(); for (int i = 0; i < data.Length; i++) filenameBuilder.Append(data[i].ToString("x2")); - return Path.Combine(Path.GetTempPath(), _config.AppName ?? "Discord.Net", filenameBuilder.ToString()); + return Path.Combine(Path.GetTempPath(), Config.AppName ?? "Discord.Net", filenameBuilder.ToString()); } } private string LoadToken(string path, byte[] key) @@ -889,7 +890,7 @@ namespace Discord } catch (Exception ex) { - _logger.Warning("Failed to load cached token. Wrong/changed password?", ex); + Logger.Warning("Failed to load cached token. Wrong/changed password?", ex); } } return null; @@ -917,7 +918,7 @@ namespace Discord } catch (Exception ex) { - _logger.Warning("Failed to cache token", ex); + Logger.Warning("Failed to cache token", ex); } } @@ -936,5 +937,14 @@ namespace Discord } return existingId; } + + public Region GetRegion(string regionName) + { + Region region; + if (_regions.TryGetValue(regionName, out region)) + return region; + else + return _unknownRegion; + } } } \ No newline at end of file diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index d06ca3854..1735d6539 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -14,8 +14,8 @@ namespace Discord Debug = 5 } - public abstract class BaseConfig - where T : BaseConfig + public abstract class Config + where T : Config { protected bool _isLocked; protected internal void Lock() { _isLocked = true; } @@ -34,20 +34,22 @@ namespace Discord } } - public class DiscordConfig : BaseConfig - { - public static string LibName => "Discord.Net"; + public class DiscordConfig : Config + { + public const int MaxMessageSize = 2000; + + public const string LibName = "Discord.Net"; public static string LibVersion => typeof(DiscordClient).GetTypeInfo().Assembly.GetName().Version.ToString(3); - public static string LibUrl => "https://github.com/RogueException/Discord.Net"; + public const string LibUrl = "https://github.com/RogueException/Discord.Net"; - public static string ClientAPIUrl => "https://discordapp.com/api/"; - public static string StatusAPIUrl => "https://status.discordapp.com/api/v2/"; - public static string CDNUrl => "https://cdn.discordapp.com/"; - public static string InviteUrl => "https://discord.gg/"; + public const string ClientAPIUrl = "https://discordapp.com/api/"; + public const string StatusAPIUrl = "https://status.discordapp.com/api/v2/"; + public const string CDNUrl = "https://cdn.discordapp.com/"; + public const string InviteUrl = "https://discord.gg/"; //Global - /// Name of your application. + /// Name of your application. This is used both for the token cache directory and user agent. public string AppName { get { return _appName; } set { SetValue(ref _appName, value); UpdateUserAgent(); } } private string _appName = null; /// Version of your application. diff --git a/src/Discord.Net/Enums/ConnectionState.cs b/src/Discord.Net/Enums/ConnectionState.cs new file mode 100644 index 000000000..42c505ccd --- /dev/null +++ b/src/Discord.Net/Enums/ConnectionState.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum ConnectionState : byte + { + Disconnected, + Connecting, + Connected, + Disconnecting + } +} diff --git a/src/Discord.Net/ImageType.cs b/src/Discord.Net/Enums/ImageType.cs similarity index 100% rename from src/Discord.Net/ImageType.cs rename to src/Discord.Net/Enums/ImageType.cs diff --git a/src/Discord.Net/Events/ChannelEventArgs.cs b/src/Discord.Net/Events/ChannelEventArgs.cs new file mode 100644 index 000000000..4f1ca23a6 --- /dev/null +++ b/src/Discord.Net/Events/ChannelEventArgs.cs @@ -0,0 +1,12 @@ +using System; + +namespace Discord +{ + public class ChannelEventArgs : EventArgs + { + public Channel Channel { get; } + public Server Server => Channel.Server; + + public ChannelEventArgs(Channel channel) { Channel = channel; } + } +} diff --git a/src/Discord.Net/Events/ChannelUserEventArgs.cs b/src/Discord.Net/Events/ChannelUserEventArgs.cs new file mode 100644 index 000000000..819c7fcfa --- /dev/null +++ b/src/Discord.Net/Events/ChannelUserEventArgs.cs @@ -0,0 +1,14 @@ +namespace Discord +{ + public class ChannelUserEventArgs + { + public Channel Channel { get; } + public User User { get; } + + public ChannelUserEventArgs(Channel channel, User user) + { + Channel = channel; + User = user; + } + } +} diff --git a/src/Discord.Net/Events/DisconnectedEventArgs.cs b/src/Discord.Net/Events/DisconnectedEventArgs.cs new file mode 100644 index 000000000..87f9ec955 --- /dev/null +++ b/src/Discord.Net/Events/DisconnectedEventArgs.cs @@ -0,0 +1,16 @@ +using System; + +namespace Discord +{ + public class DisconnectedEventArgs : EventArgs + { + public bool WasUnexpected { get; } + public Exception Exception { get; } + + public DisconnectedEventArgs(bool wasUnexpected, Exception ex) + { + WasUnexpected = wasUnexpected; + Exception = ex; + } + } +} diff --git a/src/Discord.Net/Events/LogMessageEventArgs.cs b/src/Discord.Net/Events/LogMessageEventArgs.cs new file mode 100644 index 000000000..2aca5af34 --- /dev/null +++ b/src/Discord.Net/Events/LogMessageEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Discord +{ + public sealed class LogMessageEventArgs : EventArgs + { + public LogSeverity Severity { get; } + public string Source { get; } + public string Message { get; } + public Exception Exception { get; } + + public LogMessageEventArgs(LogSeverity severity, string source, string msg, Exception exception) + { + Severity = severity; + Source = source; + Message = msg; + Exception = exception; + } + } +} diff --git a/src/Discord.Net/Events/MessageEventArgs.cs b/src/Discord.Net/Events/MessageEventArgs.cs new file mode 100644 index 000000000..b4f94d30a --- /dev/null +++ b/src/Discord.Net/Events/MessageEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord +{ + public class MessageEventArgs : EventArgs + { + public Message Message { get; } + public User User => Message.User; + public Channel Channel => Message.Channel; + public Server Server => Message.Server; + + public MessageEventArgs(Message msg) { Message = msg; } + } +} diff --git a/src/Discord.Net/Events/ProfileEventArgs.cs b/src/Discord.Net/Events/ProfileEventArgs.cs new file mode 100644 index 000000000..42c8fd6ec --- /dev/null +++ b/src/Discord.Net/Events/ProfileEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord +{ + public class ProfileEventArgs : EventArgs + { + public Profile Profile { get; } + + public ProfileEventArgs(Profile profile) + { + Profile = profile; + } + } +} diff --git a/src/Discord.Net/Events/RoleEventArgs.cs b/src/Discord.Net/Events/RoleEventArgs.cs new file mode 100644 index 000000000..60dbecad7 --- /dev/null +++ b/src/Discord.Net/Events/RoleEventArgs.cs @@ -0,0 +1,12 @@ +using System; + +namespace Discord +{ + public class RoleEventArgs : EventArgs + { + public Role Role { get; } + public Server Server => Role.Server; + + public RoleEventArgs(Role role) { Role = role; } + } +} diff --git a/src/Discord.Net/Events/ServerEventArgs.cs b/src/Discord.Net/Events/ServerEventArgs.cs new file mode 100644 index 000000000..e9e564e1b --- /dev/null +++ b/src/Discord.Net/Events/ServerEventArgs.cs @@ -0,0 +1,11 @@ +using System; + +namespace Discord +{ + public class ServerEventArgs : EventArgs + { + public Server Server { get; } + + public ServerEventArgs(Server server) { Server = server; } + } +} diff --git a/src/Discord.Net/Events/UserEventArgs.cs b/src/Discord.Net/Events/UserEventArgs.cs new file mode 100644 index 000000000..d97f58795 --- /dev/null +++ b/src/Discord.Net/Events/UserEventArgs.cs @@ -0,0 +1,11 @@ +using System; +namespace Discord +{ + public class UserEventArgs : EventArgs + { + public User User { get; } + public Server Server => User.Server; + + public UserEventArgs(User user) { User = user; } + } +} diff --git a/src/Discord.Net/Helpers/Format.cs b/src/Discord.Net/Format.cs similarity index 77% rename from src/Discord.Net/Helpers/Format.cs rename to src/Discord.Net/Format.cs index 489ac22c0..13df0a920 100644 --- a/src/Discord.Net/Helpers/Format.cs +++ b/src/Discord.Net/Format.cs @@ -10,11 +10,11 @@ namespace Discord static Format() { _patterns = new string[] { "__", "_", "**", "*", "~~", "```", "`"}; - _builder = new StringBuilder(DiscordClient.MaxMessageSize); + _builder = new StringBuilder(DiscordConfig.MaxMessageSize); } /// Removes all special formatting characters from the provided text. - private static string Escape(string text) + public static string Escape(string text) { lock (_builder) { @@ -84,10 +84,7 @@ namespace Discord } return -1; } - - /// Returns a markdown-formatted string with no formatting, optionally escaping the contents. - public static string Normal(string text, bool escape = true) - => escape ? Escape(text) : text; + /// Returns a markdown-formatted string with bold formatting, optionally escaping the contents. public static string Bold(string text, bool escape = true) => escape ? $"**{Escape(text)}**" : $"**{text}**"; @@ -109,20 +106,5 @@ namespace Discord else return $"`{text}`"; } - - /// Returns a markdown-formatted string with multiple formatting, optionally escaping the contents. - public static string Multiple(string text, bool escape = true, - bool bold = false, bool italics = false, bool underline = false, bool strikeout = false, - bool code = false, string codeLanguage = null) - { - string result = text; - if (escape) result = Escape(result); - if (bold) result = Bold(result, false); - if (italics) result = Italics(result, false); - if (underline) result = Underline(result, false); - if (strikeout) result = Strikeout(result, false); - if (code) result = Code(result, codeLanguage); - return result; - } } } diff --git a/src/Discord.Net/Helpers/AsyncCollection.cs b/src/Discord.Net/Helpers/AsyncCollection.cs deleted file mode 100644 index 4c53e194f..000000000 --- a/src/Discord.Net/Helpers/AsyncCollection.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; - -namespace Discord -{ - internal abstract class AsyncCollection : IEnumerable - where TKey : struct, IEquatable - where TValue : CachedObject - { - private readonly object _writerLock; - - public class CollectionItemEventArgs : EventArgs - { - public TValue Item { get; } - public CollectionItemEventArgs(TValue item) { Item = item; } - } - public class CollectionItemRemappedEventArgs : EventArgs - { - public TValue Item { get; } - public TKey OldId { get; } - public TKey NewId { get; } - public CollectionItemRemappedEventArgs(TValue item, TKey oldId, TKey newId) { Item = item; OldId = oldId; NewId = newId; } - } - - public EventHandler ItemCreated; - private void RaiseItemCreated(TValue item) - { - if (ItemCreated != null) - ItemCreated(this, new CollectionItemEventArgs(item)); - } - public EventHandler ItemDestroyed; - private void RaiseItemDestroyed(TValue item) - { - if (ItemDestroyed != null) - ItemDestroyed(this, new CollectionItemEventArgs(item)); - } - public EventHandler ItemRemapped; - private void RaiseItemRemapped(TValue item, TKey oldId, TKey newId) - { - if (ItemRemapped != null) - ItemRemapped(this, new CollectionItemRemappedEventArgs(item, oldId, newId)); - } - - public EventHandler Cleared; - private void RaiseCleared() - { - if (Cleared != null) - Cleared(this, EventArgs.Empty); - } - - protected readonly DiscordClient _client; - protected readonly ConcurrentDictionary _dictionary; - - public int Count => _dictionary.Count; - - protected AsyncCollection(DiscordClient client, object writerLock) - { - _client = client; - _writerLock = writerLock; - _dictionary = new ConcurrentDictionary(); - } - - public TValue this[TKey? key] - => key == null ? null : this[key.Value]; - public TValue this[TKey key] - { - get - { - if (key.Equals(default(TKey))) - return null; - - TValue result; - if (!_dictionary.TryGetValue(key, out result)) - return null; - return result; - } - } - protected TValue GetOrAdd(TKey key, Func createFunc) - { - TValue result; - if (_dictionary.TryGetValue(key, out result)) - return result; - - lock (_writerLock) - { - if (!_dictionary.ContainsKey(key)) - { - result = createFunc(); - if (result.Cache()) - { - _dictionary.TryAdd(key, result); - RaiseItemCreated(result); - } - else - result.Uncache(); - return result; - } - else - return _dictionary[key]; - } - } - protected void Import(IEnumerable> items) - { - lock (_writerLock) - { - foreach (var pair in items) - { - var value = pair.Value; - if (value.Cache()) - { - _dictionary.TryAdd(pair.Key, value); - RaiseItemCreated(value); - } - else - value.Uncache(); - } - } - } - - public TValue TryRemove(TKey key) - { - if (_dictionary.ContainsKey(key)) - { - lock (_writerLock) - { - TValue result; - if (_dictionary.TryRemove(key, out result)) - { - result.Uncache(); //TODO: If this object is accessed before OnRemoved finished firing, properties such as Server.Channels will have null elements - return result; - } - } - } - return null; - } - public void Clear() - { - lock (_writerLock) - { - _dictionary.Clear(); - RaiseCleared(); - } - } - - public TValue Remap(TKey oldKey, TKey newKey) - { - if (_dictionary.ContainsKey(oldKey)) - { - lock (_writerLock) - { - TValue result; - if (_dictionary.TryRemove(oldKey, out result)) - _dictionary[newKey] = result; - return result; - } - } - return null; - } - - public IEnumerator GetEnumerator() => _dictionary.Select(x => x.Value).GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/src/Discord.Net/Helpers/BitHelper.cs b/src/Discord.Net/Helpers/BitHelper.cs deleted file mode 100644 index 328982985..000000000 --- a/src/Discord.Net/Helpers/BitHelper.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord -{ - internal static class BitHelper - { - public static bool GetBit(uint value, int pos) => ((value >> (byte)pos) & 1U) == 1; - public static void SetBit(ref uint value, int pos, bool bitValue) - { - if (bitValue) - value |= (1U << pos); - else - value &= ~(1U << pos); - } - } -} diff --git a/src/Discord.Net/Helpers/CachedObject.cs b/src/Discord.Net/Helpers/CachedObject.cs deleted file mode 100644 index dcd3b3c4a..000000000 --- a/src/Discord.Net/Helpers/CachedObject.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Globalization; - -namespace Discord -{ - public abstract class CachedObject : CachedObject - { - private TKey _id; - - internal CachedObject(DiscordClient client, TKey id) - : base(client) - { - _id = id; - } - - /// Returns the unique identifier for this object. - public TKey Id { get { return _id; } internal set { _id = value; } } - - public override string ToString() => $"{this.GetType().Name} {Id}"; - } - - public abstract class CachedObject - { - protected readonly DiscordClient _client; - private bool _isCached; - - internal DiscordClient Client => _client; - internal bool IsCached => _isCached; - - internal CachedObject(DiscordClient client) - { - _client = client; - } - - internal bool Cache() - { - if (LoadReferences()) - { - _isCached = true; - return true; - } - return false; - } - internal void Uncache() - { - if (_isCached) - { - UnloadReferences(); - _isCached = false; - } - } - internal abstract bool LoadReferences(); - internal abstract void UnloadReferences(); - } -} diff --git a/src/Discord.Net/Helpers/CollectionExtensions.cs b/src/Discord.Net/Helpers/CollectionExtensions.cs deleted file mode 100644 index 2554ac5ab..000000000 --- a/src/Discord.Net/Helpers/CollectionExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Discord -{ - public enum EditMode : byte - { - Set, - Add, - Remove - } - - internal static class Extensions - { - public static IEnumerable Modify(this IEnumerable original, IEnumerable modified, EditMode mode) - { - if (original == null) return null; - switch (mode) - { - case EditMode.Set: - default: - return modified; - case EditMode.Add: - return original.Concat(modified); - case EditMode.Remove: - return original.Except(modified); - } - } - } -} diff --git a/src/Discord.Net/Helpers/Reference.cs b/src/Discord.Net/Helpers/Reference.cs deleted file mode 100644 index 988ab7919..000000000 --- a/src/Discord.Net/Helpers/Reference.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; - -namespace Discord -{ - internal class Reference - where T : CachedObject - { - private Action _onCache, _onUncache; - private Func _getItem; - private ulong? _id; - public ulong? Id - { - get { return _id; } - set - { - _id = value; - _value = null; - } - } - - private T _value; - public T Value - { - get - { - var v = _value; //A little trickery to make this threadsafe - var id = _id; - if (v != null && !_value.IsCached) - { - v = null; - _value = null; - } - if (v == null && id != null) - { - v = _getItem(id.Value); - if (v != null && _onCache != null) - _onCache(v); - _value = v; - } - return v; - } - } - - public bool Load() - { - return Value != null; //Used for precaching - } - - public void Unload() - { - if (_onUncache != null) - { - var v = _value; - if (v != null && _onUncache != null) - _onUncache(v); - } - } - - public Reference(Func onUpdate, Action onCache = null, Action onUncache = null) - : this(null, onUpdate, onCache, onUncache) { } - public Reference(ulong? id, Func getItem, Action onCache = null, Action onUncache = null) - { - _id = id; - _getItem = getItem; - _onCache = onCache; - _onUncache = onUncache; - _value = null; - } - } -} diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs new file mode 100644 index 000000000..b411503df --- /dev/null +++ b/src/Discord.Net/Logging/LogManager.cs @@ -0,0 +1,59 @@ +using System; + +namespace Discord.Logging +{ + public class LogManager + { + private readonly DiscordClient _client; + + public LogSeverity Level { get; } + + public event EventHandler Message = delegate { }; + + internal LogManager(DiscordClient client) + { + _client = client; + Level = client.Config.LogLevel; + } + + public void Log(LogSeverity severity, string source, FormattableString message, Exception exception = null) + { + if (severity <= Level) + { + try { Message(this, new LogMessageEventArgs(severity, source, message.ToString(), exception)); } + catch { } //We dont want to log on log errors + } + } + public void Log(LogSeverity severity, string source, string message, Exception exception = null) + { + if (severity <= Level) + { + try { Message(this, new LogMessageEventArgs(severity, source, message, exception)); } + catch { } //We dont want to log on log errors + } + } + + public void Error(string source, string message, Exception ex = null) + => Log(LogSeverity.Error, source, message, ex); + public void Error(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Error, source, message, ex); + public void Warning(string source, string message, Exception ex = null) + => Log(LogSeverity.Warning, source, message, ex); + public void Warning(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Warning, source, message, ex); + public void Info(string source, string message, Exception ex = null) + => Log(LogSeverity.Info, source, message, ex); + public void Info(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Info, source, message, ex); + public void Verbose(string source, string message, Exception ex = null) + => Log(LogSeverity.Verbose, source, message, ex); + public void Verbose(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Verbose, source, message, ex); + public void Debug(string source, string message, Exception ex = null) + => Log(LogSeverity.Debug, source, message, ex); + public void Debug(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Debug, source, message, ex); + + public Logger CreateLogger(string name) => new Logger(this, name); + } +} diff --git a/src/Discord.Net/Logging/Logger.cs b/src/Discord.Net/Logging/Logger.cs new file mode 100644 index 000000000..671afa141 --- /dev/null +++ b/src/Discord.Net/Logging/Logger.cs @@ -0,0 +1,45 @@ +using Discord.Net.WebSockets; +using System; + +namespace Discord.Logging +{ + public class Logger + { + private readonly LogManager _manager; + + public string Name { get; } + public LogSeverity Level => _manager.Level; + + internal Logger(LogManager manager, string name) + { + _manager = manager; + Name = name; + } + + public void Log(LogSeverity severity, string message, Exception exception = null) + => _manager.Log(severity, Name, message, exception); + public void Log(LogSeverity severity, FormattableString message, Exception exception = null) + => _manager.Log(severity, Name, message, exception); + + public void Error(string message, Exception exception = null) + => _manager.Error(Name, message, exception); + public void Error(FormattableString message, Exception exception = null) + => _manager.Error(Name, message, exception); + public void Warning(string message, Exception exception = null) + => _manager.Warning(Name, message, exception); + public void Warning(FormattableString message, Exception exception = null) + => _manager.Warning(Name, message, exception); + public void Info(string message, Exception exception = null) + => _manager.Info(Name, message, exception); + public void Info(FormattableString message, Exception exception = null) + => _manager.Info(Name, message, exception); + public void Verbose(string message, Exception exception = null) + => _manager.Verbose(Name, message, exception); + public void Verbose(FormattableString message, Exception exception = null) + => _manager.Verbose(Name, message, exception); + public void Debug(string message, Exception exception = null) + => _manager.Debug(Name, message, exception); + public void Debug(FormattableString message, Exception exception = null) + => _manager.Debug(Name, message, exception); + } +} diff --git a/src/Discord.Net/Helpers/Mention.cs b/src/Discord.Net/Mention.cs similarity index 60% rename from src/Discord.Net/Helpers/Mention.cs rename to src/Discord.Net/Mention.cs index 986d9b549..e9f1e3669 100644 --- a/src/Discord.Net/Helpers/Mention.cs +++ b/src/Discord.Net/Mention.cs @@ -14,10 +14,6 @@ namespace Discord [Obsolete("Use User.Mention instead")] public static string User(User user) => $"<@{user.Id}>"; - /// Returns the string used to create a user mention. - [Obsolete("Use GlobalUser.Mention instead")] - public static string User(GlobalUser user) - => $"<@{user.Id}>"; /// Returns the string used to create a channel mention. [Obsolete("Use Channel.Mention instead")] public static string Channel(Channel channel) @@ -27,12 +23,12 @@ namespace Discord public static string Everyone() => $"@everyone"; - internal static string CleanUserMentions(DiscordClient client, Server server, string text, List users = null) + internal static string CleanUserMentions(DiscordClient client, Channel channel, string text, List users = null) { return _userRegex.Replace(text, new MatchEvaluator(e => { - var id = IdConvert.ToLong(e.Value.Substring(2, e.Value.Length - 3)); - var user = client.Users[id, server?.Id]; + var id = e.Value.Substring(2, e.Value.Length - 3).ToId(); + var user = channel.GetUser(id); if (user != null) { if (users != null) @@ -43,54 +39,57 @@ namespace Discord return '@' + e.Value; })); } - internal static string CleanChannelMentions(DiscordClient client, Server server, string text, List channels = null) + internal static string CleanChannelMentions(DiscordClient client, Channel channel, string text, List channels = null) { + var server = channel.Server; + if (server == null) return text; + return _channelRegex.Replace(text, new MatchEvaluator(e => { - var id = IdConvert.ToLong(e.Value.Substring(2, e.Value.Length - 3)); - var channel = client.Channels[id]; - if (channel != null && channel.Server.Id == server.Id) + var id = e.Value.Substring(2, e.Value.Length - 3).ToId(); + var mentionedChannel = server.GetChannel(id); + if (mentionedChannel != null && mentionedChannel.Server.Id == server.Id) { if (channels != null) - channels.Add(channel); - return '#' + channel.Name; + channels.Add(mentionedChannel); + return '#' + mentionedChannel.Name; } else //Channel not found return '#' + e.Value; })); } - /*internal static string CleanRoleMentions(DiscordClient client, User user, Channel channel, string text, List roles = null) + /*internal static string CleanRoleMentions(DiscordClient client, User user, Channel channel, string text, List roles = null) { + var server = channel.Server; + if (server == null) return text; + return _roleRegex.Replace(text, new MatchEvaluator(e => { if (roles != null && user.GetPermissions(channel).MentionEveryone) - roles.Add(channel.Server.EveryoneRole); + roles.Add(server.EveryoneRole); return e.Value; })); }*/ - /// Resolves all mentions in a provided string to those users, channels or roles' names. - public static string Resolve(Message source, string text) + /// Resolves all mentions in a provided string to those users, channels or roles' names. + public static string Resolve(Message source, string text) { if (source == null) throw new ArgumentNullException(nameof(source)); if (text == null) throw new ArgumentNullException(nameof(text)); - return Resolve(source.Server, text); + return Resolve(source.Channel, text); } /// Resolves all mentions in a provided string to those users, channels or roles' names. - public static string Resolve(Server server, string text) + public static string Resolve(Channel channel, string text) { if (text == null) throw new ArgumentNullException(nameof(text)); - var client = server?.Client; - text = CleanUserMentions(client, server, text); - if (server != null) - { - text = CleanChannelMentions(client, server, text); - //text = CleanRoleMentions(_client, User, channel, text); - } - return text; + var client = channel.Client; + text = CleanUserMentions(client, channel, text); + text = CleanChannelMentions(client, channel, text); + //text = CleanRoleMentions(_client, channel, text); + return text; } } } diff --git a/src/Discord.Net/MessageQueue.cs b/src/Discord.Net/MessageQueue.cs new file mode 100644 index 000000000..c43d0174e --- /dev/null +++ b/src/Discord.Net/MessageQueue.cs @@ -0,0 +1,102 @@ +using Discord.API.Client.Rest; +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net +{ + public class MessageQueue + { + private class MessageQueueItem + { + public readonly ulong Id, ChannelId; + public readonly string Text; + public readonly ulong[] MentionedUsers; + public readonly bool IsTTS; + + public MessageQueueItem(ulong id, ulong channelId, string text, ulong[] userIds, bool isTTS) + { + Id = id; + ChannelId = channelId; + Text = text; + MentionedUsers = userIds; + IsTTS = isTTS; + } + } + + private readonly Random _nonceRand; + private readonly DiscordClient _client; + private readonly ConcurrentQueue _pending; + + internal MessageQueue(DiscordClient client) + { + _client = client; + _nonceRand = new Random(); + _pending = new ConcurrentQueue(); + } + + public void QueueSend(ulong channelId, string text, ulong[] userIds, bool isTTS) + { + _pending.Enqueue(new MessageQueueItem(0, channelId, text, userIds, isTTS)); + } + public void QueueEdit(ulong channelId, ulong messageId, string text, ulong[] userIds) + { + _pending.Enqueue(new MessageQueueItem(channelId, messageId, text, userIds, false)); + } + + internal Task Run(CancellationToken cancelToken, int interval) + { + return Task.Run(async () => + { + MessageQueueItem queuedMessage; + + while (!cancelToken.IsCancellationRequested) + { + await Task.Delay(interval).ConfigureAwait(false); + while (_pending.TryDequeue(out queuedMessage)) + { + try + { + if (queuedMessage.Id == 0) + { + var request = new SendMessageRequest(queuedMessage.ChannelId) + { + Content = queuedMessage.Text, + MentionedUserIds = queuedMessage.MentionedUsers, + Nonce = GenerateNonce().ToIdString(), + IsTTS = queuedMessage.IsTTS + }; + await _client.ClientAPI.Send(request).ConfigureAwait(false); + } + else + { + var request = new UpdateMessageRequest(queuedMessage.ChannelId, queuedMessage.Id) + { + Content = queuedMessage.Text, + MentionedUserIds = queuedMessage.MentionedUsers + }; + await _client.ClientAPI.Send(request).ConfigureAwait(false); + } + } + catch (WebException) { break; } + catch (HttpException) { /*msg.State = MessageState.Failed;*/ } + } + } + }); + } + + public void Clear() + { + MessageQueueItem ignored; + while (_pending.TryDequeue(out ignored)) { } + } + + private ulong GenerateNonce() + { + lock (_nonceRand) + return (ulong)_nonceRand.Next(1, int.MaxValue); + } + } +} diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs index f72612d9a..525c5bdd8 100644 --- a/src/Discord.Net/Models/Channel.cs +++ b/src/Discord.Net/Models/Channel.cs @@ -1,5 +1,5 @@ using Discord.API.Client; -using Newtonsoft.Json; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -7,315 +7,292 @@ using APIChannel = Discord.API.Client.Channel; namespace Discord { - public sealed class Channel : CachedObject - { - private struct ChannelMember - { - public readonly User User; - public readonly ChannelPermissions Permissions; - - public ChannelMember(User user) - { - User = user; - Permissions = new ChannelPermissions(); - Permissions.Lock(); - } - } - - public sealed class PermissionOverwrite - { - public PermissionTarget TargetType { get; } - public ulong TargetId { get; } - public DualChannelPermissions Permissions { get; } + public sealed class Channel + { + private struct Member + { + public readonly User User; + public readonly ChannelPermissions Permissions; + public Member(User user) + { + User = user; + Permissions = new ChannelPermissions(); + Permissions.Lock(); + } + } - internal PermissionOverwrite(PermissionTarget targetType, ulong targetId, uint allow, uint deny) - { - TargetType = targetType; - TargetId = targetId; - Permissions = new DualChannelPermissions(allow, deny); - Permissions.Lock(); - } - } + public sealed class PermissionOverwrite + { + public PermissionTarget TargetType { get; } + public ulong TargetId { get; } + public DualChannelPermissions Permissions { get; } + internal PermissionOverwrite(PermissionTarget targetType, ulong targetId, uint allow, uint deny) + { + TargetType = targetType; + TargetId = targetId; + Permissions = new DualChannelPermissions(allow, deny); + Permissions.Lock(); + } + } + + private readonly ConcurrentDictionary _users; + private readonly ConcurrentDictionary _messages; + private Dictionary _permissionOverwrites; - /// Returns the name of this channel. - 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 => _recipient.Id != null; - /// Returns the type of this channel (see ChannelTypes). - public string Type { get; private set; } + /// Gets the client that generated this channel object. + internal DiscordClient Client { get; } + /// Gets the unique identifier for this channel. + public ulong Id { get; } + /// Gets the server owning this channel, if this is a public chat. + public Server Server { get; } + /// Gets the target user, if this is a private chat. + public User Recipient { get; } - /// Returns the server containing this channel. - [JsonIgnore] - public Server Server => _server.Value; - [JsonProperty] - private ulong? ServerId { get { return _server.Id; } set { _server.Id = value; } } - private readonly Reference _server; + /// Gets the name of this channel. + public string Name { get; private set; } + /// Gets the topic of this channel. + public string Topic { get; private set; } + /// Gets the position of this channel relative to other channels in this server. + public int Position { get; private set; } + /// Gets the type of this channel (see ChannelTypes). + public string Type { get; private set; } - /// For private chats, returns the target user, otherwise null. - [JsonIgnore] - public User Recipient => _recipient.Value; - [JsonProperty] - private ulong? RecipientId { get { return _recipient.Id; } set { _recipient.Id = value; } } - private readonly Reference _recipient; + /// Gets true if this is a private chat with another user. + public bool IsPrivate => Recipient != null; + /// Gets the string used to mention this channel. + public string Mention => $"<#{Id}>"; + /// Gets a collection of all messages the client has seen posted in this channel. This collection does not guarantee any ordering. + public IEnumerable Messages => _messages?.Values ?? Enumerable.Empty(); + /// Gets a collection of all custom permissions used for this channel. + public IEnumerable PermissionOverwrites => _permissionOverwrites.Select(x => x.Value); - //Collections - /// Returns a collection of all users with read access to this channel. - [JsonIgnore] - public IEnumerable Members - { - get - { + /// Gets a collection of all users with read access to this channel. + public IEnumerable Users + { + get + { if (IsPrivate) - return _members.Values.Select(x => x.User); - if (_client.Config.UsePermissionsCache) + return _users.Values.Select(x => x.User); + if (Client.Config.UsePermissionsCache) { if (Type == ChannelType.Text) - return _members.Values.Where(x => x.Permissions.ReadMessages == true).Select(x => x.User); + return _users.Values.Where(x => x.Permissions.ReadMessages == true).Select(x => x.User); else if (Type == ChannelType.Voice) - return _members.Values.Select(x => x.User).Where(x => x.VoiceChannel == this); + return _users.Values.Select(x => x.User).Where(x => x.VoiceChannel == this); } else { if (Type == ChannelType.Text) { ChannelPermissions perms = new ChannelPermissions(); - return Server.Members.Where(x => + return Server.Users.Where(x => { UpdatePermissions(x, perms); return perms.ReadMessages == true; }); } else if (Type == ChannelType.Voice) - return Server.Members.Where(x => x.VoiceChannel == this); + return Server.Users.Where(x => x.VoiceChannel == this); } - return Enumerable.Empty(); + return Enumerable.Empty(); } - } - [JsonProperty] - private IEnumerable MemberIds => Members.Select(x => x.Id); - private ConcurrentDictionary _members; - - /// Returns a collection of all messages the client has seen posted in this channel. This collection does not guarantee any ordering. - [JsonIgnore] - public IEnumerable Messages => _messages?.Values ?? Enumerable.Empty(); - [JsonProperty] - private IEnumerable MessageIds => Messages.Select(x => x.Id); - private readonly ConcurrentDictionary _messages; + } - /// Returns a collection of all custom permissions used for this channel. - private PermissionOverwrite[] _permissionOverwrites; - public IEnumerable PermissionOverwrites { get { return _permissionOverwrites; } internal set { _permissionOverwrites = value.ToArray(); } } + internal Channel(DiscordClient client, ulong id, Server server) + : this(client, id) + { + Server = server; + } + internal Channel(DiscordClient client, ulong id, User recipient) + : this(client, id) + { + Recipient = recipient; + Name = $"@{recipient}"; + AddUser(client.PrivateUser); + AddUser(recipient); + } + private Channel(DiscordClient client, ulong id) + { + Client = client; + Id = id; - /// Returns the string used to mention this channel. - public string Mention => $"<#{Id}>"; + _permissionOverwrites = new Dictionary(); + _users = new ConcurrentDictionary(); + if (client.Config.MessageCacheSize > 0) + _messages = new ConcurrentDictionary(); + } - internal Channel(DiscordClient client, ulong id, ulong? serverId, ulong? recipientId) - : base(client, id) - { - _server = new Reference(serverId, - x => _client.Servers[x], - x => x.AddChannel(this), - x => x.RemoveChannel(this)); - _recipient = new Reference(recipientId, - x => _client.Users.GetOrAdd(x, _server.Id), - x => - { - Name = $"@{x}"; - if (_server.Id == null) - x.Global.PrivateChannel = this; - }, - x => - { - if (_server.Id == null) - x.Global.PrivateChannel = null; - }); - _permissionOverwrites = new PermissionOverwrite[0]; - _members = new ConcurrentDictionary(); + internal void Update(ChannelReference model) + { + if (!IsPrivate && model.Name != null) + Name = model.Name; + if (model.Type != null) + Type = model.Type; + } + internal void Update(APIChannel model) + { + Update(model as ChannelReference); - if (recipientId != null) - { - AddMember(client.PrivateUser); - AddMember(Recipient); - } + if (model.Position != null) + Position = model.Position.Value; + if (model.Topic != null) + Topic = model.Topic; + if (model.Recipient != null) + Recipient.Update(model.Recipient); - //Local Cache - if (client.Config.MessageCacheSize > 0) - _messages = new ConcurrentDictionary(); - } - internal override bool LoadReferences() - { - if (IsPrivate) - return _recipient.Load(); - else - return _server.Load(); - } - internal override void UnloadReferences() - { - _server.Unload(); - _recipient.Unload(); - - var globalMessages = _client.Messages; - if (_client.Config.MessageCacheSize > 0) + if (model.PermissionOverwrites != null) { - var messages = _messages; - foreach (var message in messages) - globalMessages.TryRemove(message.Key); - messages.Clear(); + _permissionOverwrites = model.PermissionOverwrites + .Select(x => new PermissionOverwrite(PermissionTarget.FromString(x.Type), x.Id, x.Allow, x.Deny)) + .ToDictionary(x => x.TargetId); + UpdatePermissions(); } } - internal void Update(ChannelReference model) - { - if (!IsPrivate && model.Name != null) - Name = model.Name; - if (model.Type != null) - Type = model.Type; - } - internal void Update(APIChannel model) - { - Update(model as ChannelReference); - - if (model.Position != null) - Position = model.Position; - if (model.Topic != null) - Topic = model.Topic; - - if (model.PermissionOverwrites != null) - { - _permissionOverwrites = model.PermissionOverwrites - .Select(x => new PermissionOverwrite(PermissionTarget.FromString(x.Type), x.Id, x.Allow, x.Deny)) - .ToArray(); - UpdatePermissions(); - } - } - - internal void AddMessage(Message message) - { - //Race conditions are okay here - it just means the queue will occasionally go higher than the requested cache size, and fixed later. - var cacheLength = _client.Config.MessageCacheSize; - if (cacheLength > 0) - { - var oldestIds = _messages.Where(x => x.Value.Timestamp < message.Timestamp).Select(x => x.Key).OrderBy(x => x).Take(_messages.Count - cacheLength); - foreach (var id in oldestIds) - { - Message removed; - if (_messages.TryRemove(id, out removed)) - _client.Messages.TryRemove(id); - } - _messages.TryAdd(message.Id, message); - } - } - internal void RemoveMessage(Message message) - { - if (_client.Config.MessageCacheSize > 0) - _messages.TryRemove(message.Id, out message); - } - - internal void AddMember(User user) + //Members + internal void AddUser(User user) { - if (!_client.Config.UsePermissionsCache) + if (!Client.Config.UsePermissionsCache) return; - var member = new ChannelMember(user); - if (_members.TryAdd(user.Id, member)) - UpdatePermissions(user, member.Permissions); + var member = new Member(user); + if (_users.TryAdd(user.Id, member)) + UpdatePermissions(user, member.Permissions); } - internal void RemoveMember(User user) + internal void RemoveUser(ulong id) { - if (!_client.Config.UsePermissionsCache) + if (!Client.Config.UsePermissionsCache) return; - ChannelMember ignored; - _members.TryRemove(user.Id, out ignored); - } + Member ignored; + _users.TryRemove(id, out ignored); + } + public User GetUser(ulong id) + { + Member result; + _users.TryGetValue(id, out result); + return result.User; + } - internal ChannelPermissions GetPermissions(User user) + //Messages + internal Message AddMessage(ulong id, ulong userId, DateTime timestamp) { - if (_client.Config.UsePermissionsCache) + Message message = new Message(id, this, userId); + var cacheLength = Client.Config.MessageCacheSize; + if (cacheLength > 0) { - ChannelMember member; - if (_members.TryGetValue(user.Id, out member)) - return member.Permissions; - else - return null; + var oldestIds = _messages + .Where(x => x.Value.Timestamp < timestamp) + .Select(x => x.Key).OrderBy(x => x) + .Take(_messages.Count - cacheLength); + Message removed; + foreach (var removeId in oldestIds) + _messages.TryRemove(removeId, out removed); + return _messages.GetOrAdd(message.Id, message); } - else + return message; + } + internal Message RemoveMessage(ulong id) + { + if (Client.Config.MessageCacheSize > 0) { - ChannelPermissions perms = new ChannelPermissions(); - UpdatePermissions(user, perms); - return perms; + Message msg; + _messages.TryRemove(id, out msg); + return msg; } - } - internal void UpdatePermissions() + return null; + } + public Message GetMessage(ulong id) { - if (!_client.Config.UsePermissionsCache) + Message result; + _messages.TryGetValue(id, out result); + return result; + } + + //Permissions + internal void UpdatePermissions() + { + if (!Client.Config.UsePermissionsCache) return; - foreach (var pair in _members) + foreach (var pair in _users) { - ChannelMember member = pair.Value; + Member member = pair.Value; UpdatePermissions(member.User, member.Permissions); } - } + } internal void UpdatePermissions(User user) { - if (!_client.Config.UsePermissionsCache) + if (!Client.Config.UsePermissionsCache) return; - ChannelMember member; - if (_members.TryGetValue(user.Id, out member)) + Member member; + if (_users.TryGetValue(user.Id, out member)) UpdatePermissions(member.User, member.Permissions); } internal void UpdatePermissions(User user, ChannelPermissions permissions) - { - uint newPermissions = 0; - var server = Server; + { + uint newPermissions = 0; + var server = Server; - //Load the mask of all permissions supported by this channel type - var mask = ChannelPermissions.All(this).RawValue; + //Load the mask of all permissions supported by this channel type + var mask = ChannelPermissions.All(this).RawValue; - if (server != null) - { - //Start with this user's server permissions - newPermissions = server.GetPermissions(user).RawValue; + if (server != null) + { + //Start with this user's server permissions + newPermissions = server.GetPermissions(user).RawValue; - if (IsPrivate || user.IsOwner) - newPermissions = mask; //Owners always have all permissions - else - { - var channelOverwrites = PermissionOverwrites; + if (IsPrivate || user == Server.Owner) + newPermissions = mask; //Owners always have all permissions + else + { + var channelOverwrites = PermissionOverwrites; - var roles = user.Roles; - foreach (var denyRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Permissions.Deny.RawValue != 0 && roles.Any(y => y.Id == x.TargetId))) - newPermissions &= ~denyRole.Permissions.Deny.RawValue; - foreach (var allowRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Permissions.Allow.RawValue != 0 && roles.Any(y => y.Id == x.TargetId))) - newPermissions |= allowRole.Permissions.Allow.RawValue; - foreach (var denyUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == Id && x.Permissions.Deny.RawValue != 0)) - newPermissions &= ~denyUser.Permissions.Deny.RawValue; - foreach (var allowUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == Id && x.Permissions.Allow.RawValue != 0)) - newPermissions |= allowUser.Permissions.Allow.RawValue; + var roles = user.Roles; + foreach (var denyRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Permissions.Deny.RawValue != 0 && roles.Any(y => y.Id == x.TargetId))) + newPermissions &= ~denyRole.Permissions.Deny.RawValue; + foreach (var allowRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Permissions.Allow.RawValue != 0 && roles.Any(y => y.Id == x.TargetId))) + newPermissions |= allowRole.Permissions.Allow.RawValue; + foreach (var denyUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == Id && x.Permissions.Deny.RawValue != 0)) + newPermissions &= ~denyUser.Permissions.Deny.RawValue; + foreach (var allowUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == Id && x.Permissions.Allow.RawValue != 0)) + newPermissions |= allowUser.Permissions.Allow.RawValue; - if (BitHelper.GetBit(newPermissions, (int)PermissionsBits.ManageRolesOrPermissions)) - newPermissions = mask; //ManageRolesOrPermissions gives all permisions - else if (Type == ChannelType.Text && !BitHelper.GetBit(newPermissions, (int)PermissionsBits.ReadMessages)) - newPermissions = 0; //No read permission on a text channel removes all other permissions - else - newPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from serverPerms, for example) - } - } - else - newPermissions = mask; //Private messages always have all permissions + if (newPermissions.HasBit((byte)PermissionsBits.ManageRolesOrPermissions)) + newPermissions = mask; //ManageRolesOrPermissions gives all permisions + else if (Type == ChannelType.Text && !newPermissions.HasBit((byte)PermissionsBits.ReadMessages)) + newPermissions = 0; //No read permission on a text channel removes all other permissions + else + newPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from serverPerms, for example) + } + } + else + newPermissions = mask; //Private messages always have all permissions if (newPermissions != permissions.RawValue) permissions.SetRawValueInternal(newPermissions); - } + } + internal ChannelPermissions GetPermissions(User user) + { + if (Client.Config.UsePermissionsCache) + { + Member member; + if (_users.TryGetValue(user.Id, out member)) + return member.Permissions; + else + return null; + } + else + { + ChannelPermissions perms = new ChannelPermissions(); + UpdatePermissions(user, perms); + return perms; + } + } - public override bool Equals(object obj) => obj is Channel && (obj as Channel).Id == Id; - public override int GetHashCode() => unchecked(Id.GetHashCode() + 5658); - public override string ToString() => Name ?? IdConvert.ToString(Id); - } + public override bool Equals(object obj) => obj is Channel && (obj as Channel).Id == Id; + public override int GetHashCode() => unchecked(Id.GetHashCode() + 5658); + public override string ToString() => Name ?? Id.ToIdString(); + } } diff --git a/src/Discord.Net/Models/GlobalUser.cs b/src/Discord.Net/Models/GlobalUser.cs index e8b847d96..819f4396d 100644 --- a/src/Discord.Net/Models/GlobalUser.cs +++ b/src/Discord.Net/Models/GlobalUser.cs @@ -6,7 +6,7 @@ using APIUser = Discord.API.Client.User; namespace Discord { - public sealed class GlobalUser : CachedObject + /*public sealed class GlobalUser : CachedObject { /// Returns the email for this user. Note: this field is only ever populated for the current logged in user. [JsonIgnore] @@ -75,5 +75,5 @@ namespace Discord public override bool Equals(object obj) => obj is GlobalUser && (obj as GlobalUser).Id == Id; public override int GetHashCode() => unchecked(Id.GetHashCode() + 7891); public override string ToString() => IdConvert.ToString(Id); - } + }*/ } diff --git a/src/Discord.Net/Models/Invite.cs b/src/Discord.Net/Models/Invite.cs index 5c5073d63..b73a68782 100644 --- a/src/Discord.Net/Models/Invite.cs +++ b/src/Discord.Net/Models/Invite.cs @@ -42,6 +42,7 @@ namespace Discord public ushort Discriminator { get; } /// Returns the unique identifier for this user's avatar. public string AvatarId { get; } + /// Returns the full path to this user's avatar. public string AvatarUrl => User.GetAvatarUrl(Id, AvatarId); @@ -54,24 +55,26 @@ namespace Discord } } - /// Returns information about the server this invite is attached to. - public ServerInfo Server { get; private set; } - /// Returns information about the channel this invite is attached to. - public ChannelInfo Channel { get; private set; } - + /// Gets the unique code for this invite. public string Code { get; } - /// Returns, if enabled, an alternative human-readable code for URLs. + /// Gets, if enabled, an alternative human-readable invite code. public string XkcdCode { get; } - /// 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. + + /// Gets information about the server this invite is attached to. + public ServerInfo Server { get; private set; } + /// Gets information about the channel this invite is attached to. + public ChannelInfo Channel { get; private set; } + /// Gets the time (in seconds) until the invite expires. + public int? MaxAge { get; private set; } + /// Gets the amount of times this invite has been used. public int Uses { get; private set; } - /// The max amount of times this invite may be used. - public int MaxUses { get; private set; } - /// Returns true if this invite has been destroyed, or you are banned from that server. + /// Gets the max amount of times this invite may be used. + public int? MaxUses { get; private set; } + /// Returns true if this invite has expired, been destroyed, or you are banned from that server. public bool IsRevoked { get; private set; } /// If true, a user accepting this invite will be kicked from the server after closing their client. public bool IsTemporary { get; private set; } + /// Gets when this invite was created. public DateTime CreatedAt { get; private set; } /// Returns a URL for this invite using XkcdCode if available or Id if not. @@ -99,7 +102,7 @@ namespace Discord if (model.IsTemporary != null) IsTemporary = model.IsTemporary.Value; if (model.MaxAge != null) - MaxAge = model.MaxAge.Value; + MaxAge = model.MaxAge.Value != 0 ? model.MaxAge.Value : (int?)null; if (model.MaxUses != null) MaxUses = model.MaxUses.Value; if (model.Uses != null) diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index 625628d85..21872a3b3 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -15,9 +15,9 @@ namespace Discord Failed } - public sealed class Message : CachedObject + public sealed class Message { - internal class ImportResolver : DefaultContractResolver + /*internal class ImportResolver : DefaultContractResolver { protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { @@ -31,7 +31,7 @@ namespace Discord } return property; } - } + }*/ public sealed class Attachment : File { @@ -89,17 +89,24 @@ namespace Discord internal File() { } } - /// Returns true if the logged-in user was mentioned. - public bool IsMentioningMe { get; private set; } - /// Returns true if the current user created this message. - public bool IsAuthor => _client.CurrentUser.Id == _user.Id; + private static readonly Attachment[] _initialAttachments = new Attachment[0]; + private static readonly Embed[] _initialEmbeds = new Embed[0]; + + private readonly ulong _userId; + + /// Returns the unique identifier for this message. + public ulong Id { get; } + /// Returns the channel this message was sent to. + public Channel Channel { get; } + + /// Returns true if the logged-in user was mentioned. + public bool IsMentioningMe { get; private set; } /// Returns true if the message was sent as text-to-speech by someone with permissions to do so. public bool IsTTS { get; private set; } /// Returns the state of this message. Only useful if UseMessageQueue is true. public MessageState State { get; internal set; } /// Returns the raw content of this message as it was received from the server. public string RawText { get; private set; } - [JsonIgnore] /// Returns the content of this message with any special references such as mentions converted. public string Text { get; internal set; } /// Returns the timestamp for when this message was sent. @@ -108,89 +115,26 @@ namespace Discord public DateTime? EditedTimestamp { get; private set; } /// Returns the attachments included in this message. public Attachment[] Attachments { get; private set; } - private static readonly Attachment[] _initialAttachments = new Attachment[0]; /// Returns a collection of all embeded content in this message. public Embed[] Embeds { get; private set; } - private static readonly Embed[] _initialEmbeds = new Embed[0]; /// Returns a collection of all users mentioned in this message. - [JsonIgnore] public IEnumerable MentionedUsers { get; internal set; } - [JsonProperty] - private IEnumerable MentionedUserIds - { - get { return MentionedUsers?.Select(x => x.Id); } - set { MentionedUsers = value.Select(x => _client.GetUser(Server, x)).Where(x => x != null); } - } - /// Returns a collection of all channels mentioned in this message. - [JsonIgnore] public IEnumerable MentionedChannels { get; internal set; } - [JsonProperty] - private IEnumerable MentionedChannelIds - { - get { return MentionedChannels?.Select(x => x.Id); } - set { MentionedChannels = value.Select(x => _client.GetChannel(x)).Where(x => x != null); } - } - /// Returns a collection of all roles mentioned in this message. - [JsonIgnore] public IEnumerable MentionedRoles { get; internal set; } - [JsonProperty] - private IEnumerable MentionedRoleIds - { - get { return MentionedRoles?.Select(x => x.Id); } - set { MentionedRoles = value.Select(x => _client.GetRole(x)).Where(x => x != null); } - } /// Returns the server containing the channel this message was sent to. - [JsonIgnore] - public Server Server => _channel.Value.Server; - - /// Returns the channel this message was sent to. - [JsonIgnore] - public Channel Channel => _channel.Value; - [JsonProperty] - private ulong? ChannelId => _channel.Id; - private readonly Reference _channel; + public Server Server => Channel.Server; + /// Returns the author of this message. + public User User => Channel.GetUser(_userId); - /// Returns the author of this message. - [JsonIgnore] - public User User => _user.Value; - [JsonProperty] - private ulong? UserId => _user.Id; - private readonly Reference _user; - - internal Message(DiscordClient client, ulong id, ulong channelId, ulong userId) - : base(client, id) + internal Message(ulong id, Channel channel, ulong userId) { - _channel = new Reference(channelId, - x => _client.Channels[x], - x => x.AddMessage(this), - x => x.RemoveMessage(this)); - _user = new Reference(userId, - x => - { - var channel = Channel; - if (channel == null) return null; - - if (!channel.IsPrivate) - return _client.Users[x, channel.Server.Id]; - else - return _client.Users[x, null]; - }); Attachments = _initialAttachments; Embeds = _initialEmbeds; } - internal override bool LoadReferences() - { - return _channel.Load() && _user.Load(); - } - internal override void UnloadReferences() - { - _channel.Unload(); - _user.Unload(); - } internal void Update(APIMessage model) { @@ -247,7 +191,7 @@ namespace Discord if (model.Mentions != null) { MentionedUsers = model.Mentions - .Select(x => _client.Users[x.Id, Channel.Server?.Id]) + .Select(x => Channel.GetUser(x.Id)) .Where(x => x != null) .ToArray(); } @@ -266,10 +210,10 @@ namespace Discord //var mentionedUsers = new List(); var mentionedChannels = new List(); //var mentionedRoles = new List(); - text = Mention.CleanUserMentions(_client, server, text/*, mentionedUsers*/); + text = Mention.CleanUserMentions(Channel.Client, channel, text/*, mentionedUsers*/); if (server != null) { - text = Mention.CleanChannelMentions(_client, server, text, mentionedChannels); + text = Mention.CleanChannelMentions(Channel.Client, channel, text, mentionedChannels); //text = Mention.CleanRoleMentions(_client, User, channel, text, mentionedRoles); } Text = text; @@ -287,7 +231,7 @@ namespace Discord } else { - var me = _client.PrivateUser; + var me = Channel.Client.PrivateUser; IsMentioningMe = MentionedUsers?.Contains(me) ?? false; } } diff --git a/src/Discord.Net/Models/Permissions.cs b/src/Discord.Net/Models/Permissions.cs index e98061691..3d98525b7 100644 --- a/src/Discord.Net/Models/Permissions.cs +++ b/src/Discord.Net/Models/Permissions.cs @@ -127,9 +127,15 @@ namespace Discord _rawValue = rawValue; } - internal bool GetBit(PermissionsBits pos) => BitHelper.GetBit(_rawValue, (int)pos); - internal void SetBit(PermissionsBits pos, bool value) { CheckLock(); SetBitInternal((byte)pos, value); } - internal void SetBitInternal(int pos, bool value) => BitHelper.SetBit(ref _rawValue, pos, value); + internal bool GetBit(PermissionsBits bit) => _rawValue.HasBit((byte)bit); + internal void SetBit(PermissionsBits bit, bool value) { CheckLock(); SetBitInternal((byte)bit, value); } + internal void SetBitInternal(int pos, bool value) + { + if (value) + _rawValue |= (1U << pos); + else + _rawValue &= ~(1U << pos); + } internal void Lock() => _isLocked = true; protected void CheckLock() @@ -140,7 +146,7 @@ namespace Discord public override bool Equals(object obj) => obj is Permissions && (obj as Permissions)._rawValue == _rawValue; public override int GetHashCode() => unchecked(_rawValue.GetHashCode() + 393); - } + } public sealed class DualChannelPermissions { diff --git a/src/Discord.Net/Models/Profile.cs b/src/Discord.Net/Models/Profile.cs new file mode 100644 index 000000000..62662fc76 --- /dev/null +++ b/src/Discord.Net/Models/Profile.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; +using APIUser = Discord.API.Client.User; + +namespace Discord +{ + public sealed class Profile + { + /// Gets the unique identifier for this user. + public ulong Id { get; private set; } + /// Gets the email for this user. + public string Email { get; private set; } + /// Gets if the email for this user has been verified. + public bool? IsVerified { get; private set; } + + internal Profile() { } + + internal void Update(APIUser model) + { + Id = model.Id; + Email = model.Email; + IsVerified = model.IsVerified; + } + + public override bool Equals(object obj) + => (obj is Profile && (obj as Profile).Id == Id) || (obj is User && (obj as User).Id == Id); + public override int GetHashCode() => unchecked(Id.GetHashCode() + 2061); + public override string ToString() => Id.ToIdString(); + } +} diff --git a/src/Discord.Net/Models/Role.cs b/src/Discord.Net/Models/Role.cs index 33028a083..7d7c7c82e 100644 --- a/src/Discord.Net/Models/Role.cs +++ b/src/Discord.Net/Models/Role.cs @@ -1,64 +1,58 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.Linq; using APIRole = Discord.API.Client.Role; namespace Discord { - public sealed class Role : CachedObject - { - /// Returns the name of this role. - public string Name { get; private set; } - /// If true, this role is displayed isolated from other users. - public bool IsHoisted { get; private set; } - /// Returns the position of this channel in the role list for this server. - public int Position { get; private set; } - /// Returns the color of this role. - public Color Color { get; private set; } - /// Returns whether this role is managed by server (e.g. for Twitch integration) - public bool IsManaged { get; private set; } - - /// Returns the the permissions contained by this role. - public ServerPermissions Permissions { get; } + public sealed class Role + { + private readonly DiscordClient _client; - /// Returns the server this role is a member of. - [JsonIgnore] - public Server Server => _server.Value; - [JsonProperty] - private ulong? ServerId { get { return _server.Id; } set { _server.Id = value; } } - private readonly Reference _server; + /// Gets the unique identifier for this role. + public ulong Id { get; } + /// Gets the server this role is a member of. + public Server Server { get; } + /// Gets the the permissions contained by this role. + public ServerPermissions Permissions { get; } + /// Gets the color of this role. + public Color Color { get; } - /// Returns true if this is the role representing all users in a server. - public bool IsEveryone => _server.Id == null || Id == _server.Id; + /// Gets the name of this role. + public string Name { get; private set; } + /// If true, this role is displayed isolated from other users. + public bool IsHoisted { get; private set; } + /// Gets the position of this channel relative to other channels in this server. + public int Position { get; private set; } + /// Gets whether this role is managed by server (e.g. for Twitch integration) + public bool IsManaged { get; private set; } - /// Returns a list of all members in this role. - [JsonIgnore] - public IEnumerable Members => _server.Id != null ? (IsEveryone ? Server.Members : Server.Members.Where(x => x.HasRole(this))) : new User[0]; - [JsonProperty] - private IEnumerable MemberIds => Members.Select(x => x.Id); - //TODO: Add local members cache + /// Gets true if this is the role representing all users in a server. + public bool IsEveryone => Id == Server.Id; + /// Gets a list of all members in this role. + public IEnumerable Members => IsEveryone ? Server.Users : Server.Users.Where(x => x.HasRole(this)); - /// Returns the string used to mention this role. - public string Mention { get { if (IsEveryone) return "@everyone"; else throw new InvalidOperationException("Discord currently only supports mentioning the everyone role"); } } + /// Gets the string used to mention this role. + public string Mention + { + get + { + if (IsEveryone) + return "@everyone"; + else + throw new InvalidOperationException("Roles may only be mentioned if IsEveryone is true"); + } + } - internal Role(DiscordClient client, ulong id, ulong serverId) - : base(client, id) + internal Role(ulong id, Server server) { - _server = new Reference(serverId, x => _client.Servers[x], x => x.AddRole(this), x => x.RemoveRole(this)); + Id = id; + Server = server; Permissions = new ServerPermissions(0); Permissions.Lock(); Color = new Color(0); Color.Lock(); } - internal override bool LoadReferences() - { - return _server.Load(); - } - internal override void UnloadReferences() - { - _server.Unload(); - } internal void Update(APIRole model) { @@ -81,6 +75,6 @@ namespace Discord public override bool Equals(object obj) => obj is Role && (obj as Role).Id == Id; public override int GetHashCode() => unchecked(Id.GetHashCode() + 6653); - public override string ToString() => Name ?? IdConvert.ToString(Id); + public override string ToString() => Name ?? Id.ToIdString(); } } diff --git a/src/Discord.Net/Models/Server.cs b/src/Discord.Net/Models/Server.cs index 3454f5ced..785f9df2b 100644 --- a/src/Discord.Net/Models/Server.cs +++ b/src/Discord.Net/Models/Server.cs @@ -1,283 +1,229 @@ using Discord.API.Client; -using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using APIGuild = Discord.API.Client.Guild; namespace Discord { - public sealed class Server : CachedObject - { - private struct ServerMember - { - public readonly User User; - public readonly ServerPermissions Permissions; - - public ServerMember(User user) - { - User = user; - Permissions = new ServerPermissions(); - Permissions.Lock(); - } - } - - /// Returns the name of this channel. - public string Name { get; private set; } - /// Returns the current logged-in user's data for this server. - public User CurrentUser { get; internal set; } - - /// Returns the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK channel (see AFKChannel). - public int AFKTimeout { get; private set; } - /// Returns the date and time your joined this server. - public DateTime JoinedAt { get; private set; } - /// Returns the region for this server (see Regions). - public string Region { get; private set; } - /// Returns the unique identifier for this user's current avatar. - public string IconId { get; private set; } - /// Returns the URL to this user's current avatar. - public string IconUrl => IconId != null ? $"{DiscordConfig.CDNUrl}/icons/{Id}/{IconId}.jpg" : null; - - /// Returns the user that first created this server. - [JsonIgnore] - public User Owner => _owner.Value; - [JsonProperty] - internal ulong? OwnerId => _owner.Id; - private Reference _owner; - - /// Returns the AFK voice channel for this server (see AFKTimeout). - [JsonIgnore] - public Channel AFKChannel => _afkChannel.Value; - [JsonProperty] - private ulong? AFKChannelId => _afkChannel.Id; - private Reference _afkChannel; - - /// Returns the default channel for this server. - [JsonIgnore] - public Channel DefaultChannel { get; private set; } - - /// Returns a collection of the ids of all users banned on this server. - public IEnumerable BannedUserIds => _bans.Select(x => x.Key); - private ConcurrentDictionary _bans; - - /// Returns a collection of all channels within this server. - [JsonIgnore] - public IEnumerable Channels => _channels.Select(x => x.Value); - /// Returns a collection of all text channels within this server. - [JsonIgnore] - public IEnumerable TextChannels => _channels.Select(x => x.Value).Where(x => x.Type == ChannelType.Text); - /// Returns a collection of all voice channels within this server. - [JsonIgnore] - public IEnumerable VoiceChannels => _channels.Select(x => x.Value).Where(x => x.Type == ChannelType.Voice); - [JsonProperty] - private IEnumerable ChannelIds => Channels.Select(x => x.Id); - private ConcurrentDictionary _channels; - - /// Returns a collection of all users within this server with their server-specific data. - [JsonIgnore] - public IEnumerable Members => _members.Select(x => x.Value.User); - [JsonProperty] - private IEnumerable MemberIds => Members.Select(x => x.Id); - private ConcurrentDictionary _members; - - /// Return the the role representing all users in a server. - [JsonIgnore] - public Role EveryoneRole { get; private set; } - /// Returns a collection of all roles within this server. - [JsonIgnore] - public IEnumerable Roles => _roles.Select(x => x.Value); - [JsonProperty] - private IEnumerable RoleIds => Roles.Select(x => x.Id); - private ConcurrentDictionary _roles; - - internal Server(DiscordClient client, ulong id) - : base(client, id) - { - _owner = new Reference(x => _client.Users[x, Id]); - _afkChannel = new Reference(x => _client.Channels[x]); - - //Global Cache - _channels = new ConcurrentDictionary(); - _roles = new ConcurrentDictionary(); - _members = new ConcurrentDictionary(); - - //Local Cache - _bans = new ConcurrentDictionary(); - EveryoneRole = _client.Roles.GetOrAdd(id, id); - } - internal override bool LoadReferences() - { - _afkChannel.Load(); - _owner.Load(); - return true; + /// Represents a Discord server (also known as a guild). + public sealed class Server + { + private struct Member + { + public readonly User User; + public readonly ServerPermissions Permissions; + public Member(User user) + { + User = user; + Permissions = new ServerPermissions(); + Permissions.Lock(); + } } - internal override void UnloadReferences() - { - //Global Cache - var globalChannels = _client.Channels; - var channels = _channels; - foreach (var channel in channels) - globalChannels.TryRemove(channel.Key); - channels.Clear(); - - var globalUsers = _client.Users; - var members = _members; - foreach (var member in members) - globalUsers.TryRemove(member.Key, Id); - members.Clear(); - - var globalRoles = _client.Roles; - var roles = _roles; - foreach (var role in roles) - globalRoles.TryRemove(role.Key); - roles.Clear(); - //Local Cache - _bans.Clear(); - - _afkChannel.Unload(); + private readonly ConcurrentDictionary _roles; + private readonly ConcurrentDictionary _users; + private readonly ConcurrentDictionary _channels; + private readonly ConcurrentDictionary _bans; + private ulong _ownerId; + private ulong? _afkChannelId; + + /// Gets the client that generated this server object. + internal DiscordClient Client { get; } + /// Gets the unique identifier for this server. + public ulong Id { get; } + /// Gets the default channel for this server. + public Channel DefaultChannel { get; } + /// Gets the the role representing all users in a server. + public Role EveryoneRole { get; } + + /// Gets the name of this server. + public string Name { get; private set; } + + /// Gets the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK channel, if one is set. + public int AFKTimeout { get; private set; } + /// Gets the date and time you joined this server. + public DateTime JoinedAt { get; private set; } + /// Gets the voice region for this server. + public Region Region { get; private set; } + /// Gets the unique identifier for this user's current avatar. + public string IconId { get; private set; } + /// Gets the URL to this user's current avatar. + public string IconUrl => GetIconUrl(Id, IconId); + internal static string GetIconUrl(ulong serverId, string iconId) + => iconId != null ? $"{DiscordConfig.CDNUrl}/icons/{serverId}/{iconId}.jpg" : null; + + /// Gets the user that created this server. + public User Owner => GetUser(_ownerId); + /// Gets the AFK voice channel for this server. + public Channel AFKChannel => _afkChannelId != null ? GetChannel(_afkChannelId.Value) : null; + /// Gets the current user in this server. + public User CurrentUser => GetUser(Client.CurrentUser.Id); + + /// Gets a collection of the ids of all users banned on this server. + public IEnumerable BannedUserIds => _bans.Select(x => x.Key); + /// Gets a collection of all channels within this server. + public IEnumerable Channels => _channels.Select(x => x.Value); + /// Gets a collection of all users within this server with their server-specific data. + public IEnumerable Users => _users.Select(x => x.Value.User); + /// Gets a collection of all roles within this server. + public IEnumerable Roles => _roles.Select(x => x.Value); + + internal Server(DiscordClient client, ulong id) + { + Client = client; + Id = id; + _channels = new ConcurrentDictionary(); + _roles = new ConcurrentDictionary(); + _users = new ConcurrentDictionary(); + _bans = new ConcurrentDictionary(); + DefaultChannel = AddChannel(id); + EveryoneRole = AddRole(id); } - internal void Update(GuildReference model) - { - if (model.Name != null) - Name = model.Name; - } - + internal void Update(GuildReference model) + { + if (model.Name != null) + Name = model.Name; + } internal void Update(Guild model) - { - Update(model as GuildReference); - - if (model.AFKTimeout != null) - AFKTimeout = model.AFKTimeout.Value; - if (model.AFKChannelId != null) - if (model.JoinedAt != null) - JoinedAt = model.JoinedAt.Value; - if (model.OwnerId != null) - _owner.Id = model.OwnerId.Value; - if (model.Region != null) - Region = model.Region; - if (model.Icon != null) - IconId = model.Icon; - - if (model.Roles != null) - { - var roleCache = _client.Roles; - foreach (var x in model.Roles) - { - var role = roleCache.GetOrAdd(x.Id, Id); - role.Update(x); - } + { + Update(model as GuildReference); + + if (model.AFKTimeout != null) + AFKTimeout = model.AFKTimeout.Value; + _afkChannelId = model.AFKChannelId.Value; //Can be null + if (model.JoinedAt != null) + JoinedAt = model.JoinedAt.Value; + if (model.OwnerId != null) + _ownerId = model.OwnerId.Value; + if (model.Region != null) + Region = Client.GetRegion(model.Region); + if (model.Icon != null) + IconId = model.Icon; + + if (model.Roles != null) + { + foreach (var x in model.Roles) + AddRole(x.Id).Update(x); } - - _afkChannel.Id = model.AFKChannelId; //Can be null - } - internal void Update(ExtendedGuild model) - { - Update(model as APIGuild); - - var channels = _client.Channels; - foreach (var subModel in model.Channels) - { - var channel = channels.GetOrAdd(subModel.Id, Id); - channel.Update(subModel); - } - - var usersCache = _client.Users; - foreach (var subModel in model.Members) - { - var user = usersCache.GetOrAdd(subModel.User.Id, Id); - user.Update(subModel); - } - foreach (var subModel in model.VoiceStates) - { - var user = usersCache[subModel.UserId, Id]; - if (user != null) - user.Update(subModel); - } - foreach (var subModel in model.Presences) - { - var user = usersCache[subModel.User.Id, Id]; - if (user != null) - user.Update(subModel); - } - } + } + internal void Update(ExtendedGuild model) + { + Update(model as Guild); + + if (model.Channels != null) + { + foreach (var subModel in model.Channels) + AddChannel(subModel.Id).Update(subModel); + } + if (model.Members != null) + { + foreach (var subModel in model.Members) + AddMember(subModel.User.Id).Update(subModel); + } + if (model.VoiceStates != null) + { + foreach (var subModel in model.VoiceStates) + GetUser(subModel.UserId)?.Update(subModel); + } + if (model.Presences != null) + { + foreach (var subModel in model.Presences) + GetUser(subModel.User.Id)?.Update(subModel); + } + } - internal void AddBan(ulong banId) - { - _bans.TryAdd(banId, true); - } - internal bool RemoveBan(ulong banId) - { - bool ignored; - return _bans.TryRemove(banId, out ignored); - } + //Bans + internal void AddBan(ulong banId) + => _bans.TryAdd(banId, true); + internal bool RemoveBan(ulong banId) + { + bool ignored; + return _bans.TryRemove(banId, out ignored); + } - internal void AddChannel(Channel channel) - { - if (_channels.TryAdd(channel.Id, channel)) - { - if (channel.Id == Id) - DefaultChannel = channel; - } - } - internal void RemoveChannel(Channel channel) - { - _channels.TryRemove(channel.Id, out channel); - } + //Channels + internal Channel AddChannel(ulong id) + => _channels.GetOrAdd(id, x => new Channel(Client, x, this)); + internal Channel RemoveChannel(ulong id) + { + Channel channel; + _channels.TryRemove(id, out channel); + return channel; + } + public Channel GetChannel(ulong id) + { + Channel result; + _channels.TryGetValue(id, out result); + return result; + } - internal void AddMember(User user) - { - if (_members.TryAdd(user.Id, new ServerMember(user))) + //Members + internal User AddMember(ulong id) + { + User newUser = null; + var user = _users.GetOrAdd(id, x => new Member(new User(id, this))); + if (user.User == newUser) { foreach (var channel in Channels) - channel.AddMember(user); + channel.AddUser(newUser); } + return user.User; } - internal void RemoveMember(User user) + internal User RemoveMember(ulong id) { - ServerMember ignored; - if (_members.TryRemove(user.Id, out ignored)) + Member member; + if (_users.TryRemove(id, out member)) { foreach (var channel in Channels) - channel.RemoveMember(user); + channel.RemoveUser(id); } - } - internal void HasMember(User user) => _members.ContainsKey(user.Id); + return member.User; + } + public User GetUser(ulong id) + { + Member result; + _users.TryGetValue(id, out result); + return result.User; + } - internal void AddRole(Role role) - { - if (_roles.TryAdd(role.Id, role)) - { - if (role.Id == Id) - EveryoneRole = role; - } - } - internal void RemoveRole(Role role) - { - _roles.TryRemove(role.Id, out role); - } + //Roles + internal Role AddRole(ulong id) + => _roles.GetOrAdd(id, x => new Role(x, this)); + internal Role RemoveRole(ulong id) + { + Role role; + _roles.TryRemove(id, out role); + return role; + } + public Role GetRole(ulong id) + { + Role result; + _roles.TryGetValue(id, out result); + return result; + } - internal ServerPermissions GetPermissions(User user) + //Permissions + internal ServerPermissions GetPermissions(User user) { - ServerMember member; - if (_members.TryGetValue(user.Id, out member)) + Member member; + if (_users.TryGetValue(user.Id, out member)) return member.Permissions; else return null; } internal void UpdatePermissions(User user) { - ServerMember member; - if (_members.TryGetValue(user.Id, out member)) + Member member; + if (_users.TryGetValue(user.Id, out member)) UpdatePermissions(member.User, member.Permissions); } private void UpdatePermissions(User user, ServerPermissions permissions) { uint newPermissions = 0; - if (user.IsOwner) + if (user.Id == _ownerId) newPermissions = ServerPermissions.All.RawValue; else { @@ -285,7 +231,7 @@ namespace Discord newPermissions |= serverRole.Permissions.RawValue; } - if (BitHelper.GetBit(newPermissions, (int)PermissionsBits.ManageRolesOrPermissions)) + if (newPermissions.HasBit((byte)PermissionsBits.ManageRolesOrPermissions)) newPermissions = ServerPermissions.All.RawValue; if (newPermissions != permissions.RawValue) @@ -298,6 +244,6 @@ namespace Discord public override bool Equals(object obj) => obj is Server && (obj as Server).Id == Id; public override int GetHashCode() => unchecked(Id.GetHashCode() + 5175); - public override string ToString() => Name ?? IdConvert.ToString(Id); + public override string ToString() => Name ?? Id.ToIdString(); } } diff --git a/src/Discord.Net/Models/User.cs b/src/Discord.Net/Models/User.cs index c324ead94..8a253a9cb 100644 --- a/src/Discord.Net/Models/User.cs +++ b/src/Discord.Net/Models/User.cs @@ -1,5 +1,4 @@ using Discord.API.Client; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; @@ -7,8 +6,19 @@ using APIMember = Discord.API.Client.Member; namespace Discord { - public class User : CachedObject + public class User { + [Flags] + private enum VoiceState : byte + { + None = 0x0, + SelfMuted = 0x01, + SelfDeafened = 0x02, + ServerMuted = 0x04, + ServerDeafened = 0x08, + ServerSuppressed = 0x10, + } + internal struct CompositeKey : IEquatable { public ulong ServerId, UserId; @@ -24,92 +34,71 @@ namespace Discord => unchecked(ServerId.GetHashCode() + UserId.GetHashCode() + 23); } - public static string GetAvatarUrl(ulong userId, string avatarId) => avatarId != null ? $"{DiscordConfig.CDNUrl}/avatars/{userId}/{avatarId}.jpg" : null; - - /// Returns a unique identifier combining this user's id with its server's. - internal CompositeKey UniqueId => new CompositeKey(_server.Id ?? 0, Id); - /// 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 ushort 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 => GetAvatarUrl(Id, AvatarId); - /// Returns the datetime that this user joined this server. - public DateTime JoinedAt { get; private set; } + internal static string GetAvatarUrl(ulong userId, string avatarId) => avatarId != null ? $"{DiscordConfig.CDNUrl}/avatars/{userId}/{avatarId}.jpg" : null; + private VoiceState _voiceState; + private DateTime? _lastOnline; + private ulong? _voiceChannelId; + private Dictionary _roles; - 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 IsPrivate => _server.Id == null; - public bool IsOwner => _server.Value.OwnerId == Id; + /// Gets the client that generated this user object. + internal DiscordClient Client { get; } + /// Gets the unique identifier for this user. + public ulong Id { get; } + /// Gets the server this user is a member of. + public Server Server { get; } - public string SessionId { get; private set; } - public string Token { get; private set; } + /// Gets the name of this user. + public string Name { get; private set; } + /// Gets an id uniquely identifying from others with the same name. + public ushort Discriminator { get; private set; } + /// Gets the unique identifier for this user's current avatar. + public string AvatarId { get; private set; } + /// Gets the id for the game this user is currently playing. + public string GameId { get; private set; } + /// Gets the current status for this user. + public UserStatus Status { get; private set; } + /// Gets the datetime that this user joined this server. + public DateTime JoinedAt { 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; } + // /// Gets this user's voice session id. + // public string SessionId { get; private set; } + // /// Gets this user's voice token. + // public string Token { get; private set; } - /// Returns the id for the game this user is currently playing. - public int? GameId { get; private set; } - /// Returns the current status for this user. - public UserStatus 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 string used to mention this user. + public string Mention => $"<@{Id}>"; + /// Returns true if this user has marked themselves as muted. + public bool IsSelfMuted => (_voiceState & VoiceState.SelfMuted) != 0; + /// Returns true if this user has marked themselves as deafened. + public bool IsSelfDeafened => (_voiceState & VoiceState.SelfDeafened) != 0; + /// Returns true if the server is blocking audio from this user. + public bool IsServerMuted => (_voiceState & VoiceState.ServerMuted) != 0; + /// Returns true if the server is blocking audio to this user. + public bool IsServerDeafened => (_voiceState & VoiceState.ServerDeafened) != 0; + /// Returns true if the server is temporarily blocking audio to/from this user. + public bool IsServerSuppressed => (_voiceState & VoiceState.ServerSuppressed) != 0; /// Returns the time this user was last seen online in this server. - public DateTime? LastOnlineAt => Status != UserStatus.Offline ? DateTime.UtcNow : _lastOnline; - private DateTime? _lastOnline; - - //References - [JsonIgnore] - public GlobalUser Global => _globalUser.Value; - private readonly Reference _globalUser; - - [JsonIgnore] - public Server Server => _server.Value; - private readonly Reference _server; - [JsonProperty] - private ulong? ServerId { get { return _server.Id; } set { _server.Id = value; } } - - [JsonIgnore] - public Channel VoiceChannel => _voiceChannel.Value; - private Reference _voiceChannel; - [JsonProperty] - private ulong? VoiceChannelId { get { return _voiceChannel.Id; } set { _voiceChannel.Id = value; } } - - //Collections - [JsonIgnore] - public IEnumerable Roles => _roles.Select(x => x.Value); - private Dictionary _roles; - [JsonProperty] - private IEnumerable RoleIds => _roles.Select(x => x.Key); - - /// Returns a collection of all messages this user has sent on this server that are still in cache. - [JsonIgnore] - public IEnumerable Messages - { - get - { - if (_server.Id != null) - return Server.Channels.SelectMany(x => x.Messages.Where(y => y.User.Id == Id)); - else - return Global.PrivateChannel.Messages.Where(x => x.User.Id == Id); - } - } + public DateTime? LastOnlineAt => Status != UserStatus.Offline ? DateTime.UtcNow : _lastOnline; + /// Gets this user's + public Channel VoiceChannel => _voiceChannelId != null ? Server.GetChannel(_voiceChannelId.Value) : null; + /// Gets the URL to this user's current avatar. + public string AvatarUrl => GetAvatarUrl(Id, AvatarId); + /// Gets all roles that have been assigned to this user, including the everyone role. + public IEnumerable Roles => _roles.Select(x => x.Value); - /// Returns a collection of all channels this user has permissions to join on this server. - [JsonIgnore] - public IEnumerable Channels + /// Returns a collection of all channels this user has permissions to join on this server. + public IEnumerable Channels { get { - if (_server.Id != null) + if (Server != null) { - if (_client.Config.UsePermissionsCache) + if (Client.Config.UsePermissionsCache) { - return Server.Channels - .Where(x => (x.Type == ChannelType.Text && x.GetPermissions(this).ReadMessages) || + return Server.Channels.Where(x => + (x.Type == ChannelType.Text && x.GetPermissions(this).ReadMessages) || (x.Type == ChannelType.Voice && x.GetPermissions(this).Connect)); } else @@ -120,63 +109,37 @@ namespace Discord { x.UpdatePermissions(this, perms); return (x.Type == ChannelType.Text && perms.ReadMessages) || - (x.Type == ChannelType.Voice && perms.Connect); + (x.Type == ChannelType.Voice && perms.Connect); }); } } else { - var privateChannel = Global.PrivateChannel; - if (privateChannel != null) - return new Channel[] { privateChannel }; - else - return new Channel[0]; + if (this == Client.PrivateUser) + return Client.PrivateChannels; + else + { + var privateChannel = Client.GetPrivateChannel(Id); + if (privateChannel != null) + return new Channel[] { privateChannel }; + else + return new Channel[0]; + } } } } - /// Returns the string used to mention this user. - public string Mention => $"<@{Id}>"; - internal User(DiscordClient client, ulong id, ulong? serverId) - : base(client, id) + internal User(ulong id, Server server) { - _globalUser = new Reference(id, - x => _client.GlobalUsers.GetOrAdd(x), - x => x.AddUser(this), - x => x.RemoveUser(this)); - _server = new Reference(serverId, - x => _client.Servers[x], - x => - { - x.AddMember(this); - if (Id == _client.CurrentUser.Id) - x.CurrentUser = this; - }, - x => - { - x.RemoveMember(this); - if (Id == _client.CurrentUser.Id) - x.CurrentUser = null; - }); - _voiceChannel = new Reference(x => _client.Channels[x]); + Server = server; _roles = new Dictionary(); Status = UserStatus.Offline; - if (serverId == null) + if (server == null) UpdateRoles(null); } - internal override bool LoadReferences() - { - return _globalUser.Load() && - (IsPrivate || _server.Load()); - } - internal override void UnloadReferences() - { - _globalUser.Unload(); - _server.Unload(); - } internal void Update(UserReference model) { @@ -195,24 +158,29 @@ namespace Discord if (model.JoinedAt.HasValue) JoinedAt = model.JoinedAt.Value; if (model.Roles != null) - UpdateRoles(model.Roles.Select(x => _client.Roles[x])); + UpdateRoles(model.Roles.Select(x => Server.GetRole(x))); } internal void Update(ExtendedGuild.ExtendedMemberInfo model) { Update(model as APIMember); + + if (model.IsServerMuted == true) + _voiceState |= VoiceState.ServerMuted; + else if (model.IsServerMuted == false) + _voiceState &= ~VoiceState.ServerMuted; - if (model.IsServerDeafened != null) - IsServerDeafened = model.IsServerDeafened.Value; - if (model.IsServerMuted != null) - IsServerMuted = model.IsServerMuted.Value; - } + if (model.IsServerDeafened.Value == true) + _voiceState |= VoiceState.ServerDeafened; + else if (model.IsServerDeafened.Value == false) + _voiceState &= ~VoiceState.ServerDeafened; + } internal void Update(MemberPresence model) { if (model.User != null) Update(model.User as UserReference); - if (model.Roles != null) - UpdateRoles(model.Roles.Select(x => _client.Roles[x])); + if (model.Roles != null) + UpdateRoles(model.Roles.Select(x => Server.GetRole(x))); if (model.Status != null && Status != model.Status) { Status = UserStatus.FromString(model.Status); @@ -223,42 +191,55 @@ namespace Discord GameId = model.GameId; //Allows null } internal void Update(MemberVoiceState model) - { - if (model.IsServerDeafened != null) - IsServerDeafened = model.IsServerDeafened.Value; - if (model.IsServerMuted != null) - IsServerMuted = model.IsServerMuted.Value; - if (model.SessionId != null) + { + if (model.IsSelfMuted.Value == true) + _voiceState |= VoiceState.SelfMuted; + else if (model.IsSelfMuted.Value == false) + _voiceState &= ~VoiceState.SelfMuted; + if (model.IsSelfDeafened.Value == true) + _voiceState |= VoiceState.SelfDeafened; + else if (model.IsSelfDeafened.Value == false) + _voiceState &= ~VoiceState.SelfDeafened; + if (model.IsServerMuted == true) + _voiceState |= VoiceState.ServerMuted; + else if (model.IsServerMuted == false) + _voiceState &= ~VoiceState.ServerMuted; + if (model.IsServerDeafened.Value == true) + _voiceState |= VoiceState.ServerDeafened; + else if (model.IsServerDeafened.Value == false) + _voiceState &= ~VoiceState.ServerDeafened; + if (model.IsServerSuppressed.Value == true) + _voiceState |= VoiceState.ServerSuppressed; + else if (model.IsServerSuppressed.Value == false) + _voiceState &= ~VoiceState.ServerSuppressed; + + /*if (model.SessionId != null) SessionId = model.SessionId; if (model.Token != null) - Token = model.Token; - - if (model.IsSelfDeafened != null) - IsSelfDeafened = model.IsSelfDeafened.Value; - if (model.IsSelfMuted != null) - IsSelfMuted = model.IsSelfMuted.Value; - if (model.IsServerSuppressed != null) - IsServerSuppressed = model.IsServerSuppressed.Value; + Token = model.Token;*/ - _voiceChannel.Id = model.ChannelId; //Allows null + _voiceChannelId = model.ChannelId; //Allows null } private void UpdateRoles(IEnumerable roles) { var newRoles = new Dictionary(); if (roles != null) { - foreach (var r in roles) - newRoles[r.Id] = r; + foreach (var r in roles) + { + if (r != null) + newRoles[r.Id] = r; + } } - if (_server.Id != null) + if (Server != null) { var everyone = Server.EveryoneRole; - newRoles.Add(everyone.Id, everyone); + newRoles[everyone.Id] = everyone; } _roles = newRoles; - if (!IsPrivate) + if (Server != null) Server.UpdatePermissions(this); } @@ -285,6 +266,6 @@ namespace Discord public override bool Equals(object obj) => obj is User && (obj as User).Id == Id; public override int GetHashCode() => unchecked(Id.GetHashCode() + 7230); - public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : IdConvert.ToString(Id); + public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : Id.ToIdString(); } } \ No newline at end of file diff --git a/src/Discord.Net/Net/HttpException.cs b/src/Discord.Net/Net/HttpException.cs index 192136c1a..8bfdbf73b 100644 --- a/src/Discord.Net/Net/HttpException.cs +++ b/src/Discord.Net/Net/HttpException.cs @@ -4,7 +4,7 @@ using System.Runtime.Serialization; namespace Discord.Net { -#if NET45 +#if NET46 [Serializable] #endif public class HttpException : Exception @@ -16,7 +16,7 @@ namespace Discord.Net { StatusCode = statusCode; } -#if NET45 +#if NET46 public override void GetObjectData(SerializationInfo info, StreamingContext context) => base.GetObjectData(info, context); #endif diff --git a/src/Discord.Net/Net/Rest/RestClient.Events.cs b/src/Discord.Net/Net/Rest/RestClient.Events.cs deleted file mode 100644 index 77c0d9969..000000000 --- a/src/Discord.Net/Net/Rest/RestClient.Events.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace Discord.Net.Rest -{ - public sealed partial class RestClient - { - public class RequestEventArgs : EventArgs - { - public string Method { get; } - public string Path { get; } - public string Payload { get; } - public double ElapsedMilliseconds { get; } - public RequestEventArgs(string method, string path, string payload, double milliseconds) - { - Method = method; - Path = path; - Payload = payload; - ElapsedMilliseconds = milliseconds; - } - } - - public event EventHandler OnRequest; - private void RaiseOnRequest(string method, string path, string payload, double milliseconds) - { - if (OnRequest != null) - OnRequest(this, new RequestEventArgs(method, path, payload, milliseconds)); - } - } -} diff --git a/src/Discord.Net/Net/Rest/RestClient.cs b/src/Discord.Net/Net/Rest/RestClient.cs index fb682a4c8..9f97c4444 100644 --- a/src/Discord.Net/Net/Rest/RestClient.cs +++ b/src/Discord.Net/Net/Rest/RestClient.cs @@ -1,4 +1,5 @@ using Discord.API; +using Discord.Logging; using Newtonsoft.Json; using System; using System.Diagnostics; @@ -7,25 +8,53 @@ using System.Threading.Tasks; namespace Discord.Net.Rest { - public sealed partial class RestClient + public class RequestEventArgs : EventArgs + { + public string Method { get; } + public string Path { get; } + public string Payload { get; } + public double ElapsedMilliseconds { get; } + public RequestEventArgs(string method, string path, string payload, double milliseconds) + { + Method = method; + Path = path; + Payload = payload; + ElapsedMilliseconds = milliseconds; + } + } + + public sealed partial class RestClient { private readonly DiscordConfig _config; private readonly IRestEngine _engine; - private CancellationToken _cancelToken; + private string _token; + + internal Logger Logger { get; } + + public CancellationToken CancelToken { get; set; } + + public string Token + { + get { return _token; } + set + { + _token = value; + _engine.SetToken(value); + } + } - public RestClient(DiscordConfig config, Logger logger, string baseUrl) + public RestClient(DiscordConfig config, string baseUrl, Logger logger) { _config = config; + Logger = logger; + #if !DOTNET5_4 - _engine = new RestSharpEngine(config, logger, baseUrl); + _engine = new RestSharpEngine(config, baseUrl); #else - //_engine = new BuiltInRestEngine(config, logger, baseUrl); + //_engine = new BuiltInRestEngine(config, baseUrl); #endif } - public void SetToken(string token) => _engine.SetToken(token); - public void SetCancelToken(CancellationToken token) => _cancelToken = token; - public async Task Send(IRestRequest request) where ResponseT : class { @@ -69,24 +98,26 @@ namespace Discord.Net.Rest requestJson = JsonConvert.SerializeObject(payload); Stopwatch stopwatch = null; - if (_config.LogLevel >= LogSeverity.Verbose) + if (Logger.Level >= LogSeverity.Verbose) stopwatch = Stopwatch.StartNew(); - string responseJson = await _engine.Send(method, path, requestJson, _cancelToken).ConfigureAwait(false); + string responseJson = await _engine.Send(method, path, requestJson, CancelToken).ConfigureAwait(false); - if (_config.LogLevel >= LogSeverity.Verbose) + if (Logger.Level >= LogSeverity.Verbose) { stopwatch.Stop(); - if (payload != null && _config.LogLevel >= LogSeverity.Debug) + double milliseconds = Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond, 2); + + string log = $"{method} {path}: {milliseconds} ms"; + if (payload != null && _config.LogLevel >= LogSeverity.Debug) { if (isPrivate) - RaiseOnRequest(method, path, "[Hidden]", stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); - else - RaiseOnRequest(method, path, requestJson, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); + log += $" [Hidden]"; + else + log += $" {requestJson}"; } - else - RaiseOnRequest(method, path, null, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); - } + Logger.Verbose(log); + } return responseJson; } @@ -100,19 +131,21 @@ namespace Discord.Net.Rest var isPrivate = request.IsPrivate; Stopwatch stopwatch = null; - if (_config.LogLevel >= LogSeverity.Verbose) + if (Logger.Level >= LogSeverity.Verbose) stopwatch = Stopwatch.StartNew(); - string responseJson = await _engine.SendFile(method, path, filename, stream, _cancelToken).ConfigureAwait(false); + string responseJson = await _engine.SendFile(method, path, filename, stream, CancelToken).ConfigureAwait(false); - if (_config.LogLevel >= LogSeverity.Verbose) + if (Logger.Level >= LogSeverity.Verbose) { stopwatch.Stop(); - if (_config.LogLevel >= LogSeverity.Debug && !isPrivate) - RaiseOnRequest(method, path, filename, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); - else - RaiseOnRequest(method, path, null, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); - } + double milliseconds = Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond, 2); + + string log = $"{method} {path}: {milliseconds} ms"; + if (_config.LogLevel >= LogSeverity.Debug && !isPrivate) + log += $" {filename}"; + Logger.Verbose(log); + } return responseJson; } diff --git a/src/Discord.Net/Net/Rest/SharpRestEngine.cs b/src/Discord.Net/Net/Rest/SharpRestEngine.cs index c7b7134eb..f5e225e46 100644 --- a/src/Discord.Net/Net/Rest/SharpRestEngine.cs +++ b/src/Discord.Net/Net/Rest/SharpRestEngine.cs @@ -12,15 +12,13 @@ namespace Discord.Net.Rest { private readonly DiscordConfig _config; private readonly RestSharp.RestClient _client; - private readonly Logger _logger; private readonly object _rateLimitLock; private DateTime _rateLimitTime; - public RestSharpEngine(DiscordConfig config, Logger logger, string baseUrl) + public RestSharpEngine(DiscordConfig config, string baseUrl) { _config = config; - _logger = logger; _rateLimitLock = new object(); _client = new RestSharp.RestClient(baseUrl) { @@ -28,9 +26,6 @@ namespace Discord.Net.Rest ReadWriteTimeout = _config.RestTimeout, UserAgent = config.UserAgent }; - /*if (_config.ProxyUrl != null) - _client.Proxy = new WebProxy(_config.ProxyUrl, true, new string[0], _config.ProxyCredentials); - else*/ _client.Proxy = null; _client.RemoveDefaultParameter("Accept"); _client.AddDefaultHeader("accept", "*/*"); @@ -83,21 +78,18 @@ namespace Discord.Net.Rest int milliseconds; if (retryAfter != null && int.TryParse((string)retryAfter.Value, out milliseconds)) { - if (_logger != null) + /*var now = DateTime.UtcNow; + if (now >= _rateLimitTime) { - var now = DateTime.UtcNow; - if (now >= _rateLimitTime) + lock (_rateLimitLock) { - lock (_rateLimitLock) + if (now >= _rateLimitTime) { - if (now >= _rateLimitTime) - { - _rateLimitTime = now.AddMilliseconds(milliseconds); - _logger.Warning($"Rate limit hit, waiting {Math.Round(milliseconds / 1000.0f, 2)} seconds"); - } + _rateLimitTime = now.AddMilliseconds(milliseconds); + _logger.Warning($"Rate limit hit, waiting {Math.Round(milliseconds / 1000.0f, 2)} seconds"); } } - } + }*/ await Task.Delay(milliseconds, cancelToken).ConfigureAwait(false); continue; } diff --git a/src/Discord.Net/Net/TimeoutException.cs b/src/Discord.Net/Net/TimeoutException.cs index c0385015a..542ee542a 100644 --- a/src/Discord.Net/Net/TimeoutException.cs +++ b/src/Discord.Net/Net/TimeoutException.cs @@ -2,7 +2,7 @@ namespace Discord.Net { -#if NET45 +#if NET46 [Serializable] #endif public sealed class TimeoutException : OperationCanceledException diff --git a/src/Discord.Net/Net/WebSockets/GatewaySocket.cs b/src/Discord.Net/Net/WebSockets/GatewaySocket.cs index 46b80ebe7..51f925cc2 100644 --- a/src/Discord.Net/Net/WebSockets/GatewaySocket.cs +++ b/src/Discord.Net/Net/WebSockets/GatewaySocket.cs @@ -1,5 +1,6 @@ using Discord.API.Client; using Discord.API.Client.GatewaySocket; +using Discord.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -10,14 +11,13 @@ namespace Discord.Net.WebSockets { public partial class GatewaySocket : WebSocket { - public int LastSequence => _lastSeq; - private int _lastSeq; - - public string SessionId => _sessionId; + private int _lastSequence; private string _sessionId; - public GatewaySocket(DiscordClient client, Logger logger) - : base(client, logger) + public string Token { get; private set; } + + public GatewaySocket(DiscordClient client, JsonSerializer serializer, Logger logger) + : base(client, serializer, logger) { Disconnected += async (s, e) => { @@ -26,10 +26,11 @@ namespace Discord.Net.WebSockets }; } - public async Task Connect() + public async Task Connect(string token) { - await BeginConnect().ConfigureAwait(false); - SendIdentify(); + Token = token; + await BeginConnect().ConfigureAwait(false); + SendIdentify(token); } private async Task Redirect() { @@ -46,13 +47,13 @@ namespace Discord.Net.WebSockets { try { - await Connect().ConfigureAwait(false); + await Connect(Token).ConfigureAwait(false); break; } catch (OperationCanceledException) { throw; } catch (Exception ex) { - _logger.Log(LogSeverity.Error, $"Reconnect failed", ex); + Logger.Error("Reconnect failed", ex); //Net is down? We can keep trying to reconnect until the user runs Disconnect() await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); } @@ -60,13 +61,13 @@ namespace Discord.Net.WebSockets } catch (OperationCanceledException) { } } - public Task Disconnect() => TaskManager.Stop(); + public Task Disconnect() => _taskManager.Stop(); protected override async Task Run() { List tasks = new List(); - tasks.AddRange(_engine.GetTasks(_cancelToken)); - tasks.Add(HeartbeatAsync(_cancelToken)); + tasks.AddRange(_engine.GetTasks(CancelToken)); + tasks.Add(HeartbeatAsync(CancelToken)); await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); } @@ -75,7 +76,7 @@ namespace Discord.Net.WebSockets await base.ProcessMessage(json).ConfigureAwait(false); var msg = JsonConvert.DeserializeObject(json); if (msg.Sequence.HasValue) - _lastSeq = msg.Sequence.Value; + _lastSequence = msg.Sequence.Value; var opCode = (OpCodes)msg.Operation; switch (opCode) @@ -105,20 +106,18 @@ namespace Discord.Net.WebSockets if (payload.Url != null) { Host = payload.Url; - if (_logger.Level >= LogSeverity.Info) - _logger.Info("Redirected to " + payload.Url); + Logger.Info("Redirected to " + payload.Url); await Redirect().ConfigureAwait(false); } } break; default: - if (_logger.Level >= LogSeverity.Warning) - _logger.Log(LogSeverity.Warning, $"Unknown Opcode: {opCode}"); + Logger.Warning($"Unknown Opcode: {opCode}"); break; } } - public void SendIdentify() + public void SendIdentify(string token) { var props = new Dictionary { @@ -127,7 +126,7 @@ namespace Discord.Net.WebSockets var msg = new IdentifyCommand() { Version = 3, - Token = _client.Token, + Token = token, Properties = props, LargeThreshold = _client.Config.UseLargeThreshold ? 100 : (int?)null, UseCompression = true @@ -136,7 +135,7 @@ namespace Discord.Net.WebSockets } public void SendResume() - => QueueMessage(new ResumeCommand { SessionId = _sessionId, Sequence = _lastSeq }); + => QueueMessage(new ResumeCommand { SessionId = _sessionId, Sequence = _lastSequence }); public override void SendHeartbeat() => QueueMessage(new HeartbeatCommand()); public void SendUpdateStatus(long? idleSince, int? gameId) diff --git a/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs b/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs index 39305dcdc..99b9dc817 100644 --- a/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs +++ b/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs @@ -13,30 +13,22 @@ namespace Discord.Net.WebSockets internal class WS4NetEngine : IWebSocketEngine { private readonly DiscordConfig _config; - private readonly Logger _logger; private readonly ConcurrentQueue _sendQueue; - private readonly WebSocket _parent; + private readonly TaskManager _taskManager; private WS4NetWebSocket _webSocket; private ManualResetEventSlim _waitUntilConnect; - public event EventHandler BinaryMessage; - public event EventHandler TextMessage; - private void RaiseBinaryMessage(byte[] data) - { - if (BinaryMessage != null) - BinaryMessage(this, new WebSocketBinaryMessageEventArgs(data)); - } - private void RaiseTextMessage(string msg) - { - if (TextMessage != null) - TextMessage(this, new WebSocketTextMessageEventArgs(msg)); - } + public event EventHandler BinaryMessage = delegate { }; + public event EventHandler TextMessage = delegate { }; + private void OnBinaryMessage(byte[] data) + => BinaryMessage(this, new WebSocketBinaryMessageEventArgs(data)); + private void OnTextMessage(string msg) + => TextMessage(this, new WebSocketTextMessageEventArgs(msg)); - internal WS4NetEngine(WebSocket parent, DiscordConfig config, Logger logger) + internal WS4NetEngine(DiscordConfig config, TaskManager taskManager) { - _parent = parent; _config = config; - _logger = logger; + _taskManager = taskManager; _sendQueue = new ConcurrentQueue(); _waitUntilConnect = new ManualResetEventSlim(); } @@ -57,7 +49,7 @@ namespace Discord.Net.WebSockets _waitUntilConnect.Reset(); _webSocket.Open(); _waitUntilConnect.Wait(cancelToken); - _parent.TaskManager.ThrowException(); //In case our connection failed + _taskManager.ThrowException(); //In case our connection failed return TaskHelper.CompletedTask; } @@ -84,27 +76,25 @@ namespace Discord.Net.WebSockets private void OnWebSocketError(object sender, ErrorEventArgs e) { - _parent.TaskManager.SignalError(e.Exception); + _taskManager.SignalError(e.Exception); _waitUntilConnect.Set(); } private void OnWebSocketClosed(object sender, EventArgs e) { - var ex = new Exception($"Connection lost or close message received."); - _parent.TaskManager.SignalError(ex, isUnexpected: true); + Exception ex; + if (e is ClosedEventArgs) + ex = new Exception($"Received close code {(e as ClosedEventArgs).Code}: {(e as ClosedEventArgs).Reason ?? "No reason"}"); + else + ex = new Exception($"Connection lost"); + _taskManager.SignalError(ex, isUnexpected: true); _waitUntilConnect.Set(); } private void OnWebSocketOpened(object sender, EventArgs e) - { - _waitUntilConnect.Set(); - } + => _waitUntilConnect.Set(); private void OnWebSocketText(object sender, MessageReceivedEventArgs e) - { - RaiseTextMessage(e.Message); - } + => OnTextMessage(e.Message); private void OnWebSocketBinary(object sender, DataReceivedEventArgs e) - { - RaiseBinaryMessage(e.Data); - } + => OnBinaryMessage(e.Data); public IEnumerable GetTasks(CancellationToken cancelToken) => new Task[] { SendAsync(cancelToken) }; @@ -128,9 +118,7 @@ namespace Discord.Net.WebSockets } public void QueueMessage(string message) - { - _sendQueue.Enqueue(message); - } + => _sendQueue.Enqueue(message); } } #endif \ No newline at end of file diff --git a/src/Discord.Net/Net/WebSockets/WebSocket.cs b/src/Discord.Net/Net/WebSockets/WebSocket.cs index 49db00e11..494f8a162 100644 --- a/src/Discord.Net/Net/WebSockets/WebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/WebSocket.cs @@ -1,4 +1,5 @@ using Discord.API.Client; +using Discord.Logging; using Newtonsoft.Json; using System; using System.IO; @@ -14,88 +15,59 @@ namespace Discord.Net.WebSockets protected readonly IWebSocketEngine _engine; protected readonly DiscordClient _client; protected readonly ManualResetEventSlim _connectedEvent; - - protected int _heartbeatInterval; - private DateTime _lastHeartbeat; - - public CancellationToken? ParentCancelToken { get; set; } - public CancellationToken CancelToken => _cancelToken; + protected readonly TaskManager _taskManager; + protected readonly JsonSerializer _serializer; protected CancellationTokenSource _cancelTokenSource; - protected CancellationToken _cancelToken; - - public JsonSerializer Serializer => _serializer; - protected JsonSerializer _serializer; + protected int _heartbeatInterval; + private DateTime _lastHeartbeat; + + /// Gets the logger used for this client. + internal Logger Logger { get; } - internal TaskManager TaskManager => _taskManager; - protected readonly TaskManager _taskManager; + public CancellationToken CancelToken { get; private set; } - public Logger Logger => _logger; - protected readonly Logger _logger; + public CancellationToken? ParentCancelToken { get; set; } - public string Host { get { return _host; } set { _host = value; } } - private string _host; + public string Host { get; set; } + /// Gets the current connection state of this client. + public ConnectionState State { get; private set; } - public ConnectionState State => _state; - protected ConnectionState _state; + public event EventHandler Connected = delegate { }; + private void OnConnected() + => Connected(this, EventArgs.Empty); + public event EventHandler Disconnected = delegate { }; + private void OnDisconnected(bool wasUnexpected, Exception error) + => Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); - public event EventHandler Connected; - private void RaiseConnected() - { - if (_logger.Level >= LogSeverity.Info) - _logger.Info( "Connected"); - if (Connected != null) - Connected(this, EventArgs.Empty); - } - public event EventHandler Disconnected; - private void RaiseDisconnected(bool wasUnexpected, Exception error) - { - if (_logger.Level >= LogSeverity.Info) - _logger.Info( "Disconnected"); - if (Disconnected != null) - Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); - } - - public WebSocket(DiscordClient client, Logger logger) + public WebSocket(DiscordClient client, JsonSerializer serializer, Logger logger) { _client = client; - _logger = logger; + Logger = logger; + _serializer = serializer; _lock = new Semaphore(1, 1); _taskManager = new TaskManager(Cleanup); - _cancelToken = new CancellationToken(true); + CancelToken = new CancellationToken(true); _connectedEvent = new ManualResetEventSlim(false); #if !DOTNET5_4 - _engine = new WS4NetEngine(this, client.Config, _logger); + _engine = new WS4NetEngine(client.Config, _taskManager); #else - //_engine = new BuiltInWebSocketEngine(this, client.Config, _logger); + //_engine = new BuiltInWebSocketEngine(this, client.Config); #endif _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)) - zlib.CopyTo(decompressed); - decompressed.Position = 0; + { + 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)) + zlib.CopyTo(decompressed); + decompressed.Position = 0; using (var reader = new StreamReader(decompressed)) - ProcessMessage(reader.ReadToEnd()).Wait(); - } + ProcessMessage(reader.ReadToEnd()).Wait(); + } }; _engine.TextMessage += (s, e) => ProcessMessage(e.Message).Wait(); - - _serializer = new JsonSerializer(); - _serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; -#if TEST_RESPONSES - _serializer.CheckAdditionalContent = true; - _serializer.MissingMemberHandling = MissingMemberHandling.Error; -#else - _serializer.Error += (s, e) => - { - e.ErrorContext.Handled = true; - _logger.Log(LogSeverity.Error, "Serialization Failed", e.ErrorContext.Error); - }; -#endif } protected async Task BeginConnect() @@ -107,13 +79,13 @@ namespace Discord.Net.WebSockets { await _taskManager.Stop().ConfigureAwait(false); _taskManager.ClearException(); - _state = ConnectionState.Connecting; + State = ConnectionState.Connecting; _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken.Value).Token; + CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken.Value).Token; _lastHeartbeat = DateTime.UtcNow; - await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); + await _engine.Connect(Host, CancelToken).ConfigureAwait(false); await Run().ConfigureAwait(false); } finally @@ -131,10 +103,11 @@ namespace Discord.Net.WebSockets { try { - _state = ConnectionState.Connected; + State = ConnectionState.Connected; _connectedEvent.Set(); - RaiseConnected(); + Logger.Info($"Connected"); + OnConnected(); } catch (Exception ex) { @@ -145,29 +118,32 @@ namespace Discord.Net.WebSockets protected abstract Task Run(); protected virtual async Task Cleanup() { - var oldState = _state; - _state = ConnectionState.Disconnecting; + var oldState = State; + State = ConnectionState.Disconnecting; await _engine.Disconnect().ConfigureAwait(false); _cancelTokenSource = null; _connectedEvent.Reset(); if (oldState == ConnectionState.Connected) - RaiseDisconnected(_taskManager.WasUnexpected, _taskManager.Exception); - _state = ConnectionState.Disconnected; + { + Logger.Info("Disconnected"); + OnDisconnected(_taskManager.WasUnexpected, _taskManager.Exception); + } + State = ConnectionState.Disconnected; } protected virtual Task ProcessMessage(string json) { - if (_logger.Level >= LogSeverity.Debug) - _logger.Debug( $"In: {json}"); + if (Logger.Level >= LogSeverity.Debug) + Logger.Debug( $"In: {json}"); return TaskHelper.CompletedTask; } protected void QueueMessage(IWebSocketMessage message) { string json = JsonConvert.SerializeObject(new WebSocketMessage(message)); - if (_logger.Level >= LogSeverity.Debug) - _logger.Debug( $"Out: " + json); + if (Logger.Level >= LogSeverity.Debug) + Logger.Debug( $"Out: {json}"); _engine.QueueMessage(json); } @@ -179,9 +155,9 @@ namespace Discord.Net.WebSockets { while (!cancelToken.IsCancellationRequested) { - if (_state == ConnectionState.Connected) + if (this.State == ConnectionState.Connected) { - SendHeartbeat(); + SendHeartbeat(); await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false); } else @@ -192,5 +168,20 @@ namespace Discord.Net.WebSockets }); } public abstract void SendHeartbeat(); + + public void WaitForConnection(CancellationToken cancelToken) + { + try + { + //Cancel if either DiscordClient.Disconnect is called, data socket errors or timeout is reached + cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, CancelToken).Token; + _connectedEvent.Wait(cancelToken); + } + catch (OperationCanceledException) + { + _taskManager.ThrowException(); //Throws data socket's internal error if any occured + throw; + } + } } } diff --git a/src/Discord.Net/Reference.cs b/src/Discord.Net/Reference.cs new file mode 100644 index 000000000..4a3d6035c --- /dev/null +++ b/src/Discord.Net/Reference.cs @@ -0,0 +1,71 @@ +using System; + +namespace Discord +{ + /*internal class Reference + where T : CachedObject + { + private Action _onCache, _onUncache; + private Func _getItem; + private ulong? _id; + public ulong? Id + { + get { return _id; } + set + { + _id = value; + _value = null; + } + } + + private T _value; + public T Value + { + get + { + } + } + + public T Load() + { + var v = _value; //A little trickery to make this threadsafe + var id = _id; + if (v != null && !_value.IsCached) + { + v = null; + _value = null; + } + if (v == null && id != null) + { + v = _getItem(id.Value); + if (v != null && _onCache != null) + _onCache(v); + _value = v; + } + return v; + return Value != null; //Used for precaching + } + + public void Unload() + { + if (_onUncache != null) + { + var v = _value; + if (v != null && _onUncache != null) + _onUncache(v); + } + } + + public Reference(Func onUpdate, Action onCache = null, Action onUncache = null) + : this(null, onUpdate, onCache, onUncache) + { } + public Reference(ulong? id, Func getItem, Action onCache = null, Action onUncache = null) + { + _id = id; + _getItem = getItem; + _onCache = onCache; + _onUncache = onUncache; + _value = null; + } + }*/ +} diff --git a/src/Discord.Net/Services/IService.cs b/src/Discord.Net/Services/IService.cs deleted file mode 100644 index cf60f70d1..000000000 --- a/src/Discord.Net/Services/IService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord -{ - public interface IService - { - void Install(DiscordClient client); - } -} diff --git a/src/Discord.Net/Services/LogExtensions.cs b/src/Discord.Net/Services/LogExtensions.cs deleted file mode 100644 index 7d524aa3f..000000000 --- a/src/Discord.Net/Services/LogExtensions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord -{ - public static class LogExtensions - { - public static LogService Log(this DiscordClient client, bool required = true) - => client.GetService(required); - } -} diff --git a/src/Discord.Net/Services/LogService.cs b/src/Discord.Net/Services/LogService.cs deleted file mode 100644 index bb5785f1c..000000000 --- a/src/Discord.Net/Services/LogService.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; - -namespace Discord -{ - public class LogService : IService - { - public DiscordClient Client => _client; - private DiscordClient _client; - - public LogSeverity Level => _level; - private LogSeverity _level; - - public event EventHandler LogMessage; - internal void RaiseLogMessage(LogMessageEventArgs e) - { - if (LogMessage != null) - { - try - { - LogMessage(this, e); - } - catch { } //We dont want to log on log errors - } - } - - void IService.Install(DiscordClient client) - { - _client = client; - _level = client.Config.LogLevel; - } - - public Logger CreateLogger(string source) - { - return new Logger(this, source); - } - } - - public class Logger - { - private LogService _service; - - public LogSeverity Level => _level; - private LogSeverity _level; - - public string Source => _source; - private string _source; - - internal Logger(LogService service, string source) - { - _service = service; - _level = service.Level; - _source = source; - } - - public void Log(LogSeverity severity, string message, Exception exception = null) - { - if (severity <= _service.Level) - _service.RaiseLogMessage(new LogMessageEventArgs(severity, _source, message, exception)); - } - public void Error(string message, Exception exception = null) - => Log(LogSeverity.Error, message, exception); - public void Warning(string message, Exception exception = null) - => Log(LogSeverity.Warning, message, exception); - public void Info(string message, Exception exception = null) - => Log(LogSeverity.Info, message, exception); - public void Verbose(string message, Exception exception = null) - => Log(LogSeverity.Verbose, message, exception); - public void Debug(string message, Exception exception = null) - => Log(LogSeverity.Debug, message, exception); - } -} diff --git a/src/Discord.Net/Helpers/TaskManager.cs b/src/Discord.Net/TaskManager.cs similarity index 97% rename from src/Discord.Net/Helpers/TaskManager.cs rename to src/Discord.Net/TaskManager.cs index 4f87ebc28..7aad93533 100644 --- a/src/Discord.Net/Helpers/TaskManager.cs +++ b/src/Discord.Net/TaskManager.cs @@ -146,10 +146,7 @@ namespace Discord public void ThrowException() { lock (_lock) - { - if (_stopReason != null) - _stopReason.Throw(); - } + _stopReason?.Throw(); } public void ClearException() { diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 5fedb7b7a..fa68d3b65 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -29,12 +29,11 @@ }, "dependencies": { - "Newtonsoft.Json": "7.0.1", - "StyleCop.Analyzers": "1.0.0-rc2" + "Newtonsoft.Json": "7.0.1" }, "frameworks": { - "net45": { + "net46": { "dependencies": { "WebSocket4Net": "0.14.1", "RestSharp": "105.2.3"