Files
ClubPenguinServer/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs
2022-10-20 21:44:28 +02:00

190 lines
7.9 KiB
C#

using Discord;
using MareSynchronosShared.Data;
using System;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Discord.WebSocket;
using System.Linq;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Text;
using System.Security.Cryptography;
using System.Threading;
namespace MareSynchronosServices.Discord;
public class DiscordBotServices
{
public readonly ConcurrentQueue<SocketSlashCommand> verificationQueue = new();
public ConcurrentDictionary<ulong, DateTime> LastVanityChange = new();
public ConcurrentDictionary<string, DateTime> LastVanityGidChange = new();
public ConcurrentDictionary<ulong, string> DiscordLodestoneMapping = new();
private readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "de" };
private readonly IServiceProvider _services;
private readonly IConfiguration _configuration;
private readonly ILogger<DiscordBotServices> _logger;
private readonly MareMetrics _metrics;
private readonly Random random;
private CancellationTokenSource? verificationTaskCts;
public DiscordBotServices(IServiceProvider services, IConfiguration configuration, ILogger<DiscordBotServices> logger, MareMetrics metrics)
{
_services = services;
_configuration = configuration.GetRequiredSection("MareSynchronos");
_logger = logger;
_metrics = metrics;
random = new();
}
public async Task Start()
{
_ = ProcessVerificationQueue();
}
public async Task Stop()
{
verificationTaskCts?.Cancel();
}
private async Task ProcessVerificationQueue()
{
verificationTaskCts = new CancellationTokenSource();
while (!verificationTaskCts.IsCancellationRequested)
{
if (verificationQueue.TryDequeue(out var queueitem))
{
try
{
var dataEmbed = await HandleVerifyAsync(queueitem.User.Id).ConfigureAwait(false);
await queueitem.FollowupAsync(embed: dataEmbed, ephemeral: true).ConfigureAwait(false);
_logger.LogInformation("Sent login information to user");
}
catch (Exception e)
{
_logger.LogError(e, "Error during queue work");
}
}
await Task.Delay(TimeSpan.FromSeconds(2), verificationTaskCts.Token).ConfigureAwait(false);
}
}
public static string GenerateRandomString(int length, string? allowableChars = null)
{
if (string.IsNullOrEmpty(allowableChars))
allowableChars = @"ABCDEFGHJKLMNPQRSTUVWXYZ0123456789";
// Generate random data
var rnd = RandomNumberGenerator.GetBytes(length);
// Generate the output string
var allowable = allowableChars.ToCharArray();
var l = allowable.Length;
var chars = new char[length];
for (var i = 0; i < length; i++)
chars[i] = allowable[rnd[i] % l];
return new string(chars);
}
private async Task<Embed> HandleVerifyAsync(ulong id)
{
var embedBuilder = new EmbedBuilder();
using var scope = _services.CreateScope();
var req = new HttpClient();
using var db = scope.ServiceProvider.GetService<MareDbContext>();
var lodestoneAuth = db.LodeStoneAuth.SingleOrDefault(u => u.DiscordId == id);
if (lodestoneAuth != null && DiscordLodestoneMapping.ContainsKey(id))
{
var randomServer = LodestoneServers[random.Next(LodestoneServers.Length)];
var response = await req.GetAsync($"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{DiscordLodestoneMapping[id]}").ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (content.Contains(lodestoneAuth.LodestoneAuthString))
{
DiscordLodestoneMapping.TryRemove(id, out _);
using var sha256 = SHA256.Create();
var user = new User();
var hasValidUid = false;
while (!hasValidUid)
{
var uid = 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;
}
if (_configuration.GetValue<bool>("PurgeUnusedAccounts"))
{
var purgedDays = _configuration.GetValue<int>("PurgeUnusedAccountsPeriodInDays");
user.LastLoggedIn = DateTime.UtcNow - TimeSpan.FromDays(purgedDays) + TimeSpan.FromDays(1);
}
var computedHash = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(GenerateRandomString(64) + DateTime.UtcNow.ToString()))).Replace("-", "");
var auth = new Auth()
{
HashedKey = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(computedHash)))
.Replace("-", ""),
User = user,
};
await db.Users.AddAsync(user).ConfigureAwait(false);
await db.Auth.AddAsync(auth).ConfigureAwait(false);
_logger.LogInformation("User registered: {userUID}", user.UID);
_metrics.IncGauge(MetricsAPI.GaugeUsersRegistered, 1);
lodestoneAuth.StartedAt = null;
lodestoneAuth.User = user;
lodestoneAuth.LodestoneAuthString = null;
embedBuilder.WithTitle("Registration successful");
embedBuilder.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
+ $"**{computedHash}**"
+ Environment.NewLine + Environment.NewLine
+ "Enter this key in Mare Synchronos and hit save to connect to the service."
+ Environment.NewLine
+ "You should connect as soon as possible to not get caught by the automatic cleanup process."
+ Environment.NewLine
+ "Have fun.");
}
else
{
embedBuilder.WithTitle("Failed to verify your character");
embedBuilder.WithDescription("Did not find requested authentication key on your profile. Make sure you have saved *twice*, then do **/verify** again.");
lodestoneAuth.StartedAt = DateTime.UtcNow;
}
}
await db.SaveChangesAsync().ConfigureAwait(false);
}
else
{
embedBuilder.WithTitle("Your auth has expired or something else went wrong");
embedBuilder.WithDescription("Start again with **/register**");
DiscordLodestoneMapping.TryRemove(id, out _);
}
return embedBuilder.Build();
}
}