diff --git a/DisCatSharp.Extensions.Docs/articles/extensions/twofactor_commands/intro.md b/DisCatSharp.Extensions.Docs/articles/extensions/twofactor_commands/intro.md index 6a55908..6e7f9cd 100644 --- a/DisCatSharp.Extensions.Docs/articles/extensions/twofactor_commands/intro.md +++ b/DisCatSharp.Extensions.Docs/articles/extensions/twofactor_commands/intro.md @@ -30,41 +30,32 @@ client.UseTwoFactor(); #### Enrolling a user in two factor -To enroll a user in two factor, call [EnrollTwoFactor(DiscordUser.Id)](xref:DisCatSharp.Extensions.TwoFactorCommands.TwoFactorExtensionUtilities.EnrollTwoFactor*) on your [DiscordClient](xref:DisCatSharp.DiscordClient) instance. +To enroll a user in two factor, call [EnrollTwoFactor(DiscordUser.Id)](xref:DisCatSharp.Extensions.TwoFactorCommands.ApplicationCommands.EnrollTwoFactorAsync*) on your [InteractionContext](xref:DisCatSharp.ApplicationCommands.Context.InteractionContext). ```cs -using DisCatSharp.Extensions.TwoFactorCommands; +using DisCatSharp.Extensions.TwoFactorCommands.ApplicationCommands; // ... -[SlashCommand("enroll", "Enroll in two factor")] -public static async Task EnrollTwoFactor(InteractionContext ctx) -{ - // ... - var (Secret, QrCode) = ctx.Client.EnrollTwoFactor(ctx.User); - - // Either send the QR code to the user, or the secret. - // QrCode is a MemoryStream you can use with DiscordWebhookBuilder.AddFile as example. -} +[SlashCommand("setup_two_factor", "Setup 2FA")] +public static async Task SetupTwoFactorAsync(InteractionContext ctx) + => await ctx.EnrollTwoFactorAsync(); ``` Example way to ask a user to register their two factor: -![Example Enroll](/images/two_factor_enrollment_message_example.png) +![Example Enroll (outdated)](/images/two_factor_enrollment_message_example.png) #### Disenrolling a user in two factor -To disenroll a user from two factor, call [DisenrollTwoFactor(DiscordUser.Id)](xref:DisCatSharp.Extensions.TwoFactorCommands.TwoFactorExtensionUtilities.DisenrollTwoFactor*) on your [DiscordClient](xref:DisCatSharp.DiscordClient) instance. +To disenroll a user from two factor, call [DisenrollTwoFactor(DiscordUser.Id)](xref:DisCatSharp.Extensions.TwoFactorCommands.ApplicationCommands.UnenrollTwoFactorAsync*) on your [InteractionContext](xref:DisCatSharp.ApplicationCommands.Context.InteractionContext) instance. ```cs -using DisCatSharp.Extensions.TwoFactorCommands; +using DisCatSharp.Extensions.TwoFactorCommands.ApplicationCommands; // ... -[SlashCommand("disenroll", "Disenroll from two factor"), ApplicationCommandRequireEnrolledTwoFactor] -public static async Task DisenrollTwoFactor(InteractionContext ctx) -{ - // ... - ctx.Client.DisenrollTwoFactor(ctx.User.Id); -} +[SlashCommand("remove_two_factor", "Remove 2FA"), ApplicationCommandRequireEnrolledTwoFactor] +public static async Task RemoveTwoFactorAsync(InteractionContext ctx) + => await ctx.UnenrollTwoFactorAsync(); ``` #### Check if a user is enrolled in two factor diff --git a/DisCatSharp.Extensions.TwoFactorCommands/DisCatSharp.Extensions.TwoFactorCommands.csproj b/DisCatSharp.Extensions.TwoFactorCommands/DisCatSharp.Extensions.TwoFactorCommands.csproj index 7f93548..dc2627b 100644 --- a/DisCatSharp.Extensions.TwoFactorCommands/DisCatSharp.Extensions.TwoFactorCommands.csproj +++ b/DisCatSharp.Extensions.TwoFactorCommands/DisCatSharp.Extensions.TwoFactorCommands.csproj @@ -43,6 +43,7 @@ + diff --git a/DisCatSharp.Extensions.TwoFactorCommands/Enums/TwoFactorResult.cs b/DisCatSharp.Extensions.TwoFactorCommands/Enums/TwoFactorResult.cs index b8ffcf5..24bc721 100644 --- a/DisCatSharp.Extensions.TwoFactorCommands/Enums/TwoFactorResult.cs +++ b/DisCatSharp.Extensions.TwoFactorCommands/Enums/TwoFactorResult.cs @@ -47,6 +47,16 @@ public enum TwoFactorResult /// TimedOut = 4, + /// + /// Indicates that the enrollment process has been completed successfully. + /// + Enrolled = 5, + + /// + /// Indicates that the unenrollment process has been completed successfully. + /// + Unenrolled = 6, + /// /// This function is not implemented. /// diff --git a/DisCatSharp.Extensions.TwoFactorCommands/Extensions/ApplicationCommands/ApplicationCommandRequireEnrolledTwoFactorAttribute.cs b/DisCatSharp.Extensions.TwoFactorCommands/Extensions/ApplicationCommands/ApplicationCommandRequireEnrolledTwoFactorAttribute.cs index 41c2953..f8db251 100644 --- a/DisCatSharp.Extensions.TwoFactorCommands/Extensions/ApplicationCommands/ApplicationCommandRequireEnrolledTwoFactorAttribute.cs +++ b/DisCatSharp.Extensions.TwoFactorCommands/Extensions/ApplicationCommands/ApplicationCommandRequireEnrolledTwoFactorAttribute.cs @@ -44,5 +44,5 @@ public ApplicationCommandRequireEnrolledTwoFactorAttribute() /// Runs checks. /// public override Task ExecuteChecksAsync(BaseContext ctx) - => Task.FromResult(ctx.Client.GetTwoFactor().IsEnrolled(ctx.User.Id)); + => Task.FromResult(ctx.Client.GetTwoFactor()?.IsEnrolled(ctx.User.Id) ?? false); } diff --git a/DisCatSharp.Extensions.TwoFactorCommands/Extensions/ApplicationCommands/TwoFactorApplicationCommandExtension.cs b/DisCatSharp.Extensions.TwoFactorCommands/Extensions/ApplicationCommands/TwoFactorApplicationCommandExtension.cs index 28543bc..a2c1bd5 100644 --- a/DisCatSharp.Extensions.TwoFactorCommands/Extensions/ApplicationCommands/TwoFactorApplicationCommandExtension.cs +++ b/DisCatSharp.Extensions.TwoFactorCommands/Extensions/ApplicationCommands/TwoFactorApplicationCommandExtension.cs @@ -21,7 +21,9 @@ // SOFTWARE. using System; +using System.Linq; using System.Threading.Tasks; +using System.Web; using DisCatSharp.ApplicationCommands.Context; using DisCatSharp.Entities; @@ -31,10 +33,161 @@ using DisCatSharp.Extensions.TwoFactorCommands.Enums; using DisCatSharp.Interactivity.Extensions; +using TwoFactorAuthNet; + namespace DisCatSharp.Extensions.TwoFactorCommands.ApplicationCommands; public static class TwoFactorApplicationCommandExtension { + /// + /// Enrolls the user via modal input into two factor. + /// This uses DisCatSharp.Interactivity. + /// To be used as first action for application commands. + /// + /// The base context. + /// A . + public static async Task EnrollTwoFactorAsync(this BaseContext ctx) + { + var ext = ctx.Client.GetTwoFactor() ?? throw new InvalidOperationException("Two factor extension is not registered on this client."); + + var secret = ext.TwoFactorClient.CreateSecret(160, CryptoSecureRequirement.RequireSecure); + var label = $"{ext.Configuration.ResponseConfiguration.AuthenticatorAccountPrefix}: {HttpUtility.UrlEncode(ctx.User.Username)}"; + var otpauth = $"otpauth://totp/{label}?secret={secret}&issuer={ext.TwoFactorClient.Issuer}"; + var qrBlockText = otpauth.ToBlockText(); + + DiscordInteractionModalBuilder builder = new("Register Two Factor"); + builder.AddTextDisplayComponent(new DiscordTextDisplayComponent($"## Scan this QR code with your authenticator app:\n{qrBlockText.BlockCode()}\n## Can't scan it? Enter this secret key instead:\n{secret.BlockCode()}")); + builder.AddLabelComponent(new("Enter Code", "Enter the code given by your authenticator app.", new DiscordTextInputComponent(TextComponentStyle.Small, customId: "code", minLength: ext.Configuration.Digits, maxLength: ext.Configuration.Digits))); + await ctx.CreateModalResponseAsync(builder); + + var response = new TwoFactorResponse + { + Client = ctx.Client + }; + + var inter = await ctx.Client.GetInteractivity().WaitForModalAsync(builder.CustomId, TimeSpan.FromSeconds(ext.Configuration.TwoFactorTimeout)); + if (inter.TimedOut) + { + response.Result = TwoFactorResult.TimedOut; + + return response; + } + + response.ComponentInteraction = inter.Result; + + if (ext.Configuration.ResponseConfiguration.ShowResponse) + await inter.Result.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent("Checking..")], accentColor: DiscordColor.Blurple))); + var codeResult = inter.Result.Interaction.Data.ModalComponents + .OfType() + .Select(component => component.Component as DiscordTextInputComponent) + .FirstOrDefault(component => component?.CustomId == "code") + ?.Value; + if (string.IsNullOrWhiteSpace(codeResult) || codeResult.Any(dig => !char.IsDigit(dig))) + { + if (ext.Configuration.ResponseConfiguration.ShowResponse) + await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationFailureMessage)], accentColor: DiscordColor.Red))); + response.Result = TwoFactorResult.InvalidCode; + return response; + } + + var res = ext.TwoFactorClient.VerifyCode(secret, codeResult); + if (res) + { + ext.EnrollUser(ctx.User.Id, secret); + if (ext.Configuration.ResponseConfiguration.ShowResponse) + await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationEnrolledMessage)], accentColor: DiscordColor.Green))); + + response.Result = TwoFactorResult.Enrolled; + + return response; + } + + if (ext.Configuration.ResponseConfiguration.ShowResponse) + await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationFailureMessage)], accentColor: DiscordColor.Red))); + + response.Result = TwoFactorResult.InvalidCode; + + return response; + } + + /// + /// Unenrolls the user from two factor authentication via modal verification. + /// This uses DisCatSharp.Interactivity. + /// To be used as first action for application commands. + /// + /// The base context. + /// A . + public static async Task UnenrollTwoFactorAsync(this BaseContext ctx) + { + var ext = ctx.Client.GetTwoFactor() ?? throw new InvalidOperationException("Two factor extension is not registered on this client."); + + if (!ext.IsEnrolled(ctx.User.Id)) + { + if (ext.Configuration.ResponseConfiguration.ShowResponse) + await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationNotEnrolledMessage)], accentColor: DiscordColor.Orange))); + + return new() + { + Client = ctx.Client, + Result = TwoFactorResult.NotEnrolled + }; + } + + DiscordInteractionModalBuilder builder = new("Remove Two Factor"); + builder.AddTextDisplayComponent(new DiscordTextDisplayComponent("## To confirm removal, please enter your two factor authentication code below:")); + builder.AddLabelComponent(new("Two Factor Code", "The code displayed in your authenticator app.", new DiscordTextInputComponent(TextComponentStyle.Small, customId: "code", minLength: ext.Configuration.Digits, maxLength: ext.Configuration.Digits))); + await ctx.CreateModalResponseAsync(builder); + + var response = new TwoFactorResponse + { + Client = ctx.Client + }; + + var inter = await ctx.Client.GetInteractivity().WaitForModalAsync(builder.CustomId, TimeSpan.FromSeconds(ext.Configuration.TwoFactorTimeout)); + if (inter.TimedOut) + { + response.Result = TwoFactorResult.TimedOut; + + return response; + } + + response.ComponentInteraction = inter.Result; + + if (ext.Configuration.ResponseConfiguration.ShowResponse) + await inter.Result.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent("Checking..")], accentColor: DiscordColor.Blurple))); + var codeResult = inter.Result.Interaction.Data.ModalComponents + .OfType() + .Select(component => component.Component as DiscordTextInputComponent) + .FirstOrDefault(component => component?.CustomId == "code") + ?.Value; + if (string.IsNullOrWhiteSpace(codeResult) || codeResult.Any(dig => !char.IsDigit(dig))) + { + if (ext.Configuration.ResponseConfiguration.ShowResponse) + await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationFailureMessage)], accentColor: DiscordColor.Red))); + response.Result = TwoFactorResult.InvalidCode; + return response; + } + + var res = ext.IsValidCode(ctx.User.Id, codeResult); + if (res) + { + ext.DisenrollUser(ctx.User.Id); + if (ext.Configuration.ResponseConfiguration.ShowResponse) + await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent("Two factor authentication has been removed from your account.")], accentColor: DiscordColor.Green))); + + response.Result = TwoFactorResult.Unenrolled; + + return response; + } + + if (ext.Configuration.ResponseConfiguration.ShowResponse) + await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationFailureMessage)], accentColor: DiscordColor.Red))); + + response.Result = TwoFactorResult.InvalidCode; + + return response; + } + /// /// Asks the user via modal input for the two factor code. /// This uses DisCatSharp.Interactivity. @@ -44,12 +197,12 @@ public static class TwoFactorApplicationCommandExtension /// A . public static async Task RequestTwoFactorAsync(this BaseContext ctx) { - var ext = ctx.Client.GetTwoFactor(); + var ext = ctx.Client.GetTwoFactor() ?? throw new InvalidOperationException("Two factor extension is not registered on this client."); if (!ext.IsEnrolled(ctx.User.Id)) { if (ext.Configuration.ResponseConfiguration.ShowResponse) - await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithContent(ext.Configuration.ResponseConfiguration.AuthenticationNotEnrolledMessage)); + await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationNotEnrolledMessage)], accentColor: DiscordColor.Orange))); return new() { @@ -59,7 +212,8 @@ public static async Task RequestTwoFactorAsync(this BaseConte } DiscordInteractionModalBuilder builder = new(ext.Configuration.ResponseConfiguration.AuthenticationModalRequestTitle); - builder.AddTextComponent(new(TextComponentStyle.Small, "code", "Code", "123456", ext.Configuration.Digits, ext.Configuration.Digits)); + builder.AddTextDisplayComponent(new DiscordTextDisplayComponent("Please enter your two factor authentication code below:")); + builder.AddLabelComponent(new("Two Factor Code", "The code displayed in your authenticator app.", new DiscordTextInputComponent(TextComponentStyle.Small, customId: "code", minLength: ext.Configuration.Digits, maxLength: ext.Configuration.Digits))); await ctx.CreateModalResponseAsync(builder); var response = new TwoFactorResponse @@ -78,12 +232,21 @@ public static async Task RequestTwoFactorAsync(this BaseConte response.ComponentInteraction = inter.Result; if (ext.Configuration.ResponseConfiguration.ShowResponse) - await inter.Result.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithContent("Checking..")); - var res = ext.IsValidCode(ctx.User.Id, (inter.Result.Interaction.Data.ModalComponents[0] as DiscordTextInputComponent)!.Value!); + await inter.Result.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent("Checking..")], accentColor: DiscordColor.Blurple))); + var codeResult = (inter.Result.Interaction.Data.ModalComponents.OfType().First().Component as DiscordTextInputComponent)!.Value!; + if (codeResult.Any(dig => !char.IsDigit(dig))) + { + if (ext.Configuration.ResponseConfiguration.ShowResponse) + await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationFailureMessage)], accentColor: DiscordColor.Red))); + response.Result = TwoFactorResult.InvalidCode; + return response; + } + + var res = ext.IsValidCode(ctx.User.Id, codeResult); if (res) { if (ext.Configuration.ResponseConfiguration.ShowResponse) - await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent(ext.Configuration.ResponseConfiguration.AuthenticationSuccessMessage)); + await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationSuccessMessage)], accentColor: DiscordColor.Green))); response.Result = TwoFactorResult.ValidCode; @@ -91,7 +254,7 @@ public static async Task RequestTwoFactorAsync(this BaseConte } if (ext.Configuration.ResponseConfiguration.ShowResponse) - await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent(ext.Configuration.ResponseConfiguration.AuthenticationFailureMessage)); + await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationFailureMessage)], accentColor: DiscordColor.Red))); response.Result = TwoFactorResult.InvalidCode; @@ -108,12 +271,12 @@ public static async Task RequestTwoFactorAsync(this BaseConte /// A . public static async Task RequestTwoFactorAsync(this ComponentInteractionCreateEventArgs evt, DiscordClient client) { - var ext = client.GetTwoFactor(); + var ext = client.GetTwoFactor() ?? throw new InvalidOperationException("Two factor extension is not registered on this client."); if (!ext.IsEnrolled(evt.User.Id)) { if (ext.Configuration.ResponseConfiguration.ShowResponse) - await evt.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithContent(ext.Configuration.ResponseConfiguration.AuthenticationNotEnrolledMessage)); + await evt.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationNotEnrolledMessage)], accentColor: DiscordColor.Orange))); return new() { @@ -123,7 +286,8 @@ public static async Task RequestTwoFactorAsync(this Component } DiscordInteractionModalBuilder builder = new(ext.Configuration.ResponseConfiguration.AuthenticationModalRequestTitle); - builder.AddTextComponent(new(TextComponentStyle.Small, "code", "Code", "123456", ext.Configuration.Digits, ext.Configuration.Digits)); + builder.AddTextDisplayComponent(new DiscordTextDisplayComponent("Please enter your two factor authentication code below:")); + builder.AddLabelComponent(new("Two Factor Code", "The code displayed in your authenticator app.", new DiscordTextInputComponent(TextComponentStyle.Small, customId: "code", minLength: ext.Configuration.Digits, maxLength: ext.Configuration.Digits))); await evt.Interaction.CreateInteractionModalResponseAsync(builder); var response = new TwoFactorResponse @@ -142,12 +306,21 @@ public static async Task RequestTwoFactorAsync(this Component response.ComponentInteraction = inter.Result; if (ext.Configuration.ResponseConfiguration.ShowResponse) - await inter.Result.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithContent("Checking..")); - var res = ext.IsValidCode(evt.User.Id, (inter.Result.Interaction.Data.ModalComponents[0] as DiscordTextInputComponent)!.Value!); + await inter.Result.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent("Checking..")], accentColor: DiscordColor.Blurple))); + var codeResult = (inter.Result.Interaction.Data.ModalComponents.OfType().First().Component as DiscordTextInputComponent)!.Value!; + if (codeResult.Any(dig => !char.IsDigit(dig))) + { + if (ext.Configuration.ResponseConfiguration.ShowResponse) + await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationFailureMessage)], accentColor: DiscordColor.Red))); + response.Result = TwoFactorResult.InvalidCode; + return response; + } + + var res = ext.IsValidCode(evt.User.Id, codeResult); if (res) { if (ext.Configuration.ResponseConfiguration.ShowResponse) - await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent(ext.Configuration.ResponseConfiguration.AuthenticationSuccessMessage)); + await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationSuccessMessage)], accentColor: DiscordColor.Green))); response.Result = TwoFactorResult.ValidCode; @@ -155,7 +328,7 @@ public static async Task RequestTwoFactorAsync(this Component } if (ext.Configuration.ResponseConfiguration.ShowResponse) - await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent(ext.Configuration.ResponseConfiguration.AuthenticationFailureMessage)); + await inter.Result.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithV2Components().AddComponents(new DiscordContainerComponent([new DiscordTextDisplayComponent("Two Factor"), new DiscordTextDisplayComponent(ext.Configuration.ResponseConfiguration.AuthenticationFailureMessage)], accentColor: DiscordColor.Red))); response.Result = TwoFactorResult.InvalidCode; diff --git a/DisCatSharp.Extensions.TwoFactorCommands/Extensions/CommandsNext/CommandRequireEnrolledTwoFactorAttribute.cs b/DisCatSharp.Extensions.TwoFactorCommands/Extensions/CommandsNext/CommandRequireEnrolledTwoFactorAttribute.cs index 9ec3969..806ef98 100644 --- a/DisCatSharp.Extensions.TwoFactorCommands/Extensions/CommandsNext/CommandRequireEnrolledTwoFactorAttribute.cs +++ b/DisCatSharp.Extensions.TwoFactorCommands/Extensions/CommandsNext/CommandRequireEnrolledTwoFactorAttribute.cs @@ -44,5 +44,5 @@ public CommandRequireEnrolledTwoFactorAttribute() /// Runs checks. /// public override Task ExecuteCheckAsync(CommandContext ctx, bool help) - => Task.FromResult(ctx.Client.GetTwoFactor().IsEnrolled(ctx.User.Id)); + => Task.FromResult(ctx.Client.GetTwoFactor()?.IsEnrolled(ctx.User.Id) ?? false); } diff --git a/DisCatSharp.Extensions.TwoFactorCommands/TwoFactorConfiguration.cs b/DisCatSharp.Extensions.TwoFactorCommands/TwoFactorConfiguration.cs index 9c6c490..f753141 100644 --- a/DisCatSharp.Extensions.TwoFactorCommands/TwoFactorConfiguration.cs +++ b/DisCatSharp.Extensions.TwoFactorCommands/TwoFactorConfiguration.cs @@ -143,6 +143,18 @@ public TwoFactorResponseConfiguration() /// public string AuthenticationFailureMessage { internal get; set; } = "Code invalid.."; + /// + /// Sets the message when an user successfully enrolled into two factor auth. + /// Defaults to: You successfully enrolled in two factor. + /// + public string AuthenticationEnrolledMessage { internal get; set; } = "You successfully enrolled in two factor."; + + /// + /// Sets the message when an user successfully unenrolled from two factor auth. + /// Defaults to: You successfully unenrolled from two factor. + /// + public string AuthenticationUnenrolledMessage { internal get; set; } = "You successfully unenrolled from two factor."; + /// /// Sets the message when an user is not yet enrolled into two factor auth. /// Defaults to: You are not enrolled in two factor. diff --git a/DisCatSharp.Extensions.TwoFactorCommands/TwoFactorExtension.cs b/DisCatSharp.Extensions.TwoFactorCommands/TwoFactorExtension.cs index c854860..7388cb8 100644 --- a/DisCatSharp.Extensions.TwoFactorCommands/TwoFactorExtension.cs +++ b/DisCatSharp.Extensions.TwoFactorCommands/TwoFactorExtension.cs @@ -116,7 +116,7 @@ protected internal override void Setup(DiscordClient client) else { var v = a.GetName().Version; - var vs = v.ToString(3); + var vs = v!.ToString(3); if (v.Revision > 0) this.VersionString = $"{vs}, CI build {v.Revision}"; @@ -144,7 +144,7 @@ private bool HasData(ulong user) /// /// The user id to get data for. private string GetSecretFor(ulong user) - => this.DatabaseClient.Select(this._tableName, null, 1, null, new(this._userField, OperatorEnum.Equals, user.ToString())).Rows[0].ItemArray[1].ToString(); + => this.DatabaseClient.Select(this._tableName, null, 1, null, new(this._userField, OperatorEnum.Equals, user.ToString())).Rows[0].ItemArray[1]!.ToString()!; /// /// Adds a secret for the given user id to the database. @@ -174,7 +174,7 @@ private void RemoveSecret(ulong user) /// The user id entering the code. /// The code to check. /// Whether the code is valid. - internal bool IsValidCode(ulong user, string code) + public bool IsValidCode(ulong user, string code) => this.HasData(user) && this.TwoFactorClient.VerifyCode(this.GetSecretFor(user), code); /// @@ -182,7 +182,7 @@ internal bool IsValidCode(ulong user, string code) /// /// User id to check for enrollment. /// Whether the user is enrolled. - internal bool IsEnrolled(ulong user) + public bool IsEnrolled(ulong user) => this.HasData(user); /// @@ -190,13 +190,13 @@ internal bool IsEnrolled(ulong user) /// /// User id to enroll. /// Secret to use. - internal void EnrollUser(ulong user, string secret) + public void EnrollUser(ulong user, string secret) => this.AddSecretFor(user, secret); /// /// Unenrolls given user id from two factor auth. /// /// User id to unenroll. - internal void DisenrollUser(ulong user) + public void DisenrollUser(ulong user) => this.RemoveSecret(user); } diff --git a/DisCatSharp.Extensions.TwoFactorCommands/TwoFactorExtensionUtilities.cs b/DisCatSharp.Extensions.TwoFactorCommands/TwoFactorExtensionUtilities.cs index 4a77813..adad3da 100644 --- a/DisCatSharp.Extensions.TwoFactorCommands/TwoFactorExtensionUtilities.cs +++ b/DisCatSharp.Extensions.TwoFactorCommands/TwoFactorExtensionUtilities.cs @@ -20,11 +20,16 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System; using System.IO; +using System.Linq; +using System.Text; using System.Web; using DisCatSharp.Entities; +using QRCoder; + using TwoFactorAuthNet; namespace DisCatSharp.Extensions.TwoFactorCommands; @@ -38,7 +43,7 @@ public static class TwoFactorExtensionUtilities /// The user id to check. /// Whether the user is enrolled. public static bool CheckTwoFactorEnrollmentFor(this DiscordClient client, ulong user) - => client.GetTwoFactor().IsEnrolled(user); + => client.GetTwoFactor()?.IsEnrolled(user) ?? false; /// /// Removes the two factor registration for the given user id. @@ -46,7 +51,7 @@ public static bool CheckTwoFactorEnrollmentFor(this DiscordClient client, ulong /// The discord client. /// The user id to check. public static void DisenrollTwoFactor(this DiscordClient client, ulong user) - => client.GetTwoFactor().DisenrollUser(user); + => client.GetTwoFactor()?.DisenrollUser(user); /// /// Registers two factor for the given user. @@ -54,12 +59,12 @@ public static void DisenrollTwoFactor(this DiscordClient client, ulong user) /// The discord client. /// The user to check. /// - /// A where Secret is a string with the + /// A where Secret is a string with the /// secret itself and QrCode a MemoryStream with the qr code image. /// public static (string Secret, MemoryStream QrCode) EnrollTwoFactor(this DiscordClient client, DiscordUser user) { - var ext = client.GetTwoFactor(); + var ext = client.GetTwoFactor() ?? throw new InvalidOperationException("Two factor extension is not registered on this client."); var secret = ext.TwoFactorClient.CreateSecret(160, CryptoSecureRequirement.RequireSecure); ext.EnrollUser(user.Id, secret); var label = $"{ext.Configuration.ResponseConfiguration.AuthenticatorAccountPrefix}: {HttpUtility.UrlEncode(user.UsernameWithDiscriminator)}"; @@ -71,4 +76,99 @@ public static (string Secret, MemoryStream QrCode) EnrollTwoFactor(this DiscordC }; return (secret, ms); } + + /// + /// Converts the specified payload into a QR code represented as block text. + /// + /// The payload to encode. + /// Number of quiet-zone modules. + /// A string representing the QR code. + public static string ToBlockText(this string payload, int quietZoneModules = 4) + { + using var generator = new QRCodeGenerator(); + using var data = generator.CreateQrCode(payload, QRCodeGenerator.ECCLevel.M); + + var modules = data.ModuleMatrix; + var size = modules.Count; + + var total = size + (quietZoneModules * 2); + + bool Get(int x, int y) + { + x -= quietZoneModules; + y -= quietZoneModules; + + return x >= 0 && x < size && + y >= 0 && y < size && + modules[y][x]; + } + + var sb = new StringBuilder(); + + for (var y = 0; y < total; y += 2) + { + for (var x = 0; x < total; x++) + { + var top = Get(x, y); + var bottom = Get(x, y + 1); + + sb.Append((top, bottom) switch + { + (true, true) => '█', + (true, false) => '▀', + (false, true) => '▄', + _ => ' ' + }); + } + + sb.AppendLine(); + } + + return TrimHorizontalWhitespace(sb.ToString()); + } + + /// + /// Trims horizontal whitespace from each line of the input string, preserving a specified minimum padding on both sides. + /// + /// The input string containing one or more lines to be trimmed of horizontal whitespace. + /// The minimum number of characters to retain as padding on both the left and right sides of each trimmed line. Must + /// be zero or greater. + /// A string in which each line has been trimmed of leading and trailing horizontal whitespace, with the specified + /// minimum padding preserved. Returns the original input if no lines are present. + private static string TrimHorizontalWhitespace(string input, int minPadding = 2) + { + var lines = input.Replace("\r", "").Split('\n'); + if (lines.Length == 0) + return input; + + var width = lines.Max(l => l.Length); + + bool IsEmptyColumn(int col) + { + foreach (var line in lines) + { + if (col < line.Length && line[col] != ' ') + return false; + } + return true; + } + + var left = 0; + while (left < width && IsEmptyColumn(left)) left++; + + var right = width - 1; + while (right >= 0 && IsEmptyColumn(right)) right--; + + left = Math.Max(0, left - minPadding); + right = Math.Min(width - 1, right + minPadding); + + var trimmed = lines.Select(line => + { + if (line.Length <= left) return ""; + var len = Math.Min(right - left + 1, line.Length - left); + return line.Substring(left, len).TrimEnd(); + }); + + return string.Join("\n", trimmed); + } }