@@ -47,6 +47,9 @@ | |||
<Compile Include="..\Discord.Net.Audio\AudioExtensions.cs"> | |||
<Link>AudioExtensions.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\AudioMode.cs"> | |||
<Link>AudioMode.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\AudioService.cs"> | |||
<Link>AudioService.cs</Link> | |||
</Compile> | |||
@@ -61,8 +61,8 @@ namespace Discord.Audio | |||
public Stream OutputStream { get; } | |||
public CancellationToken CancelToken { get; private set; } | |||
public string SessionId { get; private set; } | |||
public string SessionId => GatewaySocket.SessionId; | |||
public ConnectionState State => VoiceSocket.State; | |||
public Server Server => VoiceSocket.Server; | |||
public Channel Channel => VoiceSocket.Channel; | |||
@@ -71,7 +71,7 @@ namespace Discord.Audio | |||
{ | |||
Id = id; | |||
_config = client.Config; | |||
Service = client.Audio(); | |||
Service = client.Services.Get<AudioService>(); | |||
Config = Service.Config; | |||
Serializer = client.Serializer; | |||
_gatewayState = (int)ConnectionState.Disconnected; | |||
@@ -1,4 +1,5 @@ | |||
using System; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
@@ -9,14 +10,17 @@ namespace Discord.Audio | |||
client.Services.Add(new AudioService(config)); | |||
return client; | |||
} | |||
public static DiscordClient UsingAudio(this DiscordClient client, Action<AudioServiceConfig> configFunc = null) | |||
public static DiscordClient UsingAudio(this DiscordClient client, Action<AudioServiceConfigBuilder> configFunc = null) | |||
{ | |||
var config = new AudioServiceConfig(); | |||
configFunc(config); | |||
client.Services.Add(new AudioService(config)); | |||
var builder = new AudioServiceConfigBuilder(); | |||
configFunc(builder); | |||
client.Services.Add(new AudioService(builder)); | |||
return client; | |||
} | |||
public static AudioService Audio(this DiscordClient client, bool required = true) | |||
=> client.Services.Get<AudioService>(required); | |||
public static Task<IAudioClient> JoinAudio(this Channel channel) => channel.Client.Services.Get<AudioService>().Join(channel); | |||
public static Task LeaveAudio(this Channel channel) => channel.Client.Services.Get<AudioService>().Leave(channel); | |||
public static Task LeaveAudio(this Server server) => server.Client.Services.Get<AudioService>().Leave(server); | |||
public static IAudioClient GetAudioClient(Server server) => server.Client.Services.Get<AudioService>().GetClient(server); | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
namespace Discord.Audio | |||
{ | |||
public enum AudioMode : byte | |||
{ | |||
Outgoing = 1, | |||
Incoming = 2, | |||
Both = Outgoing | Incoming | |||
} | |||
} |
@@ -29,16 +29,23 @@ namespace Discord.Audio | |||
private void OnUserIsSpeakingUpdated(User user, bool isSpeaking) | |||
=> UserIsSpeakingUpdated(this, new UserIsSpeakingEventArgs(user, isSpeaking)); | |||
public AudioService(AudioServiceConfig config) | |||
public AudioService() | |||
: this(new AudioServiceConfigBuilder()) | |||
{ | |||
} | |||
public AudioService(AudioServiceConfigBuilder builder) | |||
: this(builder.Build()) | |||
{ | |||
} | |||
public AudioService(AudioServiceConfig config) | |||
{ | |||
Config = config; | |||
Config = config; | |||
_asyncLock = new AsyncLock(); | |||
} | |||
void IService.Install(DiscordClient client) | |||
{ | |||
Client = client; | |||
Config.Lock(); | |||
if (Config.EnableMultiserver) | |||
_voiceClients = new ConcurrentDictionary<ulong, AudioClient>(); | |||
@@ -1,62 +1,51 @@ | |||
| |||
using System; | |||
namespace Discord.Audio | |||
namespace Discord.Audio | |||
{ | |||
public enum AudioMode : byte | |||
public class AudioServiceConfigBuilder | |||
{ | |||
Outgoing = 1, | |||
Incoming = 2, | |||
Both = Outgoing | Incoming | |||
} | |||
/// <summary> Enables the voice websocket and UDP client and specifies how it will be used. </summary> | |||
public AudioMode Mode { get; set; } = AudioMode.Outgoing; | |||
public class AudioServiceConfig | |||
{ | |||
/// <summary> Enables the voice websocket and UDP client. This option requires the libsodium .dll or .so be in the local or system folder. </summary> | |||
public bool EnableEncryption { get; set; } = true; | |||
/// <summary> | |||
/// Enables the client to be simultaneously connected to multiple channels at once (Discord still limits you to one channel per server). | |||
/// This option uses a lot of CPU power and network bandwidth, as a new gateway connection needs to be spun up per server. Use sparingly. | |||
/// </summary> | |||
public bool EnableMultiserver { get; set; } = false; | |||
/// <summary> Gets or sets the buffer length (in milliseconds) for outgoing voice packets. </summary> | |||
public int BufferLength { get; set; } = 1000; | |||
/// <summary> Gets or sets the bitrate used (in kbit/s, between 1 and MaxBitrate inclusively) for outgoing voice packets. A null value will use default Opus settings. </summary> | |||
public int? Bitrate { get; set; } = null; | |||
/// <summary> Gets or sets the number of channels (1 or 2) used in both input provided to IAudioClient and output send to Discord. Defaults to 2 (stereo). </summary> | |||
public int Channels { get; set; } = 2; | |||
public AudioServiceConfig Build() => new AudioServiceConfig(this); | |||
} | |||
public class AudioServiceConfig | |||
{ | |||
public const int MaxBitrate = 128; | |||
/// <summary> Max time in milliseconds to wait for DiscordAudioClient to connect and initialize. </summary> | |||
public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } } | |||
private int _connectionTimeout = 30000; | |||
public AudioMode Mode { get; } | |||
//Experimental Features | |||
/// <summary> (Experimental) Enables the voice websocket and UDP client and specifies how it will be used. </summary> | |||
public AudioMode Mode { get { return _mode; } set { SetValue(ref _mode, value); } } | |||
private AudioMode _mode = AudioMode.Outgoing; | |||
public bool EnableEncryption { get; } | |||
public bool EnableMultiserver { get; } | |||
/// <summary> (Experimental) Enables the voice websocket and UDP client. This option requires the libsodium .dll or .so be in the local or system folder. </summary> | |||
public bool EnableEncryption { get { return _enableEncryption; } set { SetValue(ref _enableEncryption, value); } } | |||
private bool _enableEncryption = true; | |||
public int BufferLength { get; } | |||
public int? Bitrate { get; } | |||
public int Channels { get; } | |||
/// <summary> (Experimental) Enables the client to be simultaneously connected to multiple channels at once (Discord still limits you to one channel per server). </summary> | |||
public bool EnableMultiserver { get { return _enableMultiserver; } set { SetValue(ref _enableMultiserver, value); } } | |||
private bool _enableMultiserver = false; | |||
internal AudioServiceConfig(AudioServiceConfigBuilder builder) | |||
{ | |||
Mode = builder.Mode; | |||
/// <summary> Gets or sets the buffer length (in milliseconds) for outgoing voice packets. </summary> | |||
public int BufferLength { get { return _bufferLength; } set { SetValue(ref _bufferLength, value); } } | |||
private int _bufferLength = 1000; | |||
EnableEncryption = builder.EnableEncryption; | |||
EnableMultiserver = builder.EnableMultiserver; | |||
/// <summary> Gets or sets the bitrate used (in kbit/s, between 1 and MaxBitrate inclusively) for outgoing voice packets. A null value will use default Opus settings. </summary> | |||
public int? Bitrate { get { return _bitrate; } set { SetValue(ref _bitrate, value); } } | |||
private int? _bitrate = null; | |||
/// <summary> Gets or sets the number of channels (1 or 2) used in both input provided to IAudioClient and output send to Discord. Defaults to 2 (stereo). </summary> | |||
public int Channels { get { return _channels; } set { SetValue(ref _channels, value); } } | |||
private int _channels = 2; | |||
//Lock | |||
protected bool _isLocked; | |||
internal void Lock() { _isLocked = true; } | |||
protected void SetValue<T>(ref T storage, T value) | |||
{ | |||
if (_isLocked) | |||
throw new InvalidOperationException("Unable to modify a service's configuration after it has been created."); | |||
storage = value; | |||
} | |||
public AudioServiceConfig Clone() | |||
{ | |||
var config = MemberwiseClone() as AudioServiceConfig; | |||
config._isLocked = false; | |||
return config; | |||
} | |||
} | |||
BufferLength = builder.BufferLength; | |||
Bitrate = builder.Bitrate; | |||
Channels = builder.Channels; | |||
} | |||
} | |||
} |
@@ -9,14 +9,12 @@ namespace Discord.Commands | |||
client.Services.Add(new CommandService(config)); | |||
return client; | |||
} | |||
public static DiscordClient UsingCommands(this DiscordClient client, Action<CommandServiceConfig> configFunc = null) | |||
public static DiscordClient UsingCommands(this DiscordClient client, Action<CommandServiceConfigBuilder> configFunc = null) | |||
{ | |||
var config = new CommandServiceConfig(); | |||
configFunc(config); | |||
client.Services.Add(new CommandService(config)); | |||
var builder = new CommandServiceConfigBuilder(); | |||
configFunc(builder); | |||
client.Services.Add(new CommandService(builder)); | |||
return client; | |||
} | |||
public static CommandService Commands(this DiscordClient client, bool required = true) | |||
=> client.Services.Get<CommandService>(required); | |||
} | |||
} |
@@ -29,6 +29,18 @@ namespace Discord.Commands | |||
private void OnCommandError(CommandErrorType errorType, CommandEventArgs args, Exception ex = null) | |||
=> CommandErrored(this, new CommandErrorEventArgs(errorType, args, ex)); | |||
public CommandService() | |||
: this(new CommandServiceConfigBuilder()) | |||
{ | |||
} | |||
public CommandService(CommandServiceConfigBuilder builder) | |||
: this(builder.Build()) | |||
{ | |||
if (builder.ExecuteHandler != null) | |||
CommandExecuted += builder.ExecuteHandler; | |||
if (builder.ErrorHandler != null) | |||
CommandErrored += builder.ErrorHandler; | |||
} | |||
public CommandService(CommandServiceConfig config) | |||
{ | |||
Config = config; | |||
@@ -42,9 +54,8 @@ namespace Discord.Commands | |||
void IService.Install(DiscordClient client) | |||
{ | |||
Client = client; | |||
Config.Lock(); | |||
if (Config.HelpMode != HelpMode.Disable) | |||
if (Config.HelpMode != HelpMode.Disabled) | |||
{ | |||
CreateCommand("help") | |||
.Parameter("command", ParameterType.Multiple) | |||
@@ -2,36 +2,45 @@ | |||
namespace Discord.Commands | |||
{ | |||
public class CommandServiceConfig | |||
public class CommandServiceConfigBuilder | |||
{ | |||
/// <summary> Gets or sets the prefix character used to trigger commands, if ActivationMode has the Char flag set. </summary> | |||
public char? PrefixChar { get { return _prefixChar; } set { SetValue(ref _prefixChar, value); } } | |||
private char? _prefixChar = null; | |||
public char? PrefixChar { get; set; } = null; | |||
/// <summary> Gets or sets whether a message beginning with a mention to the logged-in user should be treated as a command. </summary> | |||
public bool AllowMentionPrefix { get { return _allowMentionPrefix; } set { SetValue(ref _allowMentionPrefix, value); } } | |||
private bool _allowMentionPrefix = true; | |||
public bool AllowMentionPrefix { get; set; } = true; | |||
/// <summary> | |||
/// Gets or sets a custom function used to detect messages that should be treated as commands. | |||
/// This function should a positive one indicating the index of where the in the message's RawText the command begins, | |||
/// and a negative value if the message should be ignored. | |||
/// </summary> | |||
public Func<Message, int> CustomPrefixHandler { get { return _customPrefixHandler; } set { SetValue(ref _customPrefixHandler, value); } } | |||
private Func<Message, int> _customPrefixHandler = null; | |||
public Func<Message, int> CustomPrefixHandler { get; set; } = null; | |||
/// <summary> Gets or sets whether a help function should be automatically generated. </summary> | |||
public HelpMode HelpMode { get; set; } = HelpMode.Disabled; | |||
/// <summary> Gets or sets a handler that is called on any successful command execution. </summary> | |||
public EventHandler<CommandEventArgs> ExecuteHandler { get; set; } | |||
/// <summary> Gets or sets a handler that is called on any error during command parsing or execution. </summary> | |||
public EventHandler<CommandErrorEventArgs> ErrorHandler { get; set; } | |||
public CommandServiceConfig Build() => new CommandServiceConfig(this); | |||
} | |||
public class CommandServiceConfig | |||
{ | |||
public char? PrefixChar { get; } | |||
public bool AllowMentionPrefix { get; } | |||
public Func<Message, int> CustomPrefixHandler { get; } | |||
/// <summary> Gets or sets whether a help function should be automatically generated. </summary> | |||
public HelpMode HelpMode { get { return _helpMode; } set { SetValue(ref _helpMode, value); } } | |||
private HelpMode _helpMode = HelpMode.Disable; | |||
public HelpMode HelpMode { get; set; } = HelpMode.Disabled; | |||
//Lock | |||
protected bool _isLocked; | |||
internal void Lock() { _isLocked = true; } | |||
protected void SetValue<T>(ref T storage, T value) | |||
{ | |||
if (_isLocked) | |||
throw new InvalidOperationException("Unable to modify a service's configuration after it has been created."); | |||
storage = value; | |||
} | |||
internal CommandServiceConfig(CommandServiceConfigBuilder builder) | |||
{ | |||
PrefixChar = builder.PrefixChar; | |||
AllowMentionPrefix = builder.AllowMentionPrefix; | |||
CustomPrefixHandler = builder.CustomPrefixHandler; | |||
HelpMode = builder.HelpMode; | |||
} | |||
} | |||
} |
@@ -3,7 +3,7 @@ | |||
public enum HelpMode | |||
{ | |||
/// <summary> Disable the automatic help command. </summary> | |||
Disable, | |||
Disabled, | |||
/// <summary> Use the automatic help command and respond in the channel the command is used. </summary> | |||
Public, | |||
/// <summary> Use the automatic help command and respond in a private message. </summary> | |||
@@ -113,7 +113,7 @@ namespace Discord.Modules | |||
public void CreateCommands(string prefix, Action<CommandGroupBuilder> config) | |||
{ | |||
var commandService = Client.Commands(true); | |||
var commandService = Client.Services.Get<CommandService>(); | |||
commandService.CreateGroup(prefix, x => | |||
{ | |||
x.Category(Name); | |||
@@ -409,6 +409,9 @@ | |||
<Compile Include="..\Discord.Net\Enums\ImageType.cs"> | |||
<Link>Enums\ImageType.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Enums\LogSeverity.cs"> | |||
<Link>Enums\LogSeverity.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Enums\PermissionTarget.cs"> | |||
<Link>Enums\PermissionTarget.cs</Link> | |||
</Compile> | |||
@@ -5,15 +5,11 @@ namespace Discord | |||
{ | |||
internal static class TaskHelper | |||
{ | |||
public static Task CompletedTask { get; } | |||
static TaskHelper() | |||
{ | |||
#if DOTNET54 | |||
CompletedTask = Task.CompletedTask; | |||
public static Task CompletedTask => Task.CompletedTask; | |||
#else | |||
CompletedTask = Task.Delay(0); | |||
public static Task CompletedTask => Task.Delay(0); | |||
#endif | |||
} | |||
public static Func<Task> ToAsync(Action action) | |||
{ | |||
@@ -1,24 +0,0 @@ | |||
using System; | |||
namespace Discord | |||
{ | |||
public abstract class Config<T> | |||
where T : Config<T> | |||
{ | |||
protected bool _isLocked; | |||
protected internal void Lock() { _isLocked = true; } | |||
protected void SetValue<U>(ref U storage, U value) | |||
{ | |||
if (_isLocked) | |||
throw new InvalidOperationException("Unable to modify a discord client's configuration after it has been created."); | |||
storage = value; | |||
} | |||
public T Clone() | |||
{ | |||
var config = MemberwiseClone() as T; | |||
config._isLocked = false; | |||
return config; | |||
} | |||
} | |||
} |
@@ -76,28 +76,33 @@ namespace Discord | |||
public IEnumerable<Region> Regions => _regions.Select(x => x.Value); | |||
/// <summary> Initializes a new instance of the DiscordClient class. </summary> | |||
public DiscordClient(Action<DiscordConfig> configFunc) | |||
public DiscordClient(Action<DiscordConfigBuilder> configFunc) | |||
: this(ProcessConfig(configFunc)) | |||
{ | |||
} | |||
private static DiscordConfig ProcessConfig(Action<DiscordConfig> func) | |||
private static DiscordConfigBuilder ProcessConfig(Action<DiscordConfigBuilder> func) | |||
{ | |||
var config = new DiscordConfig(); | |||
var config = new DiscordConfigBuilder(); | |||
func(config); | |||
return config; | |||
} | |||
/// <summary> Initializes a new instance of the DiscordClient class. </summary> | |||
public DiscordClient() | |||
: this((DiscordConfig)null) | |||
: this(new DiscordConfigBuilder()) | |||
{ | |||
} | |||
/// <summary> Initializes a new instance of the DiscordClient class. </summary> | |||
public DiscordClient(DiscordConfigBuilder builder) | |||
: this(builder.Build()) | |||
{ | |||
if (builder.LogHandler != null) | |||
Log.Message += builder.LogHandler; | |||
} | |||
/// <summary> Initializes a new instance of the DiscordClient class. </summary> | |||
public DiscordClient(DiscordConfig config) | |||
{ | |||
Config = config ?? new DiscordConfig(); | |||
Config.Lock(); | |||
Config = config; | |||
State = (int)ConnectionState.Disconnected; | |||
Status = UserStatus.Online; | |||
@@ -145,9 +150,8 @@ namespace Discord | |||
}; | |||
//GatewaySocket.Disconnected += (s, e) => OnDisconnected(e.WasUnexpected, e.Exception); | |||
GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); | |||
if (Config.UseMessageQueue) | |||
MessageQueue = new MessageQueue(ClientAPI, Log.CreateLogger("MessageQueue")); | |||
MessageQueue = new MessageQueue(ClientAPI, Log.CreateLogger("MessageQueue")); | |||
//Extensibility | |||
Services = new ServiceManager(this); | |||
@@ -194,10 +198,11 @@ namespace Discord | |||
await Login(email, password, token).ConfigureAwait(false); | |||
await GatewaySocket.Connect(ClientAPI, CancelToken).ConfigureAwait(false); | |||
List<Task> tasks = new List<Task>(); | |||
tasks.Add(CancelToken.Wait()); | |||
if (Config.UseMessageQueue) | |||
tasks.Add(MessageQueue.Run(CancelToken, Config.MessageQueueInterval)); | |||
Task[] tasks = new[] | |||
{ | |||
CancelToken.Wait(), | |||
MessageQueue.Run(CancelToken) | |||
}; | |||
await _taskManager.Start(tasks, cancelSource).ConfigureAwait(false); | |||
GatewaySocket.WaitForConnection(CancelToken); | |||
@@ -222,7 +227,7 @@ namespace Discord | |||
byte[] cacheKey = null; | |||
//Get Token | |||
if (email != null && Config.CacheToken) | |||
if (email != null && Config.CacheDir != null) | |||
{ | |||
tokenPath = GetTokenCachePath(email); | |||
if (token == null && password != null) | |||
@@ -240,7 +245,7 @@ namespace Discord | |||
var request = new LoginRequest() { Email = email, Password = password }; | |||
var response = await ClientAPI.Send(request).ConfigureAwait(false); | |||
token = response.Token; | |||
if (Config.CacheToken && token != oldToken && tokenPath != null) | |||
if (Config.CacheDir != null && token != oldToken && tokenPath != null) | |||
SaveToken(tokenPath, cacheKey, token); | |||
ClientAPI.Token = token; | |||
@@ -270,9 +275,8 @@ namespace Discord | |||
try { await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); } | |||
catch (OperationCanceledException) { } | |||
} | |||
if (Config.UseMessageQueue) | |||
MessageQueue.Clear(); | |||
MessageQueue.Clear(); | |||
await GatewaySocket.Disconnect().ConfigureAwait(false); | |||
ClientAPI.Token = null; | |||
@@ -1053,7 +1057,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(Config.CacheDir, filenameBuilder.ToString()); | |||
} | |||
} | |||
private string LoadToken(string path, byte[] key) | |||
@@ -1,127 +1,133 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
using System; | |||
using System.IO; | |||
using System.Reflection; | |||
using System.Text; | |||
namespace Discord | |||
{ | |||
public enum LogSeverity : byte | |||
{ | |||
Error = 1, | |||
Warning = 2, | |||
Info = 3, | |||
Verbose = 4, | |||
Debug = 5 | |||
} | |||
public class DiscordConfig : Config<DiscordConfig> | |||
{ | |||
public class DiscordConfigBuilder | |||
{ | |||
public const int MaxMessageSize = 2000; | |||
public const string LibName = "Discord.Net"; | |||
public static string LibVersion => typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3); | |||
public const string LibUrl = "https://github.com/RogueException/Discord.Net"; | |||
public const string ClientAPIUrl = "https://discordapp.com/api/"; | |||
public const string StatusAPIUrl = "https://srhpyqt94yxb.statuspage.io/api/v2/"; //"https://status.discordapp.com/api/v2/"; | |||
//public const string CDNUrl = "https://cdn.discordapp.com/"; | |||
public const string InviteUrl = "https://discord.gg/"; | |||
//Global | |||
/// <summary> Gets or sets name of your application, used both for the token cache directory and user agent. </summary> | |||
public string AppName { get { return _appName; } set { SetValue(ref _appName, value); UpdateUserAgent(); } } | |||
private string _appName = null; | |||
public string AppName { get; set; } = null; | |||
/// <summary> Gets or sets url to your application, used in the user agent. </summary> | |||
public string AppUrl { get { return _appUrl; } set { SetValue(ref _appUrl, value); UpdateUserAgent(); } } | |||
private string _appUrl = null; | |||
public string AppUrl { get; set; } = null; | |||
/// <summary> Gets or sets the version of your application, used in the user agent. </summary> | |||
public string AppVersion { get { return _appVersion; } set { SetValue(ref _appVersion, value); UpdateUserAgent(); } } | |||
private string _appVersion = null; | |||
public string AppVersion { get; set; } = null; | |||
/// <summary> Gets or sets the minimum log level severity that will be sent to the LogMessage event. Warning: setting this to debug will really hurt performance but should help investigate any internal issues. </summary> | |||
public LogSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } } | |||
private LogSeverity _logLevel = LogSeverity.Info; | |||
public LogSeverity LogLevel { get; set; } = LogSeverity.Info; | |||
/// <summary> Enables or disables the default event logger. </summary> | |||
public bool LogEvents { get { return _logEvents; } set { SetValue(ref _logEvents, value); } } | |||
private bool _logEvents = true; | |||
/// <summary> Gets the user agent used when connecting to Discord. </summary> | |||
public string UserAgent { get; private set; } | |||
//Rest | |||
/// <summary> Gets or sets the max time (in milliseconds) to wait for an API request to complete. </summary> | |||
public int RestTimeout { get { return _restTimeout; } set { SetValue(ref _restTimeout, value); } } | |||
private int _restTimeout = 10000; | |||
/// <summary> Enables or disables the internal message queue. This will allow SendMessage/EditMessage to return immediately and handle messages internally. </summary> | |||
public bool UseMessageQueue { get { return _useMessageQueue; } set { SetValue(ref _useMessageQueue, value); } } | |||
private bool _useMessageQueue = true; | |||
/// <summary> Gets or sets the time (in milliseconds) to wait when the message queue is empty before checking again. </summary> | |||
public int MessageQueueInterval { get { return _messageQueueInterval; } set { SetValue(ref _messageQueueInterval, value); } } | |||
private int _messageQueueInterval = 100; | |||
public bool LogEvents { get; set; } = true; | |||
//WebSocket | |||
/// <summary> Gets or sets the time (in milliseconds) to wait for the websocket to connect and initialize. </summary> | |||
public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } } | |||
private int _connectionTimeout = 30000; | |||
public int ConnectionTimeout { get; set; } = 30000; | |||
/// <summary> Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. </summary> | |||
public int ReconnectDelay { get { return _reconnectDelay; } set { SetValue(ref _reconnectDelay, value); } } | |||
private int _reconnectDelay = 1000; | |||
public int ReconnectDelay { get; set; } = 1000; | |||
/// <summary> Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. </summary> | |||
public int FailedReconnectDelay { get { return _failedReconnectDelay; } set { SetValue(ref _failedReconnectDelay, value); } } | |||
private int _failedReconnectDelay = 15000; | |||
/// <summary> Gets or sets the time (in milliseconds) to wait when the websocket's message queue is empty before checking again. </summary> | |||
public int WebSocketInterval { get { return _webSocketInterval; } set { SetValue(ref _webSocketInterval, value); } } | |||
private int _webSocketInterval = 100; | |||
public int FailedReconnectDelay { get; set; } = 15000; | |||
//Performance | |||
/// <summary> Cache an encrypted login token to temp dir after success login. </summary> | |||
public bool CacheToken { get { return _cacheToken; } set { SetValue(ref _cacheToken, value); } } | |||
private bool _cacheToken = true; | |||
/// <summary> Gets or sets whether an encrypted login token should be saved to temp dir after successful login. </summary> | |||
public bool CacheToken { get; set; } = true; | |||
/// <summary> Gets or sets whether Discord should send information about offline users, for servers with more than 100 users. </summary> | |||
public bool UseLargeThreshold { get { return _useLargeThreshold; } set { SetValue(ref _useLargeThreshold, value); } } | |||
private bool _useLargeThreshold = false; | |||
public bool UseLargeThreshold { get; set; } = false; | |||
/// <summary> Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. </summary> | |||
public int MessageCacheSize { get { return _messageCacheSize; } set { SetValue(ref _messageCacheSize, value); } } | |||
private int _messageCacheSize = 100; | |||
public int MessageCacheSize { get; set; } = 100; | |||
/// <summary> Gets or sets whether the permissions cache should be used. This makes operations such as User.GetPermissions(Channel), User.ServerPermissions and Channel.Members </summary> | |||
public bool UsePermissionsCache { get { return _usePermissionsCache; } set { SetValue(ref _usePermissionsCache, value); } } | |||
private bool _usePermissionsCache = true; | |||
public bool UsePermissionsCache { get; set; } = true; | |||
/// <summary> Gets or sets whether the a copy of a model is generated on an update event to allow a user to check which properties changed. </summary> | |||
public bool EnablePreUpdateEvents { get { return _enablePreUpdateEvents; } set { SetValue(ref _enablePreUpdateEvents, value); } } | |||
private bool _enablePreUpdateEvents = true; | |||
public bool EnablePreUpdateEvents { get; set; } = true; | |||
//Events | |||
public DiscordConfig() | |||
/// <summary> Gets or sets a handler for all log messages. </summary> | |||
public EventHandler<LogMessageEventArgs> LogHandler { get; set; } | |||
public DiscordConfig Build() => new DiscordConfig(this); | |||
} | |||
public class DiscordConfig | |||
{ | |||
public const int MaxMessageSize = 2000; | |||
internal const int RestTimeout = 10000; | |||
internal const int MessageQueueInterval = 100; | |||
internal const int WebSocketQueueInterval = 100; | |||
public const string LibName = "Discord.Net"; | |||
public static string LibVersion => typeof(DiscordConfigBuilder).GetTypeInfo().Assembly.GetName().Version.ToString(3); | |||
public const string LibUrl = "https://github.com/RogueException/Discord.Net"; | |||
public const string ClientAPIUrl = "https://discordapp.com/api/"; | |||
public const string StatusAPIUrl = "https://srhpyqt94yxb.statuspage.io/api/v2/"; //"https://status.discordapp.com/api/v2/"; | |||
public const string CDNUrl = "https://cdn.discordapp.com/"; | |||
public const string InviteUrl = "https://discord.gg/"; | |||
public LogSeverity LogLevel { get; } | |||
public bool LogEvents { get; } | |||
public int ConnectionTimeout { get; } | |||
public int ReconnectDelay { get; } | |||
public int FailedReconnectDelay { get; } | |||
public bool UseLargeThreshold { get; } | |||
public int MessageCacheSize { get; } | |||
public bool UsePermissionsCache { get; } | |||
public bool EnablePreUpdateEvents { get; } | |||
public string UserAgent { get; } | |||
public string CacheDir { get; } | |||
internal DiscordConfig(DiscordConfigBuilder builder) | |||
{ | |||
UpdateUserAgent(); | |||
LogLevel = builder.LogLevel; | |||
LogEvents = builder.LogEvents; | |||
ConnectionTimeout = builder.ConnectionTimeout; | |||
ReconnectDelay = builder.ReconnectDelay; | |||
FailedReconnectDelay = builder.FailedReconnectDelay; | |||
UseLargeThreshold = builder.UseLargeThreshold; | |||
MessageCacheSize = builder.MessageCacheSize; | |||
UsePermissionsCache = builder.UsePermissionsCache; | |||
EnablePreUpdateEvents = builder.EnablePreUpdateEvents; | |||
UserAgent = GetUserAgent(builder); | |||
CacheDir = GetCacheDir(builder); | |||
} | |||
private void UpdateUserAgent() | |||
private string GetUserAgent(DiscordConfigBuilder builder) | |||
{ | |||
StringBuilder builder = new StringBuilder(); | |||
if (!string.IsNullOrEmpty(_appName)) | |||
StringBuilder sb = new StringBuilder(); | |||
if (!string.IsNullOrEmpty(builder.AppName)) | |||
{ | |||
builder.Append(_appName); | |||
if (!string.IsNullOrEmpty(_appVersion)) | |||
sb.Append(builder.AppName); | |||
if (!string.IsNullOrEmpty(builder.AppVersion)) | |||
{ | |||
builder.Append('/'); | |||
builder.Append(_appVersion); | |||
sb.Append('/'); | |||
sb.Append(builder.AppVersion); | |||
} | |||
if (!string.IsNullOrEmpty(_appUrl)) | |||
if (!string.IsNullOrEmpty(builder.AppUrl)) | |||
{ | |||
builder.Append(" ("); | |||
builder.Append(_appUrl); | |||
builder.Append(')'); | |||
sb.Append(" ("); | |||
sb.Append(builder.AppUrl); | |||
sb.Append(')'); | |||
} | |||
builder.Append(' '); | |||
sb.Append(' '); | |||
} | |||
builder.Append($"DiscordBot ({LibUrl}, v{LibVersion})"); | |||
UserAgent = builder.ToString(); | |||
sb.Append($"DiscordBot ({LibUrl}, v{LibVersion})"); | |||
return sb.ToString(); | |||
} | |||
private string GetCacheDir(DiscordConfigBuilder builder) | |||
{ | |||
if (builder.CacheToken) | |||
return Path.Combine(Path.GetTempPath(), builder.AppName ?? "Discord.Net"); | |||
else | |||
return null; | |||
} | |||
} | |||
} |
@@ -0,0 +1,11 @@ | |||
namespace Discord | |||
{ | |||
public enum LogSeverity : byte | |||
{ | |||
Error = 1, | |||
Warning = 2, | |||
Info = 3, | |||
Verbose = 4, | |||
Debug = 5 | |||
} | |||
} |
@@ -99,10 +99,10 @@ namespace Discord.Net | |||
_pendingActions.Enqueue(new DeleteAction(msg)); | |||
} | |||
internal Task Run(CancellationToken cancelToken, int interval) | |||
internal Task Run(CancellationToken cancelToken) | |||
{ | |||
_nextWarning = WarningStart; | |||
return Task.Run(async () => | |||
return Task.Run((Func<Task>)(async () => | |||
{ | |||
try | |||
{ | |||
@@ -121,11 +121,11 @@ namespace Discord.Net | |||
while (_pendingActions.TryDequeue(out queuedAction)) | |||
await queuedAction.Do(this).ConfigureAwait(false); | |||
await Task.Delay(interval).ConfigureAwait(false); | |||
await Task.Delay((int)Discord.DiscordConfig.MessageQueueInterval).ConfigureAwait(false); | |||
} | |||
} | |||
catch (OperationCanceledException) { } | |||
}); | |||
})); | |||
} | |||
internal async Task Send(Message msg) | |||
@@ -343,26 +343,12 @@ namespace Discord | |||
if (text == "") throw new ArgumentException("Value cannot be blank", nameof(text)); | |||
return SendMessageInternal(text, true); | |||
} | |||
private async Task<Message> SendMessageInternal(string text, bool isTTS) | |||
private Task<Message> SendMessageInternal(string text, bool isTTS) | |||
{ | |||
if (text.Length > DiscordConfig.MaxMessageSize) | |||
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); | |||
if (Client.Config.UseMessageQueue) | |||
return Client.MessageQueue.QueueSend(this, text, isTTS); | |||
else | |||
{ | |||
var request = new SendMessageRequest(Id) | |||
{ | |||
Content = text, | |||
Nonce = null, | |||
IsTTS = isTTS | |||
}; | |||
var model = await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||
var msg = AddMessage(model.Id, IsPrivate ? Client.PrivateUser : Server.CurrentUser, model.Timestamp.Value); | |||
msg.Update(model); | |||
return msg; | |||
} | |||
return Task.FromResult(Client.MessageQueue.QueueSend(this, text, isTTS)); | |||
} | |||
public async Task<Message> SendFile(string filePath) | |||
@@ -310,7 +310,7 @@ namespace Discord | |||
} | |||
} | |||
public async Task Edit(string text) | |||
public Task Edit(string text) | |||
{ | |||
if (text == null) throw new ArgumentNullException(nameof(text)); | |||
@@ -318,28 +318,14 @@ namespace Discord | |||
if (text.Length > DiscordConfig.MaxMessageSize) | |||
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); | |||
if (Client.Config.UseMessageQueue) | |||
Client.MessageQueue.QueueEdit(this, text); | |||
else | |||
{ | |||
var request = new UpdateMessageRequest(Channel.Id, Id) | |||
{ | |||
Content = text | |||
}; | |||
await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||
} | |||
Client.MessageQueue.QueueEdit(this, text); | |||
return TaskHelper.CompletedTask; | |||
} | |||
public async Task Delete() | |||
public Task Delete() | |||
{ | |||
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) { } | |||
} | |||
Client.MessageQueue.QueueDelete(this); | |||
return TaskHelper.CompletedTask; | |||
} | |||
/// <summary> Returns true if the logged-in user was mentioned. </summary> | |||
@@ -31,7 +31,7 @@ namespace Discord.Net.Rest | |||
_client = new RestSharpClient(baseUrl) | |||
{ | |||
PreAuthenticate = false, | |||
ReadWriteTimeout = _config.RestTimeout, | |||
ReadWriteTimeout = DiscordConfig.RestTimeout, | |||
UserAgent = config.UserAgent | |||
}; | |||
_client.Proxy = null; | |||
@@ -65,9 +65,7 @@ namespace Discord.Net.WebSockets | |||
{ | |||
return Task.Run(async () => | |||
{ | |||
var sendInterval = _config.WebSocketInterval; | |||
//var buffer = new ArraySegment<byte>(new byte[ReceiveChunkSize]); | |||
var buffer = new byte[ReceiveChunkSize]; | |||
var buffer = new ArraySegment<byte>(new byte[ReceiveChunkSize]); | |||
var stream = new MemoryStream(); | |||
try | |||
@@ -81,7 +79,7 @@ namespace Discord.Net.WebSockets | |||
try | |||
{ | |||
result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancelToken).ConfigureAwait(false); | |||
result = await _webSocket.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); | |||
} | |||
catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) | |||
{ | |||
@@ -91,7 +89,7 @@ namespace Discord.Net.WebSockets | |||
if (result.MessageType == WebSocketMessageType.Close) | |||
throw new WebSocketException((int)result.CloseStatus.Value, result.CloseStatusDescription); | |||
else | |||
stream.Write(buffer, 0, result.Count); | |||
stream.Write(buffer.Array, buffer.Offset, buffer.Count); | |||
} | |||
while (result == null || !result.EndOfMessage); | |||
@@ -114,7 +112,6 @@ namespace Discord.Net.WebSockets | |||
return Task.Run(async () => | |||
{ | |||
byte[] bytes = new byte[SendChunkSize]; | |||
var sendInterval = _config.WebSocketInterval; | |||
try | |||
{ | |||
@@ -147,7 +144,7 @@ namespace Discord.Net.WebSockets | |||
} | |||
} | |||
} | |||
await Task.Delay(sendInterval, cancelToken).ConfigureAwait(false); | |||
await Task.Delay(DiscordConfig.WebSocketQueueInterval, cancelToken).ConfigureAwait(false); | |||
} | |||
} | |||
catch (OperationCanceledException) { } | |||
@@ -118,7 +118,6 @@ namespace Discord.Net.WebSockets | |||
private Task SendAsync(CancellationToken cancelToken) | |||
{ | |||
var sendInterval = _config.WebSocketInterval; | |||
return Task.Run(async () => | |||
{ | |||
try | |||
@@ -128,7 +127,7 @@ namespace Discord.Net.WebSockets | |||
string json; | |||
while (_sendQueue.TryDequeue(out json)) | |||
_webSocket.Send(json); | |||
await Task.Delay(sendInterval, cancelToken).ConfigureAwait(false); | |||
await Task.Delay(DiscordConfig.WebSocketQueueInterval, cancelToken).ConfigureAwait(false); | |||
} | |||
} | |||
catch (OperationCanceledException) { } | |||