Compare commits

..

10 Commits

Author SHA1 Message Date
63067869d8 remove lodestone verification
also remove relinking as there's nothing to relink with
2025-08-30 18:56:18 +02:00
e57d6b07db rebranding changes 2025-08-30 18:55:23 +02:00
7b022dae05 misc changes to make it compile right 2025-08-30 18:51:28 +02:00
rootdarkarchon
9330a883a5 add gauge 2025-08-20 10:15:51 +02:00
rootdarkarchon
d2c90a332a undo startup.cs 2025-08-20 10:15:33 +02:00
rootdarkarchon
c664ecbe26 rework concurrencyfilter 2025-08-20 10:14:58 +02:00
rootdarkarchon
282fb4f83a adjust text 2025-08-08 23:22:45 +02:00
rootdarkarchon
4326a134c6 some logging 2025-07-17 01:47:23 +02:00
rootdarkarchon
2e7672c440 add some more logging 2025-07-16 03:31:42 +02:00
rootdarkarchon
a4c760af25 fix existinguserrequirementhandler 2025-07-16 03:27:07 +02:00
23 changed files with 200 additions and 364 deletions

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@
*.user
*.userosscache
*.sln.docstates
*/.idea/*
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

2
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "MareAPI"]
path = MareAPI
url = https://github.com/Penumbra-Sync/api.git
url = https://git.namazu.gay/friendlynamazu/api.git

View File

@@ -46,18 +46,18 @@ public abstract class AuthControllerBase : Controller
if (await IsIdentBanned(dbContext, charaIdent))
{
Logger.LogWarning("Authenticate:IDENTBAN:{id}:{ident}", authResult.Uid, charaIdent);
return Unauthorized("Your XIV service account is banned from using the service.");
return Unauthorized("Your character is banned from using the service.");
}
if (!authResult.Success && !authResult.TempBan)
{
Logger.LogWarning("Authenticate:INVALID:{id}:{ident}", authResult?.Uid ?? "NOUID", charaIdent);
return Unauthorized("The provided secret key is invalid. Verify your Mare accounts existence and/or recover the secret key.");
return Unauthorized("The provided secret key is invalid. Verify your Namazu accounts existence and/or recover the secret key.");
}
if (!authResult.Success && authResult.TempBan)
{
Logger.LogWarning("Authenticate:TEMPBAN:{id}:{ident}", authResult.Uid ?? "NOUID", charaIdent);
return Unauthorized("Due to an excessive amount of failed authentication attempts you are temporarily banned. Check your Secret Key configuration and try connecting again in 5 minutes.");
return Unauthorized("Due to an excessive amount of failed authentication attempts you are temporarily locked out. Check your Secret Key configuration and try connecting again in 5 minutes.");
}
if (authResult.Permaban || authResult.MarkedForBan)
@@ -69,14 +69,14 @@ public abstract class AuthControllerBase : Controller
}
Logger.LogWarning("Authenticate:UIDBAN:{id}:{ident}", authResult.Uid, charaIdent);
return Unauthorized("Your Mare account is banned from using the service.");
return Unauthorized("Your Namazu account is banned from using the service.");
}
var existingIdent = await _redis.StringGetAsync("UID:" + authResult.Uid);
if (!string.IsNullOrEmpty(existingIdent))
{
Logger.LogWarning("Authenticate:DUPLICATE:{id}:{ident}", authResult.Uid, charaIdent);
return Unauthorized("Already logged in to this Mare account. Reconnect in 60 seconds. If you keep seeing this issue, restart your game.");
return Unauthorized("Already logged in to this Namazu account. Reconnect in 60 seconds. If you keep seeing this issue, restart your game.");
}
Logger.LogInformation("Authenticate:SUCCESS:{id}:{ident}", authResult.Uid, charaIdent);

View File

@@ -49,7 +49,7 @@ public class JwtController : AuthControllerBase
var userAuth = await dbContext.Auth.SingleAsync(u => u.UserUID == uid);
await EnsureBan(uid, userAuth.PrimaryUserUID, ident);
return Unauthorized("Your Mare account is banned.");
return Unauthorized("Your Namazu account is banned.");
}
if (await IsIdentBanned(dbContext, ident))

View File

@@ -147,9 +147,9 @@ public class OAuthController : AuthControllerBase
var mareUser = await dbContext.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == discordUserId);
if (mareUser == default)
{
Logger.LogDebug("Failed to get Mare user for {session}, DiscordId: {id}", reqId, discordUserId);
Logger.LogDebug("Failed to get Namazu user for {session}, DiscordId: {id}", reqId, discordUserId);
return BadRequest("Could not find a Mare user associated to this Discord account.");
return BadRequest("Could not find a Namazu user associated to this Discord account.");
}
JwtSecurityToken? jwt = null;

View File

@@ -36,10 +36,6 @@ Global
{326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Release|Any CPU.Build.0 = Release|Any CPU
{25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.Release|Any CPU.Build.0 = Release|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@@ -2,12 +2,13 @@
using MareSynchronosShared.Services;
using MareSynchronosShared.Utils.Configuration;
using Microsoft.AspNetCore.SignalR;
using System.Threading.RateLimiting;
namespace MareSynchronosServer.Hubs;
public sealed class ConcurrencyFilter : IHubFilter, IDisposable
{
private SemaphoreSlim _limiter;
private ConcurrencyLimiter _limiter;
private int _setLimit = 0;
private readonly IConfigurationService<ServerConfiguration> _config;
private readonly CancellationTokenSource _cts = new();
@@ -19,14 +20,19 @@ public sealed class ConcurrencyFilter : IHubFilter, IDisposable
_config = config;
_config.ConfigChangedEvent += OnConfigChange;
RecreateSemaphore();
RecreateLimiter();
_ = Task.Run(async () =>
{
var token = _cts.Token;
while (!token.IsCancellationRequested)
{
mareMetrics.SetGaugeTo(MetricsAPI.GaugeHubConcurrency, _limiter?.CurrentCount ?? 0);
var stats = _limiter?.GetStatistics();
if (stats != null)
{
mareMetrics.SetGaugeTo(MetricsAPI.GaugeHubConcurrency, stats.CurrentAvailablePermits);
mareMetrics.SetGaugeTo(MetricsAPI.GaugeHubQueuedConcurrency, stats.CurrentQueuedCount);
}
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
}
});
@@ -34,18 +40,26 @@ public sealed class ConcurrencyFilter : IHubFilter, IDisposable
private void OnConfigChange(object sender, EventArgs e)
{
RecreateSemaphore();
RecreateLimiter();
}
private void RecreateSemaphore()
private void RecreateLimiter()
{
var newLimit = _config.GetValueOrDefault(nameof(ServerConfiguration.HubExecutionConcurrencyFilter), 50);
if (newLimit != _setLimit)
if (newLimit == _setLimit && _limiter is not null)
{
return;
}
_setLimit = newLimit;
_limiter?.Dispose();
_limiter = new(initialCount: _setLimit, maxCount: _setLimit);
}
_limiter = new(new ConcurrencyLimiterOptions()
{
PermitLimit = newLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = newLimit * 100,
});
}
public async ValueTask<object> InvokeMethodAsync(
@@ -56,15 +70,25 @@ public sealed class ConcurrencyFilter : IHubFilter, IDisposable
return await next(invocationContext).ConfigureAwait(false);
}
await _limiter.WaitAsync(invocationContext.Context.ConnectionAborted).ConfigureAwait(false);
var ct = invocationContext.Context.ConnectionAborted;
RateLimitLease lease;
try
{
return await next(invocationContext).ConfigureAwait(false);
lease = await _limiter.AcquireAsync(1, ct).ConfigureAwait(false);
}
finally
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
_limiter.Release();
throw;
}
if (!lease.IsAcquired)
{
throw new HubException("Concurrency limit exceeded. Try again later.");
}
using (lease)
{
return await next(invocationContext).ConfigureAwait(false);
}
}
@@ -77,6 +101,8 @@ public sealed class ConcurrencyFilter : IHubFilter, IDisposable
_disposed = true;
_cts.Cancel();
_limiter?.Dispose();
_config.ConfigChangedEvent -= OnConfigChange;
_cts.Dispose();
}
}

View File

@@ -159,11 +159,11 @@ public partial class MareHub
}
var gid = StringUtils.GenerateRandomString(12);
while (await DbContext.Groups.AnyAsync(g => g.GID == "MSS-" + gid).ConfigureAwait(false))
while (await DbContext.Groups.AnyAsync(g => g.GID == "NSS-" + gid).ConfigureAwait(false))
{
gid = StringUtils.GenerateRandomString(12);
}
gid = "MSS-" + gid;
gid = "NSS-" + gid;
var passwd = StringUtils.GenerateRandomString(16);
using var sha = SHA256.Create();

View File

@@ -88,7 +88,7 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
var dbUser = await DbContext.Users.SingleAsync(f => f.UID == UserUID).ConfigureAwait(false);
dbUser.LastLoggedIn = DateTime.UtcNow;
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Welcome to Mare Synchronos \"" + _shardName + "\", Current Online Users: " + _systemInfoService.SystemInfoDto.OnlineUsers).ConfigureAwait(false);
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Welcome to Namazu Sync \"" + _shardName + "\", Current Online Users: " + _systemInfoService.SystemInfoDto.OnlineUsers).ConfigureAwait(false);
var defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID).ConfigureAwait(false);
if (defaultPermissions == null)

View File

@@ -48,7 +48,7 @@ public class SignalRLimitFilter : IHubFilter
}
// Optional method
public async Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next)
/* public async Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next)
{
await ConnectionLimiterSemaphore.WaitAsync().ConfigureAwait(false);
try
@@ -108,5 +108,5 @@ public class SignalRLimitFilter : IHubFilter
{
DisconnectLimiterSemaphore.Release();
}
}
} */
}

