diff --git a/src/Discord.Net.Audio.Net45/Discord.Net.Audio.csproj b/src/Discord.Net.Audio.Net45/Discord.Net.Audio.csproj index 99cfdcc0b..f7326b4a9 100644 --- a/src/Discord.Net.Audio.Net45/Discord.Net.Audio.csproj +++ b/src/Discord.Net.Audio.Net45/Discord.Net.Audio.csproj @@ -47,6 +47,9 @@ AudioExtensions.cs + + AudioMode.cs + AudioService.cs diff --git a/src/Discord.Net.Audio/AudioClient.cs b/src/Discord.Net.Audio/AudioClient.cs index c7d9d182d..46474f6a4 100644 --- a/src/Discord.Net.Audio/AudioClient.cs +++ b/src/Discord.Net.Audio/AudioClient.cs @@ -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(); Config = Service.Config; Serializer = client.Serializer; _gatewayState = (int)ConnectionState.Disconnected; diff --git a/src/Discord.Net.Audio/AudioExtensions.cs b/src/Discord.Net.Audio/AudioExtensions.cs index eca362f80..50f508ff5 100644 --- a/src/Discord.Net.Audio/AudioExtensions.cs +++ b/src/Discord.Net.Audio/AudioExtensions.cs @@ -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 configFunc = null) + public static DiscordClient UsingAudio(this DiscordClient client, Action 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(required); + + public static Task JoinAudio(this Channel channel) => channel.Client.Services.Get().Join(channel); + public static Task LeaveAudio(this Channel channel) => channel.Client.Services.Get().Leave(channel); + public static Task LeaveAudio(this Server server) => server.Client.Services.Get().Leave(server); + public static IAudioClient GetAudioClient(Server server) => server.Client.Services.Get().GetClient(server); } } diff --git a/src/Discord.Net.Audio/AudioMode.cs b/src/Discord.Net.Audio/AudioMode.cs new file mode 100644 index 000000000..b9acdbf89 --- /dev/null +++ b/src/Discord.Net.Audio/AudioMode.cs @@ -0,0 +1,9 @@ +namespace Discord.Audio +{ + public enum AudioMode : byte + { + Outgoing = 1, + Incoming = 2, + Both = Outgoing | Incoming + } +} diff --git a/src/Discord.Net.Audio/AudioService.cs b/src/Discord.Net.Audio/AudioService.cs index a8844bc46..3de3531ae 100644 --- a/src/Discord.Net.Audio/AudioService.cs +++ b/src/Discord.Net.Audio/AudioService.cs @@ -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(); diff --git a/src/Discord.Net.Audio/AudioServiceConfig.cs b/src/Discord.Net.Audio/AudioServiceConfig.cs index a9b4a4b33..89d05d85b 100644 --- a/src/Discord.Net.Audio/AudioServiceConfig.cs +++ b/src/Discord.Net.Audio/AudioServiceConfig.cs @@ -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 - } + /// Enables the voice websocket and UDP client and specifies how it will be used. + public AudioMode Mode { get; set; } = AudioMode.Outgoing; - public class AudioServiceConfig - { + /// Enables the voice websocket and UDP client. This option requires the libsodium .dll or .so be in the local or system folder. + public bool EnableEncryption { get; set; } = true; + /// + /// 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. + /// + public bool EnableMultiserver { get; set; } = false; + + /// Gets or sets the buffer length (in milliseconds) for outgoing voice packets. + public int BufferLength { get; set; } = 1000; + /// 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. + public int? Bitrate { get; set; } = null; + /// 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). + public int Channels { get; set; } = 2; + + public AudioServiceConfig Build() => new AudioServiceConfig(this); + } + + public class AudioServiceConfig + { public const int MaxBitrate = 128; - /// Max time in milliseconds to wait for DiscordAudioClient to connect and initialize. - public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } } - private int _connectionTimeout = 30000; + public AudioMode Mode { get; } - //Experimental Features - /// (Experimental) Enables the voice websocket and UDP client and specifies how it will be used. - public AudioMode Mode { get { return _mode; } set { SetValue(ref _mode, value); } } - private AudioMode _mode = AudioMode.Outgoing; + public bool EnableEncryption { get; } + public bool EnableMultiserver { get; } - /// (Experimental) Enables the voice websocket and UDP client. This option requires the libsodium .dll or .so be in the local or system folder. - 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; } - /// (Experimental) Enables the client to be simultaneously connected to multiple channels at once (Discord still limits you to one channel per server). - public bool EnableMultiserver { get { return _enableMultiserver; } set { SetValue(ref _enableMultiserver, value); } } - private bool _enableMultiserver = false; + internal AudioServiceConfig(AudioServiceConfigBuilder builder) + { + Mode = builder.Mode; - /// Gets or sets the buffer length (in milliseconds) for outgoing voice packets. - public int BufferLength { get { return _bufferLength; } set { SetValue(ref _bufferLength, value); } } - private int _bufferLength = 1000; + EnableEncryption = builder.EnableEncryption; + EnableMultiserver = builder.EnableMultiserver; - /// 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. - public int? Bitrate { get { return _bitrate; } set { SetValue(ref _bitrate, value); } } - private int? _bitrate = null; - /// 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). - 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(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; + } + } } diff --git a/src/Discord.Net.Commands/CommandExtensions.cs b/src/Discord.Net.Commands/CommandExtensions.cs index 1ac1e030b..557f5ac5a 100644 --- a/src/Discord.Net.Commands/CommandExtensions.cs +++ b/src/Discord.Net.Commands/CommandExtensions.cs @@ -9,14 +9,12 @@ namespace Discord.Commands client.Services.Add(new CommandService(config)); return client; } - public static DiscordClient UsingCommands(this DiscordClient client, Action configFunc = null) + public static DiscordClient UsingCommands(this DiscordClient client, Action 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(required); } } diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 27ebe0927..647bd01f1 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -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) diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index 982ce2cd9..f43c838fb 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -2,36 +2,45 @@ namespace Discord.Commands { - public class CommandServiceConfig + public class CommandServiceConfigBuilder { /// Gets or sets the prefix character used to trigger commands, if ActivationMode has the Char flag set. - public char? PrefixChar { get { return _prefixChar; } set { SetValue(ref _prefixChar, value); } } - private char? _prefixChar = null; - + public char? PrefixChar { get; set; } = null; /// Gets or sets whether a message beginning with a mention to the logged-in user should be treated as a command. - public bool AllowMentionPrefix { get { return _allowMentionPrefix; } set { SetValue(ref _allowMentionPrefix, value); } } - private bool _allowMentionPrefix = true; - + public bool AllowMentionPrefix { get; set; } = true; /// /// 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. /// - public Func CustomPrefixHandler { get { return _customPrefixHandler; } set { SetValue(ref _customPrefixHandler, value); } } - private Func _customPrefixHandler = null; + public Func CustomPrefixHandler { get; set; } = null; + + /// Gets or sets whether a help function should be automatically generated. + public HelpMode HelpMode { get; set; } = HelpMode.Disabled; + + + /// Gets or sets a handler that is called on any successful command execution. + public EventHandler ExecuteHandler { get; set; } + /// Gets or sets a handler that is called on any error during command parsing or execution. + public EventHandler ErrorHandler { get; set; } + + public CommandServiceConfig Build() => new CommandServiceConfig(this); + } + public class CommandServiceConfig + { + public char? PrefixChar { get; } + public bool AllowMentionPrefix { get; } + public Func CustomPrefixHandler { get; } /// Gets or sets whether a help function should be automatically generated. - 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(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; + } } } diff --git a/src/Discord.Net.Commands/HelpMode.cs b/src/Discord.Net.Commands/HelpMode.cs index 27f5c444f..272403f42 100644 --- a/src/Discord.Net.Commands/HelpMode.cs +++ b/src/Discord.Net.Commands/HelpMode.cs @@ -3,7 +3,7 @@ public enum HelpMode { /// Disable the automatic help command. - Disable, + Disabled, /// Use the automatic help command and respond in the channel the command is used. Public, /// Use the automatic help command and respond in a private message. diff --git a/src/Discord.Net.Modules/ModuleManager.cs b/src/Discord.Net.Modules/ModuleManager.cs index 17b2b166b..0427219d3 100644 --- a/src/Discord.Net.Modules/ModuleManager.cs +++ b/src/Discord.Net.Modules/ModuleManager.cs @@ -113,7 +113,7 @@ namespace Discord.Modules public void CreateCommands(string prefix, Action config) { - var commandService = Client.Commands(true); + var commandService = Client.Services.Get(); commandService.CreateGroup(prefix, x => { x.Category(Name); diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj index 7bc06ff71..42b76726f 100644 --- a/src/Discord.Net.Net45/Discord.Net.csproj +++ b/src/Discord.Net.Net45/Discord.Net.csproj @@ -409,6 +409,9 @@ Enums\ImageType.cs + + Enums\LogSeverity.cs + Enums\PermissionTarget.cs diff --git a/src/Discord.Net.Shared/TaskHelper.cs b/src/Discord.Net.Shared/TaskHelper.cs index 82c70f500..83408b8e9 100644 --- a/src/Discord.Net.Shared/TaskHelper.cs +++ b/src/Discord.Net.Shared/TaskHelper.cs @@ -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 ToAsync(Action action) { diff --git a/src/Discord.Net/Config.cs b/src/Discord.Net/Config.cs deleted file mode 100644 index af7da9529..000000000 --- a/src/Discord.Net/Config.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Discord -{ - public abstract class Config - where T : Config - { - protected bool _isLocked; - protected internal void Lock() { _isLocked = true; } - protected void SetValue(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; - } - } -} diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 5e6ce6650..bdcc8c74a 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -76,28 +76,33 @@ namespace Discord public IEnumerable Regions => _regions.Select(x => x.Value); /// Initializes a new instance of the DiscordClient class. - public DiscordClient(Action configFunc) + public DiscordClient(Action configFunc) : this(ProcessConfig(configFunc)) { } - private static DiscordConfig ProcessConfig(Action func) + private static DiscordConfigBuilder ProcessConfig(Action func) { - var config = new DiscordConfig(); + var config = new DiscordConfigBuilder(); func(config); return config; } /// Initializes a new instance of the DiscordClient class. public DiscordClient() - : this((DiscordConfig)null) + : this(new DiscordConfigBuilder()) { } - + /// Initializes a new instance of the DiscordClient class. + public DiscordClient(DiscordConfigBuilder builder) + : this(builder.Build()) + { + if (builder.LogHandler != null) + Log.Message += builder.LogHandler; + } /// Initializes a new instance of the DiscordClient class. 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 tasks = new List(); - 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) diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index 62512aae9..c22b60163 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -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 +{ + 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 /// Gets or sets name of your application, 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; + public string AppName { get; set; } = null; /// Gets or sets url to your application, used in the user agent. - public string AppUrl { get { return _appUrl; } set { SetValue(ref _appUrl, value); UpdateUserAgent(); } } - private string _appUrl = null; + public string AppUrl { get; set; } = null; /// Gets or sets the version of your application, used in the user agent. - public string AppVersion { get { return _appVersion; } set { SetValue(ref _appVersion, value); UpdateUserAgent(); } } - private string _appVersion = null; + public string AppVersion { get; set; } = null; /// 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. - public LogSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } } - private LogSeverity _logLevel = LogSeverity.Info; + public LogSeverity LogLevel { get; set; } = LogSeverity.Info; /// Enables or disables the default event logger. - public bool LogEvents { get { return _logEvents; } set { SetValue(ref _logEvents, value); } } - private bool _logEvents = true; - - /// Gets the user agent used when connecting to Discord. - public string UserAgent { get; private set; } - - //Rest - - /// Gets or sets the max time (in milliseconds) to wait for an API request to complete. - public int RestTimeout { get { return _restTimeout; } set { SetValue(ref _restTimeout, value); } } - private int _restTimeout = 10000; - - /// Enables or disables the internal message queue. This will allow SendMessage/EditMessage to return immediately and handle messages internally. - public bool UseMessageQueue { get { return _useMessageQueue; } set { SetValue(ref _useMessageQueue, value); } } - private bool _useMessageQueue = true; - /// Gets or sets the time (in milliseconds) to wait when the message queue is empty before checking again. - public int MessageQueueInterval { get { return _messageQueueInterval; } set { SetValue(ref _messageQueueInterval, value); } } - private int _messageQueueInterval = 100; + public bool LogEvents { get; set; } = true; //WebSocket /// Gets or sets the time (in milliseconds) to wait for the websocket to connect and initialize. - public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } } - private int _connectionTimeout = 30000; + public int ConnectionTimeout { get; set; } = 30000; /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. - public int ReconnectDelay { get { return _reconnectDelay; } set { SetValue(ref _reconnectDelay, value); } } - private int _reconnectDelay = 1000; + 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 { return _failedReconnectDelay; } set { SetValue(ref _failedReconnectDelay, value); } } - private int _failedReconnectDelay = 15000; - - /// Gets or sets the time (in milliseconds) to wait when the websocket's message queue is empty before checking again. - public int WebSocketInterval { get { return _webSocketInterval; } set { SetValue(ref _webSocketInterval, value); } } - private int _webSocketInterval = 100; + public int FailedReconnectDelay { get; set; } = 15000; //Performance - /// Cache an encrypted login token to temp dir after success login. - public bool CacheToken { get { return _cacheToken; } set { SetValue(ref _cacheToken, value); } } - private bool _cacheToken = true; + /// Gets or sets whether an encrypted login token should be saved to temp dir after successful login. + public bool CacheToken { get; set; } = true; /// Gets or sets whether Discord should send information about offline users, for servers with more than 100 users. - public bool UseLargeThreshold { get { return _useLargeThreshold; } set { SetValue(ref _useLargeThreshold, value); } } - private bool _useLargeThreshold = false; + public bool UseLargeThreshold { get; set; } = false; /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. - public int MessageCacheSize { get { return _messageCacheSize; } set { SetValue(ref _messageCacheSize, value); } } - private int _messageCacheSize = 100; + public int MessageCacheSize { get; set; } = 100; /// Gets or sets whether the permissions cache should be used. This makes operations such as User.GetPermissions(Channel), User.ServerPermissions and Channel.Members - public bool UsePermissionsCache { get { return _usePermissionsCache; } set { SetValue(ref _usePermissionsCache, value); } } - private bool _usePermissionsCache = true; + public bool UsePermissionsCache { get; set; } = true; /// 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. - public bool EnablePreUpdateEvents { get { return _enablePreUpdateEvents; } set { SetValue(ref _enablePreUpdateEvents, value); } } - private bool _enablePreUpdateEvents = true; + public bool EnablePreUpdateEvents { get; set; } = true; + + //Events - public DiscordConfig() + /// Gets or sets a handler for all log messages. + public EventHandler 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; } } } diff --git a/src/Discord.Net/Enums/LogSeverity.cs b/src/Discord.Net/Enums/LogSeverity.cs new file mode 100644 index 000000000..c62d8c250 --- /dev/null +++ b/src/Discord.Net/Enums/LogSeverity.cs @@ -0,0 +1,11 @@ +namespace Discord +{ + public enum LogSeverity : byte + { + Error = 1, + Warning = 2, + Info = 3, + Verbose = 4, + Debug = 5 + } +} diff --git a/src/Discord.Net/MessageQueue.cs b/src/Discord.Net/MessageQueue.cs index a75963935..6860a1b59 100644 --- a/src/Discord.Net/MessageQueue.cs +++ b/src/Discord.Net/MessageQueue.cs @@ -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)(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) diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs index 447baefe0..73b11441f 100644 --- a/src/Discord.Net/Models/Channel.cs +++ b/src/Discord.Net/Models/Channel.cs @@ -343,26 +343,12 @@ namespace Discord if (text == "") throw new ArgumentException("Value cannot be blank", nameof(text)); return SendMessageInternal(text, true); } - private async Task SendMessageInternal(string text, bool isTTS) + private Task 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 SendFile(string filePath) diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index 77cad6e2a..ed2c634e9 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -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; } /// Returns true if the logged-in user was mentioned. diff --git a/src/Discord.Net/Net/Rest/SharpRestEngine.cs b/src/Discord.Net/Net/Rest/SharpRestEngine.cs index a4ae3d392..f9c54064b 100644 --- a/src/Discord.Net/Net/Rest/SharpRestEngine.cs +++ b/src/Discord.Net/Net/Rest/SharpRestEngine.cs @@ -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; diff --git a/src/Discord.Net/Net/WebSockets/BuiltInEngine.cs b/src/Discord.Net/Net/WebSockets/BuiltInEngine.cs index 34224720f..bda5ccb15 100644 --- a/src/Discord.Net/Net/WebSockets/BuiltInEngine.cs +++ b/src/Discord.Net/Net/WebSockets/BuiltInEngine.cs @@ -65,9 +65,7 @@ namespace Discord.Net.WebSockets { return Task.Run(async () => { - var sendInterval = _config.WebSocketInterval; - //var buffer = new ArraySegment(new byte[ReceiveChunkSize]); - var buffer = new byte[ReceiveChunkSize]; + var buffer = new ArraySegment(new byte[ReceiveChunkSize]); var stream = new MemoryStream(); try @@ -81,7 +79,7 @@ namespace Discord.Net.WebSockets try { - result = await _webSocket.ReceiveAsync(new ArraySegment(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) { } diff --git a/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs b/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs index 98eb7db02..420299d6b 100644 --- a/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs +++ b/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs @@ -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) { }