diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index ffae61cfd..e39a739f1 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -847,7 +847,6 @@ namespace Discord msg.Update(data); user.UpdateActivity(); - msg.State = MessageState.Normal; if (Config.LogEvents) Logger.Verbose($"Message Received: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); OnMessageReceived(msg); @@ -869,7 +868,6 @@ namespace Discord if (msg != null) { msg.Update(data); - msg.State = MessageState.Normal; if (Config.LogEvents) Logger.Info($"Message Update: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); OnMessageUpdated(msg); diff --git a/src/Discord.Net/MessageQueue.cs b/src/Discord.Net/MessageQueue.cs index bb6872673..395ccb51e 100644 --- a/src/Discord.Net/MessageQueue.cs +++ b/src/Discord.Net/MessageQueue.cs @@ -11,19 +11,42 @@ namespace Discord.Net /// Manages an outgoing message queue for DiscordClient. public sealed class MessageQueue { - private struct MessageQueueItem + private interface IQueuedAction { - public readonly ulong Id, ChannelId; - public readonly string Text; - public readonly bool IsTTS; + Task Do(MessageQueue queue); + } + + private struct SendAction : IQueuedAction + { + private readonly Message _msg; + + public SendAction(Message msg) + { + _msg = msg; + } + Task IQueuedAction.Do(MessageQueue queue) => queue.Send(_msg); + } + private struct EditAction : IQueuedAction + { + private readonly Message _msg; + private readonly string _text; - public MessageQueueItem(ulong id, ulong channelId, string text, bool isTTS) + public EditAction(Message msg, string text) { - Id = id; - ChannelId = channelId; - Text = text; - IsTTS = isTTS; + _msg = msg; + _text = text; } + Task IQueuedAction.Do(MessageQueue queue) => queue.Edit(_msg, _text); + } + private struct DeleteAction : IQueuedAction + { + private readonly Message _msg; + + public DeleteAction(Message msg) + { + _msg = msg; + } + Task IQueuedAction.Do(MessageQueue queue) => queue.Delete(_msg); } private const int WarningStart = 30; @@ -31,10 +54,11 @@ namespace Discord.Net private readonly Random _nonceRand; private readonly DiscordClient _client; private readonly Logger _logger; - private readonly ConcurrentQueue _pending; + private readonly ConcurrentQueue _pendingActions; + private readonly ConcurrentDictionary _pendingSends; private int _nextWarning; - /// Gets the current number of queued message sends/edits. + /// Gets the current number of queued actions. public int Count { get; private set; } internal MessageQueue(DiscordClient client, Logger logger) @@ -43,16 +67,35 @@ namespace Discord.Net _logger = logger; _nonceRand = new Random(); - _pending = new ConcurrentQueue(); + _pendingActions = new ConcurrentQueue(); + _pendingSends = new ConcurrentDictionary(); } - internal void QueueSend(ulong channelId, string text, bool isTTS) + internal Message QueueSend(Channel channel, string text, bool isTTS) + { + Message msg = new Message(0, channel, channel.IsPrivate ? _client.PrivateUser : channel.Server.CurrentUser); + msg.RawText = text; + msg.Text = msg.Resolve(text); + msg.Nonce = GenerateNonce(); + msg.State = MessageState.Queued; + _pendingSends.TryAdd(msg.Nonce, msg); + _pendingActions.Enqueue(new SendAction(msg)); + return msg; + } + internal void QueueEdit(Message msg, string text) { - _pending.Enqueue(new MessageQueueItem(0, channelId, text, isTTS)); + _pendingActions.Enqueue(new EditAction(msg, text)); } - internal void QueueEdit(ulong channelId, ulong messageId, string text) + internal void QueueDelete(Message msg) { - _pending.Enqueue(new MessageQueueItem(channelId, messageId, text, false)); + Message ignored; + if (msg.State == MessageState.Queued && _pendingSends.TryRemove(msg.Nonce, out ignored)) + { + msg.State = MessageState.Aborted; + return; + } + + _pendingActions.Enqueue(new DeleteAction(msg)); } internal Task Run(CancellationToken cancelToken, int interval) @@ -60,63 +103,84 @@ namespace Discord.Net _nextWarning = WarningStart; return Task.Run(async () => { - MessageQueueItem queuedMessage; - while (!cancelToken.IsCancellationRequested) { - await Task.Delay(interval).ConfigureAwait(false); - - Count = _pending.Count; + Count = _pendingActions.Count; if (Count >= _nextWarning) { _nextWarning *= 2; - _logger.Warning($"Queue is backed up, currently at {Count} messages."); + _logger.Warning($"Queue is backed up, currently at {Count} actions."); } else if (Count < WarningStart) //Reset once the problem is solved _nextWarning = WarningStart; - while (_pending.TryDequeue(out queuedMessage)) - { - try - { - if (queuedMessage.Id == 0) - { - var request = new SendMessageRequest(queuedMessage.ChannelId) - { - Content = queuedMessage.Text, - 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 - }; - await _client.ClientAPI.Send(request).ConfigureAwait(false); - } - } - catch (WebException) { break; } - catch (HttpException) { /*msg.State = MessageState.Failed;*/ } - catch (Exception ex) { _logger.Error(ex); } - } + IQueuedAction queuedAction; + while (_pendingActions.TryDequeue(out queuedAction)) + await queuedAction.Do(this); + + await Task.Delay(interval).ConfigureAwait(false); } }); } - /// Clears all queued message sends/edits + internal async Task Send(Message msg) + { + if (_pendingSends.TryRemove(msg.Nonce, out msg)) //Remove it from pending + { + try + { + var request = new SendMessageRequest(msg.Channel.Id) + { + Content = msg.Text, + Nonce = msg.Nonce.ToString(), + IsTTS = msg.IsTTS + }; + var response = await _client.ClientAPI.Send(request).ConfigureAwait(false); + msg.Update(response); + msg.State = MessageState.Normal; + } + catch (Exception ex) { msg.State = MessageState.Failed; _logger.Error("Failed to send message", ex); } + } + } + internal async Task Edit(Message msg, string text) + { + if (msg.State == MessageState.Normal) + { + try + { + var request = new UpdateMessageRequest(msg.Channel.Id, msg.Id) + { + Content = text + }; + await _client.ClientAPI.Send(request).ConfigureAwait(false); + } + catch (Exception ex) { msg.State = MessageState.Failed; _logger.Error("Failed to edit message", ex); } + } + } + internal async Task Delete(Message msg) + { + if (msg.State == MessageState.Normal) + { + try + { + var request = new DeleteMessageRequest(msg.Channel.Id, msg.Id); + await _client.ClientAPI.Send(request).ConfigureAwait(false); + } + catch (Exception ex) { msg.State = MessageState.Failed; _logger.Error("Failed to delete message", ex); } + } + } + + /// Clears all queued message sends/edits/deletes public void Clear() { - MessageQueueItem ignored; - while (_pending.TryDequeue(out ignored)) { } + IQueuedAction ignored; + while (_pendingActions.TryDequeue(out ignored)) { } } - private ulong GenerateNonce() + private int GenerateNonce() { lock (_nonceRand) - return (ulong)_nonceRand.Next(1, int.MaxValue); + return _nonceRand.Next(1, int.MaxValue); } } } diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs index 25b91354a..99e23ca67 100644 --- a/src/Discord.Net/Models/Channel.cs +++ b/src/Discord.Net/Models/Channel.cs @@ -243,6 +243,7 @@ namespace Discord internal Message AddMessage(ulong id, User user, DateTime timestamp) { Message message = new Message(id, this, user); + message.State = MessageState.Normal; var cacheLength = Client.Config.MessageCacheSize; if (cacheLength > 0) { @@ -336,12 +337,11 @@ namespace Discord } private async Task SendMessageInternal(string text, bool isTTS) { - Message msg = null; if (text.Length > DiscordConfig.MaxMessageSize) throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); if (Client.Config.UseMessageQueue) - Client.MessageQueue.QueueSend(Id, text, isTTS); + return Client.MessageQueue.QueueSend(this, text, isTTS); else { var request = new SendMessageRequest(Id) @@ -351,10 +351,10 @@ namespace Discord IsTTS = isTTS }; var model = await Client.ClientAPI.Send(request).ConfigureAwait(false); - msg = AddMessage(model.Id, IsPrivate ? Client.PrivateUser : Server.CurrentUser, model.Timestamp.Value); + var msg = AddMessage(model.Id, IsPrivate ? Client.PrivateUser : Server.CurrentUser, model.Timestamp.Value); msg.Update(model); + return msg; } - return msg; } public async Task SendFile(string filePath) diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index b4802d95f..4aff9688a 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -14,11 +14,16 @@ using APIMessage = Discord.API.Client.Message; namespace Discord { public enum MessageState : byte - { + { + /// Message did not originate from this session, or was successfully sent. Normal = 0, + /// Message is current queued. Queued, + /// Message was deleted before it was sent. + Aborted, + /// Message failed to be sent. Failed - } + } public sealed class Message { @@ -177,7 +182,7 @@ namespace Discord /// 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; } + public string RawText { get; internal set; } /// 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. @@ -189,15 +194,17 @@ namespace Discord /// Returns a collection of all embeded content in this message. public Embed[] Embeds { get; private set; } - /// Returns a collection of all users mentioned in this message. - public IEnumerable MentionedUsers { get; internal set; } + /// Returns a collection of all users mentioned in this message. + public IEnumerable MentionedUsers { get; internal set; } /// Returns a collection of all channels mentioned in this message. public IEnumerable MentionedChannels { get; internal set; } /// Returns a collection of all roles mentioned in this message. public IEnumerable MentionedRoles { get; internal set; } - - /// Returns the server containing the channel this message was sent to. - public Server Server => Channel.Server; + + internal int Nonce { get; set; } + + /// Returns the server containing the channel this message was sent to. + public Server Server => Channel.Server; /// Returns if this message was sent from the logged-in accounts. public bool IsAuthor => User.Id == Client.CurrentUser?.Id; @@ -309,7 +316,7 @@ namespace Discord throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); if (Client.Config.UseMessageQueue) - Client.MessageQueue.QueueEdit(channel.Id, Id, text); + Client.MessageQueue.QueueEdit(this, text); else { var request = new UpdateMessageRequest(Channel.Id, Id) @@ -321,9 +328,14 @@ namespace Discord } public async Task Delete() { - var request = new DeleteMessageRequest(Channel.Id, Id); - try { await Client.ClientAPI.Send(request).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + if (Client.Config.UseMessageQueue) + Client.MessageQueue.QueueDelete(this); + else + { + var request = new DeleteMessageRequest(Channel.Id, Id); + try { await Client.ClientAPI.Send(request).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } } /// Returns true if the logged-in user was mentioned.