View File

@@ -30,7 +30,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
</ItemGroup>
<ItemGroup>

View File

@@ -303,7 +303,8 @@ public class Startup
MetricsAPI.GaugeUserPairCacheUsers,
MetricsAPI.GaugeGposeLobbies,
MetricsAPI.GaugeGposeLobbyUsers,
MetricsAPI.GaugeHubConcurrency
MetricsAPI.GaugeHubConcurrency,
MetricsAPI.GaugeHubQueuedConcurrency,
}));
}

View File

@@ -184,9 +184,9 @@ internal class DiscordBot : IHostedService
private async Task GenerateOrUpdateWizardMessage(SocketTextChannel channel, IUserMessage? prevMessage)
{
EmbedBuilder eb = new EmbedBuilder();
eb.WithTitle("Mare Services Bot Interaction Service");
eb.WithTitle("Namazu Services Bot Interaction Service");
eb.WithDescription("Press \"Start\" to interact with this bot!" + Environment.NewLine + Environment.NewLine
+ "You can handle all of your Mare account needs in this server through the easy to use interactive bot prompt. Just follow the instructions!");
+ "You can handle all of your Namazu account needs in this server through the easy to use interactive bot prompt. Just follow the instructions!");
eb.WithThumbnailUrl("https://raw.githubusercontent.com/Penumbra-Sync/repo/main/MareSynchronos/images/icon.png");
var cb = new ComponentBuilder();
cb.WithButton("Start", style: ButtonStyle.Primary, customId: "wizard-captcha:true", emote: Emoji.Parse("➡️"));
@@ -427,7 +427,7 @@ internal class DiscordBot : IHostedService
var onlineUsers = await _connectionMultiplexer.GetServer(endPoint).KeysAsync(pattern: "UID:*").CountAsync().ConfigureAwait(false);
_logger.LogInformation("Users online: " + onlineUsers);
await _discordClient.SetActivityAsync(new Game("Mare for " + onlineUsers + " Users")).ConfigureAwait(false);
await _discordClient.SetActivityAsync(new Game("Namazu for " + onlineUsers + " Users")).ConfigureAwait(false);
await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false);
}
}

