using Discord; using Discord.Interactions; using Discord.WebSocket; using MareSynchronosShared.Data; using MareSynchronosShared.Models; using MareSynchronosShared.Services; using MareSynchronosShared.Utils; using Microsoft.EntityFrameworkCore; using StackExchange.Redis; using System.Text.RegularExpressions; namespace MareSynchronosServices.Discord; public partial class MareWizardModule : InteractionModuleBase { private ILogger _logger; private IServiceProvider _services; private DiscordBotServices _botServices; private IConfigurationService _mareClientConfigurationService; private IConfigurationService _mareServicesConfiguration; private IConnectionMultiplexer _connectionMultiplexer; private Random random = new(); public MareWizardModule(ILogger logger, IServiceProvider services, DiscordBotServices botServices, IConfigurationService mareClientConfigurationService, IConfigurationService mareServicesConfiguration, IConnectionMultiplexer connectionMultiplexer) { _logger = logger; _services = services; _botServices = botServices; _mareClientConfigurationService = mareClientConfigurationService; _mareServicesConfiguration = mareServicesConfiguration; _connectionMultiplexer = connectionMultiplexer; } [ComponentInteraction("wizard-home:*")] public async Task StartWizard(bool init = false) { if (!init && !(await ValidateInteraction().ConfigureAwait(false))) return; _logger.LogInformation("{method}:{userId}", nameof(StartWizard), Context.Interaction.User.Id); using var mareDb = GetDbContext(); bool hasAccount = await mareDb.LodeStoneAuth.AnyAsync(u => u.DiscordId == Context.User.Id && u.StartedAt == null).ConfigureAwait(false); if (init) { bool isBanned = await mareDb.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 = _mareServicesConfiguration.GetValueOrDefault(nameof(ServicesConfiguration.DiscordRoleAprilFools2024), null) != null && DateTime.UtcNow.Month == 4 && DateTime.UtcNow.Day == 1 && DateTime.UtcNow.Year == 2024 && DateTime.UtcNow.Hour >= 12; #elif DEBUG bool isInAprilFoolsMode = true; #endif EmbedBuilder eb = new(); eb.WithTitle("Welcome to the Mare Synchronos 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 Mare 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 MareToken© 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 MareToken© management", "wizard-fools", ButtonStyle.Primary, new Emoji("💲")); } cb.WithButton("Delete", "wizard-delete", ButtonStyle.Danger, new Emoji("⚠️")); } 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); } } public class VanityUidModal : IModal { public string Title => "Set Vanity UID"; [InputLabel("Set your Vanity UID")] [ModalTextInput("vanity_uid", TextInputStyle.Short, "5-15 characters, underscore, dash", 5, 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 MareDbContext GetDbContext() { return _services.CreateScope().ServiceProvider.GetService(); } private async Task 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.Embed = eb.Build(); m.Components = cb.Build(); }).ConfigureAwait(false); } private async Task AddUserSelection(MareDbContext mareDb, ComponentBuilder cb, string customId) { var discordId = Context.User.Id; var existingAuth = await mareDb.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 mareDb.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(MareDbContext 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 GenerateLodestoneAuth(ulong discordid, string hashedLodestoneId, MareDbContext dbContext) { var auth = StringUtils.GenerateRandomString(32); 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+"); var matches = regex.Match(lodestoneUrl); var isLodestoneUrl = matches.Success; if (!isLodestoneUrl || matches.Groups.Count < 1) return null; lodestoneUrl = matches.Groups[0].ToString(); var stringId = lodestoneUrl.Split('/', StringSplitOptions.RemoveEmptyEntries).Last(); if (!int.TryParse(stringId, out int lodestoneId)) { return null; } return lodestoneId; } }