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:
-
+
#### 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);
+ }
}