diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj
index 58aa7a35d..ecb4ff3f4 100644
--- a/src/Discord.Net.Net45/Discord.Net.csproj
+++ b/src/Discord.Net.Net45/Discord.Net.csproj
@@ -76,6 +76,9 @@
DiscordClient.Events.cs
+
+ DiscordClientConfig.cs
+
DiscordWebSocket.cs
diff --git a/src/Discord.Net/API/Models/Common.cs b/src/Discord.Net/API/Models/Common.cs
index a630f37c4..79aa5a88e 100644
--- a/src/Discord.Net/API/Models/Common.cs
+++ b/src/Discord.Net/API/Models/Common.cs
@@ -211,7 +211,7 @@ namespace Discord.API.Models
}
public sealed class Embed
{
- public sealed class ProviderInfo
+ public sealed class Reference
{
[JsonProperty(PropertyName = "url")]
public string Url;
@@ -238,8 +238,10 @@ namespace Discord.API.Models
public string Title;
[JsonProperty(PropertyName = "description")]
public string Description;
+ [JsonProperty(PropertyName = "author")]
+ public Reference Author;
[JsonProperty(PropertyName = "provider")]
- public ProviderInfo Provider;
+ public Reference Provider;
[JsonProperty(PropertyName = "thumbnail")]
public ThumbnailInfo Thumbnail;
}
diff --git a/src/Discord.Net/DiscordClient.Events.cs b/src/Discord.Net/DiscordClient.Events.cs
index 0c34685e1..2a5745ddb 100644
--- a/src/Discord.Net/DiscordClient.Events.cs
+++ b/src/Discord.Net/DiscordClient.Events.cs
@@ -127,6 +127,12 @@ namespace Discord
if (MessageRead != null)
MessageRead(this, new MessageEventArgs(msg));
}
+ public event EventHandler MessageSent;
+ private void RaiseMessageSent(Message msg)
+ {
+ if (MessageSent != null)
+ MessageSent(this, new MessageEventArgs(msg));
+ }
//Role
public sealed class RoleEventArgs : EventArgs
diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs
index 39cb20bf2..bc686f376 100644
--- a/src/Discord.Net/DiscordClient.cs
+++ b/src/Discord.Net/DiscordClient.cs
@@ -3,6 +3,7 @@ using Discord.API.Models;
using Discord.Helpers;
using Newtonsoft.Json;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -16,8 +17,11 @@ namespace Discord
/// Provides a connection to the DiscordApp service.
public partial class DiscordClient
{
+ private DiscordClientConfig _config;
private DiscordWebSocket _webSocket;
- private ManualResetEventSlim _isStopping;
+ private ManualResetEventSlim _blockEvent;
+ private volatile CancellationTokenSource _disconnectToken;
+ private volatile Task _tasks;
private readonly Regex _userRegex, _channelRegex;
private readonly MatchEvaluator _userRegexEvaluator, _channelRegexEvaluator;
private readonly JsonSerializer _serializer;
@@ -32,43 +36,40 @@ namespace Discord
/// Returns a collection of all users the client can see across all servers.
/// This collection does not guarantee any ordering.
public IEnumerable Users => _users;
- private AsyncCache _users;
+ private readonly AsyncCache _users;
/// Returns a collection of all servers the client is a member of.
/// This collection does not guarantee any ordering.
public IEnumerable Servers => _servers;
- private AsyncCache _servers;
+ private readonly AsyncCache _servers;
/// Returns a collection of all channels the client can see across all servers.
/// This collection does not guarantee any ordering.
public IEnumerable Channels => _channels;
- private AsyncCache _channels;
+ private readonly AsyncCache _channels;
/// Returns a collection of all messages the client has in cache.
/// This collection does not guarantee any ordering.
public IEnumerable Messages => _messages;
- private AsyncCache _messages;
+ private readonly AsyncCache _messages;
+ private readonly ConcurrentQueue _pendingMessages;
/// Returns a collection of all roles the client can see across all servers.
/// This collection does not guarantee any ordering.
public IEnumerable Roles => _roles;
- private AsyncCache _roles;
+ private readonly AsyncCache _roles;
/// Returns true if the user has successfully logged in and the websocket connection has been established.
- public bool IsConnected => _isReady;
- private bool _isReady;
-
- /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting.
- public int ReconnectDelay { get; set; } = 1000;
- /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying.
- public int FailedReconnectDelay { get; set; } = 10000;
-
+ public bool IsConnected => _isConnected;
+ private bool _isConnected;
/// Initializes a new instance of the DiscordClient class.
- public DiscordClient()
+ public DiscordClient(DiscordClientConfig config = null)
{
- _rand = new Random();
- _isStopping = new ManualResetEventSlim(false);
+ _blockEvent = new ManualResetEventSlim(false);
+
+ _config = config ?? new DiscordClientConfig();
+ _rand = new Random();
_serializer = new JsonSerializer();
#if TEST_RESPONSES
@@ -79,7 +80,7 @@ namespace Discord
_userRegex = new Regex(@"<@\d+?>", RegexOptions.Compiled);
_channelRegex = new Regex(@"<#\d+?>", RegexOptions.Compiled);
_userRegexEvaluator = new MatchEvaluator(e =>
- {
+ {
string id = e.Value.Substring(2, e.Value.Length - 3);
var user = _users[id];
if (user != null)
@@ -125,7 +126,7 @@ namespace Discord
server.UpdateMember(membership);
foreach (var membership in extendedModel.Presences)
server.UpdateMember(membership);
- }
+ }
},
server => { }
);
@@ -162,7 +163,7 @@ namespace Discord
channel.PermissionOverwrites = null;
}
},
- channel =>
+ channel =>
{
if (channel.IsPrivate)
{
@@ -206,12 +207,20 @@ namespace Discord
};
if (x.Provider != null)
{
- embed.Provider = new Message.EmbedProvider
+ embed.Provider = new Message.EmbedReference
{
Url = x.Provider.Url,
Name = x.Provider.Name
};
}
+ if (x.Author != null)
+ {
+ embed.Author = new Message.EmbedReference
+ {
+ Url = x.Author.Url,
+ Name = x.Author.Name
+ };
+ }
if (x.Thumbnail != null)
{
embed.Thumbnail = new Message.File
@@ -240,13 +249,14 @@ namespace Discord
},
message => { }
);
+ _pendingMessages = new ConcurrentQueue();
_roles = new AsyncCache(
(key, parentKey) => new Role(key, parentKey, this),
(role, model) =>
{
role.Name = model.Name;
role.Permissions.RawValue = (uint)model.Permissions;
- },
+ },
role => { }
);
_users = new AsyncCache(
@@ -261,22 +271,22 @@ namespace Discord
var extendedModel = model as SelfUserInfo;
user.Email = extendedModel.Email;
user.IsVerified = extendedModel.IsVerified;
- }
+ }
},
user => { }
);
- _webSocket = new DiscordWebSocket();
+ _webSocket = new DiscordWebSocket(_config.WebSocketInterval);
_webSocket.Connected += (s, e) => RaiseConnected();
_webSocket.Disconnected += async (s, e) =>
{
//Reconnect if we didn't cause the disconnect
RaiseDisconnected();
- while (!_isStopping.IsSet)
+ while (!_disconnectToken.IsCancellationRequested)
{
try
{
- await Task.Delay(ReconnectDelay);
+ await Task.Delay(_config.ReconnectDelay);
await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, true);
break;
}
@@ -284,7 +294,7 @@ namespace Discord
{
RaiseOnDebugMessage($"Reconnect Failed: {ex.Message}");
//Net is down? We can keep trying to reconnect until the user runs Disconnect()
- await Task.Delay(FailedReconnectDelay);
+ await Task.Delay(_config.FailedReconnectDelay);
}
}
};
@@ -308,7 +318,7 @@ namespace Discord
_servers.Update(server.Id, server);
foreach (var channel in data.PrivateChannels)
_channels.Update(channel.Id, null, channel);
- }
+ }
break;
//Servers
@@ -446,8 +456,23 @@ namespace Discord
case "MESSAGE_CREATE":
{
var data = e.Event.ToObject(_serializer);
- var msg = _messages.Update(data.Id, data.ChannelId, data);
+ Message msg = null;
+ bool wasLocal = _config.UseMessageQueue && data.Author.Id == UserId && data.Nonce != null;
+ if (wasLocal)
+ {
+ msg = _messages.Remap("nonce" + data.Nonce, data.Id);
+ if (msg != null)
+ {
+ msg.IsQueued = false;
+ msg.Id = data.Id;
+ }
+ }
+ msg = _messages.Update(data.Id, data.ChannelId, data);
msg.User.UpdateActivity(data.Timestamp);
+ if (wasLocal)
+ {
+ try { RaiseMessageSent(msg); } catch { }
+ }
try { RaiseMessageCreated(msg); } catch { }
}
break;
@@ -549,6 +574,50 @@ namespace Discord
_webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message);
}
+ private async Task SendAsync()
+ {
+ var cancelToken = _disconnectToken.Token;
+ try
+ {
+ Message msg;
+ while (!cancelToken.IsCancellationRequested)
+ {
+ while (_pendingMessages.TryDequeue(out msg))
+ {
+ bool hasFailed = false;
+ APIResponses.SendMessage apiMsg = null;
+ try
+ {
+ apiMsg = await DiscordAPI.SendMessage(msg.ChannelId, msg.RawText, msg.MentionIds, msg.Nonce);
+ }
+ catch (WebException) { break; }
+ catch (HttpException) { hasFailed = true; }
+
+ if (!hasFailed)
+ {
+ _messages.Remap("nonce_", apiMsg.Id);
+ _messages.Update(msg.Id, msg.ChannelId, apiMsg);
+ }
+ msg.IsQueued = false;
+ msg.HasFailed = hasFailed;
+ try { RaiseMessageSent(msg); } catch { }
+ }
+ await Task.Delay(_config.MessageQueueInterval);
+ }
+ }
+ catch { }
+ finally { _disconnectToken.Cancel(); }
+ }
+ private async Task EmptyAsync()
+ {
+ var cancelToken = _disconnectToken.Token;
+ try
+ {
+ await Task.Delay(-1, cancelToken);
+ }
+ catch { }
+ }
+
/// Returns the user with the specified id, or null if none was found.
public User GetUser(string id) => _users[id];
@@ -727,77 +796,109 @@ namespace Discord
//Auth
/// Connects to the Discord server with the provided token.
- public async Task Connect(string token)
- {
- _isStopping.Reset();
-
- Http.Token = token;
- await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, true);
-
- _isReady = true;
- }
+ public Task Connect(string token)
+ => ConnectInternal(null, null, token);
/// Connects to the Discord server with the provided email and password.
/// Returns a token for future connections.
- public async Task Connect(string email, string password)
- {
- _isStopping.Reset();
-
- //Open websocket while we wait for login response
- Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, false);
- var response = await DiscordAPI.Login(email, password);
- Http.Token = response.Token;
-
- //Wait for websocket to finish connecting, then send token
- await socketTask;
- _webSocket.Login();
-
- _isReady = true;
- return response.Token;
- }
+ public Task Connect(string email, string password)
+ => ConnectInternal(email, password, null);
/// Connects to the Discord server with the provided token, and will fall back to username and password.
/// Returns a token for future connections.
- public async Task Connect(string email, string password, string token)
+ public Task Connect(string email, string password, string token)
+ => ConnectInternal(email, password, token);
+ /// Connects to the Discord server as an anonymous user with the provided username.
+ /// Returns a token for future connections.
+ public Task ConnectAnonymous(string username)
+ => ConnectInternal(username, null, null);
+ public async Task ConnectInternal(string emailOrUsername, string password, string token)
{
- try
+ bool success = false;
+ await Disconnect();
+ _blockEvent.Reset();
+ _disconnectToken = new CancellationTokenSource();
+
+ //Connect by Token
+ if (token != null)
{
- await Connect(token);
- return token;
+ try
+ {
+ Http.Token = token;
+ await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, true);
+ success = true;
+ }
+ catch (InvalidOperationException) //Bad Token
+ {
+ if (password == null) //If we don't have an alternate login, throw this error
+ throw;
+ }
}
- catch (InvalidOperationException) //Bad Token
+ if (!success && password != null) //Email/Password login
{
- return await Connect(email, password);
+ //Open websocket while we wait for login response
+ Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, false);
+ var response = await DiscordAPI.Login(emailOrUsername, password);
+ await socketTask;
+
+ //Wait for websocket to finish connecting, then send token
+ token = response.Token;
+ Http.Token = token;
+ _webSocket.Login();
+ success = true;
}
- }
- /// Connects to the Discord server as an anonymous user with the provided username.
- /// Returns a token for future connections.
- public async Task ConnectAnonymous(string username)
- {
- _isStopping.Reset();
-
- //Open websocket while we wait for login response
- Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, false);
- var response = await DiscordAPI.LoginAnonymous(username);
- Http.Token = response.Token;
-
- //Wait for websocket to finish connecting, then send token
- await socketTask;
- _webSocket.Login();
-
- _isReady = true;
- return response.Token;
- }
+ if (!success && password == null) //Anonymous login
+ {
+ //Open websocket while we wait for login response
+ Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, false);
+ var response = await DiscordAPI.LoginAnonymous(emailOrUsername);
+ await socketTask;
+
+ //Wait for websocket to finish connecting, then send token
+ token = response.Token;
+ Http.Token = token;
+ _webSocket.Login();
+ success = true;
+ }
+ if (success)
+ {
+ var cancelToken = _disconnectToken.Token;
+ if (_config.UseMessageQueue)
+ _tasks = Task.WhenAll(await Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default));
+ else
+ _tasks = Task.WhenAll(await Task.Factory.StartNew(EmptyAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default));
+ _tasks = _tasks.ContinueWith(async x =>
+ {
+ await _webSocket.DisconnectAsync();
+
+ //Do not clean up until all tasks have ended
+ _webSocket.Dispose();
+ _webSocket = null;
+ _blockEvent.Dispose();
+ _blockEvent = null;
+ _tasks = null;
+
+ //Clear send queue
+ Message ignored;
+ while (_pendingMessages.TryDequeue(out ignored)) { }
+
+ _channels.Clear();
+ _messages.Clear();
+ _roles.Clear();
+ _servers.Clear();
+ _users.Clear();
+ });
+ _isConnected = true;
+ }
+ else
+ token = null;
+ return token;
+ }
/// Disconnects from the Discord server, canceling any pending requests.
public async Task Disconnect()
{
- _isReady = false;
- _isStopping.Set();
- await _webSocket.DisconnectAsync();
-
- _channels.Clear();
- _messages.Clear();
- _roles.Clear();
- _servers.Clear();
- _users.Clear();
+ _blockEvent.Set();
+
+ if (_tasks != null)
+ await _tasks;
}
//Servers
@@ -1004,26 +1105,36 @@ namespace Discord
{
CheckReady();
- if (text.Length <= 2000)
+ int blockCount = (int)Math.Ceiling(text.Length / (double)DiscordAPI.MaxMessageSize);
+ Message[] result = new Message[blockCount];
+ for (int i = 0; i < blockCount; i++)
{
- var nonce = GenerateNonce();
- var msg = await DiscordAPI.SendMessage(channelId, text, mentions, nonce);
- return new Message[] { _messages.Update(msg.Id, channelId, msg) };
- }
- else
- {
- int blockCount = (int)Math.Ceiling(text.Length / (double)DiscordAPI.MaxMessageSize);
- Message[] result = new Message[blockCount];
- for (int i = 0; i < blockCount; i++)
+ int index = i * DiscordAPI.MaxMessageSize;
+ string blockText = text.Substring(index, Math.Min(2000, text.Length - index));
+ var nonce = GenerateNonce();
+ if (_config.UseMessageQueue)
+ {
+ var msg = _messages.Update("nonce_" + nonce, channelId, new API.Models.Message
+ {
+ Content = blockText,
+ Timestamp = DateTime.UtcNow,
+ Author = new UserReference { Avatar = User.AvatarId, Discriminator = User.Discriminator, Id = User.Id, Username = User.Name },
+ ChannelId = channelId
+ });
+ msg.IsQueued = true;
+ msg.Nonce = nonce;
+ _pendingMessages.Enqueue(msg);
+ }
+ else
{
- int index = i * DiscordAPI.MaxMessageSize;
- var nonce = GenerateNonce();
- var msg = await DiscordAPI.SendMessage(channelId, text.Substring(index, Math.Min(2000, text.Length - index)), mentions, nonce);
+ var msg = await DiscordAPI.SendMessage(channelId, blockText, mentions, nonce);
result[i] = _messages.Update(msg.Id, channelId, msg);
- await Task.Delay(1000);
+ result[i].Nonce = nonce;
+ try { RaiseMessageSent(result[i]); } catch { }
}
- return result;
+ await Task.Delay(1000);
}
+ return result;
}
/// Edits a message the provided message.
@@ -1180,7 +1291,9 @@ namespace Discord
//Helpers
private void CheckReady()
{
- if (!_isReady)
+ if (_blockEvent.IsSet)
+ throw new InvalidOperationException("The client is currently disconnecting.");
+ else if (!_isConnected)
throw new InvalidOperationException("The client is not currently connected to Discord");
}
internal string CleanMessageText(string text)
@@ -1198,7 +1311,7 @@ namespace Discord
/// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications.
public void Block()
{
- _isStopping.Wait();
+ _blockEvent.Wait();
}
}
}
diff --git a/src/Discord.Net/DiscordClientConfig.cs b/src/Discord.Net/DiscordClientConfig.cs
new file mode 100644
index 000000000..abd0f2e28
--- /dev/null
+++ b/src/Discord.Net/DiscordClientConfig.cs
@@ -0,0 +1,18 @@
+namespace Discord
+{
+ public class DiscordClientConfig
+ {
+ /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting.
+ public int ReconnectDelay { get; set; } = 1000;
+ /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying.
+ public int FailedReconnectDelay { get; set; } = 10000;
+ /// Gets or sets the time (in milliseconds) to wait when the websocket's message queue is empty before checking again.
+ public int WebSocketInterval { get; set; } = 100;
+ /// Enables or disables the internal message queue. This will allow SendMessage to return immediately and handle messages internally. Messages will set the IsQueued and HasFailed properties to show their progress.
+ public bool UseMessageQueue { get; set; } = false;
+ /// Gets or sets the time (in milliseconds) to wait when the message queue is empty before checking again.
+ public int MessageQueueInterval { get; set; } = 100;
+
+ public DiscordClientConfig() { }
+ }
+}
diff --git a/src/Discord.Net/DiscordWebSocket.cs b/src/Discord.Net/DiscordWebSocket.cs
index 22a26ee59..81b6f868a 100644
--- a/src/Discord.Net/DiscordWebSocket.cs
+++ b/src/Discord.Net/DiscordWebSocket.cs
@@ -21,13 +21,14 @@ namespace Discord
private volatile CancellationTokenSource _disconnectToken;
private volatile Task _tasks;
private ConcurrentQueue _sendQueue;
- private int _heartbeatInterval;
+ private int _heartbeatInterval, _sendInterval;
private DateTime _lastHeartbeat;
private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2;
private bool _isConnected;
- public DiscordWebSocket()
+ public DiscordWebSocket(int interval)
{
+ _sendInterval = interval;
_connectWaitOnLogin = new ManualResetEventSlim(false);
_connectWaitOnLogin2 = new ManualResetEventSlim(false);
@@ -186,7 +187,7 @@ namespace Discord
}
while (_sendQueue.TryDequeue(out bytes))
await SendMessage(bytes, cancelToken);
- await Task.Delay(100);
+ await Task.Delay(_sendInterval);
}
}
catch { }
diff --git a/src/Discord.Net/Helpers/AsyncCache.cs b/src/Discord.Net/Helpers/AsyncCache.cs
index 570c20879..46a4fa3b1 100644
--- a/src/Discord.Net/Helpers/AsyncCache.cs
+++ b/src/Discord.Net/Helpers/AsyncCache.cs
@@ -33,8 +33,20 @@ namespace Discord.Helpers
return value;
}
}
-
- public TValue Update(string key, TModel model)
+
+ public TValue Add(string key, TValue obj)
+ {
+ _dictionary[key] = obj;
+ return obj;
+ }
+ public TValue Remap(string oldKey, string newKey)
+ {
+ var obj = Remove(oldKey);
+ if (obj != null)
+ Add(newKey, obj);
+ return obj;
+ }
+ public TValue Update(string key, TModel model)
{
return Update(key, null, model);
}
diff --git a/src/Discord.Net/Message.cs b/src/Discord.Net/Message.cs
index eef21ee28..f6b7464b4 100644
--- a/src/Discord.Net/Message.cs
+++ b/src/Discord.Net/Message.cs
@@ -26,12 +26,14 @@ namespace Discord
public string Title { get; internal set; }
/// Summary of this embed.
public string Description { get; internal set; }
+ /// Returns information about the author of this embed.
+ public EmbedReference Author { get; internal set; }
/// Returns information about the providing website of this embed.
- public EmbedProvider Provider { get; internal set; }
+ public EmbedReference Provider { get; internal set; }
/// Returns the thumbnail of this embed.
public File Thumbnail { get; internal set; }
}
- public sealed class EmbedProvider
+ public sealed class EmbedReference
{
/// URL of this embed provider.
public string Url { get; internal set; }
@@ -53,8 +55,10 @@ namespace Discord
private readonly DiscordClient _client;
private string _cleanText;
- /// Returns the unique identifier for this message.
- public string Id { get; }
+ /// Returns the global unique identifier for this message.
+ public string Id { get; internal set; }
+ /// Returns the local unique identifier for this message.
+ public string Nonce { get; internal set; }
/// Returns true if the logged-in user was mentioned.
/// This is not set to true if the user was mentioned with @everyone (see IsMentioningEverone).
@@ -63,6 +67,10 @@ namespace Discord
public bool IsMentioningEveryone { get; internal set; }
/// Returns true if the message was sent as text-to-speech by someone with permissions to do so.
public bool IsTTS { get; internal set; }
+ /// Returns true if the message is still in the outgoing message queue.
+ public bool IsQueued { get; internal set; }
+ /// Returns true if the message was rejected by the server.
+ public bool HasFailed { get; internal set; }
/// Returns the raw content of this message as it was received from the server..
public string RawText { get; internal set; }
/// Returns the content of this message with any special references such as mentions converted.
diff --git a/src/Discord.Net/PackedPermissions.cs b/src/Discord.Net/PackedPermissions.cs
index 3b1bd4b50..1d6b7ba02 100644
--- a/src/Discord.Net/PackedPermissions.cs
+++ b/src/Discord.Net/PackedPermissions.cs
@@ -14,8 +14,7 @@
public bool General_BanMembers => ((_rawValue >> 1) & 0x1) == 1;
/// If True, a user may kick users from the server.
public bool General_KickMembers => ((_rawValue >> 2) & 0x1) == 1;
- /// If True, a user adjust roles.
- /// Having this permission effectively gives all the others as a user may add them to themselves.
+ /// If True, a user may adjust roles. This also bypasses all other permissions, granting all the others.
public bool General_ManageRoles => ((_rawValue >> 3) & 0x1) == 1;
/// If True, a user may create, delete and modify channels.
public bool General_ManageChannels => ((_rawValue >> 4) & 0x1) == 1;