Skip to content

Commit

Permalink
I don't know what I'm doing :p (#1)
Browse files Browse the repository at this point in the history
* fix: Fixed CommandExecuted firing twice for failed RuntimeResults (discord-net#1192)

* Fixed CommandExecuted firing twice for failed RuntimeResults

* Changed to just checking the result type

* Amendments

* Resolve Issue discord-net#1188 (Allow Users to specify position when creating a new channel) (discord-net#1196)

* Added ability to specify position when creating a channel

* Adjusted categories to include guildproperties and allow specifying position when creating channel categories

* fixed unimplemented methods (for CreateCategoryChannelAsync) and added appropriate documentation

* feature: add Format.Url, Format.EscapeUrl

Format.Url formats a URL into a markdown `[]()` masked URL.

Format.EscapeUrl formats a URL into a Discord `<>` escaped URL.

* feature: add extensions for bulk reactions

this is not api-level bulk reactions (!)

* fix: Update ChannelCreateAuditLogData.cs (discord-net#1195)

* feature: Implement Dispose for types which have disposable data (discord-net#1171)

* Initial set of dispose implementations

Not handled yet:
- Discord.Net.Websocket/Entities/SocketGuild
- Discord.Net.Tests

* Refactor DiscordSocketClient init into ctor

This way we remove an IDisposableAnalyzer warning for not disposing
the client when we set the client variable.

* Dispose of clients when disposing sharded client

* Finish implementing IDisposable where appropriate

I opted to use NoWarn in the Tests project as it wasn't really necessary
considering that our tests only run once

* Tweak samples after feedback

* fix: Added Msg.Content Null Check For Prefixes (discord-net#1200)

* Added Msg.Content Null Check

* Minor Change

* Grouped Params In If Statement

* Minor Change

* api: [brk] Move Invites-related Methods from IGuildChannel to INestedChannel (discord-net#1172)

* Move invites-related methods from IGuildChannel to INestedChannel

* Add missing implementation

...because I somehow forgot it the first time around

* fix: Solves AudioClient Lockup On Disconnect (discord-net#1203)

* Solves Audio Disconnect Lockup

* Execute Disconnected Event Before Logger & State

* fix: Solves UdpClient "ObjectDisposedException" (discord-net#1202)

* Solves "ObjectDisposedException"

* Corrected Spelling Error

* Fixed Spelling

* test: bump dependency versions

* fix: Update minimum Bot Token length to 58 char (discord-net#1204)

* Update the minimum bot token length to 58 char

- Updates the minimum length of a bot token to be 58 characters. An older 58 char bot token was found by Moiph
- Makes this value an internal const instead of a magic number

* update the TokenUtils tests for 58 char min

* fix: Fix after message remaining in MessageUpdated if message author wasn't in the payload (discord-net#1209)

* Fix leaving updated message as null when Discords doesn't include a message author in MESSAGE_UPDATE

* Name! That! Parameter!

* fix: Improve validation of Bot Tokens (discord-net#1206)

* improve bot token validation by trying to decode user id from token

Try to decode the user id from the supplied bot token as a way of validating the token. If this should fail, indicate that the token is invalid.

* Update the tokenutils tests to pass the new validation checks

* Add test case for CheckBotTokenValidity method

* lint: clean up whitespace

* Add check for null or whitespace string, lint whitespace

* fix userid conversion

* Add hint to user to check that token is not an oauth client secret

* Catch exception that can be thrown by GetString

* Refactor token conversion logic into it's own testable method

* feature: Allow setting custom error messages for preconditions (discord-net#1124)

* Rebase and use in all in-box preconditions

* Silly git....

* Add configurable 'NotAGuild' message

* Respond to feedback

* docs: add example of custom error message to sample

* feature: add DiscordSocketRestClient (discord-net#1198)

* feature: add DiscordSocketRestClient

this resolves discord-net#803.

Users can access a DiscordSocketRestClient from the new
`DiscordSocketClient.Rest` property.

DiscordSocketRestClient is a wrapper over DiscordRestClient with certain
state-modifying methods, such as Login/Logout disabled, to prevent users
from breaking the client state.

DiscordSocketRestClient uses the same API client as the
DiscordSocketClient, allowing for shared ratelimiting - meaning users
can now force HTTP requests without needing to wory about running into
429s.

* fix: disallow users from bypassing shadowed login
  • Loading branch information
brankin3 authored Dec 5, 2018
1 parent 6c8b862 commit f6c55df
Show file tree
Hide file tree
Showing 58 changed files with 785 additions and 333 deletions.
13 changes: 10 additions & 3 deletions samples/01_basic_ping_bot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,28 @@ namespace _01_basic_ping_bot
// - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library
class Program
{
private DiscordSocketClient _client;
private readonly DiscordSocketClient _client;

// Discord.Net heavily utilizes TAP for async, so we create
// an asynchronous context from the beginning.
static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult();
{
new Program().MainAsync().GetAwaiter().GetResult();
}

public async Task MainAsync()
public Program()
{
// It is recommended to Dispose of a client when you are finished
// using it, at the end of your app's lifetime.
_client = new DiscordSocketClient();

_client.Log += LogAsync;
_client.Ready += ReadyAsync;
_client.MessageReceived += MessageReceivedAsync;
}

public async Task MainAsync()
{
// Tokens should be considered secret data, and never hard-coded.
await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
await _client.StartAsync();
Expand Down
5 changes: 5 additions & 0 deletions samples/02_commands_framework/Modules/PublicModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,10 @@ public Task EchoAsync([Remainder] string text)
[Command("list")]
public Task ListAsync(params string[] objects)
=> ReplyAsync("You listed: " + string.Join("; ", objects));

[Command("guild_only")]
[RequireContext(ContextType.Guild, ErrorMessage = "Sorry, this command must be ran from within a server, not a DM!")]
public Task GuildOnlyCommand()
=> ReplyAsync("Nothing to see here!");
}
}
28 changes: 18 additions & 10 deletions samples/02_commands_framework/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,32 @@ namespace _02_commands_framework
// - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library
class Program
{
// There is no need to implement IDisposable like before as we are
// using dependency injection, which handles calling Dispose for us.
static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult();

public async Task MainAsync()
{
var services = ConfigureServices();
// You should dispose a service provider created using ASP.NET
// when you are finished using it, at the end of your app's lifetime.
// If you use another dependency injection framework, you should inspect
// its documentation for the best way to do this.
using (var services = ConfigureServices())
{
var client = services.GetRequiredService<DiscordSocketClient>();

var client = services.GetRequiredService<DiscordSocketClient>();
client.Log += LogAsync;
services.GetRequiredService<CommandService>().Log += LogAsync;

client.Log += LogAsync;
services.GetRequiredService<CommandService>().Log += LogAsync;
// Tokens should be considered secret data, and never hard-coded.
await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
await client.StartAsync();

await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
await client.StartAsync();
await services.GetRequiredService<CommandHandlingService>().InitializeAsync();

await services.GetRequiredService<CommandHandlingService>().InitializeAsync();

await Task.Delay(-1);
await Task.Delay(-1);
}
}

private Task LogAsync(LogMessage log)
Expand All @@ -46,7 +54,7 @@ private Task LogAsync(LogMessage log)
return Task.CompletedTask;
}

private IServiceProvider ConfigureServices()
private ServiceProvider ConfigureServices()
{
return new ServiceCollection()
.AddSingleton<DiscordSocketClient>()
Expand Down
37 changes: 21 additions & 16 deletions samples/03_sharded_client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,46 @@ namespace _03_sharded_client
// DiscordSocketClient instances (or shards) to serve a large number of guilds.
class Program
{
private DiscordShardedClient _client;

static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult();
public async Task MainAsync()
{
// You specify the amount of shards you'd like to have with the
// DiscordSocketConfig. Generally, it's recommended to
// DiscordSocketConfig. Generally, it's recommended to
// have 1 shard per 1500-2000 guilds your bot is in.
var config = new DiscordSocketConfig
{
TotalShards = 2
};

_client = new DiscordShardedClient(config);
var services = ConfigureServices();
// You should dispose a service provider created using ASP.NET
// when you are finished using it, at the end of your app's lifetime.
// If you use another dependency injection framework, you should inspect
// its documentation for the best way to do this.
using (var services = ConfigureServices(config))
{
var client = services.GetRequiredService<DiscordShardedClient>();

// The Sharded Client does not have a Ready event.
// The ShardReady event is used instead, allowing for individual
// control per shard.
_client.ShardReady += ReadyAsync;
_client.Log += LogAsync;
// The Sharded Client does not have a Ready event.
// The ShardReady event is used instead, allowing for individual
// control per shard.
client.ShardReady += ReadyAsync;
client.Log += LogAsync;

await services.GetRequiredService<CommandHandlingService>().InitializeAsync();
await services.GetRequiredService<CommandHandlingService>().InitializeAsync();

await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
await _client.StartAsync();
// Tokens should be considered secret data, and never hard-coded.
await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
await client.StartAsync();

await Task.Delay(-1);
await Task.Delay(-1);
}
}

private IServiceProvider ConfigureServices()
private ServiceProvider ConfigureServices(DiscordSocketConfig config)
{
return new ServiceCollection()
.AddSingleton(_client)
.AddSingleton(new DiscordShardedClient(config))
.AddSingleton<CommandService>()
.AddSingleton<CommandHandlingService>()
.BuildServiceProvider();
Expand Down
8 changes: 8 additions & 0 deletions src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ public abstract class PreconditionAttribute : Attribute
/// </remarks>
public string Group { get; set; } = null;

/// <summary>
/// When overridden in a derived class, uses the supplied string
/// as the error message if the precondition doesn't pass.
/// Setting this for a class that doesn't override
/// this property is a no-op.
/// </summary>
public virtual string ErrorMessage { get { return null; } set { } }

/// <summary>
/// Checks if the <paramref name="command"/> has the sufficient permission to be executed.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class RequireBotPermissionAttribute : PreconditionAttribute
/// Gets the specified <see cref="Discord.ChannelPermission" /> of the precondition.
/// </summary>
public ChannelPermission? ChannelPermission { get; }
public override string ErrorMessage { get; set; }
public string NotAGuildErrorMessage { get; set; }

/// <summary>
/// Requires the bot account to have a specific <see cref="Discord.GuildPermission"/>.
Expand Down Expand Up @@ -56,9 +58,9 @@ public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandCon
if (GuildPermission.HasValue)
{
if (guildUser == null)
return PreconditionResult.FromError("Command must be used in a guild channel.");
return PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel.");
if (!guildUser.GuildPermissions.Has(GuildPermission.Value))
return PreconditionResult.FromError($"Bot requires guild permission {GuildPermission.Value}.");
return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires guild permission {GuildPermission.Value}.");
}

if (ChannelPermission.HasValue)
Expand All @@ -70,7 +72,7 @@ public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandCon
perms = ChannelPermissions.All(context.Channel);

if (!perms.Has(ChannelPermission.Value))
return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}.");
return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires channel permission {ChannelPermission.Value}.");
}

return PreconditionResult.FromSuccess();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class RequireContextAttribute : PreconditionAttribute
/// Gets the context required to execute the command.
/// </summary>
public ContextType Contexts { get; }
public override string ErrorMessage { get; set; }

/// <summary> Requires the command to be invoked in the specified context. </summary>
/// <param name="contexts">The type of context the command can be invoked in. Multiple contexts can be specified by ORing the contexts together.</param>
Expand Down Expand Up @@ -66,7 +67,7 @@ public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext c
if (isValid)
return Task.FromResult(PreconditionResult.FromSuccess());
else
return Task.FromResult(PreconditionResult.FromError($"Invalid context for command; accepted contexts: {Contexts}."));
return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"Invalid context for command; accepted contexts: {Contexts}."));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ namespace Discord.Commands
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RequireNsfwAttribute : PreconditionAttribute
{
public override string ErrorMessage { get; set; }

/// <inheritdoc />
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
if (context.Channel is ITextChannel text && text.IsNsfw)
return Task.FromResult(PreconditionResult.FromSuccess());
else
return Task.FromResult(PreconditionResult.FromError("This command may only be invoked in an NSFW channel."));
return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? "This command may only be invoked in an NSFW channel."));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ namespace Discord.Commands
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RequireOwnerAttribute : PreconditionAttribute
{
public override string ErrorMessage { get; set; }

/// <inheritdoc />
public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
Expand All @@ -42,10 +44,10 @@ public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandCon
case TokenType.Bot:
var application = await context.Client.GetApplicationInfoAsync().ConfigureAwait(false);
if (context.User.Id != application.Owner.Id)
return PreconditionResult.FromError("Command can only be run by the owner of the bot.");
return PreconditionResult.FromError(ErrorMessage ?? "Command can only be run by the owner of the bot.");
return PreconditionResult.FromSuccess();
default:
return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");
return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class RequireUserPermissionAttribute : PreconditionAttribute
/// Gets the specified <see cref="Discord.ChannelPermission" /> of the precondition.
/// </summary>
public ChannelPermission? ChannelPermission { get; }
public override string ErrorMessage { get; set; }
public string NotAGuildErrorMessage { get; set; }

/// <summary>
/// Requires that the user invoking the command to have a specific <see cref="Discord.GuildPermission"/>.
Expand Down Expand Up @@ -54,9 +56,9 @@ public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext c
if (GuildPermission.HasValue)
{
if (guildUser == null)
return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel."));
return Task.FromResult(PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel."));
if (!guildUser.GuildPermissions.Has(GuildPermission.Value))
return Task.FromResult(PreconditionResult.FromError($"User requires guild permission {GuildPermission.Value}."));
return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires guild permission {GuildPermission.Value}."));
}

if (ChannelPermission.HasValue)
Expand All @@ -68,7 +70,7 @@ public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext c
perms = ChannelPermissions.All(context.Channel);

if (!perms.Has(ChannelPermission.Value))
return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}."));
return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires channel permission {ChannelPermission.Value}."));
}

return Task.FromResult(PreconditionResult.FromSuccess());
Expand Down
28 changes: 24 additions & 4 deletions src/Discord.Net.Commands/CommandService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ namespace Discord.Commands
/// been successfully executed.
/// </para>
/// </remarks>
public class CommandService
public class CommandService : IDisposable
{
/// <summary>
/// Occurs when a command-related information is received.
Expand Down Expand Up @@ -67,6 +67,8 @@ public class CommandService
internal readonly LogManager _logManager;
internal readonly IReadOnlyDictionary<char, char> _quotationMarkAliasMap;

internal bool _isDisposed;

/// <summary>
/// Represents all modules loaded within <see cref="CommandService"/>.
/// </summary>
Expand Down Expand Up @@ -330,9 +332,9 @@ private bool RemoveModuleInternal(ModuleInfo module)

//Type Readers
/// <summary>
/// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object
/// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object
/// type.
/// If <typeparamref name="T" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> will
/// If <typeparamref name="T" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> will
/// also be added.
/// If a default <see cref="TypeReader" /> exists for <typeparamref name="T" />, a warning will be logged
/// and the default <see cref="TypeReader" /> will be replaced.
Expand Down Expand Up @@ -603,9 +605,27 @@ float CalculateScore(CommandMatch match, ParseResult parseResult)
//If we get this far, at least one parse was successful. Execute the most likely overload.
var chosenOverload = successfulParses[0];
var result = await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false);
if (!result.IsSuccess) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution)
if (!result.IsSuccess && !(result is RuntimeResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution)
await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result);
return result;
}

protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_moduleLock?.Dispose();
}

_isDisposed = true;
}
}

void IDisposable.Dispose()
{
Dispose(true);
}
}
}
2 changes: 1 addition & 1 deletion src/Discord.Net.Commands/Discord.Net.Commands.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard2.0' ">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.1" />
</ItemGroup>
</Project>
</Project>
6 changes: 3 additions & 3 deletions src/Discord.Net.Commands/Extensions/MessageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static class MessageExtensions
public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos)
{
var text = msg.Content;
if (text.Length > 0 && text[0] == c)
if (!string.IsNullOrEmpty(text) && text[0] == c)
{
argPos = 1;
return true;
Expand All @@ -32,7 +32,7 @@ public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos)
public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal)
{
var text = msg.Content;
if (text.StartsWith(str, comparisonType))
if (!string.IsNullOrEmpty(text) && text.StartsWith(str, comparisonType))
{
argPos = str.Length;
return true;
Expand All @@ -45,7 +45,7 @@ public static bool HasStringPrefix(this IUserMessage msg, string str, ref int ar
public static bool HasMentionPrefix(this IUserMessage msg, IUser user, ref int argPos)
{
var text = msg.Content;
if (text.Length <= 3 || text[0] != '<' || text[1] != '@') return false;
if (string.IsNullOrEmpty(text) || text.Length <= 3 || text[0] != '<' || text[1] != '@') return false;

int endPos = text.IndexOf('>');
if (endPos == -1) return false;
Expand Down
3 changes: 3 additions & 0 deletions src/Discord.Net.Core/Discord.Net.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@
<PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
<PackageReference Include="System.Interactive.Async" Version="3.1.1" />
</ItemGroup>
<ItemGroup Condition=" '$(Configuration)' != 'Release' ">
<PackageReference Include="IDisposableAnalyzers" Version="2.0.3.3" />
</ItemGroup>
</Project>
Loading

0 comments on commit f6c55df

Please sign in to comment.