Delta-v/Content.DiscordBot/CommandHandler.cs

280 lines
11 KiB
C#

using System.Collections.Immutable;
using System.Reflection;
using Content.DiscordBot.Modules;
using Content.Server.Database;
using Discord;
using Discord.Commands;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
namespace Content.DiscordBot;
public sealed class CommandHandler(DiscordSocketClient client, InteractionService interaction, ServerDbContext db, Config config)
{
private ImmutableDictionary<ulong, PatronTier>? _patronTiers;
private ImmutableArray<PatronTier> _tierPriority;
private Task? _refreshPatronsTask;
public int Running = 1;
public async Task InstallCommandsAsync()
{
var patronTiers = await db.PatronTiers.ToListAsync();
_tierPriority = [..patronTiers.OrderBy(t => t.Priority)];
_patronTiers = patronTiers.ToImmutableDictionary(t => t.DiscordRole, t => t);
client.Ready += ReadyAsync;
client.SlashCommandExecuted += HandleCommandAsync;
client.ButtonExecuted += HandleButtonAsync;
client.ModalSubmitted += HandleModalAsync;
client.GuildMemberUpdated += HandleGuildMemberUpdated;
// If you get a log that says '[ERROR]' here, you probably don't have the corrent intents
await client.LoginAsync(TokenType.Bot, config.Token);
await client.StartAsync();
interaction.AddModalInfo<LinkAccountModal>();
_refreshPatronsTask = Task.Run(async () => await RefreshPatrons());
}
private async Task ReadyAsync()
{
await interaction.AddModulesAsync(Assembly.GetEntryAssembly(), null);
await interaction.RegisterCommandsToGuildAsync(config.Guild);
}
private async Task HandleGuildMemberUpdated(Cacheable<SocketGuildUser, ulong> old, SocketGuildUser user)
{
if (_patronTiers == null)
return;
var rolesChanged = !old.HasValue || old.Value.Roles.Count != user.Roles.Count || !old.Value.Roles.SequenceEqual(user.Roles);
if (!rolesChanged)
return;
var wasPatron = old.HasValue || old.Value.Roles.Any(r => _patronTiers.ContainsKey(r.Id));
var isPatron = user.Roles.Any(r => _patronTiers.ContainsKey(r.Id));
if (wasPatron && !isPatron)
{
var linked = await db.DiscordLinkedAccounts
.Include(l => l.Player)
.ThenInclude(p => p.Patron)
.ThenInclude(p => p!.Tier)
.Where(l => l.Player.Patron != null)
.FirstOrDefaultAsync(p => p.DiscordId == user.Id);
if (linked?.Player.Patron is { } patron)
{
db.Patrons.Remove(patron);
await db.SaveChangesAsync();
await Logger.Info($"Removed patron {user.Username}:{linked.DiscordId}:{linked.Player.LastSeenUserName} with tier {patron.Tier.Name}");
}
return;
}
if (isPatron)
{
foreach (var tier in _tierPriority)
{
if (user.Roles.Any(r => r.Id == tier.DiscordRole))
{
var linked = await db.DiscordLinkedAccounts
.Include(l => l.Player)
.ThenInclude(p => p.Patron)
.FirstOrDefaultAsync(p => p.DiscordId == user.Id);
if (linked?.Player is not { } player)
return;
player.Patron ??= db.Patrons.Add(new Patron { PlayerId = player.UserId }).Entity;
player.Patron.TierId = tier.Id;
await db.SaveChangesAsync();
await Logger.Info($"Updated patron {user.Username}:{linked.DiscordId}:{linked.Player.LastSeenUserName} with tier {tier.Name}");
}
}
}
}
private async Task HandleCommandAsync(SocketSlashCommand message)
{
if (message.GuildId != config.Guild)
return;
// Create a WebSocket-based command context based on the message
var context = new SocketInteractionContext(client, message);
// Execute the command with the command context we just
// created, along with the service provider for precondition checks.
await interaction.ExecuteCommandAsync(context, null);
}
private async Task HandleButtonAsync(SocketMessageComponent component)
{
switch (component.Data.CustomId)
{
case "link-ss14-account":
await component.RespondWithModalAsync<LinkAccountModal>("link-ss14-account");
break;
}
}
private async Task HandleModalAsync(SocketModal modal)
{
switch (modal.Data.CustomId)
{
case "link-ss14-account":
if (modal.GuildId is not { } guildId)
break;
var codeStr = modal.Data.Components.First(c => c.CustomId == "account_code").Value.Trim();
if (string.IsNullOrWhiteSpace(codeStr))
break;
await modal.DeferAsync(true);
if (!Guid.TryParse(codeStr, out var code))
{
await modal.FollowupAsync($"{codeStr} isn't a valid code! Get one in-game from the lobby at the top left of the screen.", ephemeral: true);
}
var author = modal.User;
var authorId = author.Id;
var discord = await db.DiscordAccounts
.Include(d => d.LinkedAccount)
.ThenInclude(l => l.Player)
.ThenInclude(p => p.Patron)
.FirstOrDefaultAsync(a => a.Id == authorId);
var codes = await db.DiscordLinkingCodes
.Include(l => l.Player)
.ThenInclude(player => player.Patron)
.FirstOrDefaultAsync(p => p.Code == code);
if (codes == null)
{
await modal.FollowupAsync($"No player found with code {codeStr}, join the game server and get another code before trying again, or ask for help in another channel.", ephemeral: true);
break;
}
if (codes.CreationTime < DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)))
{
await modal.FollowupAsync($"Code {codeStr} were generated too long ago, join the game server and get another code before trying again.", ephemeral: true);
}
if (discord?.LinkedAccount is { } linked)
{
if (linked.Player.Patron is { } patron)
db.Patrons.Remove(patron);
linked.Player.Patron = null;
db.DiscordLinkedAccounts.Remove(linked);
}
discord ??= db.DiscordAccounts.Add(new DiscordAccount { Id = authorId }).Entity;
discord.LinkedAccount = db.DiscordLinkedAccounts.Add(new DiscordLinkedAccount { Discord = discord }).Entity;
discord.LinkedAccount.Player = codes.Player;
var roles = client.GetGuild(guildId).GetUser(authorId).Roles.Select(r => r.Id).ToArray();
var tiers = await db.PatronTiers
.Where(t => roles.Contains(t.DiscordRole))
.ToListAsync();
if (tiers.Count == 0)
{
discord.LinkedAccount.Player.Patron = null;
}
else
{
tiers.Sort((a, b) => a.Priority.CompareTo(b.Priority));
var tier = tiers[0];
discord.LinkedAccount.Player.Patron = db.Patrons.Add(new Patron { Tier = tier }).Entity;
discord.LinkedAccount.Player.Patron.Tier = tier;
}
db.DiscordLinkedAccountLogs.Add(new DiscordLinkedAccountLogs
{
Discord = discord,
Player = discord.LinkedAccount.Player,
});
db.ChangeTracker.DetectChanges();
await db.SaveChangesAsync();
var msg = $"Linked SS14 account with name {codes.Player.LastSeenUserName}";
if (codes.Player.Patron != null)
msg += $" and tier {codes.Player.Patron.Tier.Name}";
await modal.FollowupAsync(msg, ephemeral: true);
break;
}
}
private async Task RefreshPatrons()
{
while (Interlocked.CompareExchange(ref Running, 1, 1) == 1)
{
try
{
var patrons = await db.DiscordLinkedAccounts
.Include(l => l.Player)
.ThenInclude(p => p.Patron)
.ThenInclude(p => p!.Tier)
.ToListAsync();
foreach (var linked in patrons)
{
try
{
var user = await client.Rest.GetGuildUserAsync(config.Guild, linked.DiscordId);
if (user == null)
{
if (linked.Player.Patron != null)
{
linked.Player.Patron = null;
await Logger.Info($"Removed patron {linked.DiscordId}:{linked.Player.LastSeenUserName}");
}
continue;
}
var isPatron = false;
foreach (var tier in _tierPriority)
{
if (user.RoleIds.Contains(tier.DiscordRole))
{
isPatron = true;
if (linked.Player.Patron?.Tier.DiscordRole == tier.DiscordRole)
break;
linked.Player.Patron ??= db.Patrons.Add(new Patron { PlayerId = linked.PlayerId })
.Entity;
linked.Player.Patron.TierId = tier.Id;
await Logger.Info($"Updated patron {user.Username}:{linked.DiscordId}:{linked.Player.LastSeenUserName} with tier {tier.Name}");
break;
}
}
if (!isPatron && linked.Player.Patron != null)
{
linked.Player.Patron = null;
await Logger.Info($"Removed patron {user.Username}:{linked.DiscordId}:{linked.Player.LastSeenUserName}");
}
}
catch (Exception e)
{
await Logger.Error($"Error updating patron with discord id {linked.DiscordId} and player id {linked.PlayerId}", e);
}
}
await db.SaveChangesAsync();
await Task.Delay(60000); // 60 seconds
}
catch (Exception e)
{
await Logger.Error("Error refreshing patrons", e);
}
}
}
}