Initial
This commit is contained in:
434
LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs
Normal file
434
LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs
Normal file
@@ -0,0 +1,434 @@
|
||||
using Discord;
|
||||
using Discord.Interactions;
|
||||
using Discord.Rest;
|
||||
using Discord.WebSocket;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
internal class DiscordBot : IHostedService
|
||||
{
|
||||
private readonly DiscordBotServices _botServices;
|
||||
private readonly IConfigurationService<ServicesConfiguration> _configurationService;
|
||||
private readonly IConnectionMultiplexer _connectionMultiplexer;
|
||||
private readonly DiscordSocketClient _discordClient;
|
||||
private readonly ILogger<DiscordBot> _logger;
|
||||
private readonly IDbContextFactory<LightlessDbContext> _dbContextFactory;
|
||||
private readonly IServiceProvider _services;
|
||||
private InteractionService _interactionModule;
|
||||
private readonly CancellationTokenSource? _processReportQueueCts;
|
||||
private CancellationTokenSource? _clientConnectedCts;
|
||||
|
||||
public DiscordBot(DiscordBotServices botServices, IServiceProvider services, IConfigurationService<ServicesConfiguration> configuration,
|
||||
IDbContextFactory<LightlessDbContext> dbContextFactory,
|
||||
ILogger<DiscordBot> logger, IConnectionMultiplexer connectionMultiplexer)
|
||||
{
|
||||
_botServices = botServices;
|
||||
_services = services;
|
||||
_configurationService = configuration;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_logger = logger;
|
||||
_connectionMultiplexer = connectionMultiplexer;
|
||||
_discordClient = new(new DiscordSocketConfig()
|
||||
{
|
||||
DefaultRetryMode = RetryMode.AlwaysRetry,
|
||||
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers
|
||||
});
|
||||
|
||||
_discordClient.Log += Log;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var token = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty);
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
_logger.LogInformation("Starting DiscordBot");
|
||||
_logger.LogInformation("Using Configuration: " + _configurationService.ToString());
|
||||
|
||||
_interactionModule?.Dispose();
|
||||
_interactionModule = new InteractionService(_discordClient);
|
||||
_interactionModule.Log += Log;
|
||||
await _interactionModule.AddModuleAsync(typeof(LightlessModule), _services).ConfigureAwait(false);
|
||||
await _interactionModule.AddModuleAsync(typeof(LightlessWizardModule), _services).ConfigureAwait(false);
|
||||
|
||||
await _discordClient.LoginAsync(TokenType.Bot, token).ConfigureAwait(false);
|
||||
await _discordClient.StartAsync().ConfigureAwait(false);
|
||||
|
||||
_discordClient.Ready += DiscordClient_Ready;
|
||||
_discordClient.InteractionCreated += async (x) =>
|
||||
{
|
||||
var ctx = new SocketInteractionContext(_discordClient, x);
|
||||
await _interactionModule.ExecuteCommandAsync(ctx, _services).ConfigureAwait(false);
|
||||
};
|
||||
_discordClient.UserJoined += OnUserJoined;
|
||||
|
||||
await _botServices.Start().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnUserJoined(SocketGuildUser arg)
|
||||
{
|
||||
try
|
||||
{
|
||||
using LightlessDbContext dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
var alreadyRegistered = await dbContext.LodeStoneAuth.AnyAsync(u => u.DiscordId == arg.Id).ConfigureAwait(false);
|
||||
if (alreadyRegistered)
|
||||
{
|
||||
await _botServices.AddRegisteredRoleAsync(arg).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to set user role on join");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty)))
|
||||
{
|
||||
await _botServices.Stop().ConfigureAwait(false);
|
||||
_processReportQueueCts?.Cancel();
|
||||
_clientConnectedCts?.Cancel();
|
||||
|
||||
await _discordClient.LogoutAsync().ConfigureAwait(false);
|
||||
await _discordClient.StopAsync().ConfigureAwait(false);
|
||||
_interactionModule?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DiscordClient_Ready()
|
||||
{
|
||||
var guild = (await _discordClient.Rest.GetGuildsAsync().ConfigureAwait(false)).First();
|
||||
await _interactionModule.RegisterCommandsToGuildAsync(guild.Id, true).ConfigureAwait(false);
|
||||
_clientConnectedCts?.Cancel();
|
||||
_clientConnectedCts?.Dispose();
|
||||
_clientConnectedCts = new();
|
||||
_ = UpdateStatusAsync(_clientConnectedCts.Token);
|
||||
|
||||
await CreateOrUpdateModal(guild).ConfigureAwait(false);
|
||||
_botServices.UpdateGuild(guild);
|
||||
await _botServices.LogToChannel("Bot startup complete.").ConfigureAwait(false);
|
||||
_ = UpdateVanityRoles(guild, _clientConnectedCts.Token);
|
||||
_ = RemoveUsersNotInVanityRole(_clientConnectedCts.Token);
|
||||
_ = RemoveUnregisteredUsers(_clientConnectedCts.Token);
|
||||
}
|
||||
|
||||
private async Task UpdateVanityRoles(RestGuild guild, CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Updating Vanity Roles");
|
||||
Dictionary<ulong, string> vanityRoles = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.VanityRoles), new Dictionary<ulong, string>());
|
||||
if (vanityRoles.Keys.Count != _botServices.VanityRoles.Count)
|
||||
{
|
||||
_botServices.VanityRoles.Clear();
|
||||
foreach (var role in vanityRoles)
|
||||
{
|
||||
_logger.LogInformation("Adding Role: {id} => {desc}", role.Key, role.Value);
|
||||
|
||||
var restrole = guild.GetRole(role.Key);
|
||||
if (restrole != null)
|
||||
_botServices.VanityRoles[restrole] = role.Value;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during UpdateVanityRoles");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateOrUpdateModal(RestGuild guild)
|
||||
{
|
||||
_logger.LogInformation("Creating Wizard: Getting Channel");
|
||||
|
||||
var discordChannelForCommands = _configurationService.GetValue<ulong?>(nameof(ServicesConfiguration.DiscordChannelForCommands));
|
||||
if (discordChannelForCommands == null)
|
||||
{
|
||||
_logger.LogWarning("Creating Wizard: No channel configured");
|
||||
return;
|
||||
}
|
||||
|
||||
IUserMessage? message = null;
|
||||
var socketchannel = await _discordClient.GetChannelAsync(discordChannelForCommands.Value).ConfigureAwait(false) as SocketTextChannel;
|
||||
var pinnedMessages = await socketchannel.GetPinnedMessagesAsync().ConfigureAwait(false);
|
||||
foreach (var msg in pinnedMessages)
|
||||
{
|
||||
_logger.LogInformation("Creating Wizard: Checking message id {id}, author is: {author}, hasEmbeds: {embeds}", msg.Id, msg.Author.Id, msg.Embeds.Any());
|
||||
if (msg.Author.Id == _discordClient.CurrentUser.Id
|
||||
&& msg.Embeds.Any())
|
||||
{
|
||||
message = await socketchannel.GetMessageAsync(msg.Id).ConfigureAwait(false) as IUserMessage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Creating Wizard: Found message id: {id}", message?.Id ?? 0);
|
||||
|
||||
await GenerateOrUpdateWizardMessage(socketchannel, message).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task GenerateOrUpdateWizardMessage(SocketTextChannel channel, IUserMessage? prevMessage)
|
||||
{
|
||||
EmbedBuilder eb = new EmbedBuilder();
|
||||
eb.WithTitle("Lightless Services Bot Interaction Service");
|
||||
eb.WithDescription("Press \"Start\" to interact with this bot!" + Environment.NewLine + Environment.NewLine
|
||||
+ "You can handle all of your Lightless account needs in this server through the easy to use interactive bot prompt. Just follow the instructions!");
|
||||
eb.WithThumbnailUrl("https://raw.githubusercontent.com/Light-Public-Syncshells/LightlessSync/refs/heads/main/LightlessSync/icon.png");
|
||||
var cb = new ComponentBuilder();
|
||||
cb.WithButton("Start", style: ButtonStyle.Primary, customId: "wizard-captcha:true", emote: Emoji.Parse("➡️"));
|
||||
if (prevMessage == null)
|
||||
{
|
||||
var msg = await channel.SendMessageAsync(embed: eb.Build(), components: cb.Build()).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await msg.PinAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// swallow
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await prevMessage.ModifyAsync(p =>
|
||||
{
|
||||
p.Embed = eb.Build();
|
||||
p.Components = cb.Build();
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private Task Log(LogMessage msg)
|
||||
{
|
||||
switch (msg.Severity)
|
||||
{
|
||||
case LogSeverity.Critical:
|
||||
case LogSeverity.Error:
|
||||
_logger.LogError(msg.Exception, msg.Message); break;
|
||||
case LogSeverity.Warning:
|
||||
_logger.LogWarning(msg.Exception, msg.Message); break;
|
||||
default:
|
||||
_logger.LogInformation(msg.Message); break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task RemoveUnregisteredUsers(CancellationToken token)
|
||||
{
|
||||
var guild = (await _discordClient.Rest.GetGuildsAsync().ConfigureAwait(false)).First();
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessUserRoles(guild, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _botServices.LogToChannel($"Error during user procesing: {ex.Message}").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromDays(1)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessUserRoles(RestGuild guild, CancellationToken token)
|
||||
{
|
||||
using LightlessDbContext dbContext = await _dbContextFactory.CreateDbContextAsync(token).ConfigureAwait(false);
|
||||
var roleId = _configurationService.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordRoleRegistered), 0);
|
||||
var kickUnregistered = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.KickNonRegisteredUsers), false);
|
||||
if (roleId == null) return;
|
||||
|
||||
var registrationRole = guild.Roles.FirstOrDefault(f => f.Id == roleId.Value);
|
||||
var registeredUsers = new HashSet<ulong>(await dbContext.LodeStoneAuth.AsNoTracking().Select(c => c.DiscordId).ToListAsync().ConfigureAwait(false));
|
||||
|
||||
var executionStartTime = DateTimeOffset.UtcNow;
|
||||
|
||||
int processedUsers = 0;
|
||||
int addedRoles = 0;
|
||||
int kickedUsers = 0;
|
||||
int totalRoles = 0;
|
||||
int toRemoveUsers = 0;
|
||||
int freshUsers = 0;
|
||||
|
||||
await _botServices.LogToChannel($"Starting to process registered users: Adding Role {registrationRole.Name}. Kick Stale Unregistered: {kickUnregistered}.").ConfigureAwait(false);
|
||||
|
||||
await foreach (var userList in guild.GetUsersAsync(new RequestOptions { CancelToken = token }).ConfigureAwait(false))
|
||||
{
|
||||
_logger.LogInformation("Processing chunk of {count} users, total processed: {proc}, total roles: {total}, roles added: {added}, users kicked: {kicked}, users plan to kick: {planToKick}, fresh user: {fresh}",
|
||||
userList.Count, processedUsers, totalRoles + addedRoles, addedRoles, kickedUsers, toRemoveUsers, freshUsers);
|
||||
foreach (var user in userList)
|
||||
{
|
||||
if (user.IsBot) continue;
|
||||
|
||||
if (registeredUsers.Contains(user.Id))
|
||||
{
|
||||
bool roleAdded = await _botServices.AddRegisteredRoleAsync(user, registrationRole).ConfigureAwait(false);
|
||||
if (roleAdded) addedRoles++;
|
||||
else totalRoles++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((executionStartTime - user.JoinedAt.Value).TotalDays > 7)
|
||||
{
|
||||
if (kickUnregistered)
|
||||
{
|
||||
await _botServices.KickUserAsync(user).ConfigureAwait(false);
|
||||
kickedUsers++;
|
||||
}
|
||||
else
|
||||
{
|
||||
toRemoveUsers++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
freshUsers++;
|
||||
}
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
processedUsers++;
|
||||
}
|
||||
}
|
||||
|
||||
await _botServices.LogToChannel($"Processing registered users finished. Processed {processedUsers} users, added {addedRoles} roles and kicked {kickedUsers} users").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RemoveUsersNotInVanityRole(CancellationToken token)
|
||||
{
|
||||
var guild = (await _discordClient.Rest.GetGuildsAsync().ConfigureAwait(false)).First();
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation($"Cleaning up Vanity UIDs");
|
||||
await _botServices.LogToChannel("Cleaning up Vanity UIDs").ConfigureAwait(false);
|
||||
_logger.LogInformation("Getting rest guild {guildName}", guild.Name);
|
||||
var restGuild = await _discordClient.Rest.GetGuildAsync(guild.Id).ConfigureAwait(false);
|
||||
|
||||
Dictionary<ulong, string> allowedRoleIds = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.VanityRoles), new Dictionary<ulong, string>());
|
||||
_logger.LogInformation($"Allowed role ids: {string.Join(", ", allowedRoleIds)}");
|
||||
|
||||
if (allowedRoleIds.Any())
|
||||
{
|
||||
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);
|
||||
var aliasedGroups = await db.Groups.Include(u => u.Owner)
|
||||
.Where(c => !string.IsNullOrEmpty(c.Alias)).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
foreach (var lodestoneAuth in aliasedUsers)
|
||||
{
|
||||
await CheckVanityForUser(restGuild, allowedRoleIds, db, lodestoneAuth, token).ConfigureAwait(false);
|
||||
|
||||
await Task.Delay(1000, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var group in aliasedGroups)
|
||||
{
|
||||
await CheckVanityForGroup(restGuild, allowedRoleIds, db, group, token).ConfigureAwait(false);
|
||||
|
||||
await Task.Delay(1000, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("No roles for command defined, no cleanup performed");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Something failed during checking vanity user uids");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Vanity UID cleanup complete");
|
||||
await Task.Delay(TimeSpan.FromHours(12), token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckVanityForGroup(RestGuild restGuild, Dictionary<ulong, string> allowedRoleIds, LightlessDbContext db, Group group, CancellationToken token)
|
||||
{
|
||||
var groupPrimaryUser = group.OwnerUID;
|
||||
var groupOwner = await db.Auth.Include(u => u.User).SingleOrDefaultAsync(u => u.UserUID == group.OwnerUID).ConfigureAwait(false);
|
||||
if (groupOwner != null && !string.IsNullOrEmpty(groupOwner.PrimaryUserUID))
|
||||
{
|
||||
groupPrimaryUser = groupOwner.PrimaryUserUID;
|
||||
}
|
||||
|
||||
var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(f => f.User.UID == groupPrimaryUser).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 {group.OwnerUID} ({groupPrimaryUser}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List<ulong>())}");
|
||||
|
||||
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<ulong, string> allowedRoleIds, LightlessDbContext 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<ulong>())}");
|
||||
|
||||
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)
|
||||
{
|
||||
var endPoint = _connectionMultiplexer.GetEndPoints().First();
|
||||
var onlineUsers = await _connectionMultiplexer.GetServer(endPoint).KeysAsync(pattern: "UID:*").CountAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Users online: " + onlineUsers);
|
||||
await _discordClient.SetActivityAsync(new Game("Lightless for " + onlineUsers + " Users")).ConfigureAwait(false);
|
||||
await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Discord;
|
||||
using Discord.Net;
|
||||
using Discord.Rest;
|
||||
using Discord.WebSocket;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public class DiscordBotServices
|
||||
{
|
||||
public readonly string[] LodestoneServers = ["eu", "na", "jp", "fr", "de"];
|
||||
public ConcurrentDictionary<ulong, string> DiscordLodestoneMapping = new();
|
||||
public ConcurrentDictionary<ulong, string> DiscordRelinkLodestoneMapping = new();
|
||||
public ConcurrentDictionary<ulong, bool> DiscordVerifiedUsers { get; } = new();
|
||||
public ConcurrentDictionary<ulong, DateTime> LastVanityChange = new();
|
||||
public ConcurrentDictionary<string, DateTime> LastVanityGidChange = new(StringComparer.Ordinal);
|
||||
public ConcurrentDictionary<ulong, ulong> ValidInteractions { get; } = new();
|
||||
public ConcurrentDictionary<RestRole, string> VanityRoles { get; set; } = new();
|
||||
public ConcurrentBag<ulong> VerifiedCaptchaUsers { get; } = new();
|
||||
private readonly IConfigurationService<ServicesConfiguration> _configuration;
|
||||
private readonly CancellationTokenSource verificationTaskCts = new();
|
||||
private RestGuild? _guild;
|
||||
private ulong? _logChannelId;
|
||||
private RestTextChannel? _logChannel;
|
||||
|
||||
public DiscordBotServices(ILogger<DiscordBotServices> logger, LightlessMetrics metrics,
|
||||
IConfigurationService<ServicesConfiguration> configuration)
|
||||
{
|
||||
Logger = logger;
|
||||
Metrics = metrics;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public ILogger<DiscordBotServices> Logger { get; init; }
|
||||
public LightlessMetrics Metrics { get; init; }
|
||||
public ConcurrentQueue<KeyValuePair<ulong, Func<DiscordBotServices, Task>>> VerificationQueue { get; } = new();
|
||||
|
||||
public Task Start()
|
||||
{
|
||||
_ = ProcessVerificationQueue();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Stop()
|
||||
{
|
||||
verificationTaskCts.Cancel();
|
||||
verificationTaskCts.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task LogToChannel(string msg)
|
||||
{
|
||||
if (_guild == null) return;
|
||||
Logger.LogInformation("LogToChannel: {msg}", msg);
|
||||
var logChannelId = _configuration.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordChannelForBotLog), null);
|
||||
if (logChannelId == null) return;
|
||||
if (logChannelId != _logChannelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logChannelId = logChannelId;
|
||||
_logChannel = await _guild.GetTextChannelAsync(logChannelId.Value).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Could not get bot log channel");
|
||||
}
|
||||
}
|
||||
|
||||
if (_logChannel == null) return;
|
||||
await _logChannel.SendMessageAsync(msg).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RetryAsync(Task action, IUser user, string operation, bool logInfoToChannel = true)
|
||||
{
|
||||
int retryCount = 0;
|
||||
int maxRetries = 5;
|
||||
var retryDelay = TimeSpan.FromSeconds(5);
|
||||
|
||||
while (retryCount < maxRetries)
|
||||
{
|
||||
try
|
||||
{
|
||||
await action.ConfigureAwait(false);
|
||||
if (logInfoToChannel)
|
||||
await LogToChannel($"{user.Mention} {operation} SUCCESS").ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
catch (RateLimitedException)
|
||||
{
|
||||
retryCount++;
|
||||
await LogToChannel($"{user.Mention} {operation} RATELIMIT, retry {retryCount} in {retryDelay}.").ConfigureAwait(false);
|
||||
await Task.Delay(retryDelay).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await LogToChannel($"{user.Mention} {operation} FAILED: {ex.Message}").ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (retryCount == maxRetries)
|
||||
{
|
||||
await LogToChannel($"{user.Mention} FAILED: RetryCount exceeded.").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveRegisteredRoleAsync(IUser user)
|
||||
{
|
||||
var registeredRole = _configuration.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordRoleRegistered), null);
|
||||
if (registeredRole == null) return;
|
||||
var restUser = await _guild.GetUserAsync(user.Id).ConfigureAwait(false);
|
||||
if (restUser == null) return;
|
||||
if (!restUser.RoleIds.Contains(registeredRole.Value)) return;
|
||||
await RetryAsync(restUser.RemoveRoleAsync(registeredRole.Value), user, $"Remove Registered Role").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task AddRegisteredRoleAsync(IUser user)
|
||||
{
|
||||
var registeredRole = _configuration.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordRoleRegistered), null);
|
||||
if (registeredRole == null) return;
|
||||
var restUser = await _guild.GetUserAsync(user.Id).ConfigureAwait(false);
|
||||
if (restUser == null) return;
|
||||
if (restUser.RoleIds.Contains(registeredRole.Value)) return;
|
||||
await RetryAsync(restUser.AddRoleAsync(registeredRole.Value), user, $"Add Registered Role").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> AddRegisteredRoleAsync(RestGuildUser user, RestRole role)
|
||||
{
|
||||
if (user.RoleIds.Contains(role.Id)) return false;
|
||||
await RetryAsync(user.AddRoleAsync(role), user, $"Add Registered Role", false).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task KickUserAsync(RestGuildUser user)
|
||||
{
|
||||
await RetryAsync(user.KickAsync("No registration found"), user, "Kick").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ProcessVerificationQueue()
|
||||
{
|
||||
while (!verificationTaskCts.IsCancellationRequested)
|
||||
{
|
||||
Logger.LogDebug("Processing Verification Queue, Entries: {entr}", VerificationQueue.Count);
|
||||
if (VerificationQueue.TryPeek(out var queueitem))
|
||||
{
|
||||
try
|
||||
{
|
||||
await queueitem.Value.Invoke(this).ConfigureAwait(false);
|
||||
Logger.LogInformation("Processed Verification for {key}", queueitem.Key);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "Error during queue work");
|
||||
}
|
||||
finally
|
||||
{
|
||||
VerificationQueue.TryDequeue(out _);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), verificationTaskCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateGuild(RestGuild guild)
|
||||
{
|
||||
_guild = guild;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Discord;
|
||||
using Discord.Interactions;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
// todo: remove all this crap at some point
|
||||
|
||||
public class LodestoneModal : IModal
|
||||
{
|
||||
public string Title => "Verify with Lodestone";
|
||||
|
||||
[InputLabel("Enter the Lodestone URL of your Character")]
|
||||
[ModalTextInput("lodestone_url", TextInputStyle.Short, "https://*.finalfantasyxiv.com/lodestone/character/<CHARACTERID>/")]
|
||||
public string LodestoneUrl { get; set; }
|
||||
}
|
||||
293
LightlessSyncServer/LightlessSyncServices/Discord/MareModule.cs
Normal file
293
LightlessSyncServer/LightlessSyncServices/Discord/MareModule.cs
Normal file
@@ -0,0 +1,293 @@
|
||||
using Discord;
|
||||
using Discord.Interactions;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Prometheus;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Services;
|
||||
using StackExchange.Redis;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public class LightlessModule : InteractionModuleBase
|
||||
{
|
||||
private readonly ILogger<LightlessModule> _logger;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IConfigurationService<ServicesConfiguration> _lightlessServicesConfiguration;
|
||||
private readonly IConnectionMultiplexer _connectionMultiplexer;
|
||||
|
||||
public LightlessModule(ILogger<LightlessModule> logger, IServiceProvider services,
|
||||
IConfigurationService<ServicesConfiguration> lightlessServicesConfiguration,
|
||||
IConnectionMultiplexer connectionMultiplexer)
|
||||
{
|
||||
_logger = logger;
|
||||
_services = services;
|
||||
_lightlessServicesConfiguration = lightlessServicesConfiguration;
|
||||
_connectionMultiplexer = connectionMultiplexer;
|
||||
}
|
||||
|
||||
[SlashCommand("userinfo", "Shows you your user information")]
|
||||
public async Task UserInfo([Summary("secondary_uid", "(Optional) Your secondary UID")] string? secondaryUid = null,
|
||||
[Summary("discord_user", "ADMIN ONLY: Discord User to check for")] IUser? discordUser = null,
|
||||
[Summary("uid", "ADMIN ONLY: UID to check for")] string? uid = null)
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}",
|
||||
Context.Interaction.User.Id, nameof(UserInfo));
|
||||
|
||||
try
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
|
||||
eb = await HandleUserInfo(eb, Context.User.Id, secondaryUid, discordUser?.Id ?? null, uid);
|
||||
|
||||
await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("An error occured");
|
||||
eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine);
|
||||
|
||||
await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[SlashCommand("useradd", "ADMIN ONLY: add a user unconditionally to the Database")]
|
||||
public async Task UserAdd([Summary("desired_uid", "Desired UID")] string desiredUid)
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}:{params}",
|
||||
Context.Interaction.User.Id, nameof(UserAdd),
|
||||
string.Join(",", new[] { $"{nameof(desiredUid)}:{desiredUid}" }));
|
||||
|
||||
try
|
||||
{
|
||||
var embed = await HandleUserAdd(desiredUid, Context.User.Id);
|
||||
|
||||
await RespondAsync(embeds: new[] { embed }, ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("An error occured");
|
||||
eb.WithDescription("Please report this error to bug-reports: " + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine);
|
||||
|
||||
await RespondAsync(embeds: new Embed[] { eb.Build() }, ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[SlashCommand("message", "ADMIN ONLY: sends a message to clients")]
|
||||
public async Task SendMessageToClients([Summary("message", "Message to send")] string message,
|
||||
[Summary("severity", "Severity of the message")] MessageSeverity messageType = MessageSeverity.Information,
|
||||
[Summary("uid", "User ID to the person to send the message to")] string? uid = null)
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}:{message}:{type}:{uid}", Context.Interaction.User.Id, nameof(SendMessageToClients), message, messageType, uid);
|
||||
|
||||
using var scope = _services.CreateScope();
|
||||
using var db = scope.ServiceProvider.GetService<LightlessDbContext>();
|
||||
|
||||
if (!(await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(a => a.DiscordId == Context.Interaction.User.Id))?.User?.IsAdmin ?? true)
|
||||
{
|
||||
await RespondAsync("No permission", ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(uid) && !await db.Users.AnyAsync(u => u.UID == uid))
|
||||
{
|
||||
await RespondAsync("Specified UID does not exist", ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using HttpClient c = new HttpClient();
|
||||
await c.PostAsJsonAsync(new Uri(_lightlessServicesConfiguration.GetValue<Uri>
|
||||
(nameof(ServicesConfiguration.MainServerAddress)), "/msgc/sendMessage"), new ClientMessage(messageType, message, uid ?? string.Empty))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var discordChannelForMessages = _lightlessServicesConfiguration.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordChannelForMessages), null);
|
||||
if (uid == null && discordChannelForMessages != null)
|
||||
{
|
||||
var discordChannel = await Context.Guild.GetChannelAsync(discordChannelForMessages.Value) as IMessageChannel;
|
||||
if (discordChannel != null)
|
||||
{
|
||||
var embedColor = messageType switch
|
||||
{
|
||||
MessageSeverity.Information => Color.Blue,
|
||||
MessageSeverity.Warning => new Color(255, 255, 0),
|
||||
MessageSeverity.Error => Color.Red,
|
||||
_ => Color.Blue
|
||||
};
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle(messageType + " server message");
|
||||
eb.WithColor(embedColor);
|
||||
eb.WithDescription(message);
|
||||
|
||||
await discordChannel.SendMessageAsync(embed: eb.Build());
|
||||
}
|
||||
}
|
||||
|
||||
await RespondAsync("Message sent", ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await RespondAsync("Failed to send message: " + ex.ToString(), ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Embed> HandleUserAdd(string desiredUid, ulong discordUserId)
|
||||
{
|
||||
var embed = new EmbedBuilder();
|
||||
|
||||
using var scope = _services.CreateScope();
|
||||
using var db = scope.ServiceProvider.GetService<LightlessDbContext>();
|
||||
if (!(await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(a => a.DiscordId == discordUserId))?.User?.IsAdmin ?? true)
|
||||
{
|
||||
embed.WithTitle("Failed to add user");
|
||||
embed.WithDescription("No permission");
|
||||
}
|
||||
else if (db.Users.Any(u => u.UID == desiredUid || u.Alias == desiredUid))
|
||||
{
|
||||
embed.WithTitle("Failed to add user");
|
||||
embed.WithDescription("Already in Database");
|
||||
}
|
||||
else
|
||||
{
|
||||
User newUser = new()
|
||||
{
|
||||
IsAdmin = false,
|
||||
IsModerator = false,
|
||||
LastLoggedIn = DateTime.UtcNow,
|
||||
UID = desiredUid,
|
||||
};
|
||||
|
||||
var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
|
||||
var auth = new Auth()
|
||||
{
|
||||
HashedKey = StringUtils.Sha256String(computedHash),
|
||||
User = newUser,
|
||||
};
|
||||
|
||||
await db.Users.AddAsync(newUser);
|
||||
await db.Auth.AddAsync(auth);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
embed.WithTitle("Successfully added " + desiredUid);
|
||||
embed.WithDescription("Secret Key: " + computedHash);
|
||||
}
|
||||
|
||||
return embed.Build();
|
||||
}
|
||||
|
||||
private async Task<EmbedBuilder> HandleUserInfo(EmbedBuilder eb, ulong id, string? secondaryUserUid = null, ulong? optionalUser = null, string? uid = null)
|
||||
{
|
||||
bool showForSecondaryUser = secondaryUserUid != null;
|
||||
using var scope = _services.CreateScope();
|
||||
await using var db = scope.ServiceProvider.GetRequiredService<LightlessDbContext>();
|
||||
|
||||
var primaryUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == id).ConfigureAwait(false);
|
||||
|
||||
ulong userToCheckForDiscordId = id;
|
||||
|
||||
if (primaryUser == null)
|
||||
{
|
||||
eb.WithTitle("No account");
|
||||
eb.WithDescription("No Lightless account was found associated to your Discord user");
|
||||
return eb;
|
||||
}
|
||||
|
||||
bool isAdminCall = primaryUser.User.IsModerator || primaryUser.User.IsAdmin;
|
||||
|
||||
if ((optionalUser != null || uid != null) && !isAdminCall)
|
||||
{
|
||||
eb.WithTitle("Unauthorized");
|
||||
eb.WithDescription("You are not authorized to view another users' information");
|
||||
return eb;
|
||||
}
|
||||
else if ((optionalUser != null || uid != null) && isAdminCall)
|
||||
{
|
||||
LodeStoneAuth userInDb = null;
|
||||
if (optionalUser != null)
|
||||
{
|
||||
userInDb = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == optionalUser).ConfigureAwait(false);
|
||||
}
|
||||
else if (uid != null)
|
||||
{
|
||||
userInDb = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.User.UID == uid || u.User.Alias == uid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (userInDb == null)
|
||||
{
|
||||
eb.WithTitle("No account");
|
||||
eb.WithDescription("The Discord user has no valid Lightless account");
|
||||
return eb;
|
||||
}
|
||||
|
||||
userToCheckForDiscordId = userInDb.DiscordId;
|
||||
}
|
||||
|
||||
var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == userToCheckForDiscordId).ConfigureAwait(false);
|
||||
var dbUser = lodestoneUser.User;
|
||||
if (showForSecondaryUser)
|
||||
{
|
||||
dbUser = (await db.Auth.Include(u => u.User).SingleOrDefaultAsync(u => u.PrimaryUserUID == dbUser.UID && u.UserUID == secondaryUserUid))?.User;
|
||||
if (dbUser == null)
|
||||
{
|
||||
eb.WithTitle("No such secondary UID");
|
||||
eb.WithDescription($"A secondary UID {secondaryUserUid} was not found attached to your primary UID {primaryUser.User.UID}.");
|
||||
return eb;
|
||||
}
|
||||
}
|
||||
|
||||
var auth = await db.Auth.Include(u => u.PrimaryUser).SingleOrDefaultAsync(u => u.UserUID == dbUser.UID).ConfigureAwait(false);
|
||||
var groups = await db.Groups.Where(g => g.OwnerUID == dbUser.UID).ToListAsync().ConfigureAwait(false);
|
||||
var groupsJoined = await db.GroupPairs.Where(g => g.GroupUserUID == dbUser.UID).ToListAsync().ConfigureAwait(false);
|
||||
var identity = await _connectionMultiplexer.GetDatabase().StringGetAsync("UID:" + dbUser.UID).ConfigureAwait(false);
|
||||
|
||||
eb.WithTitle("User Information");
|
||||
eb.WithDescription("This is the user information for Discord User <@" + userToCheckForDiscordId + ">" + Environment.NewLine + Environment.NewLine
|
||||
+ "If you want to verify your secret key is valid, go to https://emn178.github.io/online-tools/sha256.html and copy your secret key into there and compare it to the Hashed Secret Key provided below.");
|
||||
eb.AddField("UID", dbUser.UID);
|
||||
if (!string.IsNullOrEmpty(dbUser.Alias))
|
||||
{
|
||||
eb.AddField("Vanity UID", dbUser.Alias);
|
||||
}
|
||||
if (showForSecondaryUser)
|
||||
{
|
||||
eb.AddField("Primary UID for " + dbUser.UID, auth.PrimaryUserUID);
|
||||
}
|
||||
else
|
||||
{
|
||||
var secondaryUIDs = await db.Auth.Where(p => p.PrimaryUserUID == dbUser.UID).Select(p => p.UserUID).ToListAsync();
|
||||
if (secondaryUIDs.Any())
|
||||
{
|
||||
eb.AddField("Secondary UIDs", string.Join(Environment.NewLine, secondaryUIDs));
|
||||
}
|
||||
}
|
||||
eb.AddField("Last Online (UTC)", dbUser.LastLoggedIn.ToString("U"));
|
||||
eb.AddField("Currently online ", !string.IsNullOrEmpty(identity));
|
||||
eb.AddField("Hashed Secret Key", auth.HashedKey);
|
||||
eb.AddField("Joined Syncshells", groupsJoined.Count);
|
||||
eb.AddField("Owned Syncshells", groups.Count);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var syncShellUserCount = await db.GroupPairs.CountAsync(g => g.GroupGID == group.GID).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(group.Alias))
|
||||
{
|
||||
eb.AddField("Owned Syncshell " + group.GID + " Vanity ID", group.Alias);
|
||||
}
|
||||
eb.AddField("Owned Syncshell " + group.GID + " User Count", syncShellUserCount);
|
||||
}
|
||||
|
||||
if (isAdminCall && !string.IsNullOrEmpty(identity))
|
||||
{
|
||||
eb.AddField("Character Ident", identity);
|
||||
}
|
||||
|
||||
return eb;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using Discord;
|
||||
using Discord.Interactions;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule : InteractionModuleBase
|
||||
{
|
||||
private const int _totalAprilFoolsRoles = 200;
|
||||
private const string _persistentFileName = "april2024.json";
|
||||
|
||||
private static readonly SemaphoreSlim _fileSemaphore = new(1, 1);
|
||||
|
||||
[ComponentInteraction("wizard-fools")]
|
||||
public async Task ComponentFools()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentFools), Context.Interaction.User.Id);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("WorryCoin™ and LightlessToken© Balance");
|
||||
eb.WithColor(Color.Gold);
|
||||
eb.WithDescription("You currently have" + Environment.NewLine + Environment.NewLine
|
||||
+ "**200000** MaTE©" + Environment.NewLine
|
||||
+ "**0** WorryCoin™" + Environment.NewLine + Environment.NewLine
|
||||
+ "You have no payment method set up. Press the button below to add a payment method.");
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
cb.WithButton("Add Payment Method", "wizard-fools-start", ButtonStyle.Primary, emote: new Emoji("💲"));
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-fools-start")]
|
||||
public async Task ComponentFoolsStart()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentFoolsStart), Context.Interaction.User.Id);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
var user = await Context.Guild.GetUserAsync(Context.User.Id).ConfigureAwait(false);
|
||||
bool userIsInPermanentVanityRole = _botServices.VanityRoles.Where(v => !v.Value.Contains('$', StringComparison.Ordinal))
|
||||
.Select(v => v.Key).Any(u => user.RoleIds.Contains(u.Id)) || !_botServices.VanityRoles.Any();
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
|
||||
var participatedUsers = await GetParticipants().ConfigureAwait(false);
|
||||
var remainingRoles = _totalAprilFoolsRoles - participatedUsers.Count(c => c.Value == true);
|
||||
|
||||
if (userIsInPermanentVanityRole)
|
||||
{
|
||||
eb.WithColor(Color.Green);
|
||||
eb.WithTitle("Happy April Fools!");
|
||||
eb.WithDescription("Thank you for participating in Lightlesss 2024 April Fools event."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "As you might have already guessed from the post, nothing that was written there had any truth behind it."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "This entire thing was a jab at the ridiculousness of cryptocurrency, microtransactions and games featuring multiple currencies. I hope you enjoyed the announcement post!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "__As you already have a role that gives you a permanent Vanity ID, you cannot win another one here. "
|
||||
+ "However, tell your friends as this bot will give them a chance to win one of " + _totalAprilFoolsRoles + " lifetime vanity roles.__"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "The giveaway is active until <t:" + (new DateTime(2024, 04, 01, 23, 59, 59, DateTimeKind.Utc).Subtract(DateTime.UnixEpoch).TotalSeconds) + ":f>.");
|
||||
}
|
||||
else if (participatedUsers.ContainsKey(Context.User.Id))
|
||||
{
|
||||
eb.WithColor(Color.Orange);
|
||||
eb.WithTitle("Happy April Fools!");
|
||||
eb.WithDescription("Thank you for participating in Lightlesss 2024 April Fools event."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "As you might have already guessed from the post, nothing that was written there had any truth behind it."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "This entire thing was a jab at the ridiculousness of cryptocurrency, microtransactions and games featuring multiple currencies. I hope you enjoyed the announcement post!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "__You already participated in the giveaway of the permanent Vanity roles and therefore cannot participate again. Better luck next time!__");
|
||||
}
|
||||
else if (remainingRoles > 0)
|
||||
{
|
||||
eb.WithColor(Color.Green);
|
||||
eb.WithTitle("Happy April Fools!");
|
||||
eb.WithDescription("Thank you for participating in Lightlesss 2024 April Fools event."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "As you might have already guessed from the post, nothing that was written there had any truth behind it."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "This entire thing was a jab at the ridiculousness of cryptocurrency, microtransactions and games featuring multiple currencies. I hope you enjoyed the announcement post!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "You have currently no permanent role that allows you to set a Vanity ID, however I am giving away a total of " + _totalAprilFoolsRoles + " permanent vanity roles "
|
||||
+ "(" + remainingRoles + " still remain) and you can win one using this bot!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "To win you simply have to pick one of the buttons labeled \"Win\" below this post. Which button will win is random. "
|
||||
+ "There is a 1 in 5 chance that you can win the role. __You can only participate once.__"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "The giveaway is active until <t:" + (new DateTime(2024, 04, 01, 23, 59, 59, DateTimeKind.Utc).Subtract(DateTime.UnixEpoch).TotalSeconds) + ":f>.");
|
||||
cb.WithButton("Win", "wizard-fools-win:1", ButtonStyle.Primary, new Emoji("1️⃣"));
|
||||
cb.WithButton("Win", "wizard-fools-win:2", ButtonStyle.Primary, new Emoji("2️⃣"));
|
||||
cb.WithButton("Win", "wizard-fools-win:3", ButtonStyle.Primary, new Emoji("3️⃣"));
|
||||
cb.WithButton("Win", "wizard-fools-win:4", ButtonStyle.Primary, new Emoji("4️⃣"));
|
||||
cb.WithButton("Win", "wizard-fools-win:5", ButtonStyle.Primary, new Emoji("5️⃣"));
|
||||
}
|
||||
else
|
||||
{
|
||||
eb.WithColor(Color.Orange);
|
||||
eb.WithTitle("Happy April Fools!");
|
||||
eb.WithDescription("Thank you for participating in Lightlesss 2024 April Fools event."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "As you might have already guessed from the post, nothing that was written there had any truth behind it."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "This entire thing was a jab at the ridiculousness of cryptocurrency, microtransactions and games featuring multiple currencies. I hope you enjoyed the announcement post!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "__I have been giving away " + _totalAprilFoolsRoles + " permanent Vanity ID roles for this server, however you are sadly too late as they ran out by now. "
|
||||
+ "Better luck next year with whatever I will come up with!__");
|
||||
}
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-fools-win:*")]
|
||||
public async Task ComponentFoolsWin(int number)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentFoolsWin), Context.Interaction.User.Id);
|
||||
|
||||
var winningNumber = new Random().Next(1, 6);
|
||||
EmbedBuilder eb = new();
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
bool hasWon = winningNumber == number;
|
||||
|
||||
await WriteParticipants(Context.Interaction.User.Id, hasWon).ConfigureAwait(false);
|
||||
|
||||
if (hasWon)
|
||||
{
|
||||
eb.WithColor(Color.Gold);
|
||||
eb.WithTitle("Congratulations you are winner!");
|
||||
eb.WithDescription("You, by pure accident and sheer luck, picked the right number and have won yourself a lifetime Vanity ID role on this server!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "The role will remain as long as you remain on this server, if you happen to leave it you will not get the role back."
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Head over to Home and to the Vanity IDs section to set it up for your account!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Once again, thank you for participating and have a great day.");
|
||||
|
||||
var user = await Context.Guild.GetUserAsync(Context.User.Id).ConfigureAwait(false);
|
||||
await user.AddRoleAsync(_lightlessServicesConfiguration.GetValue<ulong?>(nameof(ServicesConfiguration.DiscordRoleAprilFools2024)).Value).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Fortune did not bless you");
|
||||
eb.WithDescription("You, through sheer misfortune, sadly did not pick the right number. (The winning number was " + winningNumber + ")"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Better luck next time!"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Once again, thank you for participating and regardless, have a great day.");
|
||||
}
|
||||
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<ulong, bool>> GetParticipants()
|
||||
{
|
||||
await _fileSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_persistentFileName))
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(_persistentFileName).ConfigureAwait(false);
|
||||
return JsonSerializer.Deserialize<Dictionary<ulong, bool>>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fileSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteParticipants(ulong participant, bool win)
|
||||
{
|
||||
await _fileSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
Dictionary<ulong, bool> participants = new();
|
||||
if (File.Exists(_persistentFileName))
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(_persistentFileName).ConfigureAwait(false);
|
||||
participants = JsonSerializer.Deserialize<Dictionary<ulong, bool>>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// probably empty file just deal with it
|
||||
}
|
||||
}
|
||||
|
||||
participants[participant] = win;
|
||||
|
||||
await File.WriteAllTextAsync(_persistentFileName, JsonSerializer.Serialize(participants)).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fileSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Discord.WebSocket;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-delete")]
|
||||
public async Task ComponentDelete()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentDelete), Context.Interaction.User.Id);
|
||||
|
||||
using var lightlessDb = 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
|
||||
+ "__Note: deleting your primary UID will delete all associated secondary UIDs as well.__" + 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.");
|
||||
eb.WithColor(Color.Blue);
|
||||
|
||||
ComponentBuilder cb = new();
|
||||
await AddUserSelection(lightlessDb, cb, "wizard-delete-select").ConfigureAwait(false);
|
||||
AddHome(cb);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-delete-select")]
|
||||
public async Task SelectionDeleteAccount(string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionDeleteAccount), Context.Interaction.User.Id, uid);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
bool isPrimary = lightlessDb.Auth.Single(u => u.UserUID == uid).PrimaryUserUID == null;
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle($"Are you sure you want to delete {uid}?");
|
||||
eb.WithDescription($"This operation is irreversible. All your pairs, joined syncshells and information stored on the service for {uid} will be " +
|
||||
$"irrevocably deleted." +
|
||||
(isPrimary ? (Environment.NewLine + Environment.NewLine +
|
||||
"⚠️ **You are about to delete a Primary UID, all attached Secondary UIDs and their information will be deleted as well.** ⚠️") : string.Empty));
|
||||
eb.WithColor(Color.Purple);
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Cancel", "wizard-delete", emote: new Emoji("❌"));
|
||||
cb.WithButton($"Delete {uid}", "wizard-delete-confirm:" + uid, ButtonStyle.Danger, emote: new Emoji("🗑️"));
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-delete-confirm:*")]
|
||||
public async Task ComponentDeleteAccountConfirm(string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(ComponentDeleteAccountConfirm), Context.Interaction.User.Id, uid);
|
||||
|
||||
await RespondWithModalAsync<ConfirmDeletionModal>("wizard-delete-confirm-modal:" + uid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ModalInteraction("wizard-delete-confirm-modal:*")]
|
||||
public async Task ModalDeleteAccountConfirm(string uid, ConfirmDeletionModal modal)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(ModalDeleteAccountConfirm), Context.Interaction.User.Id, uid);
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.Equals("DELETE", modal.Delete, StringComparison.Ordinal))
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("Did not confirm properly");
|
||||
eb.WithDescription($"You entered {modal.Delete} but requested was DELETE. Please try again and enter DELETE to confirm.");
|
||||
eb.WithColor(Color.Red);
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Cancel", "wizard-delete", emote: new Emoji("❌"));
|
||||
cb.WithButton("Retry", "wizard-delete-confirm:" + uid, emote: new Emoji("🔁"));
|
||||
|
||||
await ModifyModalInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var maxGroupsByUser = _lightlessClientConfigurationService.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 3);
|
||||
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
var user = await db.Users.SingleAsync(u => u.UID == uid).ConfigureAwait(false);
|
||||
var lodestone = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.User.UID == uid).ConfigureAwait(false);
|
||||
await SharedDbFunctions.PurgeUser(_logger, user, db, maxGroupsByUser).ConfigureAwait(false);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle($"Account {uid} successfully deleted");
|
||||
eb.WithColor(Color.Green);
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
|
||||
await ModifyModalInteraction(eb, cb).ConfigureAwait(false);
|
||||
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} DELETE SUCCESS: {uid}").ConfigureAwait(false);
|
||||
|
||||
// only remove role if deleted uid has lodestone attached (== primary uid)
|
||||
if (lodestone != null)
|
||||
{
|
||||
await _botServices.RemoveRegisteredRoleAsync(Context.Interaction.User).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error handling modal delete account confirm");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-recover")]
|
||||
public async Task ComponentRecover()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentRecover), Context.Interaction.User.Id);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
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 Lightless 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
|
||||
+ "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(lightlessDb, cb, "wizard-recover-select").ConfigureAwait(false);
|
||||
AddHome(cb);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-recover-select")]
|
||||
public async Task SelectionRecovery(string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionRecovery), Context.Interaction.User.Id, uid);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithColor(Color.Green);
|
||||
await HandleRecovery(lightlessDb, eb, uid).ConfigureAwait(false);
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task HandleRecovery(LightlessDbContext db, EmbedBuilder embed, string uid)
|
||||
{
|
||||
string computedHash = string.Empty;
|
||||
Auth auth;
|
||||
var previousAuth = await db.Auth.Include(u => u.User).FirstOrDefaultAsync(u => u.UserUID == uid).ConfigureAwait(false);
|
||||
if (previousAuth != null)
|
||||
{
|
||||
db.Auth.Remove(previousAuth);
|
||||
}
|
||||
|
||||
computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
|
||||
string hashedKey = StringUtils.Sha256String(computedHash);
|
||||
auth = new Auth()
|
||||
{
|
||||
HashedKey = hashedKey,
|
||||
User = previousAuth.User,
|
||||
PrimaryUserUID = previousAuth.PrimaryUserUID
|
||||
};
|
||||
|
||||
await db.Auth.AddAsync(auth).ConfigureAwait(false);
|
||||
|
||||
embed.WithTitle($"Recovery for {uid} complete");
|
||||
embed.WithDescription("This is your new 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 authentication. If you are using the suggested OAuth2 authentication, you do not need to use the Secret Key or recover ever again.__**"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ $"||**`{computedHash}`**||"
|
||||
+ Environment.NewLine
|
||||
+ "__NOTE: The Secret Key only contains the letters ABCDEF and numbers 0 - 9.__"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Enter this key in the Lightless Sync Service Settings and reconnect to the service.");
|
||||
|
||||
await db.Auth.AddAsync(auth).ConfigureAwait(false);
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_botServices.Logger.LogInformation("User recovered: {userUID}:{hashedKey}", previousAuth.UserUID, hashedKey);
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} RECOVER SUCCESS: {previousAuth.UserUID}").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Discord.Rest;
|
||||
using Discord.WebSocket;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-register")]
|
||||
public async Task ComponentRegister()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_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 Lightless 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("🌒"));
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-register-start")]
|
||||
public async Task ComponentRegisterStart()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentRegisterStart), Context.Interaction.User.Id);
|
||||
|
||||
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)
|
||||
{
|
||||
db.LodeStoneAuth.Remove(entry);
|
||||
}
|
||||
_botServices.DiscordLodestoneMapping.TryRemove(Context.User.Id, out _);
|
||||
_botServices.DiscordVerifiedUsers.TryRemove(Context.User.Id, out _);
|
||||
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await RespondWithModalAsync<LodestoneModal>("wizard-register-lodestone-modal").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ModalInteraction("wizard-register-lodestone-modal")]
|
||||
public async Task ModalRegister(LodestoneModal lodestoneModal)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{lodestone}", nameof(ModalRegister), Context.Interaction.User.Id, lodestoneModal.LodestoneUrl);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithColor(Color.Purple);
|
||||
var success = await HandleRegisterModalAsync(eb, lodestoneModal).ConfigureAwait(false);
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Cancel", "wizard-register", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
if (success.Item1) cb.WithButton("Verify", "wizard-register-verify:" + success.Item2, ButtonStyle.Primary, emote: new Emoji("✅"));
|
||||
else cb.WithButton("Try again", "wizard-register-start", ButtonStyle.Primary, emote: new Emoji("🔁"));
|
||||
await ModifyModalInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-register-verify:*")]
|
||||
public async Task ComponentRegisterVerify(string verificationCode)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{verificationcode}", nameof(ComponentRegisterVerify), Context.Interaction.User.Id, verificationCode);
|
||||
|
||||
_botServices.VerificationQueue.Enqueue(new KeyValuePair<ulong, Func<DiscordBotServices, Task>>(Context.User.Id,
|
||||
(service) => HandleVerifyAsync(Context.User.Id, verificationCode, service)));
|
||||
EmbedBuilder eb = new();
|
||||
ComponentBuilder cb = new();
|
||||
eb.WithColor(Color.Purple);
|
||||
cb.WithButton("Cancel", "wizard-register", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Check", "wizard-register-verify-check:" + verificationCode, ButtonStyle.Primary, emote: new Emoji("❓"));
|
||||
eb.WithTitle("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-register-verify-check:*")]
|
||||
public async Task ComponentRegisterVerifyCheck(string verificationCode)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(ComponentRegisterVerifyCheck), Context.Interaction.User.Id, 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 registerSuccess = false;
|
||||
if (!verificationRan)
|
||||
{
|
||||
if (stillEnqueued)
|
||||
{
|
||||
eb.WithColor(Color.Gold);
|
||||
eb.WithTitle("Your verification is still pending");
|
||||
eb.WithDescription("Please try again and click Check in a few seconds");
|
||||
cb.WithButton("Cancel", "wizard-register", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Check", "wizard-register-verify-check:" + verificationCode, ButtonStyle.Primary, emote: new Emoji("❓"));
|
||||
}
|
||||
else
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Something went wrong");
|
||||
eb.WithDescription("Your verification was processed but did not arrive properly. Please try to start the registration from the start.");
|
||||
cb.WithButton("Restart", "wizard-register", ButtonStyle.Primary, emote: new Emoji("🔁"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (verified)
|
||||
{
|
||||
eb.WithColor(Color.Green);
|
||||
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.**"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "**__NOTE: Secret keys are considered legacy. Using the suggested OAuth2 authentication in Lightless, you do not need to use this Secret Key.__**"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ $"||**`{key}`**||"
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "If you want to continue using legacy authentication, enter this key in Lightless 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
|
||||
+ "You should connect as soon as possible to not get caught by the automatic cleanup process."
|
||||
+ Environment.NewLine
|
||||
+ "Have fun.");
|
||||
AddHome(cb);
|
||||
registerSuccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
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
|
||||
+ "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("🔁"));
|
||||
}
|
||||
}
|
||||
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
if (registerSuccess)
|
||||
await _botServices.AddRegisteredRoleAsync(Context.Interaction.User).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<(bool, string)> HandleRegisterModalAsync(EmbedBuilder embed, LodestoneModal arg)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// 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.WithDescription("This account is banned");
|
||||
return (false, string.Empty);
|
||||
}
|
||||
|
||||
if (db.LodeStoneAuth.Any(a => a.HashedLodestoneId == hashedLodestoneId))
|
||||
{
|
||||
// character already in db
|
||||
embed.WithDescription("This lodestone character already exists in the Database. If you want to attach this character to your current Discord account use relink.");
|
||||
return (false, string.Empty);
|
||||
}
|
||||
|
||||
string lodestoneAuth = await GenerateLodestoneAuth(Context.User.Id, hashedLodestoneId, db).ConfigureAwait(false);
|
||||
// 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
|
||||
+ $"**! THIS IS NOT THE KEY YOU HAVE TO ENTER IN LIGHTLESS !**"
|
||||
+ 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 Lightless Sync."
|
||||
+ 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 registration will be invalidated and you have to register again.");
|
||||
_botServices.DiscordLodestoneMapping[Context.User.Id] = lodestoneId.ToString();
|
||||
|
||||
return (true, lodestoneAuth);
|
||||
}
|
||||
|
||||
private async Task HandleVerifyAsync(ulong userid, string authString, DiscordBotServices services)
|
||||
{
|
||||
using var req = new HttpClient();
|
||||
|
||||
services.DiscordVerifiedUsers.Remove(userid, out _);
|
||||
if (services.DiscordLodestoneMapping.ContainsKey(userid))
|
||||
{
|
||||
var randomServer = services.LodestoneServers[random.Next(services.LodestoneServers.Length)];
|
||||
var url = $"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{services.DiscordLodestoneMapping[userid]}";
|
||||
using var response = await req.GetAsync(url).ConfigureAwait(false);
|
||||
_logger.LogInformation("Verifying {userid} with URL {url}", userid, url);
|
||||
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("Verified {userid} from lodestone {lodestone}", userid, services.DiscordLodestoneMapping[userid]);
|
||||
await _botServices.LogToChannel($"<@{userid}> REGISTER VERIFY: Success.").ConfigureAwait(false);
|
||||
services.DiscordLodestoneMapping.TryRemove(userid, out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
services.DiscordVerifiedUsers[userid] = false;
|
||||
_logger.LogInformation("Could not verify {userid} from lodestone {lodestone}, did not find authString: {authString}, status code was: {code}",
|
||||
userid, services.DiscordLodestoneMapping[userid], authString, response.StatusCode);
|
||||
await _botServices.LogToChannel($"<@{userid}> REGISTER VERIFY: Failed: No Authstring ({authString}). (<{url}>)").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Could not verify {userid}, HttpStatusCode: {code}", userid, response.StatusCode);
|
||||
await _botServices.LogToChannel($"<@{userid}> REGISTER VERIFY: Failed: HttpStatusCode {response.StatusCode}. (<{url}>)").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(string, string)> HandleAddUser(LightlessDbContext db)
|
||||
{
|
||||
var lodestoneAuth = db.LodeStoneAuth.SingleOrDefault(u => u.DiscordId == Context.User.Id);
|
||||
|
||||
var user = new User();
|
||||
|
||||
var hasValidUid = false;
|
||||
while (!hasValidUid)
|
||||
{
|
||||
var uid = StringUtils.GenerateRandomString(10);
|
||||
if (db.Users.Any(u => u.UID == uid || u.Alias == uid)) continue;
|
||||
user.UID = uid;
|
||||
hasValidUid = true;
|
||||
}
|
||||
|
||||
// make the first registered user on the service to admin
|
||||
if (!await db.Users.AnyAsync().ConfigureAwait(false))
|
||||
{
|
||||
user.IsAdmin = true;
|
||||
}
|
||||
|
||||
user.LastLoggedIn = DateTime.UtcNow;
|
||||
|
||||
var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
|
||||
string hashedKey = StringUtils.Sha256String(computedHash);
|
||||
var auth = new Auth()
|
||||
{
|
||||
HashedKey = hashedKey,
|
||||
User = user,
|
||||
};
|
||||
|
||||
await db.Users.AddAsync(user).ConfigureAwait(false);
|
||||
await db.Auth.AddAsync(auth).ConfigureAwait(false);
|
||||
|
||||
lodestoneAuth.StartedAt = null;
|
||||
lodestoneAuth.User = user;
|
||||
lodestoneAuth.LodestoneAuthString = null;
|
||||
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_botServices.Logger.LogInformation("User registered: {userUID}:{hashedKey}", user.UID, hashedKey);
|
||||
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} REGISTER COMPLETE: => {user.UID}").ConfigureAwait(false);
|
||||
|
||||
_botServices.DiscordVerifiedUsers.Remove(Context.User.Id, out _);
|
||||
|
||||
return (user.UID, computedHash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[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 Lightless 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 Lightless Sync 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 LIGHTLESS !**"
|
||||
+ 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(LightlessDbContext 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Utils;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-secondary")]
|
||||
public async Task ComponentSecondary()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentSecondary), Context.Interaction.User.Id);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
var primaryUID = (await lightlessDb.LodeStoneAuth.Include(u => u.User).SingleAsync(u => u.DiscordId == Context.User.Id).ConfigureAwait(false)).User.UID;
|
||||
var secondaryUids = await lightlessDb.Auth.CountAsync(p => p.PrimaryUserUID == primaryUID).ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
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 Lightless accounts with their own pair list, joined syncshells, UID and so on." + Environment.NewLine
|
||||
+ "Use this to create UIDs if you want to use Lightless 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 Lightless for alts." + Environment.NewLine + Environment.NewLine
|
||||
+ $"You currently have {secondaryUids} Secondary UIDs out of a maximum of 20.");
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
cb.WithButton("Create Secondary UID", "wizard-secondary-create:" + primaryUID, ButtonStyle.Primary, emote: new Emoji("2️⃣"), disabled: secondaryUids >= 20);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-secondary-create:*")]
|
||||
public async Task ComponentSecondaryCreate(string primaryUid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{primary}", nameof(ComponentSecondaryCreate), Context.Interaction.User.Id, primaryUid);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("Secondary UID created");
|
||||
eb.WithColor(Color.Green);
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
await HandleAddSecondary(lightlessDb, eb, primaryUid).ConfigureAwait(false);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task HandleAddSecondary(LightlessDbContext db, EmbedBuilder embed, string primaryUID)
|
||||
{
|
||||
User newUser = new()
|
||||
{
|
||||
IsAdmin = false,
|
||||
IsModerator = false,
|
||||
LastLoggedIn = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
var hasValidUid = false;
|
||||
while (!hasValidUid)
|
||||
{
|
||||
var uid = StringUtils.GenerateRandomString(10);
|
||||
if (await db.Users.AnyAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false)) continue;
|
||||
newUser.UID = uid;
|
||||
hasValidUid = true;
|
||||
}
|
||||
|
||||
var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
|
||||
var auth = new Auth()
|
||||
{
|
||||
HashedKey = StringUtils.Sha256String(computedHash),
|
||||
User = newUser,
|
||||
PrimaryUserUID = primaryUID
|
||||
};
|
||||
|
||||
await db.Users.AddAsync(newUser).ConfigureAwait(false);
|
||||
await db.Auth.AddAsync(auth).ConfigureAwait(false);
|
||||
|
||||
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 Lightless setings in the Service Settings tab.");
|
||||
embed.AddField("UID", newUser.UID);
|
||||
embed.AddField("Secret Key", computedHash);
|
||||
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} SECONDARY SUCCESS: {newUser.UID}").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using LightlessSyncShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-userinfo")]
|
||||
public async Task ComponentUserinfo()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentUserinfo), Context.Interaction.User.Id);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("User Info");
|
||||
eb.WithColor(Color.Blue);
|
||||
eb.WithDescription("You can see information about your user account(s) here." + Environment.NewLine
|
||||
+ "Use the selection below to select a user account to see info for." + 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.");
|
||||
ComponentBuilder cb = new();
|
||||
await AddUserSelection(lightlessDb, cb, "wizard-userinfo-select").ConfigureAwait(false);
|
||||
AddHome(cb);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-userinfo-select")]
|
||||
public async Task SelectionUserinfo(string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionUserinfo), Context.Interaction.User.Id, uid);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle($"User Info for {uid}");
|
||||
await HandleUserInfo(eb, lightlessDb, uid).ConfigureAwait(false);
|
||||
eb.WithColor(Color.Green);
|
||||
ComponentBuilder cb = new();
|
||||
await AddUserSelection(lightlessDb, cb, "wizard-userinfo-select").ConfigureAwait(false);
|
||||
AddHome(cb);
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task HandleUserInfo(EmbedBuilder eb, LightlessDbContext db, string uid)
|
||||
{
|
||||
ulong userToCheckForDiscordId = Context.User.Id;
|
||||
|
||||
var dbUser = await db.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false);
|
||||
|
||||
var groups = await db.Groups.Where(g => g.OwnerUID == dbUser.UID).ToListAsync().ConfigureAwait(false);
|
||||
var groupsJoined = await db.GroupPairs.Where(g => g.GroupUserUID == dbUser.UID).ToListAsync().ConfigureAwait(false);
|
||||
var identity = await _connectionMultiplexer.GetDatabase().StringGetAsync("UID:" + dbUser.UID).ConfigureAwait(false);
|
||||
|
||||
eb.WithDescription("This is the user info for your selected UID. You can check other UIDs or go back using the menu below.");
|
||||
if (!string.IsNullOrEmpty(dbUser.Alias))
|
||||
{
|
||||
eb.AddField("Vanity UID", dbUser.Alias);
|
||||
}
|
||||
eb.AddField("Last Online (UTC)", dbUser.LastLoggedIn.ToString("U"));
|
||||
eb.AddField("Currently online ", !string.IsNullOrEmpty(identity));
|
||||
eb.AddField("Joined Syncshells", groupsJoined.Count);
|
||||
eb.AddField("Owned Syncshells", groups.Count);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var syncShellUserCount = await db.GroupPairs.CountAsync(g => g.GroupGID == group.GID).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(group.Alias))
|
||||
{
|
||||
eb.AddField("Owned Syncshell " + group.GID + " Vanity ID", group.Alias);
|
||||
}
|
||||
eb.AddField("Owned Syncshell " + group.GID + " User Count", syncShellUserCount);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using Discord.Interactions;
|
||||
using Discord;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule
|
||||
{
|
||||
[ComponentInteraction("wizard-vanity")]
|
||||
public async Task ComponentVanity()
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(ComponentVanity), Context.Interaction.User.Id);
|
||||
|
||||
StringBuilder sb = new();
|
||||
var user = await Context.Guild.GetUserAsync(Context.User.Id).ConfigureAwait(false);
|
||||
bool userIsInVanityRole = _botServices.VanityRoles.Keys.Any(u => user.RoleIds.Contains(u.Id)) || !_botServices.VanityRoles.Any();
|
||||
if (!userIsInVanityRole)
|
||||
{
|
||||
sb.AppendLine("To be able to set Vanity IDs you must have one of the following roles:");
|
||||
foreach (var role in _botServices.VanityRoles)
|
||||
{
|
||||
sb.Append("- ").Append(role.Key.Mention).Append(" (").Append(role.Value).AppendLine(")");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("Your current roles on this server allow you to set Vanity IDs.");
|
||||
}
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("Vanity IDs");
|
||||
eb.WithDescription("You are able to set your Vanity IDs here." + Environment.NewLine
|
||||
+ "Vanity IDs are a way to customize your displayed UID or Syncshell ID to others." + Environment.NewLine + Environment.NewLine
|
||||
+ sb.ToString());
|
||||
eb.WithColor(Color.Blue);
|
||||
ComponentBuilder cb = new();
|
||||
AddHome(cb);
|
||||
if (userIsInVanityRole)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-vanity-uid")]
|
||||
public async Task SelectionVanityUid(string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionVanityUid), Context.Interaction.User.Id, uid);
|
||||
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
var user = db.Users.Single(u => u.UID == uid);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithColor(Color.Purple);
|
||||
eb.WithTitle($"Set Vanity UID for {uid}");
|
||||
eb.WithDescription($"You are about to change the Vanity UID for {uid}" + Environment.NewLine + Environment.NewLine
|
||||
+ "The current Vanity UID is set to: **" + (user.Alias == null ? "No Vanity UID set" : user.Alias) + "**");
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Set Vanity ID", "wizard-vanity-uid-set:" + uid, ButtonStyle.Primary, new Emoji("💅"));
|
||||
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-vanity-uid-set:*")]
|
||||
public async Task SelectionVanityUidSet(string uid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionVanityUidSet), Context.Interaction.User.Id, uid);
|
||||
|
||||
await RespondWithModalAsync<VanityUidModal>("wizard-vanity-uid-modal:" + uid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ModalInteraction("wizard-vanity-uid-modal:*")]
|
||||
public async Task ConfirmVanityUidModal(string uid, VanityUidModal modal)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{uid}:{vanity}", nameof(ConfirmVanityUidModal), Context.Interaction.User.Id, uid, modal.DesiredVanityUID);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
ComponentBuilder cb = new();
|
||||
var desiredVanityUid = modal.DesiredVanityUID;
|
||||
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);
|
||||
if (!rgx.Match(desiredVanityUid).Success)
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Invalid Vanity UID");
|
||||
eb.WithDescription("A Vanity UID must be between 5 and 15 characters long and only contain the letters A-Z, numbers 0-9, dashes (-) and underscores (_).");
|
||||
cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Pick Different UID", "wizard-vanity-uid-set:" + uid, ButtonStyle.Primary, new Emoji("💅"));
|
||||
}
|
||||
else if (!canAddVanityId)
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Vanity UID already taken");
|
||||
eb.WithDescription($"The Vanity UID {desiredVanityUid} has already been claimed. Please pick a different one.");
|
||||
cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Pick Different UID", "wizard-vanity-uid-set:" + uid, ButtonStyle.Primary, new Emoji("💅"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = await db.Users.SingleAsync(u => u.UID == uid).ConfigureAwait(false);
|
||||
user.Alias = desiredVanityUid;
|
||||
db.Update(user);
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
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 Lightless service.");
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} VANITY UID SET: UID: {user.UID}, Vanity: {desiredVanityUid}").ConfigureAwait(false);
|
||||
AddHome(cb);
|
||||
}
|
||||
|
||||
await ModifyModalInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-vanity-gid")]
|
||||
public async Task SelectionVanityGid(string gid)
|
||||
{
|
||||
_logger.LogInformation("{method}:{userId}:{uid}", nameof(SelectionVanityGid), Context.Interaction.User.Id, gid);
|
||||
|
||||
using var db = await GetDbContext().ConfigureAwait(false);
|
||||
var group = db.Groups.Single(u => u.GID == gid);
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithColor(Color.Purple);
|
||||
eb.WithTitle($"Set Vanity GID for {gid}");
|
||||
eb.WithDescription($"You are about to change the Vanity Syncshell ID for {gid}" + Environment.NewLine + Environment.NewLine
|
||||
+ "The current Vanity Syncshell ID is set to: **" + (group.Alias == null ? "No Vanity Syncshell ID set" : group.Alias) + "**");
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Set Vanity ID", "wizard-vanity-gid-set:" + gid, ButtonStyle.Primary, new Emoji("💅"));
|
||||
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-vanity-gid-set:*")]
|
||||
public async Task SelectionVanityGidSet(string gid)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{gid}", nameof(SelectionVanityGidSet), Context.Interaction.User.Id, gid);
|
||||
|
||||
await RespondWithModalAsync<VanityGidModal>("wizard-vanity-gid-modal:" + gid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ModalInteraction("wizard-vanity-gid-modal:*")]
|
||||
public async Task ConfirmVanityGidModal(string gid, VanityGidModal modal)
|
||||
{
|
||||
if (!(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
_logger.LogInformation("{method}:{userId}:{gid}:{vanity}", nameof(ConfirmVanityGidModal), Context.Interaction.User.Id, gid, modal.DesiredVanityGID);
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
ComponentBuilder cb = new();
|
||||
var desiredVanityGid = modal.DesiredVanityGID;
|
||||
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);
|
||||
if (!rgx.Match(desiredVanityGid).Success)
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Invalid Vanity Syncshell ID");
|
||||
eb.WithDescription("A Vanity Syncshell ID must be between 5 and 20 characters long and only contain the letters A-Z, numbers 0-9, dashes (-) and underscores (_).");
|
||||
cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Pick Different ID", "wizard-vanity-gid-set:" + gid, ButtonStyle.Primary, new Emoji("💅"));
|
||||
}
|
||||
else if (!canAddVanityId)
|
||||
{
|
||||
eb.WithColor(Color.Red);
|
||||
eb.WithTitle("Vanity Syncshell ID already taken");
|
||||
eb.WithDescription($"The Vanity Synshell ID \"{desiredVanityGid}\" has already been claimed. Please pick a different one.");
|
||||
cb.WithButton("Cancel", "wizard-vanity", ButtonStyle.Secondary, emote: new Emoji("❌"));
|
||||
cb.WithButton("Pick Different ID", "wizard-vanity-gid-set:" + gid, ButtonStyle.Primary, new Emoji("💅"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var group = await db.Groups.SingleAsync(u => u.GID == gid).ConfigureAwait(false);
|
||||
group.Alias = desiredVanityGid;
|
||||
db.Update(group);
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
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 Lightless service.");
|
||||
AddHome(cb);
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} VANITY GID SET: GID: {group.GID}, Vanity: {desiredVanityGid}").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ModifyModalInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
using Discord;
|
||||
using Discord.Interactions;
|
||||
using Discord.WebSocket;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StackExchange.Redis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace LightlessSyncServices.Discord;
|
||||
|
||||
public partial class LightlessWizardModule : InteractionModuleBase
|
||||
{
|
||||
private ILogger<LightlessModule> _logger;
|
||||
private DiscordBotServices _botServices;
|
||||
private IConfigurationService<ServerConfiguration> _lightlessClientConfigurationService;
|
||||
private IConfigurationService<ServicesConfiguration> _lightlessServicesConfiguration;
|
||||
private IConnectionMultiplexer _connectionMultiplexer;
|
||||
private readonly IDbContextFactory<LightlessDbContext> _dbContextFactory;
|
||||
private Random random = new();
|
||||
|
||||
public LightlessWizardModule(ILogger<LightlessModule> logger, DiscordBotServices botServices,
|
||||
IConfigurationService<ServerConfiguration> lightlessClientConfigurationService,
|
||||
IConfigurationService<ServicesConfiguration> lightlessServicesConfiguration,
|
||||
IConnectionMultiplexer connectionMultiplexer, IDbContextFactory<LightlessDbContext> dbContextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_botServices = botServices;
|
||||
_lightlessClientConfigurationService = lightlessClientConfigurationService;
|
||||
_lightlessServicesConfiguration = lightlessServicesConfiguration;
|
||||
_connectionMultiplexer = connectionMultiplexer;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-captcha:*")]
|
||||
public async Task WizardCaptcha(bool init = false)
|
||||
{
|
||||
if (!init && !(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
if (_botServices.VerifiedCaptchaUsers.Contains(Context.Interaction.User.Id))
|
||||
{
|
||||
await StartWizard(true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
|
||||
Random rnd = new Random();
|
||||
var correctButton = rnd.Next(4) + 1;
|
||||
string nthButtonText = correctButton switch
|
||||
{
|
||||
1 => "first",
|
||||
2 => "second",
|
||||
3 => "third",
|
||||
4 => "fourth",
|
||||
_ => "unknown",
|
||||
};
|
||||
|
||||
Emoji nthButtonEmoji = correctButton switch
|
||||
{
|
||||
1 => new Emoji("⬅️"),
|
||||
2 => new Emoji("🤖"),
|
||||
3 => new Emoji("‼️"),
|
||||
4 => new Emoji("✉️"),
|
||||
_ => "unknown",
|
||||
};
|
||||
|
||||
eb.WithTitle("Lightless Bot Services Captcha");
|
||||
eb.WithDescription("You are seeing this embed because you interact with this bot for the first time since the bot has been restarted." + Environment.NewLine + Environment.NewLine
|
||||
+ "This bot __requires__ embeds for its function. To proceed, please verify you have embeds enabled." + Environment.NewLine
|
||||
+ $"## To verify you have embeds enabled __press on the **{nthButtonText}** button ({nthButtonEmoji}).__");
|
||||
eb.WithColor(Color.LightOrange);
|
||||
|
||||
int incorrectButtonHighlight = 1;
|
||||
do
|
||||
{
|
||||
incorrectButtonHighlight = rnd.Next(4) + 1;
|
||||
}
|
||||
while (incorrectButtonHighlight == correctButton);
|
||||
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("This", correctButton == 1 ? "wizard-home:false" : "wizard-captcha-fail:1", emote: new Emoji("⬅️"), style: incorrectButtonHighlight == 1 ? ButtonStyle.Primary : ButtonStyle.Secondary);
|
||||
cb.WithButton("Bot", correctButton == 2 ? "wizard-home:false" : "wizard-captcha-fail:2", emote: new Emoji("🤖"), style: incorrectButtonHighlight == 2 ? ButtonStyle.Primary : ButtonStyle.Secondary);
|
||||
cb.WithButton("Requires", correctButton == 3 ? "wizard-home:false" : "wizard-captcha-fail:3", emote: new Emoji("‼️"), style: incorrectButtonHighlight == 3 ? ButtonStyle.Primary : ButtonStyle.Secondary);
|
||||
cb.WithButton("Embeds", correctButton == 4 ? "wizard-home:false" : "wizard-captcha-fail:4", emote: new Emoji("✉️"), style: incorrectButtonHighlight == 4 ? ButtonStyle.Primary : ButtonStyle.Secondary);
|
||||
|
||||
await InitOrUpdateInteraction(init, eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task InitOrUpdateInteraction(bool init, EmbedBuilder eb, ComponentBuilder cb)
|
||||
{
|
||||
if (init)
|
||||
{
|
||||
await RespondAsync(embed: eb.Build(), components: cb.Build(), ephemeral: true).ConfigureAwait(false);
|
||||
var resp = await GetOriginalResponseAsync().ConfigureAwait(false);
|
||||
_botServices.ValidInteractions[Context.User.Id] = resp.Id;
|
||||
_logger.LogInformation("Init Msg: {id}", resp.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[ComponentInteraction("wizard-captcha-fail:*")]
|
||||
public async Task WizardCaptchaFail(int button)
|
||||
{
|
||||
ComponentBuilder cb = new();
|
||||
cb.WithButton("Restart (with Embeds enabled)", "wizard-captcha:false", emote: new Emoji("↩️"));
|
||||
await ((Context.Interaction) as IComponentInteraction).UpdateAsync(m =>
|
||||
{
|
||||
m.Embed = null;
|
||||
m.Content = "You pressed the wrong button. You likely have embeds disabled. Enable embeds in your Discord client (Settings -> Chat -> \"Show embeds and preview website links pasted into chat\") and try again.";
|
||||
m.Components = cb.Build();
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
await _botServices.LogToChannel($"{Context.User.Mention} FAILED CAPTCHA").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
[ComponentInteraction("wizard-home:*")]
|
||||
public async Task StartWizard(bool init = false)
|
||||
{
|
||||
if (!init && !(await ValidateInteraction().ConfigureAwait(false))) return;
|
||||
|
||||
if (!_botServices.VerifiedCaptchaUsers.Contains(Context.Interaction.User.Id))
|
||||
_botServices.VerifiedCaptchaUsers.Add(Context.Interaction.User.Id);
|
||||
|
||||
_logger.LogInformation("{method}:{userId}", nameof(StartWizard), Context.Interaction.User.Id);
|
||||
|
||||
using var lightlessDb = await GetDbContext().ConfigureAwait(false);
|
||||
bool hasAccount = await lightlessDb.LodeStoneAuth.AnyAsync(u => u.DiscordId == Context.User.Id && u.StartedAt == null).ConfigureAwait(false);
|
||||
|
||||
if (init)
|
||||
{
|
||||
bool isBanned = await lightlessDb.BannedRegistrations.AnyAsync(u => u.DiscordIdOrLodestoneAuth == Context.User.Id.ToString()).ConfigureAwait(false);
|
||||
|
||||
if (isBanned)
|
||||
{
|
||||
EmbedBuilder ebBanned = new();
|
||||
ebBanned.WithTitle("You are not welcome here");
|
||||
ebBanned.WithDescription("Your Discord account is banned");
|
||||
await RespondAsync(embed: ebBanned.Build(), ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#if !DEBUG
|
||||
bool isInAprilFoolsMode = _lightlessServicesConfiguration.GetValueOrDefault<ulong?>(nameof(ServicesConfiguration.DiscordRoleAprilFools2024), null) != null
|
||||
&& DateTime.UtcNow.Month == 4 && DateTime.UtcNow.Day == 1 && DateTime.UtcNow.Year == 2024 && DateTime.UtcNow.Hour >= 10;
|
||||
#elif DEBUG
|
||||
bool isInAprilFoolsMode = true;
|
||||
#endif
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("Welcome to the Lightless Sync Service Bot for this server");
|
||||
eb.WithDescription("Here is what you can do:" + Environment.NewLine + Environment.NewLine
|
||||
+ (!hasAccount ? string.Empty : ("- Check your account status press \"ℹ️ User Info\"" + Environment.NewLine))
|
||||
+ (hasAccount ? string.Empty : ("- Register a new Lightless Account press \"🌒 Register\"" + Environment.NewLine))
|
||||
+ (!hasAccount ? string.Empty : ("- You lost your secret key press \"🏥 Recover\"" + Environment.NewLine))
|
||||
+ (hasAccount ? string.Empty : ("- If you have changed your Discord account press \"🔗 Relink\"" + Environment.NewLine))
|
||||
+ (!hasAccount ? string.Empty : ("- Create a secondary UIDs press \"2️⃣ Secondary UID\"" + Environment.NewLine))
|
||||
+ (!hasAccount ? string.Empty : ("- Set a Vanity UID press \"💅 Vanity IDs\"" + Environment.NewLine))
|
||||
+ (!hasAccount ? string.Empty : (!isInAprilFoolsMode ? string.Empty : ("- Check your WorryCoin™ and LightlessToken© balance and add payment options" + Environment.NewLine)))
|
||||
+ (!hasAccount ? string.Empty : ("- Delete your primary or secondary accounts with \"⚠️ Delete\""))
|
||||
);
|
||||
eb.WithColor(Color.Blue);
|
||||
ComponentBuilder cb = new();
|
||||
if (!hasAccount)
|
||||
{
|
||||
cb.WithButton("Register", "wizard-register", ButtonStyle.Primary, new Emoji("🌒"));
|
||||
cb.WithButton("Relink", "wizard-relink", ButtonStyle.Secondary, new Emoji("🔗"));
|
||||
}
|
||||
else
|
||||
{
|
||||
cb.WithButton("User Info", "wizard-userinfo", ButtonStyle.Secondary, new Emoji("ℹ️"));
|
||||
cb.WithButton("Recover", "wizard-recover", ButtonStyle.Secondary, new Emoji("🏥"));
|
||||
cb.WithButton("Secondary UID", "wizard-secondary", ButtonStyle.Secondary, new Emoji("2️⃣"));
|
||||
cb.WithButton("Vanity IDs", "wizard-vanity", ButtonStyle.Secondary, new Emoji("💅"));
|
||||
if (isInAprilFoolsMode)
|
||||
{
|
||||
cb.WithButton("WorryCoin™ and LightlessToken© management", "wizard-fools", ButtonStyle.Primary, new Emoji("💲"));
|
||||
}
|
||||
cb.WithButton("Delete", "wizard-delete", ButtonStyle.Danger, new Emoji("⚠️"));
|
||||
}
|
||||
|
||||
await InitOrUpdateInteraction(init, eb, cb).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public class VanityUidModal : IModal
|
||||
{
|
||||
public string Title => "Set Vanity UID";
|
||||
|
||||
[InputLabel("Set your Vanity UID")]
|
||||
[ModalTextInput("vanity_uid", TextInputStyle.Short, "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 async Task<LightlessDbContext> GetDbContext()
|
||||
{
|
||||
return await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateInteraction()
|
||||
{
|
||||
if (Context.Interaction is not IComponentInteraction componentInteraction) return true;
|
||||
|
||||
if (_botServices.ValidInteractions.TryGetValue(Context.User.Id, out ulong interactionId) && interactionId == componentInteraction.Message.Id)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
EmbedBuilder eb = new();
|
||||
eb.WithTitle("Session expired");
|
||||
eb.WithDescription("This session has expired since you have either again pressed \"Start\" on the initial message or the bot has been restarted." + Environment.NewLine + Environment.NewLine
|
||||
+ "Please use the newly started interaction or start a new one.");
|
||||
eb.WithColor(Color.Red);
|
||||
ComponentBuilder cb = new();
|
||||
await ModifyInteraction(eb, cb).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AddHome(ComponentBuilder cb)
|
||||
{
|
||||
cb.WithButton("Return to Home", "wizard-home:false", ButtonStyle.Secondary, new Emoji("🏠"));
|
||||
}
|
||||
|
||||
private async Task ModifyModalInteraction(EmbedBuilder eb, ComponentBuilder cb)
|
||||
{
|
||||
await (Context.Interaction as SocketModal).UpdateAsync(m =>
|
||||
{
|
||||
m.Embed = eb.Build();
|
||||
m.Components = cb.Build();
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ModifyInteraction(EmbedBuilder eb, ComponentBuilder cb)
|
||||
{
|
||||
await ((Context.Interaction) as IComponentInteraction).UpdateAsync(m =>
|
||||
{
|
||||
m.Content = null;
|
||||
m.Embed = eb.Build();
|
||||
m.Components = cb.Build();
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task AddUserSelection(LightlessDbContext lightlessDb, ComponentBuilder cb, string customId)
|
||||
{
|
||||
var discordId = Context.User.Id;
|
||||
var existingAuth = await lightlessDb.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(e => e.DiscordId == discordId).ConfigureAwait(false);
|
||||
if (existingAuth != null)
|
||||
{
|
||||
SelectMenuBuilder sb = new();
|
||||
sb.WithPlaceholder("Select a UID");
|
||||
sb.WithCustomId(customId);
|
||||
var existingUids = await lightlessDb.Auth.Include(u => u.User).Where(u => u.UserUID == existingAuth.User.UID || u.PrimaryUserUID == existingAuth.User.UID)
|
||||
.OrderByDescending(u => u.PrimaryUser == null).ToListAsync().ConfigureAwait(false);
|
||||
foreach (var entry in existingUids)
|
||||
{
|
||||
sb.AddOption(string.IsNullOrEmpty(entry.User.Alias) ? entry.UserUID : entry.User.Alias,
|
||||
entry.UserUID,
|
||||
!string.IsNullOrEmpty(entry.User.Alias) ? entry.User.UID : null,
|
||||
entry.PrimaryUserUID == null ? new Emoji("1️⃣") : new Emoji("2️⃣"));
|
||||
}
|
||||
cb.WithSelectMenu(sb);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddGroupSelection(LightlessDbContext db, ComponentBuilder cb, string customId)
|
||||
{
|
||||
var primary = (await db.LodeStoneAuth.Include(u => u.User).SingleAsync(u => u.DiscordId == Context.User.Id).ConfigureAwait(false)).User;
|
||||
var secondary = await db.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == primary.UID).Select(u => u.User).ToListAsync().ConfigureAwait(false);
|
||||
var primaryGids = (await db.Groups.Include(u => u.Owner).Where(u => u.OwnerUID == primary.UID).ToListAsync().ConfigureAwait(false));
|
||||
var secondaryGids = (await db.Groups.Include(u => u.Owner).Where(u => secondary.Select(u => u.UID).Contains(u.OwnerUID)).ToListAsync().ConfigureAwait(false));
|
||||
SelectMenuBuilder gids = new();
|
||||
if (primaryGids.Any() || secondaryGids.Any())
|
||||
{
|
||||
foreach (var item in primaryGids)
|
||||
{
|
||||
gids.AddOption(item.Alias ?? item.GID, item.GID, (item.Alias == null ? string.Empty : item.GID) + $" ({item.Owner.Alias ?? item.Owner.UID})", new Emoji("1️⃣"));
|
||||
}
|
||||
foreach (var item in secondaryGids)
|
||||
{
|
||||
gids.AddOption(item.Alias ?? item.GID, item.GID, (item.Alias == null ? string.Empty : item.GID) + $" ({item.Owner.Alias ?? item.Owner.UID})", new Emoji("2️⃣"));
|
||||
}
|
||||
gids.WithCustomId(customId);
|
||||
gids.WithPlaceholder("Select a Syncshell");
|
||||
cb.WithSelectMenu(gids);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GenerateLodestoneAuth(ulong discordid, string hashedLodestoneId, LightlessDbContext dbContext)
|
||||
{
|
||||
var auth = StringUtils.GenerateRandomString(12, "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz");
|
||||
LodeStoneAuth lsAuth = new LodeStoneAuth()
|
||||
{
|
||||
DiscordId = discordid,
|
||||
HashedLodestoneId = hashedLodestoneId,
|
||||
LodestoneAuthString = auth,
|
||||
StartedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
dbContext.Add(lsAuth);
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return (auth);
|
||||
}
|
||||
|
||||
private int? ParseCharacterIdFromLodestoneUrl(string lodestoneUrl)
|
||||
{
|
||||
var regex = new Regex(@"https:\/\/(na|eu|de|fr|jp)\.finalfantasyxiv\.com\/lodestone\/character\/\d+");
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="appsettings.Development.json" />
|
||||
<Content Remove="appsettings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="appsettings.Development.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net" Version="3.17.0" />
|
||||
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="2.0.184">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj" />
|
||||
<ProjectReference Include="..\LightlessSyncShared\LightlessSyncShared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
57
LightlessSyncServer/LightlessSyncServices/Program.cs
Normal file
57
LightlessSyncServer/LightlessSyncServices/Program.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using LightlessSyncServices;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var hostBuilder = CreateHostBuilder(args);
|
||||
var host = hostBuilder.Build();
|
||||
|
||||
using (var scope = host.Services.CreateScope())
|
||||
{
|
||||
var options = host.Services.GetService<IConfigurationService<ServicesConfiguration>>();
|
||||
var optionsServer = host.Services.GetService<IConfigurationService<ServerConfiguration>>();
|
||||
var logger = host.Services.GetService<ILogger<Program>>();
|
||||
logger.LogInformation("Loaded LightlessSync Services Configuration (IsMain: {isMain})", options.IsMain);
|
||||
logger.LogInformation(options.ToString());
|
||||
logger.LogInformation("Loaded LightlessSync Server Configuration (IsMain: {isMain})", optionsServer.IsMain);
|
||||
logger.LogInformation(optionsServer.ToString());
|
||||
}
|
||||
|
||||
host.Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.UseSystemd()
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureAppConfiguration((ctx, config) =>
|
||||
{
|
||||
var appSettingsPath = Environment.GetEnvironmentVariable("APPSETTINGS_PATH");
|
||||
if (!string.IsNullOrEmpty(appSettingsPath))
|
||||
{
|
||||
config.AddJsonFile(appSettingsPath, optional: true, reloadOnChange: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
|
||||
}
|
||||
|
||||
config.AddEnvironmentVariables();
|
||||
})
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseContentRoot(AppContext.BaseDirectory);
|
||||
webBuilder.ConfigureLogging((ctx, builder) =>
|
||||
{
|
||||
builder.AddConfiguration(ctx.Configuration.GetSection("Logging"));
|
||||
builder.AddFile(o => o.RootPath = AppContext.BaseDirectory);
|
||||
});
|
||||
webBuilder.ConfigureKestrel((opt) =>
|
||||
{
|
||||
});
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"profiles": {
|
||||
"LightlessSyncServices": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5294;https://localhost:7294",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
LightlessSyncServer/LightlessSyncServices/Startup.cs
Normal file
76
LightlessSyncServer/LightlessSyncServices/Startup.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using LightlessSyncServices.Discord;
|
||||
using LightlessSyncShared.Data;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Prometheus;
|
||||
using LightlessSyncShared.Utils;
|
||||
using LightlessSyncShared.Services;
|
||||
using StackExchange.Redis;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
|
||||
namespace LightlessSyncServices;
|
||||
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<LightlessConfigurationBase>>();
|
||||
|
||||
var metricServer = new KestrelMetricServer(config.GetValueOrDefault<int>(nameof(LightlessConfigurationBase.MetricsPort), 4982));
|
||||
metricServer.Start();
|
||||
}
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
var lightlessConfig = Configuration.GetSection("LightlessSync");
|
||||
|
||||
services.AddDbContextPool<LightlessDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
}, Configuration.GetValue(nameof(LightlessConfigurationBase.DbContextPoolSize), 1024));
|
||||
services.AddDbContextFactory<LightlessDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("LightlessSyncShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
});
|
||||
|
||||
services.AddSingleton(m => new LightlessMetrics(m.GetService<ILogger<LightlessMetrics>>(), new List<string> { },
|
||||
new List<string> { }));
|
||||
|
||||
var redis = lightlessConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty);
|
||||
var options = ConfigurationOptions.Parse(redis);
|
||||
options.ClientName = "Lightless";
|
||||
options.ChannelPrefix = "UserData";
|
||||
ConnectionMultiplexer connectionMultiplexer = ConnectionMultiplexer.Connect(options);
|
||||
services.AddSingleton<IConnectionMultiplexer>(connectionMultiplexer);
|
||||
|
||||
services.Configure<ServicesConfiguration>(Configuration.GetRequiredSection("LightlessSync"));
|
||||
services.Configure<ServerConfiguration>(Configuration.GetRequiredSection("LightlessSync"));
|
||||
services.Configure<LightlessConfigurationBase>(Configuration.GetRequiredSection("LightlessSync"));
|
||||
services.AddSingleton(Configuration);
|
||||
services.AddSingleton<ServerTokenGenerator>();
|
||||
services.AddSingleton<DiscordBotServices>();
|
||||
services.AddHostedService<DiscordBot>();
|
||||
services.AddSingleton<IConfigurationService<ServicesConfiguration>, LightlessConfigurationServiceServer<ServicesConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<ServerConfiguration>, LightlessConfigurationServiceClient<ServerConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<LightlessConfigurationBase>, LightlessConfigurationServiceClient<LightlessConfigurationBase>>();
|
||||
|
||||
services.AddHostedService(p => (LightlessConfigurationServiceClient<LightlessConfigurationBase>)p.GetService<IConfigurationService<LightlessConfigurationBase>>());
|
||||
services.AddHostedService(p => (LightlessConfigurationServiceClient<ServerConfiguration>)p.GetService<IConfigurationService<ServerConfiguration>>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
29
LightlessSyncServer/LightlessSyncServices/appsettings.json
Normal file
29
LightlessSyncServer/LightlessSyncServices/appsettings.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=;Username=;Password="
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Http": {
|
||||
"Protocols": "Http2",
|
||||
"Url": "http://+:5002"
|
||||
}
|
||||
}
|
||||
},
|
||||
"LightlessSync": {
|
||||
"DbContextPoolSize": 1024,
|
||||
"DiscordBotToken": "",
|
||||
"DiscordChannelForMessages": "",
|
||||
"PurgeUnusedAccounts": true,
|
||||
"PurgeUnusedAccountsPeriodInDays": 14,
|
||||
"FailedAuthForTempBan": 5,
|
||||
"TempBanDurationInMinutes": 30
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user