From 946820d08a2863238994d04ee6000df0630d45c0 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 9 Dec 2015 20:34:46 -0400 Subject: [PATCH] Added SendFile(User), reworked the message queue, adding edit message queuing --- src/Discord.Net/DiscordClient.Messages.cs | 220 ++++++++++++-------- src/Discord.Net/DiscordClient.cs | 42 ++-- src/Discord.Net/Net/Rest/SharpRestEngine.cs | 1 - 3 files changed, 157 insertions(+), 106 deletions(-) diff --git a/src/Discord.Net/DiscordClient.Messages.cs b/src/Discord.Net/DiscordClient.Messages.cs index 61b614bf0..759224db7 100644 --- a/src/Discord.Net/DiscordClient.Messages.cs +++ b/src/Discord.Net/DiscordClient.Messages.cs @@ -2,8 +2,8 @@ using Discord.API; using Discord.Net; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -37,7 +37,20 @@ namespace Discord => base.Import(messages); } - public class MessageEventArgs : EventArgs + internal class MessageQueueItem + { + public readonly Message Message; + public readonly string Text; + public readonly long[] MentionedUsers; + public MessageQueueItem(Message msg, string text, long[] userIds) + { + Message = msg; + Text = text; + MentionedUsers = userIds; + } + } + + public class MessageEventArgs : EventArgs { public Message Message { get; } public User User => Message.User; @@ -83,10 +96,13 @@ namespace Discord } internal Messages Messages => _messages; - private readonly 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(long id) + /// Returns the message with the specified id, or null if none was found. + public Message GetMessage(long id) { if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id)); CheckReady(); @@ -101,93 +117,112 @@ namespace Discord if (text == null) throw new ArgumentNullException(nameof(text)); CheckReady(); - return SendMessage(channel, text, false); - } - /// Sends a text-to-speech message to the provided channel. To include a mention, see the Mention static helper class. - public Task SendTTSMessage(Channel channel, string text) + 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 SendMessage(channel, text, true); + return SendMessageInternal(channel, text, true); } - /// Sends a private message to the provided user. - public async Task SendPrivateMessage(User user, string text) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (text == null) throw new ArgumentNullException(nameof(text)); + /// 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(); - var channel = await CreatePMChannel(user).ConfigureAwait(false); - return await SendMessage(channel, text).ConfigureAwait(false); - } - private async Task SendMessage(Channel channel, string text, bool isTextToSpeech) + 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 model = await _api.SendFile(channel.Id, filename, stream).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; - if (Config.UseMessageQueue) + 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 = _messages.GetOrAdd(nonce, channel.Id, _userId.Value); var currentUser = msg.User; - msg.Update(new MessageInfo - { - Content = text, + msg.Update(new MessageInfo + { + Content = text, Timestamp = DateTime.UtcNow, - Author = new UserReference { Avatar = currentUser.AvatarId, Discriminator = currentUser.Discriminator, Id = _userId.Value, Username = currentUser.Name }, - ChannelId = channel.Id, + Author = new UserReference { Avatar = currentUser.AvatarId, Discriminator = currentUser.Discriminator, Id = _userId.Value, Username = currentUser.Name }, + ChannelId = channel.Id, + Nonce = IdConvert.ToString(nonce), IsTextToSpeech = isTextToSpeech }); msg.State = MessageState.Queued; - - if (text.Length > MaxMessageSize) - throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less."); - - _pendingMessages.Enqueue(msg); - } + + _pendingMessages.Enqueue(new MessageQueueItem(msg, text, mentionedUsers.Select(x => x.Id).ToArray())); + } else { - var mentionedUsers = new List(); - if (!channel.IsPrivate) - text = Mention.CleanUserMentions(this, server, text, mentionedUsers); - - if (text.Length > MaxMessageSize) - throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less."); - - var model = await _api.SendMessage(channel.Id, text, mentionedUsers.Select(x => x.Id), null, isTextToSpeech).ConfigureAwait(false); - msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); - msg.Update(model); - RaiseMessageSent(msg); - } + var model = await _api.SendMessage(channel.Id, text, mentionedUsers.Select(x => x.Id), null, isTextToSpeech).ConfigureAwait(false); + 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 Task SendFile(Channel channel, string filePath) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (filePath == null) throw new ArgumentNullException(nameof(filePath)); - CheckReady(); - - return _api.SendFile(channel.Id, Path.GetFileName(filePath), File.OpenRead(filePath)); - } - /// Sends a file to the provided channel. - public 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(); - - return _api.SendFile(channel.Id, filename, stream); - } - - /// Edits the provided message, changing only non-null attributes. - /// While not required, it is recommended to include a mention reference in the text (see Mention.User). - public Task EditMessage(Message message, string text) + /// 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)); @@ -200,8 +235,11 @@ namespace Discord if (text.Length > MaxMessageSize) throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less."); - - return _api.EditMessage(message.Id, message.Channel.Id, text, mentionedUsers.Select(x => x.Id)); + + if (Config.UseMessageQueue) + _pendingMessages.Enqueue(new MessageQueueItem(message, text, mentionedUsers.Select(x => x.Id).ToArray())); + else + await _api.EditMessage(message.Id, message.Channel.Id, text, mentionedUsers.Select(x => x.Id)).ConfigureAwait(false); } /// Deletes the provided message. @@ -302,46 +340,44 @@ namespace Discord return JsonConvert.SerializeObject(channel.Messages); } - private Task MessageQueueLoop() + private Task MessageQueueAsync() { var cancelToken = _cancelToken; int interval = Config.MessageQueueInterval; return Task.Run(async () => { - Message msg; + MessageQueueItem queuedMessage; + while (!cancelToken.IsCancellationRequested) { - while (_pendingMessages.TryDequeue(out msg)) + while (_pendingMessages.TryDequeue(out queuedMessage)) { - bool hasFailed = false; SendMessageResponse response = null; - try - { - response = await _api.SendMessage(msg.Channel.Id, msg.RawText, msg.MentionedUsers.Select(x => x.Id), IdConvert.ToString(msg.Id), msg.IsTTS).ConfigureAwait(false); - } - catch (WebException) { break; } - catch (HttpException) { hasFailed = true; } - - if (!hasFailed) - { - _messages.Remap(msg.Id, response.Id); - msg.Id = response.Id; - msg.Update(response); - msg.State = MessageState.Normal; + var msg = queuedMessage.Message; + try + { + response = await _api.SendMessage( + msg.Channel.Id, + queuedMessage.Text, + queuedMessage.MentionedUsers, + IdConvert.ToString(msg.Id), //Nonce + msg.IsTTS) + .ConfigureAwait(false); } - else - msg.State = MessageState.Failed; + catch (WebException) { break; } + catch (HttpException) { msg.State = MessageState.Failed; } + RaiseMessageSent(msg); - } - await Task.Delay(interval).ConfigureAwait(false); + } + await Task.Delay(interval).ConfigureAwait(false); } }); } private long GenerateNonce() { - lock (_rand) - return -_rand.Next(1, int.MaxValue - 1); + lock (_nonceRand) + return -_nonceRand.Next(1, int.MaxValue - 1); } } } \ No newline at end of file diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index f8a782ebe..107600f81 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -53,9 +53,6 @@ namespace Discord private readonly ManualResetEvent _disconnectedEvent; private readonly ManualResetEventSlim _connectedEvent; - private readonly Random _rand; - private readonly JsonSerializer _messageImporter; - private readonly ConcurrentQueue _pendingMessages; private readonly Dictionary _singletons; private readonly LogService _log; private readonly object _cacheLock; @@ -114,7 +111,7 @@ namespace Discord _config = config ?? new DiscordConfig(); _config.Lock(); - _rand = new Random(); + _nonceRand = new Random(); _state = (int)DiscordClientState.Disconnected; _status = UserStatus.Online; @@ -155,7 +152,7 @@ namespace Discord _api = new DiscordAPIClient(_config); if (Config.UseMessageQueue) - _pendingMessages = new ConcurrentQueue(); + _pendingMessages = new ConcurrentQueue(); Connected += async (s, e) => { _api.CancelToken = _cancelToken; @@ -393,7 +390,7 @@ namespace Discord List tasks = new List(); tasks.Add(_cancelToken.Wait()); if (_config.UseMessageQueue) - tasks.Add(MessageQueueLoop()); + tasks.Add(MessageQueueAsync()); Task[] tasksArray = tasks.ToArray(); Task firstTask = Task.WhenAny(tasksArray); @@ -432,7 +429,7 @@ namespace Discord { if (Config.UseMessageQueue) { - Message ignored; + MessageQueueItem ignored; while (_pendingMessages.TryDequeue(out ignored)) { } } @@ -668,9 +665,19 @@ namespace Discord Message msg = null; bool isAuthor = data.Author.Id == _userId; + int nonce = 0; + + if (data.Author.Id == _privateUser.Id && Config.UseMessageQueue) + { + if (data.Nonce != null && int.TryParse(data.Nonce, out nonce)) + msg = _messages[nonce]; + } + if (msg == null) + { + msg = _messages.GetOrAdd(data.Id, data.ChannelId, data.Author.Id); + nonce = 0; + } - if (msg == null) - msg = _messages.GetOrAdd(data.Id, data.ChannelId, data.Author.Id); msg.Update(data); if (Config.TrackActivity) { @@ -683,7 +690,15 @@ namespace Discord } } - RaiseMessageReceived(msg); + //Remapped queued message + if (nonce != 0) + { + msg = _messages.Remap(nonce, data.Id); + msg.Id = data.Id; + } + + msg.State = MessageState.Normal; + RaiseMessageReceived(msg); if (Config.AckMessages && !isAuthor) await _api.AckMessage(data.Id, data.ChannelId).ConfigureAwait(false); @@ -694,9 +709,10 @@ namespace Discord var data = e.Payload.ToObject(_webSocket.Serializer); var msg = _messages[data.Id]; if (msg != null) - { - msg.Update(data); - RaiseMessageUpdated(msg); + { + msg.Update(data); + msg.State = MessageState.Normal; + RaiseMessageUpdated(msg); } } break; diff --git a/src/Discord.Net/Net/Rest/SharpRestEngine.cs b/src/Discord.Net/Net/Rest/SharpRestEngine.cs index fcbd4122c..a897febf6 100644 --- a/src/Discord.Net/Net/Rest/SharpRestEngine.cs +++ b/src/Discord.Net/Net/Rest/SharpRestEngine.cs @@ -75,7 +75,6 @@ namespace Discord.Net.Rest { var retryAfter = response.Headers .FirstOrDefault(x => x.Name.Equals("Retry-After", StringComparison.OrdinalIgnoreCase)); - int milliseconds; if (retryAfter != null) { await Task.Delay((int)retryAfter.Value);