using System.Collections.Concurrent; using Discord; using Discord.Net; using Discord.Rest; using Discord.WebSocket; using MareSynchronosShared.Metrics; using MareSynchronosShared.Models; using MareSynchronosShared.Services; using MareSynchronosShared.Utils.Configuration; using StackExchange.Redis; namespace MareSynchronosServices.Discord; public class DiscordBotServices { public readonly string[] LodestoneServers = ["eu", "na", "jp", "fr", "de"]; public ConcurrentDictionary DiscordLodestoneMapping = new(); public ConcurrentDictionary DiscordRelinkLodestoneMapping = new(); public ConcurrentDictionary DiscordVerifiedUsers { get; } = new(); public ConcurrentDictionary LastVanityChange = new(); public ConcurrentDictionary LastVanityGidChange = new(StringComparer.Ordinal); public ConcurrentDictionary ValidInteractions { get; } = new(); public ConcurrentDictionary VanityRoles { get; set; } = new(); public ConcurrentBag VerifiedCaptchaUsers { get; } = new(); private readonly IConfigurationService _configuration; private readonly CancellationTokenSource verificationTaskCts = new(); private RestGuild? _guild; private ulong? _logChannelId; private RestTextChannel? _logChannel; public DiscordBotServices(ILogger logger, MareMetrics metrics, IConfigurationService configuration) { Logger = logger; Metrics = metrics; _configuration = configuration; } public ILogger Logger { get; init; } public MareMetrics Metrics { get; init; } public ConcurrentQueue>> 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(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(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(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 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; } }