Files
LightlessServer/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.cs
celine 0f95f26c1c Implemented match group instead of tinkering with the URL string
We're using regex already anyways, so might as well take advantage of matching groups. Group 1 will always be the country code and group 2 always the ID
2025-11-01 22:47:05 +01:00

346 lines
16 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using LightlessSyncShared.Data;
using LightlessSyncShared.Models;
using LightlessSyncShared.Services;
using LightlessSyncShared.Utils;
using LightlessSyncShared.Utils.Configuration;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
using System.Text.RegularExpressions;
namespace LightlessSyncServices.Discord;
public partial class LightlessWizardModule : InteractionModuleBase
{
private ILogger<LightlessModule> _logger;
private DiscordBotServices _botServices;
private IConfigurationService<ServerConfiguration> _lightlessClientConfigurationService;
private IConfigurationService<ServicesConfiguration> _lightlessServicesConfiguration;
private IConnectionMultiplexer _connectionMultiplexer;
private readonly IDbContextFactory<LightlessDbContext> _dbContextFactory;
private Random random = new();
public LightlessWizardModule(ILogger<LightlessModule> logger, DiscordBotServices botServices,
IConfigurationService<ServerConfiguration> lightlessClientConfigurationService,
IConfigurationService<ServicesConfiguration> lightlessServicesConfiguration,
IConnectionMultiplexer connectionMultiplexer, IDbContextFactory<LightlessDbContext> dbContextFactory)
{
_logger = logger;
_botServices = botServices;
_lightlessClientConfigurationService = lightlessClientConfigurationService;
_lightlessServicesConfiguration = lightlessServicesConfiguration;
_connectionMultiplexer = connectionMultiplexer;
_dbContextFactory = dbContextFactory;
}
[ComponentInteraction("wizard-captcha:*")]
public async Task WizardCaptcha(bool init = false)
{
if (!init && !(await ValidateInteraction().ConfigureAwait(false))) return;
if (_botServices.VerifiedCaptchaUsers.Contains(Context.Interaction.User.Id))
{
await StartWizard(true).ConfigureAwait(false);
return;
}
EmbedBuilder eb = new();
Random rnd = new Random();
var correctButton = rnd.Next(4) + 1;
string nthButtonText = correctButton switch
{
1 => "first",
2 => "second",
3 => "third",
4 => "fourth",
_ => "unknown",
};
Emoji nthButtonEmoji = correctButton switch
{
1 => new Emoji("⬅️"),
2 => new Emoji("🤖"),
3 => new Emoji("‼️"),
4 => new Emoji("✉️"),
_ => "unknown",
};
eb.WithTitle("Lightless Bot Services Captcha");
eb.WithDescription("You are seeing this embed because you interact with this bot for the first time since the bot has been restarted." + Environment.NewLine + Environment.NewLine
+ "This bot __requires__ embeds for its function. To proceed, please verify you have embeds enabled." + Environment.NewLine
+ $"## To verify you have embeds enabled __press on the **{nthButtonText}** button ({nthButtonEmoji}).__");
eb.WithColor(Color.LightOrange);
int incorrectButtonHighlight = 1;
do
{
incorrectButtonHighlight = rnd.Next(4) + 1;
}
while (incorrectButtonHighlight == correctButton);
ComponentBuilder cb = new();
cb.WithButton("This", correctButton == 1 ? "wizard-home:false" : "wizard-captcha-fail:1", emote: new Emoji("⬅️"), style: incorrectButtonHighlight == 1 ? ButtonStyle.Primary : ButtonStyle.Secondary);
cb.WithButton("Bot", correctButton == 2 ? "wizard-home:false" : "wizard-captcha-fail:2", emote: new Emoji("🤖"), style: incorrectButtonHighlight == 2 ? ButtonStyle.Primary : ButtonStyle.Secondary);
cb.WithButton("Requires", correctButton == 3 ? "wizard-home:false" : "wizard-captcha-fail:3", emote: new Emoji("‼️"), style: incorrectButtonHighlight == 3 ? ButtonStyle.Primary : ButtonStyle.Secondary);
cb.WithButton("Embeds", correctButton == 4 ? "wizard-home:false" : "wizard-captcha-fail:4", emote: new Emoji("✉️"), style: incorrectButtonHighlight == 4 ? ButtonStyle.Primary : ButtonStyle.Secondary);
await InitOrUpdateInteraction(init, eb, cb).ConfigureAwait(false);
}
private async Task InitOrUpdateInteraction(bool init, EmbedBuilder eb, ComponentBuilder cb)
{
if (init)
{
await RespondAsync(embed: eb.Build(), components: cb.Build(), ephemeral: true).ConfigureAwait(false);
var resp = await GetOriginalResponseAsync().ConfigureAwait(false);
_botServices.ValidInteractions[Context.User.Id] = resp.Id;
_logger.LogInformation("Init Msg: {id}", resp.Id);
}
else
{
await ModifyInteraction(eb, cb).ConfigureAwait(false);
}
}
[ComponentInteraction("wizard-captcha-fail:*")]
public async Task WizardCaptchaFail(int button)
{
ComponentBuilder cb = new();
cb.WithButton("Restart (with Embeds enabled)", "wizard-captcha:false", emote: new Emoji("↩️"));
await ((Context.Interaction) as IComponentInteraction).UpdateAsync(m =>
{
m.Embed = null;
m.Content = "You pressed the wrong button. You likely have embeds disabled. Enable embeds in your Discord client (Settings -> Chat -> \"Show embeds and preview website links pasted into chat\") and try again.";
m.Components = cb.Build();
}).ConfigureAwait(false);
await _botServices.LogToChannel($"{Context.User.Mention} FAILED CAPTCHA").ConfigureAwait(false);
}
[ComponentInteraction("wizard-home:*")]
public async Task StartWizard(bool init = false)
{
if (!init && !(await ValidateInteraction().ConfigureAwait(false))) return;
if (!_botServices.VerifiedCaptchaUsers.Contains(Context.Interaction.User.Id))
_botServices.VerifiedCaptchaUsers.Add(Context.Interaction.User.Id);
_logger.LogInformation("{method}:{userId}", nameof(StartWizard), Context.Interaction.User.Id);
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
bool hasAccount = await lightlessDb.LodeStoneAuth.AnyAsync(u => u.DiscordId == Context.User.Id && u.StartedAt == null).ConfigureAwait(false);
if (init)
{
bool isBanned = await lightlessDb.BannedRegistrations.AnyAsync(u => u.DiscordIdOrLodestoneAuth == Context.User.Id.ToString()).ConfigureAwait(false);
if (isBanned)
{
EmbedBuilder ebBanned = new();
ebBanned.WithTitle("You are not welcome here");
ebBanned.WithDescription("Your Discord account is banned");
await RespondAsync(embed: ebBanned.Build(), ephemeral: true).ConfigureAwait(false);
return;
}
}
#if !DEBUG
bool isInAprilFoolsMode = _lightlessServicesConfiguration.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordRoleAprilFools2024), null) != null
&& DateTime.UtcNow.Month == 4 && DateTime.UtcNow.Day == 1 && DateTime.UtcNow.Year == 2024 && DateTime.UtcNow.Hour >= 10;
#elif DEBUG
bool isInAprilFoolsMode = true;
#endif
EmbedBuilder eb = new();
eb.WithTitle("Welcome to the Lightless Sync Service Bot for this server");
eb.WithDescription("Here is what you can do:" + Environment.NewLine + Environment.NewLine
+ (!hasAccount ? string.Empty : ("- Check your account status press \" User Info\"" + Environment.NewLine))
+ (hasAccount ? string.Empty : ("- Register a new Lightless Account press \"🌒 Register\"" + Environment.NewLine))
+ (!hasAccount ? string.Empty : ("- You lost your secret key press \"🏥 Recover\"" + Environment.NewLine))
+ (hasAccount ? string.Empty : ("- If you have changed your Discord account press \"🔗 Relink\"" + Environment.NewLine))
+ (!hasAccount ? string.Empty : ("- Create a secondary UIDs press \"2⃣ Secondary UID\"" + Environment.NewLine))
+ (!hasAccount ? string.Empty : ("- Set a Vanity UID press \"💅 Vanity IDs\"" + Environment.NewLine))
+ (!hasAccount ? string.Empty : (!isInAprilFoolsMode ? string.Empty : ("- Check your WorryCoin™ and LightlessToken© balance and add payment options" + Environment.NewLine)))
+ (!hasAccount ? string.Empty : ("- Delete your primary or secondary accounts with \"⚠️ Delete\""))
);
eb.WithColor(Color.Blue);
ComponentBuilder cb = new();
if (!hasAccount)
{
cb.WithButton("Register", "wizard-register", ButtonStyle.Primary, new Emoji("🌒"));
cb.WithButton("Relink", "wizard-relink", ButtonStyle.Secondary, new Emoji("🔗"));
}
else
{
cb.WithButton("User Info", "wizard-userinfo", ButtonStyle.Secondary, new Emoji(""));
cb.WithButton("Recover", "wizard-recover", ButtonStyle.Secondary, new Emoji("🏥"));
cb.WithButton("Secondary UID", "wizard-secondary", ButtonStyle.Secondary, new Emoji("2⃣"));
cb.WithButton("Vanity IDs", "wizard-vanity", ButtonStyle.Secondary, new Emoji("💅"));
if (isInAprilFoolsMode)
{
cb.WithButton("WorryCoin™ and LightlessToken© management", "wizard-fools", ButtonStyle.Primary, new Emoji("💲"));
}
cb.WithButton("Delete", "wizard-delete", ButtonStyle.Danger, new Emoji("⚠️"));
}
await InitOrUpdateInteraction(init, eb, cb).ConfigureAwait(false);
}
public class VanityUidModal : IModal
{
public string Title => "Set Vanity UID";
[InputLabel("Set your Vanity UID")]
[ModalTextInput("vanity_uid", TextInputStyle.Short, "3-15 characters, underscore, dash", 3, 15)]
public string DesiredVanityUID { get; set; }
}
public class VanityGidModal : IModal
{
public string Title => "Set Vanity Syncshell ID";
[InputLabel("Set your Vanity Syncshell ID")]
[ModalTextInput("vanity_gid", TextInputStyle.Short, "5-20 characters, underscore, dash", 5, 20)]
public string DesiredVanityGID { get; set; }
}
public class ConfirmDeletionModal : IModal
{
public string Title => "Confirm Account Deletion";
[InputLabel("Enter \"DELETE\" in all Caps")]
[ModalTextInput("confirmation", TextInputStyle.Short, "Enter DELETE")]
public string Delete { get; set; }
}
private async Task<LightlessDbContext> GetDbContext()
{
return await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
}
private async Task<bool> ValidateInteraction()
{
if (Context.Interaction is not IComponentInteraction componentInteraction) return true;
if (_botServices.ValidInteractions.TryGetValue(Context.User.Id, out ulong interactionId) && interactionId == componentInteraction.Message.Id)
{
return true;
}
EmbedBuilder eb = new();
eb.WithTitle("Session expired");
eb.WithDescription("This session has expired since you have either again pressed \"Start\" on the initial message or the bot has been restarted." + Environment.NewLine + Environment.NewLine
+ "Please use the newly started interaction or start a new one.");
eb.WithColor(Color.Red);
ComponentBuilder cb = new();
await ModifyInteraction(eb, cb).ConfigureAwait(false);
return false;
}
private void AddHome(ComponentBuilder cb)
{
cb.WithButton("Return to Home", "wizard-home:false", ButtonStyle.Secondary, new Emoji("🏠"));
}
private async Task ModifyModalInteraction(EmbedBuilder eb, ComponentBuilder cb)
{
await (Context.Interaction as SocketModal).UpdateAsync(m =>
{
m.Embed = eb.Build();
m.Components = cb.Build();
}).ConfigureAwait(false);
}
private async Task ModifyInteraction(EmbedBuilder eb, ComponentBuilder cb)
{
await ((Context.Interaction) as IComponentInteraction).UpdateAsync(m =>
{
m.Content = null;
m.Embed = eb.Build();
m.Components = cb.Build();
}).ConfigureAwait(false);
}
private async Task AddUserSelection(LightlessDbContext lightlessDb, ComponentBuilder cb, string customId)
{
var discordId = Context.User.Id;
var existingAuth = await lightlessDb.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(e => e.DiscordId == discordId).ConfigureAwait(false);
if (existingAuth != null)
{
SelectMenuBuilder sb = new();
sb.WithPlaceholder("Select a UID");
sb.WithCustomId(customId);
var existingUids = await lightlessDb.Auth.Include(u => u.User).Where(u => u.UserUID == existingAuth.User.UID || u.PrimaryUserUID == existingAuth.User.UID)
.OrderByDescending(u => u.PrimaryUser == null).ToListAsync().ConfigureAwait(false);
foreach (var entry in existingUids)
{
sb.AddOption(string.IsNullOrEmpty(entry.User.Alias) ? entry.UserUID : entry.User.Alias,
entry.UserUID,
!string.IsNullOrEmpty(entry.User.Alias) ? entry.User.UID : null,
entry.PrimaryUserUID == null ? new Emoji("1⃣") : new Emoji("2⃣"));
}
cb.WithSelectMenu(sb);
}
}
private async Task AddGroupSelection(LightlessDbContext db, ComponentBuilder cb, string customId)
{
var primary = (await db.LodeStoneAuth.Include(u => u.User).SingleAsync(u => u.DiscordId == Context.User.Id).ConfigureAwait(false)).User;
var secondary = await db.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == primary.UID).Select(u => u.User).ToListAsync().ConfigureAwait(false);
var primaryGids = (await db.Groups.Include(u => u.Owner).Where(u => u.OwnerUID == primary.UID).ToListAsync().ConfigureAwait(false));
var secondaryGids = (await db.Groups.Include(u => u.Owner).Where(u => secondary.Select(u => u.UID).Contains(u.OwnerUID)).ToListAsync().ConfigureAwait(false));
SelectMenuBuilder gids = new();
if (primaryGids.Any() || secondaryGids.Any())
{
foreach (var item in primaryGids)
{
gids.AddOption(item.Alias ?? item.GID, item.GID, (item.Alias == null ? string.Empty : item.GID) + $" ({item.Owner.Alias ?? item.Owner.UID})", new Emoji("1⃣"));
}
foreach (var item in secondaryGids)
{
gids.AddOption(item.Alias ?? item.GID, item.GID, (item.Alias == null ? string.Empty : item.GID) + $" ({item.Owner.Alias ?? item.Owner.UID})", new Emoji("2⃣"));
}
gids.WithCustomId(customId);
gids.WithPlaceholder("Select a Syncshell");
cb.WithSelectMenu(gids);
}
}
private async Task<string> GenerateLodestoneAuth(ulong discordid, string hashedLodestoneId, LightlessDbContext dbContext)
{
var auth = StringUtils.GenerateRandomString(12, "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz");
LodeStoneAuth lsAuth = new LodeStoneAuth()
{
DiscordId = discordid,
HashedLodestoneId = hashedLodestoneId,
LodestoneAuthString = auth,
StartedAt = DateTime.UtcNow
};
dbContext.Add(lsAuth);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
return (auth);
}
private int? ParseCharacterIdFromLodestoneUrl(string lodestoneUrl)
{
var regex = new Regex(@"^https:\/\/(na|eu|de|fr|jp)\.finalfantasyxiv\.com\/lodestone\/character\/(\d{8})/?$");
var matches = regex.Match(lodestoneUrl);
var isLodestoneUrl = matches.Success;
if (!isLodestoneUrl || matches.Groups.Count < 1) return null;
var stringId = matches.Groups[2].ToString();
if (!int.TryParse(stringId, out int lodestoneId))
{
return null;
}
return lodestoneId;
}
}