View File

@@ -196,7 +196,7 @@ public class MareModule : InteractionModuleBase
if (primaryUser == null)
{
eb.WithTitle("No account");
eb.WithDescription("No Mare account was found associated to your Discord user");
eb.WithDescription("No Namazu account was found associated to your Discord user");
return eb;
}
@@ -223,7 +223,7 @@ public class MareModule : InteractionModuleBase
if (userInDb == null)
{
eb.WithTitle("No account");
eb.WithDescription("The Discord user has no valid Mare account");
eb.WithDescription("The Discord user has no valid Namazu account");
return eb;
}

View File

@@ -21,7 +21,7 @@ public partial class MareWizardModule
eb.WithColor(Color.Blue);
eb.WithTitle("Recover");
eb.WithDescription("In case you have lost your secret key you can recover it here." + Environment.NewLine + Environment.NewLine
+ "## ⚠️ **Once you recover your key, the previously used key will be invalidated. If you use Mare on multiple devices you will have to update the key everywhere you use it.** ⚠️" + Environment.NewLine + Environment.NewLine
+ "## ⚠️ **Once you recover your key, the previously used key will be invalidated. If you use Namazu on multiple devices you will have to update the key everywhere you use it.** ⚠️" + Environment.NewLine + Environment.NewLine
+ "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
@@ -79,7 +79,7 @@ public partial class MareWizardModule
+ Environment.NewLine
+ "__NOTE: The Secret Key only contains the letters ABCDEF and numbers 0 - 9.__"
+ Environment.NewLine + Environment.NewLine
+ "Enter this key in the Mare Synchronos Service Settings and reconnect to the service.");
+ "Enter this key in the Namazu Sync Service Settings and reconnect to the service.");
await db.Auth.AddAsync(auth).ConfigureAwait(false);
await db.SaveChangesAsync().ConfigureAwait(false);

View File

