diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs index 40efde4..94070ad 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs @@ -2,15 +2,10 @@ using Discord.Interactions; using Discord.Rest; using Discord.WebSocket; -using MareSynchronos.API.Data.Enum; -using MareSynchronos.API.Dto.User; -using MareSynchronos.API.SignalR; -using MareSynchronosServer.Hubs; using MareSynchronosShared.Data; using MareSynchronosShared.Models; using MareSynchronosShared.Services; using MareSynchronosShared.Utils.Configuration; -using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using StackExchange.Redis; @@ -23,20 +18,20 @@ internal class DiscordBot : IHostedService private readonly IConnectionMultiplexer _connectionMultiplexer; private readonly DiscordSocketClient _discordClient; private readonly ILogger _logger; - private readonly IHubContext _mareHubContext; + private readonly IDbContextFactory _dbContextFactory; private readonly IServiceProvider _services; private InteractionService _interactionModule; - private CancellationTokenSource? _processReportQueueCts; + private readonly CancellationTokenSource? _processReportQueueCts; private CancellationTokenSource? _updateStatusCts; public DiscordBot(DiscordBotServices botServices, IServiceProvider services, IConfigurationService configuration, - IHubContext mareHubContext, + IDbContextFactory dbContextFactory, ILogger logger, IConnectionMultiplexer connectionMultiplexer) { _botServices = botServices; _services = services; _configurationService = configuration; - _mareHubContext = mareHubContext; + _dbContextFactory = dbContextFactory; _logger = logger; _connectionMultiplexer = connectionMultiplexer; _discordClient = new(new DiscordSocketConfig() @@ -55,6 +50,7 @@ internal class DiscordBot : IHostedService _logger.LogInformation("Starting DiscordBot"); _logger.LogInformation("Using Configuration: " + _configurationService.ToString()); + _interactionModule?.Dispose(); _interactionModule = new InteractionService(_discordClient); _interactionModule.Log += Log; await _interactionModule.AddModuleAsync(typeof(MareModule), _services).ConfigureAwait(false); @@ -64,7 +60,6 @@ internal class DiscordBot : IHostedService await _discordClient.StartAsync().ConfigureAwait(false); _discordClient.Ready += DiscordClient_Ready; - _discordClient.ButtonExecuted += ButtonExecutedHandler; _discordClient.InteractionCreated += async (x) => { var ctx = new SocketInteractionContext(_discordClient, x); @@ -79,124 +74,16 @@ internal class DiscordBot : IHostedService { if (!string.IsNullOrEmpty(_configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty))) { - _discordClient.ButtonExecuted -= ButtonExecutedHandler; - await _botServices.Stop().ConfigureAwait(false); _processReportQueueCts?.Cancel(); _updateStatusCts?.Cancel(); await _discordClient.LogoutAsync().ConfigureAwait(false); await _discordClient.StopAsync().ConfigureAwait(false); + _interactionModule?.Dispose(); } } - private async Task ButtonExecutedHandler(SocketMessageComponent arg) - { - var id = arg.Data.CustomId; - if (!id.StartsWith("mare-report-button", StringComparison.Ordinal)) return; - - var userId = arg.User.Id; - using var scope = _services.CreateScope(); - using var dbContext = scope.ServiceProvider.GetRequiredService(); - var user = await dbContext.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == userId).ConfigureAwait(false); - - if (user == null || (!user.User.IsModerator && !user.User.IsAdmin)) - { - EmbedBuilder eb = new(); - eb.WithTitle($"Cannot resolve report"); - eb.WithDescription($"<@{userId}>: You have no rights to resolve this report"); - await arg.RespondAsync(embed: eb.Build()).ConfigureAwait(false); - return; - } - - id = id.Remove(0, "mare-report-button-".Length); - var split = id.Split('-', StringSplitOptions.RemoveEmptyEntries); - - var profile = await dbContext.UserProfileData.SingleAsync(u => u.UserUID == split[1]).ConfigureAwait(false); - - var embed = arg.Message.Embeds.First(); - - var builder = embed.ToEmbedBuilder(); - var otherPairs = await dbContext.ClientPairs.Where(p => p.UserUID == split[1]).Select(p => p.OtherUserUID).ToListAsync().ConfigureAwait(false); - switch (split[0]) - { - case "dismiss": - builder.AddField("Resolution", $"Dismissed by <@{userId}>"); - builder.WithColor(Color.Green); - profile.FlaggedForReport = false; - await _mareHubContext.Clients.User(split[1]).SendAsync(nameof(IMareHub.Client_ReceiveServerMessage), - MessageSeverity.Warning, "The Mare profile report against you has been evaluated and your profile re-enabled.") - .ConfigureAwait(false); - break; - - case "banreporting": - builder.AddField("Resolution", $"Dismissed by <@{userId}>, Reporting user banned"); - builder.WithColor(Color.DarkGreen); - profile.FlaggedForReport = false; - var reportingUser = await dbContext.Auth.SingleAsync(u => u.UserUID == split[2]).ConfigureAwait(false); - reportingUser.MarkForBan = true; - var regReporting = await dbContext.LodeStoneAuth.SingleAsync(u => u.User.UID == reportingUser.UserUID).ConfigureAwait(false); - dbContext.BannedRegistrations.Add(new MareSynchronosShared.Models.BannedRegistrations() - { - DiscordIdOrLodestoneAuth = regReporting.HashedLodestoneId - }); - dbContext.BannedRegistrations.Add(new MareSynchronosShared.Models.BannedRegistrations() - { - DiscordIdOrLodestoneAuth = regReporting.DiscordId.ToString() - }); - await _mareHubContext.Clients.User(split[1]).SendAsync(nameof(IMareHub.Client_ReceiveServerMessage), - MessageSeverity.Warning, "The Mare profile report against you has been evaluated and your profile re-enabled.") - .ConfigureAwait(false); - break; - - case "banprofile": - builder.AddField("Resolution", $"Profile has been banned by <@{userId}>"); - builder.WithColor(Color.Red); - profile.Base64ProfileImage = null; - profile.UserDescription = null; - profile.ProfileDisabled = true; - profile.FlaggedForReport = false; - await _mareHubContext.Clients.User(split[1]).SendAsync(nameof(IMareHub.Client_ReceiveServerMessage), - MessageSeverity.Warning, "The Mare profile report against you has been evaluated and the profile functionality permanently disabled.") - .ConfigureAwait(false); - break; - - case "banuser": - builder.AddField("Resolution", $"User has been banned by <@{userId}>"); - builder.WithColor(Color.DarkRed); - var offendingUser = await dbContext.Auth.SingleAsync(u => u.UserUID == split[1]).ConfigureAwait(false); - offendingUser.MarkForBan = true; - profile.Base64ProfileImage = null; - profile.UserDescription = null; - profile.ProfileDisabled = true; - var reg = await dbContext.LodeStoneAuth.SingleAsync(u => u.User.UID == offendingUser.UserUID).ConfigureAwait(false); - dbContext.BannedRegistrations.Add(new MareSynchronosShared.Models.BannedRegistrations() - { - DiscordIdOrLodestoneAuth = reg.HashedLodestoneId - }); - dbContext.BannedRegistrations.Add(new MareSynchronosShared.Models.BannedRegistrations() - { - DiscordIdOrLodestoneAuth = reg.DiscordId.ToString() - }); - await _mareHubContext.Clients.User(split[1]).SendAsync(nameof(IMareHub.Client_ReceiveServerMessage), - MessageSeverity.Warning, "The Mare profile report against you has been evaluated and your account permanently banned.") - .ConfigureAwait(false); - break; - } - - await dbContext.SaveChangesAsync().ConfigureAwait(false); - - await _mareHubContext.Clients.Users(otherPairs).SendAsync(nameof(IMareHub.Client_UserUpdateProfile), new UserDto(new(split[1]))).ConfigureAwait(false); - await _mareHubContext.Clients.User(split[1]).SendAsync(nameof(IMareHub.Client_UserUpdateProfile), new UserDto(new(split[1]))).ConfigureAwait(false); - - await arg.Message.ModifyAsync(msg => - { - msg.Content = arg.Message.Content; - msg.Components = null; - msg.Embed = new Optional(builder.Build()); - }).ConfigureAwait(false); - } - private async Task DiscordClient_Ready() { var guild = (await _discordClient.Rest.GetGuildsAsync().ConfigureAwait(false)).First(); @@ -229,7 +116,8 @@ internal class DiscordBot : IHostedService _logger.LogInformation("Adding Role: {id} => {desc}", role.Key, role.Value); var restrole = guild.GetRole(role.Key); - if (restrole != null) _botServices.VanityRoles.Add(restrole, role.Value); + if (restrole != null) + _botServices.VanityRoles[restrole] = role.Value; } } @@ -330,7 +218,7 @@ internal class DiscordBot : IHostedService { _logger.LogInformation($"Cleaning up Vanity UIDs"); await _botServices.LogToChannel("Cleaning up Vanity UIDs").ConfigureAwait(false); - _logger.LogInformation("Getting application commands from guild {guildName}", guild.Name); + _logger.LogInformation("Getting rest guild {guildName}", guild.Name); var restGuild = await _discordClient.Rest.GetGuildAsync(guild.Id).ConfigureAwait(false); Dictionary allowedRoleIds = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.VanityRoles), new Dictionary()); @@ -338,8 +226,7 @@ internal class DiscordBot : IHostedService if (allowedRoleIds.Any()) { - await using var scope = _services.CreateAsyncScope(); - await using var db = scope.ServiceProvider.GetRequiredService(); + using var db = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); var aliasedUsers = await db.LodeStoneAuth.Include("User") .Where(c => c.User != null && !string.IsNullOrEmpty(c.User.Alias)).ToListAsync().ConfigureAwait(false); @@ -348,49 +235,14 @@ internal class DiscordBot : IHostedService foreach (var lodestoneAuth in aliasedUsers) { - var discordUser = await restGuild.GetUserAsync(lodestoneAuth.DiscordId).ConfigureAwait(false); - _logger.LogInformation($"Checking User: {lodestoneAuth.DiscordId}, {lodestoneAuth.User.UID} ({lodestoneAuth.User.Alias}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List())}"); - - if (discordUser == null || !discordUser.RoleIds.Any(u => allowedRoleIds.Keys.Contains(u))) - { - _logger.LogInformation($"User {lodestoneAuth.User.UID} not in allowed roles, deleting alias"); - await _botServices.LogToChannel($"VANITY UID REMOVAL: <@{lodestoneAuth.DiscordId}> - UID: {lodestoneAuth.User.UID}, Vanity: {lodestoneAuth.User.Alias}").ConfigureAwait(false); - lodestoneAuth.User.Alias = null; - var secondaryUsers = await db.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == lodestoneAuth.User.UID).ToListAsync().ConfigureAwait(false); - foreach (var secondaryUser in secondaryUsers) - { - _logger.LogInformation($"Secondary User {secondaryUser.User.UID} not in allowed roles, deleting alias"); - - secondaryUser.User.Alias = null; - db.Update(secondaryUser.User); - } - db.Update(lodestoneAuth.User); - await db.SaveChangesAsync(token).ConfigureAwait(false); - } + await CheckVanityForUser(restGuild, allowedRoleIds, db, lodestoneAuth, token).ConfigureAwait(false); await Task.Delay(1000, token).ConfigureAwait(false); } foreach (var group in aliasedGroups) { - var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(f => f.User.UID == group.OwnerUID).ConfigureAwait(false); - RestGuildUser discordUser = null; - if (lodestoneUser != null) - { - discordUser = await restGuild.GetUserAsync(lodestoneUser.DiscordId).ConfigureAwait(false); - } - - _logger.LogInformation($"Checking Group: {group.GID}, owned by {lodestoneUser?.User?.UID ?? string.Empty} ({lodestoneUser?.User?.Alias ?? string.Empty}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List())}"); - - if (lodestoneUser == null || discordUser == null || !discordUser.RoleIds.Any(u => allowedRoleIds.Keys.Contains(u))) - { - await _botServices.LogToChannel($"VANITY GID REMOVAL: <@{lodestoneUser.DiscordId}> ({lodestoneUser.User.UID}) - GID: {group.GID}, Vanity: {group.Alias}").ConfigureAwait(false); - - _logger.LogInformation($"User {lodestoneUser.User.UID} not in allowed roles, deleting group alias"); - group.Alias = null; - db.Update(group); - await db.SaveChangesAsync(token).ConfigureAwait(false); - } + await CheckVanityForGroup(restGuild, allowedRoleIds, db, group, token).ConfigureAwait(false); await Task.Delay(1000, token).ConfigureAwait(false); } @@ -410,6 +262,51 @@ internal class DiscordBot : IHostedService } } + private async Task CheckVanityForGroup(RestGuild restGuild, Dictionary allowedRoleIds, MareDbContext db, Group group, CancellationToken token) + { + var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(f => f.User.UID == group.OwnerUID).ConfigureAwait(false); + RestGuildUser discordUser = null; + if (lodestoneUser != null) + { + discordUser = await restGuild.GetUserAsync(lodestoneUser.DiscordId).ConfigureAwait(false); + } + + _logger.LogInformation($"Checking Group: {group.GID} [{group.Alias}], owned by {lodestoneUser?.User?.UID ?? string.Empty} ({lodestoneUser?.User?.Alias ?? string.Empty}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List())}"); + + if (lodestoneUser == null || discordUser == null || !discordUser.RoleIds.Any(allowedRoleIds.Keys.Contains)) + { + await _botServices.LogToChannel($"VANITY GID REMOVAL: <@{lodestoneUser?.DiscordId ?? 0}> ({lodestoneUser?.User?.UID}) - GID: {group.GID}, Vanity: {group.Alias}").ConfigureAwait(false); + + _logger.LogInformation($"User {lodestoneUser?.User?.UID ?? "unknown"} not in allowed roles, deleting group alias for {group.GID}"); + group.Alias = null; + db.Update(group); + await db.SaveChangesAsync(token).ConfigureAwait(false); + } + } + + private async Task CheckVanityForUser(RestGuild restGuild, Dictionary allowedRoleIds, MareDbContext db, LodeStoneAuth lodestoneAuth, CancellationToken token) + { + var discordUser = await restGuild.GetUserAsync(lodestoneAuth.DiscordId).ConfigureAwait(false); + _logger.LogInformation($"Checking User: {lodestoneAuth.DiscordId}, {lodestoneAuth.User.UID} ({lodestoneAuth.User.Alias}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List())}"); + + if (discordUser == null || !discordUser.RoleIds.Any(u => allowedRoleIds.Keys.Contains(u))) + { + _logger.LogInformation($"User {lodestoneAuth.User.UID} not in allowed roles, deleting alias"); + await _botServices.LogToChannel($"VANITY UID REMOVAL: <@{lodestoneAuth.DiscordId}> - UID: {lodestoneAuth.User.UID}, Vanity: {lodestoneAuth.User.Alias}").ConfigureAwait(false); + lodestoneAuth.User.Alias = null; + var secondaryUsers = await db.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == lodestoneAuth.User.UID).ToListAsync().ConfigureAwait(false); + foreach (var secondaryUser in secondaryUsers) + { + _logger.LogInformation($"Secondary User {secondaryUser.User.UID} not in allowed roles, deleting alias"); + + secondaryUser.User.Alias = null; + db.Update(secondaryUser.User); + } + db.Update(lodestoneAuth.User); + await db.SaveChangesAsync(token).ConfigureAwait(false); + } + } + private async Task UpdateStatusAsync(CancellationToken token) { while (!token.IsCancellationRequested) @@ -419,7 +316,7 @@ internal class DiscordBot : IHostedService _logger.LogInformation("Users online: " + onlineUsers); await _discordClient.SetActivityAsync(new Game("Mare for " + onlineUsers + " Users")).ConfigureAwait(false); - await Task.Delay(TimeSpan.FromSeconds(15)).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs index 6caa263..cdef26f 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs @@ -8,18 +8,17 @@ namespace MareSynchronosServices.Discord; public class DiscordBotServices { - public readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "de" }; + public readonly string[] LodestoneServers = ["eu", "na", "jp", "fr", "de"]; public ConcurrentDictionary DiscordLodestoneMapping = new(); public ConcurrentDictionary DiscordRelinkLodestoneMapping = new(); public ConcurrentDictionary DiscordVerifiedUsers { get; } = new(); public ConcurrentDictionary LastVanityChange = new(); - public ConcurrentDictionary LastVanityGidChange = new(); + public ConcurrentDictionary LastVanityGidChange = new(StringComparer.Ordinal); public ConcurrentDictionary ValidInteractions { get; } = new(); - public Dictionary VanityRoles { get; set; } = new(); + public ConcurrentDictionary VanityRoles { get; set; } = new(); public ConcurrentBag VerifiedCaptchaUsers { get; } = new(); - private readonly IServiceProvider _serviceProvider; private readonly IConfigurationService _configuration; - private CancellationTokenSource verificationTaskCts; + private readonly CancellationTokenSource verificationTaskCts = new(); private RestGuild? _guild; private ulong? _logChannelId; private RestTextChannel? _logChannel; @@ -44,7 +43,8 @@ public class DiscordBotServices public Task Stop() { - verificationTaskCts?.Cancel(); + verificationTaskCts.Cancel(); + verificationTaskCts.Dispose(); return Task.CompletedTask; } @@ -73,7 +73,6 @@ public class DiscordBotServices private async Task ProcessVerificationQueue() { - verificationTaskCts = new CancellationTokenSource(); while (!verificationTaskCts.IsCancellationRequested) { Logger.LogDebug("Processing Verification Queue, Entries: {entr}", VerificationQueue.Count); diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Delete.cs b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Delete.cs index 14ac0bd..8259ed2 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Delete.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Delete.cs @@ -1,6 +1,5 @@ using Discord.Interactions; using Discord; -using MareSynchronosShared.Data; using MareSynchronosShared.Utils; using MareSynchronosShared.Utils.Configuration; @@ -15,7 +14,7 @@ public partial class MareWizardModule _logger.LogInformation("{method}:{userId}", nameof(ComponentDelete), Context.Interaction.User.Id); - using var mareDb = GetDbContext(); + using var mareDb = await GetDbContext().ConfigureAwait(false); EmbedBuilder eb = new(); eb.WithTitle("Delete Account"); eb.WithDescription("You can delete your primary or secondary UIDs here." + Environment.NewLine + Environment.NewLine @@ -38,7 +37,7 @@ public partial class MareWizardModule _logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionDeleteAccount), Context.Interaction.User.Id, uid); - using var mareDb = GetDbContext(); + using var mareDb = await GetDbContext().ConfigureAwait(false); bool isPrimary = mareDb.Auth.Single(u => u.UserUID == uid).PrimaryUserUID == null; EmbedBuilder eb = new(); eb.WithTitle($"Are you sure you want to delete {uid}?"); @@ -88,7 +87,7 @@ public partial class MareWizardModule { var maxGroupsByUser = _mareClientConfigurationService.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 3); - using var db = GetDbContext(); + using var db = await GetDbContext().ConfigureAwait(false); var user = db.Users.Single(u => u.UID == uid); await SharedDbFunctions.PurgeUser(_logger, user, db, maxGroupsByUser).ConfigureAwait(false); diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Recover.cs b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Recover.cs index fc99936..56569c9 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Recover.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Recover.cs @@ -16,7 +16,7 @@ public partial class MareWizardModule _logger.LogInformation("{method}:{userId}", nameof(ComponentRecover), Context.Interaction.User.Id); - using var mareDb = GetDbContext(); + using var mareDb = await GetDbContext().ConfigureAwait(false); EmbedBuilder eb = new(); eb.WithColor(Color.Blue); eb.WithTitle("Recover"); @@ -25,7 +25,8 @@ public partial class MareWizardModule + "Use the selection below to select the user account you want to recover." + Environment.NewLine + Environment.NewLine + "- 1️⃣ is your primary account/UID" + Environment.NewLine + "- 2️⃣ are all your secondary accounts/UIDs" + Environment.NewLine - + "If you are using Vanity UIDs the original UID is displayed in the second line of the account selection."); + + "If you are using Vanity UIDs the original UID is displayed in the second line of the account selection." + Environment.NewLine + + "# Note: instead of recovery and handling secret keys the switch to OAuth2 authentication is strongly suggested."); ComponentBuilder cb = new(); await AddUserSelection(mareDb, cb, "wizard-recover-select").ConfigureAwait(false); AddHome(cb); @@ -39,7 +40,7 @@ public partial class MareWizardModule _logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionRecovery), Context.Interaction.User.Id, uid); - using var mareDb = GetDbContext(); + using var mareDb = await GetDbContext().ConfigureAwait(false); EmbedBuilder eb = new(); eb.WithColor(Color.Green); await HandleRecovery(mareDb, eb, uid).ConfigureAwait(false); @@ -75,6 +76,8 @@ public partial class MareWizardModule + $"**{computedHash}**" + Environment.NewLine + "__NOTE: The Secret Key only contains the letters ABCDEF and numbers 0 - 9.__" + + Environment.NewLine + +" __NOTE: If you are using the suggested OAuth2 authentication, you do not need to use the Secret Key__" + Environment.NewLine + Environment.NewLine + "Enter this key in the Mare Synchronos Service Settings and reconnect to the service."); diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Register.cs b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Register.cs index 4d297a6..044da3b 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Register.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Register.cs @@ -22,8 +22,8 @@ public partial class MareWizardModule eb.WithDescription("Here you can start the registration process with the Mare Synchronos server of this Discord." + Environment.NewLine + Environment.NewLine + "- Have your Lodestone URL ready (i.e. https://eu.finalfantasyxiv.com/lodestone/character/XXXXXXXXX)" + Environment.NewLine + " - The registration requires you to modify your Lodestone profile with a generated code for verification" + Environment.NewLine - + " - You need to have a paid FFXIV account or someone who can assist you with registration if you can't edit your own Lodestone" + Environment.NewLine - + "- Do not use this on mobile because you will need to be able to copy the generated secret key" + Environment.NewLine); + + "- Do not use this on mobile because you will need to be able to copy the generated secret key" + Environment.NewLine + + "# Follow the bot instructions precisely. Slow down and read."); ComponentBuilder cb = new(); AddHome(cb); cb.WithButton("Start Registration", "wizard-register-start", ButtonStyle.Primary, emote: new Emoji("🌒")); @@ -37,7 +37,7 @@ public partial class MareWizardModule _logger.LogInformation("{method}:{userId}", nameof(ComponentRegisterStart), Context.Interaction.User.Id); - using var db = GetDbContext(); + using var db = await GetDbContext().ConfigureAwait(false); var entry = await db.LodeStoneAuth.SingleOrDefaultAsync(u => u.DiscordId == Context.User.Id && u.StartedAt != null).ConfigureAwait(false); if (entry != null) { @@ -123,7 +123,7 @@ public partial class MareWizardModule if (verified) { eb.WithColor(Color.Green); - using var db = _services.CreateScope().ServiceProvider.GetRequiredService(); + using var db = await GetDbContext().ConfigureAwait(false); var (uid, key) = await HandleAddUser(db).ConfigureAwait(false); eb.WithTitle($"Registration successful, your UID: {uid}"); eb.WithDescription("This is your private secret key. Do not share this private secret key with anyone. **If you lose it, it is irrevocably lost.**" @@ -134,6 +134,8 @@ public partial class MareWizardModule + Environment.NewLine + "__NOTE: The Secret Key only contains the letters ABCDEF and numbers 0 - 9.__" + Environment.NewLine + + " __NOTE: Secret keys are considered legacy. Using the suggested OAuth2 authentication, you do not need to use this Secret Key.__" + + Environment.NewLine + "You should connect as soon as possible to not get caught by the automatic cleanup process." + Environment.NewLine + "Have fun."); @@ -143,10 +145,16 @@ public partial class MareWizardModule { eb.WithColor(Color.Gold); eb.WithTitle("Failed to verify registration"); - eb.WithDescription("The bot was not able to find the required verification code on your Lodestone profile." + Environment.NewLine + Environment.NewLine - + "Please restart your verification process, make sure to save your profile _twice_ for it to be properly saved." + Environment.NewLine + Environment.NewLine - + "**Make sure your profile is set to public (All Users) for your character. The bot cannot read profiles with privacy settings set to \"logged in\" or \"private\".**" + Environment.NewLine + Environment.NewLine - + "The code the bot is looking for is" + Environment.NewLine + Environment.NewLine + eb.WithDescription("The bot was not able to find the required verification code on your Lodestone profile." + + Environment.NewLine + Environment.NewLine + + "Please restart your verification process, make sure to save your profile _twice_ for it to be properly saved." + + Environment.NewLine + Environment.NewLine + + "If this link does not lead to your profile edit page, you __need__ to configure the privacy settings first: https://na.finalfantasyxiv.com/lodestone/my/setting/profile/" + + Environment.NewLine + Environment.NewLine + + "**Make sure your profile is set to public (All Users) for your character. The bot cannot read profiles with privacy settings set to \"logged in\" or \"private\".**" + + Environment.NewLine + Environment.NewLine + + "## You __need__ to enter following the code this bot provided onto your lodestone in the character profile:" + + Environment.NewLine + Environment.NewLine + "**" + verificationCode + "**"); cb.WithButton("Cancel", "wizard-register", emote: new Emoji("❌")); cb.WithButton("Retry", "wizard-register-verify:" + verificationCode, ButtonStyle.Primary, emote: new Emoji("🔁")); @@ -168,11 +176,9 @@ public partial class MareWizardModule } // check if userid is already in db - using var scope = _services.CreateScope(); - var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString()); - using var db = scope.ServiceProvider.GetService(); + using var db = await GetDbContext().ConfigureAwait(false); // check if discord id or lodestone id is banned if (db.BannedRegistrations.Any(a => a.DiscordIdOrLodestoneAuth == hashedLodestoneId)) @@ -192,6 +198,8 @@ public partial class MareWizardModule // check if lodestone id is already in db embed.WithTitle("Authorize your character"); embed.WithDescription("Add following key to your character profile at https://na.finalfantasyxiv.com/lodestone/my/setting/profile/" + + Environment.NewLine + + "__NOTE: If the link does not lead you to your character edit profile page, you need to log in and set up your privacy settings!__" + Environment.NewLine + Environment.NewLine + $"**{lodestoneAuth}**" + Environment.NewLine + Environment.NewLine diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Relink.cs b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Relink.cs index 16bdae8..bad84e3 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Relink.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Relink.cs @@ -4,7 +4,6 @@ using MareSynchronosShared.Data; using MareSynchronosShared.Utils; using MareSynchronosShared.Models; using Microsoft.EntityFrameworkCore; -using System.Security.Policy; namespace MareSynchronosServices.Discord; @@ -37,7 +36,7 @@ public partial class MareWizardModule _logger.LogInformation("{method}:{userId}", nameof(ComponentRelinkStart), Context.Interaction.User.Id); - using var db = GetDbContext(); + using var db = await GetDbContext().ConfigureAwait(false); db.LodeStoneAuth.RemoveRange(db.LodeStoneAuth.Where(u => u.DiscordId == Context.User.Id)); _botServices.DiscordVerifiedUsers.TryRemove(Context.User.Id, out _); _botServices.DiscordRelinkLodestoneMapping.TryRemove(Context.User.Id, out _); @@ -119,7 +118,7 @@ public partial class MareWizardModule if (verified) { eb.WithColor(Color.Green); - using var db = _services.CreateScope().ServiceProvider.GetRequiredService(); + using var db = await GetDbContext().ConfigureAwait(false); var (_, key) = await HandleRelinkUser(db, uid).ConfigureAwait(false); eb.WithTitle($"Relink successful, your UID is again: {uid}"); eb.WithDescription("This is your private secret key. Do not share this private secret key with anyone. **If you lose it, it is irrevocably lost.**" @@ -161,11 +160,9 @@ public partial class MareWizardModule return (false, string.Empty, string.Empty); } // check if userid is already in db - using var scope = _services.CreateScope(); - var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString()); - using var db = scope.ServiceProvider.GetService(); + using var db = await GetDbContext().ConfigureAwait(false); // check if discord id or lodestone id is banned if (db.BannedRegistrations.Any(a => a.DiscordIdOrLodestoneAuth == hashedLodestoneId)) diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Secondary.cs b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Secondary.cs index 1dbf530..c7540a6 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Secondary.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Secondary.cs @@ -16,7 +16,7 @@ public partial class MareWizardModule _logger.LogInformation("{method}:{userId}", nameof(ComponentSecondary), Context.Interaction.User.Id); - using var mareDb = GetDbContext(); + using var mareDb = await GetDbContext().ConfigureAwait(false); var primaryUID = (await mareDb.LodeStoneAuth.Include(u => u.User).SingleAsync(u => u.DiscordId == Context.User.Id).ConfigureAwait(false)).User.UID; var secondaryUids = await mareDb.Auth.CountAsync(p => p.PrimaryUserUID == primaryUID).ConfigureAwait(false); EmbedBuilder eb = new(); @@ -40,7 +40,7 @@ public partial class MareWizardModule _logger.LogInformation("{method}:{userId}:{primary}", nameof(ComponentSecondaryCreate), Context.Interaction.User.Id, primaryUid); - using var mareDb = GetDbContext(); + using var mareDb = await GetDbContext().ConfigureAwait(false); EmbedBuilder eb = new(); eb.WithTitle("Secondary UID created"); eb.WithColor(Color.Green); diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.UserInfo.cs b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.UserInfo.cs index c96ddc9..05111d0 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.UserInfo.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.UserInfo.cs @@ -14,7 +14,7 @@ public partial class MareWizardModule _logger.LogInformation("{method}:{userId}", nameof(ComponentUserinfo), Context.Interaction.User.Id); - using var mareDb = GetDbContext(); + using var mareDb = await GetDbContext().ConfigureAwait(false); EmbedBuilder eb = new(); eb.WithTitle("User Info"); eb.WithColor(Color.Blue); @@ -36,7 +36,7 @@ public partial class MareWizardModule _logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionUserinfo), Context.Interaction.User.Id, uid); - using var mareDb = GetDbContext(); + using var mareDb = await GetDbContext().ConfigureAwait(false); EmbedBuilder eb = new(); eb.WithTitle($"User Info for {uid}"); await HandleUserInfo(eb, mareDb, uid).ConfigureAwait(false); diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Vanity.cs b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Vanity.cs index 66b35cd..5d79847 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Vanity.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.Vanity.cs @@ -41,7 +41,7 @@ public partial class MareWizardModule AddHome(cb); if (userIsInVanityRole) { - using var db = GetDbContext(); + using var db = await GetDbContext().ConfigureAwait(false); await AddUserSelection(db, cb, "wizard-vanity-uid").ConfigureAwait(false); await AddGroupSelection(db, cb, "wizard-vanity-gid").ConfigureAwait(false); } @@ -56,7 +56,7 @@ public partial class MareWizardModule _logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionVanityUid), Context.Interaction.User.Id, uid); - using var db = GetDbContext(); + using var db = await GetDbContext().ConfigureAwait(false); var user = db.Users.Single(u => u.UID == uid); EmbedBuilder eb = new(); eb.WithColor(Color.Purple); @@ -90,7 +90,7 @@ public partial class MareWizardModule EmbedBuilder eb = new(); ComponentBuilder cb = new(); var desiredVanityUid = modal.DesiredVanityUID; - using var db = GetDbContext(); + using var db = await GetDbContext().ConfigureAwait(false); bool canAddVanityId = !db.Users.Any(u => u.UID == modal.DesiredVanityUID || u.Alias == modal.DesiredVanityUID); Regex rgx = new(@"^[_\-a-zA-Z0-9]{5,15}$", RegexOptions.ECMAScript); @@ -132,7 +132,7 @@ public partial class MareWizardModule { _logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionVanityGid), Context.Interaction.User.Id, gid); - using var db = GetDbContext(); + using var db = await GetDbContext().ConfigureAwait(false); var group = db.Groups.Single(u => u.GID == gid); EmbedBuilder eb = new(); eb.WithColor(Color.Purple); @@ -166,7 +166,7 @@ public partial class MareWizardModule EmbedBuilder eb = new(); ComponentBuilder cb = new(); var desiredVanityGid = modal.DesiredVanityGID; - using var db = GetDbContext(); + using var db = await GetDbContext().ConfigureAwait(false); bool canAddVanityId = !db.Groups.Any(u => u.GID == modal.DesiredVanityGID || u.Alias == modal.DesiredVanityGID); Regex rgx = new(@"^[_\-a-zA-Z0-9]{5,20}$", RegexOptions.ECMAScript); diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.cs b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.cs index d572c96..a6768e1 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/MareWizardModule.cs @@ -15,24 +15,24 @@ 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 readonly IDbContextFactory _dbContextFactory; private Random random = new(); - public MareWizardModule(ILogger logger, IServiceProvider services, DiscordBotServices botServices, + public MareWizardModule(ILogger logger, DiscordBotServices botServices, IConfigurationService mareClientConfigurationService, IConfigurationService mareServicesConfiguration, - IConnectionMultiplexer connectionMultiplexer) + IConnectionMultiplexer connectionMultiplexer, IDbContextFactory dbContextFactory) { _logger = logger; - _services = services; _botServices = botServices; _mareClientConfigurationService = mareClientConfigurationService; _mareServicesConfiguration = mareServicesConfiguration; _connectionMultiplexer = connectionMultiplexer; + _dbContextFactory = dbContextFactory; } [ComponentInteraction("wizard-captcha:*")] @@ -115,7 +115,7 @@ public partial class MareWizardModule : InteractionModuleBase _logger.LogInformation("{method}:{userId}", nameof(StartWizard), Context.Interaction.User.Id); - using var mareDb = GetDbContext(); + using var mareDb = await GetDbContext().ConfigureAwait(false); bool hasAccount = await mareDb.LodeStoneAuth.AnyAsync(u => u.DiscordId == Context.User.Id && u.StartedAt == null).ConfigureAwait(false); if (init) @@ -200,9 +200,9 @@ public partial class MareWizardModule : InteractionModuleBase public string Delete { get; set; } } - private MareDbContext GetDbContext() + private async Task GetDbContext() { - return _services.CreateScope().ServiceProvider.GetService(); + return await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); } private async Task ValidateInteraction() diff --git a/MareSynchronosServer/MareSynchronosServices/DummyHub.cs b/MareSynchronosServer/MareSynchronosServices/DummyHub.cs deleted file mode 100644 index afcb665..0000000 --- a/MareSynchronosServer/MareSynchronosServices/DummyHub.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.AspNetCore.SignalR; - -// this is a very hacky way to attach this file server to the main mare hub signalr instance via redis -// signalr publishes the namespace and hubname into the redis backend so this needs to be equal to the original -// but I don't need to reimplement the hub completely as I only exclusively use it for internal connection calling -// from the queue service so I keep the namespace and name of the class the same so it can connect to the same channel -// if anyone finds a better way to do this let me know - -#pragma warning disable IDE0130 // Namespace does not match folder structure -#pragma warning disable MA0048 // File name must match type name -namespace MareSynchronosServer.Hubs; -public class MareHub : Hub -{ - public override Task OnConnectedAsync() - { - throw new NotSupportedException(); - } - - public override Task OnDisconnectedAsync(Exception exception) - { - throw new NotSupportedException(); - } -} -#pragma warning restore IDE0130 // Namespace does not match folder structure -#pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/Program.cs b/MareSynchronosServer/MareSynchronosServices/Program.cs index 33c21ce..48f17e8 100644 --- a/MareSynchronosServer/MareSynchronosServices/Program.cs +++ b/MareSynchronosServer/MareSynchronosServices/Program.cs @@ -1,5 +1,4 @@ using MareSynchronosServices; -using MareSynchronosShared.Data; using MareSynchronosShared.Services; using MareSynchronosShared.Utils.Configuration; @@ -12,9 +11,6 @@ public class Program using (var scope = host.Services.CreateScope()) { - var services = scope.ServiceProvider; - using var dbContext = services.GetRequiredService(); - var options = host.Services.GetService>(); var optionsServer = host.Services.GetService>(); var logger = host.Services.GetService>(); diff --git a/MareSynchronosServer/MareSynchronosServices/Startup.cs b/MareSynchronosServer/MareSynchronosServices/Startup.cs index e8b7437..09983e8 100644 --- a/MareSynchronosServer/MareSynchronosServices/Startup.cs +++ b/MareSynchronosServer/MareSynchronosServices/Startup.cs @@ -6,8 +6,6 @@ using Prometheus; using MareSynchronosShared.Utils; using MareSynchronosShared.Services; using StackExchange.Redis; -using MessagePack.Resolvers; -using MessagePack; using MareSynchronosShared.Utils.Configuration; namespace MareSynchronosServices; @@ -27,12 +25,6 @@ public class Startup var metricServer = new KestrelMetricServer(config.GetValueOrDefault(nameof(MareConfigurationBase.MetricsPort), 4982)); metricServer.Start(); - - app.UseRouting(); - app.UseEndpoints(e => - { - e.MapHub("/dummyhub"); - }); } public void ConfigureServices(IServiceCollection services) @@ -47,6 +39,15 @@ public class Startup }).UseSnakeCaseNamingConvention(); options.EnableThreadSafetyChecks(false); }, Configuration.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024)); + services.AddDbContextFactory(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("MareSynchronosShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }); services.AddSingleton(m => new MareMetrics(m.GetService>(), new List { }, new List { })); @@ -58,35 +59,6 @@ public class Startup ConnectionMultiplexer connectionMultiplexer = ConnectionMultiplexer.Connect(options); services.AddSingleton(connectionMultiplexer); - var signalRServiceBuilder = services.AddSignalR(hubOptions => - { - hubOptions.MaximumReceiveMessageSize = long.MaxValue; - hubOptions.EnableDetailedErrors = true; - hubOptions.MaximumParallelInvocationsPerClient = 10; - hubOptions.StreamBufferCapacity = 200; - }).AddMessagePackProtocol(opt => - { - var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance, - BuiltinResolver.Instance, - AttributeFormatterResolver.Instance, - // replace enum resolver - DynamicEnumAsStringResolver.Instance, - DynamicGenericResolver.Instance, - DynamicUnionResolver.Instance, - DynamicObjectResolver.Instance, - PrimitiveObjectResolver.Instance, - // final fallback(last priority) - StandardResolver.Instance); - - opt.SerializerOptions = MessagePackSerializerOptions.Standard - .WithCompression(MessagePackCompression.Lz4Block) - .WithResolver(resolver); - }); - - // configure redis for SignalR - var redisConnection = mareConfig.GetValue(nameof(MareConfigurationBase.RedisConnectionString), string.Empty); - signalRServiceBuilder.AddStackExchangeRedis(redisConnection, options => { }); - services.Configure(Configuration.GetRequiredSection("MareSynchronos")); services.Configure(Configuration.GetRequiredSection("MareSynchronos")); services.Configure(Configuration.GetRequiredSection("MareSynchronos"));