@@ -21,16 +21,44 @@ public partial class MareWizardModule
_logger.LogInformation("{method}:{userId}", nameof(ComponentRegister), Context.Interaction.User.Id);
EmbedBuilder eb = new();
eb.WithColor(Color.Blue);
eb.WithTitle("Start Registration");
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
+ "- 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.");
// eb.WithColor(Color.Blue);
// eb.WithTitle("Start Registration");
// eb.WithDescription("Here you can start the registration process with the Namazu Sync 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
// + "- 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("🌒"));
var registerSuccess = false;
eb.WithColor(Color.Green);
using var db = await GetDbContext().ConfigureAwait(false);
string lodestoneAuth = await GenerateLodestoneAuth(Context.User.Id, null, db).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.**"
+ Environment.NewLine + Environment.NewLine
+ $"**{key}**"
+ Environment.NewLine + Environment.NewLine
+ "Enter this key in Namazu Sync and hit save to connect to the service."
+ 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."
);
ComponentBuilder cb = new();
AddHome(cb);
cb.WithButton("Start Registration", "wizard-register-start", ButtonStyle.Primary, emote: new Emoji("🌒"));
if (registerSuccess) {
await _botServices.AddRegisteredRoleAsync(Context.Interaction.User).ConfigureAwait(false);
}
await ModifyInteraction(eb, cb).ConfigureAwait(false);
}
@@ -133,14 +161,14 @@ public partial class MareWizardModule
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.**"
+ Environment.NewLine + Environment.NewLine
+ "**__NOTE: Secret keys are considered legacy. Using the suggested OAuth2 authentication in Mare, you do not need to use this Secret Key.__**"
+ $"**{key}**"
+ Environment.NewLine + Environment.NewLine
+ $"||**`{key}`**||"
+ Environment.NewLine + Environment.NewLine
+ "If you want to continue using legacy authentication, enter this key in Mare Synchronos and hit save to connect to the service."
+ "Enter this key in Namazu Sync and hit save to connect to the service."
+ 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.");
@@ -159,9 +187,9 @@ public partial class MareWizardModule
+ 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:"
+ "## You __need__ to enter following the code this bot provided onto your lodestone in the character profile:"
+ Environment.NewLine + Environment.NewLine
+ "**`" + verificationCode + "`**");
+ "**" + verificationCode + "**");
cb.WithButton("Cancel", "wizard-register", emote: new Emoji("❌"));
cb.WithButton("Retry", "wizard-register-verify:" + verificationCode, ButtonStyle.Primary, emote: new Emoji("🔁"));
}
@@ -209,11 +237,11 @@ public partial class MareWizardModule
+ 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}`**"
+ $"**{lodestoneAuth}**"
+ Environment.NewLine + Environment.NewLine
+ $"**! THIS IS NOT THE KEY YOU HAVE TO ENTER IN MARE !**"
+ $"**! THIS IS NOT THE KEY YOU HAVE TO ENTER IN NAMAZU !**"
+ Environment.NewLine + Environment.NewLine
+ "Once added and saved, use the button below to Verify and finish registration and receive a secret key to use for Mare Synchronos."
+ "Once added and saved, use the button below to Verify and finish registration and receive a secret key to use for Namazu Sync."
+ Environment.NewLine
+ "__You can delete the entry from your profile after verification.__"
+ Environment.NewLine + Environment.NewLine

View File

@@ -1,281 +0,0 @@
using Discord.Interactions;
using Discord;
using MareSynchronosShared.Data;
using MareSynchronosShared.Utils;
using MareSynchronosShared.Models;
using Microsoft.EntityFrameworkCore;
namespace MareSynchronosServices.Discord;
public partial class MareWizardModule
{
[ComponentInteraction("wizard-relink")]
public async Task ComponentRelink()
{
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
_logger.LogInformation("{method}:{userId}", nameof(ComponentRelink), Context.Interaction.User.Id);
EmbedBuilder eb = new();
eb.WithTitle("Relink");
eb.WithColor(Color.Blue);
eb.WithDescription("Use this in case you already have a registered Mare account, but lost access to your previous Discord account." + Environment.NewLine + Environment.NewLine
+ "- Have your original registered Lodestone URL ready (i.e. https://eu.finalfantasyxiv.com/lodestone/character/XXXXXXXXX)" + Environment.NewLine
+ " - The relink process requires you to modify your Lodestone profile with a generated code for verification" + Environment.NewLine
+ "- Do not use this on mobile because you will need to be able to copy the generated secret key");
ComponentBuilder cb = new();
AddHome(cb);
cb.WithButton("Start Relink", "wizard-relink-start", ButtonStyle.Primary, emote: new Emoji("🔗"));
await ModifyInteraction(eb, cb).ConfigureAwait(false);
}
[ComponentInteraction("wizard-relink-start")]
public async Task ComponentRelinkStart()
{
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
_logger.LogInformation("{method}:{userId}", nameof(ComponentRelinkStart), Context.Interaction.User.Id);
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 _);
await db.SaveChangesAsync().ConfigureAwait(false);
await RespondWithModalAsync<LodestoneModal>("wizard-relink-lodestone-modal").ConfigureAwait(false);
}
[ModalInteraction("wizard-relink-lodestone-modal")]
public async Task ModalRelink(LodestoneModal lodestoneModal)
{
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
_logger.LogInformation("{method}:{userId}:{url}", nameof(ModalRelink), Context.Interaction.User.Id, lodestoneModal.LodestoneUrl);
EmbedBuilder eb = new();
eb.WithColor(Color.Purple);
var result = await HandleRelinkModalAsync(eb, lodestoneModal).ConfigureAwait(false);
ComponentBuilder cb = new();
cb.WithButton("Cancel", "wizard-relink", ButtonStyle.Secondary, emote: new Emoji("❌"));
if (result.Success) cb.WithButton("Verify", "wizard-relink-verify:" + result.LodestoneAuth + "," + result.UID, ButtonStyle.Primary, emote: new Emoji("✅"));
else cb.WithButton("Try again", "wizard-relink-start", ButtonStyle.Primary, emote: new Emoji("🔁"));
await ModifyModalInteraction(eb, cb).ConfigureAwait(false);
}
[ComponentInteraction("wizard-relink-verify:*,*")]
public async Task ComponentRelinkVerify(string verificationCode, string uid)
{
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
_logger.LogInformation("{method}:{userId}:{uid}:{verificationCode}", nameof(ComponentRelinkVerify), Context.Interaction.User.Id, uid, verificationCode);
_botServices.VerificationQueue.Enqueue(new KeyValuePair<ulong, Func<DiscordBotServices, Task>>(Context.User.Id,
(services) => HandleVerifyRelinkAsync(Context.User.Id, verificationCode, services)));
EmbedBuilder eb = new();
ComponentBuilder cb = new();
eb.WithColor(Color.Purple);
cb.WithButton("Cancel", "wizard-relink", ButtonStyle.Secondary, emote: new Emoji("❌"));
cb.WithButton("Check", "wizard-relink-verify-check:" + verificationCode + "," + uid, ButtonStyle.Primary, emote: new Emoji("❓"));
eb.WithTitle("Relink Verification Pending");
eb.WithDescription("Please wait until the bot verifies your registration." + Environment.NewLine
+ "Press \"Check\" to check if the verification has been already processed" + Environment.NewLine + Environment.NewLine
+ "__This will not advance automatically, you need to press \"Check\".__");
await ModifyInteraction(eb, cb).ConfigureAwait(false);
}
[ComponentInteraction("wizard-relink-verify-check:*,*")]
public async Task ComponentRelinkVerifyCheck(string verificationCode, string uid)
{
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
_logger.LogInformation("{method}:{userId}:{uid}:{verificationCode}", nameof(ComponentRelinkVerifyCheck), Context.Interaction.User.Id, uid, verificationCode);
EmbedBuilder eb = new();
ComponentBuilder cb = new();
bool stillEnqueued = _botServices.VerificationQueue.Any(k => k.Key == Context.User.Id);
bool verificationRan = _botServices.DiscordVerifiedUsers.TryGetValue(Context.User.Id, out bool verified);
bool relinkSuccess = false;
if (!verificationRan)
{
if (stillEnqueued)
{
eb.WithColor(Color.Gold);
eb.WithTitle("Your relink verification is still pending");
eb.WithDescription("Please try again and click Check in a few seconds");
cb.WithButton("Cancel", "wizard-relink", ButtonStyle.Secondary, emote: new Emoji("❌"));
cb.WithButton("Check", "wizard-relink-verify-check:" + verificationCode + "," + uid, ButtonStyle.Primary, emote: new Emoji("❓"));
}
else
{
eb.WithColor(Color.Red);
eb.WithTitle("Something went wrong");
eb.WithDescription("Your relink verification was processed but did not arrive properly. Please try to start the relink process from the start.");
cb.WithButton("Restart", "wizard-relink", ButtonStyle.Primary, emote: new Emoji("🔁"));
}
}
else
{
if (verified)
{
eb.WithColor(Color.Green);
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.**"
+ Environment.NewLine + Environment.NewLine
+ $"||**`{key}`**||"
+ Environment.NewLine + Environment.NewLine
+ "Enter this key in Mare Synchronos and hit save to connect to the service."
+ Environment.NewLine + Environment.NewLine
+ "NOTE: If you are using OAuth2, you do not require to use this secret key."
+ Environment.NewLine
+ "Have fun.");
AddHome(cb);
relinkSuccess = true;
}
else
{
eb.WithColor(Color.Gold);
eb.WithTitle("Failed to verify relink");
eb.WithDescription("The bot was not able to find the required verification code on your Lodestone profile." + Environment.NewLine + Environment.NewLine
+ "Please restart your relink 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
+ "**`" + verificationCode + "`**");
cb.WithButton("Cancel", "wizard-relink", emote: new Emoji("❌"));
cb.WithButton("Retry", "wizard-relink-verify:" + verificationCode + "," + uid, ButtonStyle.Primary, emote: new Emoji("🔁"));
}
}
await ModifyInteraction(eb, cb).ConfigureAwait(false);
if (relinkSuccess)
await _botServices.AddRegisteredRoleAsync(Context.Interaction.User).ConfigureAwait(false);
}
private async Task<(bool Success, string LodestoneAuth, string UID)> HandleRelinkModalAsync(EmbedBuilder embed, LodestoneModal arg)
{
ulong userId = Context.User.Id;
var lodestoneId = ParseCharacterIdFromLodestoneUrl(arg.LodestoneUrl);
if (lodestoneId == null)
{
embed.WithTitle("Invalid Lodestone URL");
embed.WithDescription("The lodestone URL was not valid. It should have following format:" + Environment.NewLine
+ "https://eu.finalfantasyxiv.com/lodestone/character/YOUR_LODESTONE_ID/");
return (false, string.Empty, string.Empty);
}
// check if userid is already in db
var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString());
using var db = await GetDbContext().ConfigureAwait(false);
// check if discord id or lodestone id is banned
if (db.BannedRegistrations.Any(a => a.DiscordIdOrLodestoneAuth == hashedLodestoneId))
{
embed.WithTitle("Illegal operation");
embed.WithDescription("Your account is banned");
return (false, string.Empty, string.Empty);
}
if (!db.LodeStoneAuth.Any(a => a.HashedLodestoneId == hashedLodestoneId))
{
// character already in db
embed.WithTitle("Impossible operation");
embed.WithDescription("This lodestone character does not exist in the database.");
return (false, string.Empty, string.Empty);
}
var expectedUser = await db.LodeStoneAuth.Include(u => u.User).SingleAsync(u => u.HashedLodestoneId == hashedLodestoneId).ConfigureAwait(false);
string lodestoneAuth = await GenerateLodestoneAuth(Context.User.Id, hashedLodestoneId, db).ConfigureAwait(false);
// check if lodestone id is already in db
embed.WithTitle("Authorize your character for relinking");
embed.WithDescription("Add following key to your character profile at https://na.finalfantasyxiv.com/lodestone/my/setting/profile/"
+ Environment.NewLine + Environment.NewLine
+ $"**`{lodestoneAuth}`**"
+ Environment.NewLine + Environment.NewLine
+ $"**! THIS IS NOT THE KEY YOU HAVE TO ENTER IN MARE !**"
+ Environment.NewLine
+ "__You can delete the entry from your profile after verification.__"
+ Environment.NewLine + Environment.NewLine
+ "The verification will expire in approximately 15 minutes. If you fail to verify the relink will be invalidated and you have to relink again.");
_botServices.DiscordRelinkLodestoneMapping[Context.User.Id] = lodestoneId.ToString();
return (true, lodestoneAuth, expectedUser.User.UID);
}
private async Task HandleVerifyRelinkAsync(ulong userid, string authString, DiscordBotServices services)
{
using var req = new HttpClient();
services.DiscordVerifiedUsers.Remove(userid, out _);
if (services.DiscordRelinkLodestoneMapping.ContainsKey(userid))
{
var randomServer = services.LodestoneServers[random.Next(services.LodestoneServers.Length)];
var url = $"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{services.DiscordRelinkLodestoneMapping[userid]}";
_logger.LogInformation("Verifying {userid} with URL {url}", userid, url);
using var response = await req.GetAsync(url).ConfigureAwait(false);
if (response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (content.Contains(authString))
{
services.DiscordVerifiedUsers[userid] = true;
_logger.LogInformation("Relink: Verified {userid} from lodestone {lodestone}", userid, services.DiscordRelinkLodestoneMapping[userid]);
await _botServices.LogToChannel($"<@{userid}> RELINK VERIFY: Success.").ConfigureAwait(false);
services.DiscordRelinkLodestoneMapping.TryRemove(userid, out _);
}
else
{
services.DiscordVerifiedUsers[userid] = false;
_logger.LogInformation("Relink: Could not verify {userid} from lodestone {lodestone}, did not find authString: {authString}, status code was: {code}",
userid, services.DiscordRelinkLodestoneMapping[userid], authString, response.StatusCode);
await _botServices.LogToChannel($"<@{userid}> RELINK VERIFY: Failed: No Authstring ({authString}). (<{url}>)").ConfigureAwait(false);
}
}
else
{
_logger.LogWarning("Could not verify {userid}, HttpStatusCode: {code}", userid, response.StatusCode);
await _botServices.LogToChannel($"<@{userid}> RELINK VERIFY: Failed: HttpStatusCode {response.StatusCode}. (<{url}>)").ConfigureAwait(false);
}
}
}
private async Task<(string, string)> HandleRelinkUser(MareDbContext db, string uid)
{
var oldLodestoneAuth = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.User.UID == uid && u.DiscordId != Context.User.Id).ConfigureAwait(false);
var newLodestoneAuth = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == Context.User.Id).ConfigureAwait(false);
var user = oldLodestoneAuth.User;
var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
var auth = new Auth()
{
HashedKey = StringUtils.Sha256String(computedHash),
User = user,
};
var previousAuth = await db.Auth.SingleOrDefaultAsync(u => u.UserUID == user.UID).ConfigureAwait(false);
if (previousAuth != null)
{
db.Remove(previousAuth);
}
newLodestoneAuth.LodestoneAuthString = null;
newLodestoneAuth.StartedAt = null;
newLodestoneAuth.User = user;
db.Update(newLodestoneAuth);
db.Remove(oldLodestoneAuth);
await db.Auth.AddAsync(auth).ConfigureAwait(false);
_botServices.Logger.LogInformation("User relinked: {userUID}", user.UID);
await db.SaveChangesAsync().ConfigureAwait(false);
await _botServices.LogToChannel($"{Context.User.Mention} RELINK COMPLETE: => {user.UID}").ConfigureAwait(false);
return (user.UID, computedHash);
}
}

View File

@@ -23,9 +23,9 @@ public partial class MareWizardModule
eb.WithColor(Color.Blue);
eb.WithTitle("Secondary UID");
eb.WithDescription("You can create secondary UIDs here. " + Environment.NewLine + Environment.NewLine
+ "Secondary UIDs act as completely separate Mare accounts with their own pair list, joined syncshells, UID and so on." + Environment.NewLine
+ "Use this to create UIDs if you want to use Mare on two separate game instances at once or keep your alts private." + Environment.NewLine + Environment.NewLine
+ "__Note:__ Creating a Secondary UID is _not_ necessary to use Mare for alts." + Environment.NewLine + Environment.NewLine
+ "Secondary UIDs act as completely separate Namazu accounts with their own pair list, joined syncshells, UID and so on." + Environment.NewLine
+ "Use this to create UIDs if you want to use Namazu on two separate game instances at once or keep your alts private." + Environment.NewLine + Environment.NewLine
+ "__Note:__ Creating a Secondary UID is _not_ necessary to use Namazu for alts." + Environment.NewLine + Environment.NewLine
+ $"You currently have {secondaryUids} Secondary UIDs out of a maximum of 20.");
ComponentBuilder cb = new();
AddHome(cb);
@@ -81,7 +81,7 @@ public partial class MareWizardModule
await db.SaveChangesAsync().ConfigureAwait(false);
embed.WithDescription("A secondary UID for you was created, use the information below and add the secret key to the Mare setings in the Service Settings tab.");
embed.WithDescription("A secondary UID for you was created, use the information below and add the secret key to the Namazu setings in the Service Settings tab.");
embed.AddField("UID", newUser.UID);
embed.AddField("Secret Key", computedHash);

View File

@@ -119,7 +119,7 @@ public partial class MareWizardModule
eb.WithColor(Color.Green);
eb.WithTitle("Vanity UID successfully set");
eb.WithDescription($"Your Vanity UID for \"{uid}\" was successfully changed to \"{desiredVanityUid}\"." + Environment.NewLine + Environment.NewLine
+ "For changes to take effect you need to reconnect to the Mare service.");
+ "For changes to take effect you need to reconnect to the Namazu service.");
await _botServices.LogToChannel($"{Context.User.Mention} VANITY UID SET: UID: {user.UID}, Vanity: {desiredVanityUid}").ConfigureAwait(false);
AddHome(cb);
}
@@ -195,7 +195,7 @@ public partial class MareWizardModule
eb.WithColor(Color.Green);
eb.WithTitle("Vanity Syncshell ID successfully set");
eb.WithDescription($"Your Vanity Syncshell ID for {gid} was successfully changed to \"{desiredVanityGid}\"." + Environment.NewLine + Environment.NewLine
+ "For changes to take effect you need to reconnect to the Mare service.");
+ "For changes to take effect you need to reconnect to the Namazu service.");
AddHome(cb);
await _botServices.LogToChannel($"{Context.User.Mention} VANITY GID SET: GID: {group.GID}, Vanity: {desiredVanityGid}").ConfigureAwait(false);
}

View File

@@ -155,12 +155,11 @@ public partial class MareWizardModule : InteractionModuleBase
#endif
EmbedBuilder eb = new();
eb.WithTitle("Welcome to the Mare Synchronos Service Bot for this server");
eb.WithTitle("Welcome to the Namazu 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 Mare Account press \"🌒 Register\"" + Environment.NewLine))
+ (hasAccount ? string.Empty : ("- Register a new Namazu 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)))
@@ -171,7 +170,6 @@ public partial class MareWizardModule : InteractionModuleBase
if (!hasAccount)
{
cb.WithButton("Register", "wizard-register", ButtonStyle.Primary, new Emoji("🌒"));
cb.WithButton("Relink", "wizard-relink", ButtonStyle.Secondary, new Emoji("🔗"));
}
else
{

View File

@@ -49,4 +49,5 @@ public class MetricsAPI
public const string GaugeGposeLobbies = "mare_gpose_lobbies";
public const string GaugeGposeLobbyUsers = "mare_gpose_lobby_users";
public const string GaugeHubConcurrency = "mare_free_concurrent_hub_calls";
public const string GaugeHubQueuedConcurrency = "mare_free_concurrent_queued_hub_calls";
}

View File

@@ -3,12 +3,15 @@ using MareSynchronosShared.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
namespace MareSynchronosShared.RequirementHandlers;
public class ExistingUserRequirementHandler : AuthorizationHandler<ExistingUserRequirement>
{
private readonly IDbContextFactory<MareDbContext> _dbContextFactory;
private readonly ILogger<ExistingUserRequirementHandler> _logger;
private readonly static ConcurrentDictionary<string, (bool Exists, DateTime LastCheck)> _existingUserDict = [];
private readonly static ConcurrentDictionary<ulong, (bool Exists, DateTime LastCheck)> _existingDiscordDict = [];
public ExistingUserRequirementHandler(IDbContextFactory<MareDbContext> dbContext, ILogger<ExistingUserRequirementHandler> logger)
{
@@ -17,22 +20,65 @@ public class ExistingUserRequirementHandler : AuthorizationHandler<ExistingUserR
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ExistingUserRequirement requirement)
{
try
{
var uid = context.User.Claims.SingleOrDefault(g => string.Equals(g.Type, MareClaimTypes.Uid, StringComparison.Ordinal))?.Value;
if (uid == null) context.Fail();
if (uid == null)
{
context.Fail();
_logger.LogWarning("Failed to find UID in claims");
return;
}
var discordIdString = context.User.Claims.SingleOrDefault(g => string.Equals(g.Type, MareClaimTypes.DiscordId, StringComparison.Ordinal))?.Value;
if (discordIdString == null) context.Fail();
if (discordIdString == null)
{
context.Fail();
_logger.LogWarning("Failed to find DiscordId in claims");
return;
}
if (!ulong.TryParse(discordIdString, out ulong discordId))
{
_logger.LogWarning("Failed to parse DiscordId");
context.Fail();
return;
}
using var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
var user = await dbContext.Users.AsNoTracking().SingleOrDefaultAsync(b => b.UID == uid).ConfigureAwait(false);
if (user == null) context.Fail();
if (!ulong.TryParse(discordIdString, out ulong discordId)) context.Fail();
if (!_existingUserDict.TryGetValue(uid, out (bool Exists, DateTime LastCheck) existingUser)
|| DateTime.UtcNow.Subtract(existingUser.LastCheck).TotalHours > 1)
{
var userExists = await dbContext.Users.SingleOrDefaultAsync(context => context.UID == uid).ConfigureAwait(false) != null;
_existingUserDict[uid] = existingUser = (userExists, DateTime.UtcNow);
}
if (!existingUser.Exists)
{
_logger.LogWarning("Failed to find Mare User {User} in DB", uid);
context.Fail();
return;
}
var discordUser = await dbContext.LodeStoneAuth.AsNoTracking().SingleOrDefaultAsync(b => b.DiscordId == discordId).ConfigureAwait(false);
if (discordUser == null) context.Fail();
if (!_existingDiscordDict.TryGetValue(discordId, out (bool Exists, DateTime LastCheck) existingDiscordUser)
|| DateTime.UtcNow.Subtract(existingDiscordUser.LastCheck).TotalHours > 1)
{
var discordUserExists = await dbContext.LodeStoneAuth.AsNoTracking().SingleOrDefaultAsync(b => b.DiscordId == discordId).ConfigureAwait(false) != null;
_existingDiscordDict[discordId] = existingDiscordUser = (discordUserExists, DateTime.UtcNow);
}
if (!existingDiscordUser.Exists)
{
_logger.LogWarning("Failed to find Discord User {User} in DB", discordId);
context.Fail();
return;
}
context.Succeed(requirement);
}
catch (Exception e)
{
_logger.LogWarning(e, "ExistingUserRequirementHandler failed");
}
}
}

View File

@@ -26,19 +26,34 @@ public class UserRequirementHandler : AuthorizationHandler<UserRequirement, HubI
{
var uid = context.User.Claims.SingleOrDefault(g => string.Equals(g.Type, MareClaimTypes.Uid, StringComparison.Ordinal))?.Value;
if (uid == null) context.Fail();
if (uid == null)
{
context.Fail();
_logger.LogWarning("No user UID found in claims");
return;
}
if ((requirement.Requirements & UserRequirements.Identified) is UserRequirements.Identified)
{
var ident = await _redis.GetAsync<string>("UID:" + uid).ConfigureAwait(false);
if (ident == RedisValue.EmptyString) context.Fail();
if (ident == RedisValue.EmptyString)
{
context.Fail();
_logger.LogWarning("User {uid} not online", uid);
return;
}
}
if ((requirement.Requirements & UserRequirements.Administrator) is UserRequirements.Administrator)
{
using var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
var user = await dbContext.Users.AsNoTracking().SingleOrDefaultAsync(b => b.UID == uid).ConfigureAwait(false);
if (user == null || !user.IsAdmin) context.Fail();
if (user == null || !user.IsAdmin)
{
context.Fail();
_logger.LogWarning("Admin request for {uid} unauthenticated", uid);
return;
}
_logger.LogInformation("Admin {uid} authenticated", uid);
}
@@ -46,7 +61,12 @@ public class UserRequirementHandler : AuthorizationHandler<UserRequirement, HubI
{
using var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
var user = await dbContext.Users.AsNoTracking().SingleOrDefaultAsync(b => b.UID == uid).ConfigureAwait(false);
if (user == null || !user.IsAdmin && !user.IsModerator) context.Fail();
if (user == null || !user.IsAdmin && !user.IsModerator)
{
context.Fail();
_logger.LogWarning("Admin/Moderator for {uid} unauthenticated", uid);
return;
}
_logger.LogInformation("Admin/Moderator {uid} authenticated", uid);
}