Catalogger.NET/Catalogger.Backend/Bot/Commands/InviteCommands.cs

253 lines
8.5 KiB
C#

using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Catalogger.Backend.Cache;
using Catalogger.Backend.Cache.InMemoryCache;
using Catalogger.Backend.Database;
using Catalogger.Backend.Extensions;
using Microsoft.EntityFrameworkCore;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Objects;
using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Autocomplete;
using Remora.Discord.Commands.Extensions;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Commands.Services;
using Remora.Discord.Pagination.Extensions;
using Remora.Rest.Core;
using Invite = Catalogger.Backend.Database.Models.Invite;
using IResult = Remora.Results.IResult;
namespace Catalogger.Backend.Bot.Commands;
[Group("invites")]
[Description("Manage server invites.")]
[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
public class InviteCommands(
ILogger logger,
DatabaseContext db,
GuildCache guildCache,
IInviteCache inviteCache,
IDiscordRestChannelAPI channelApi,
FeedbackService feedbackService,
ContextInjectionService contextInjection
) : CommandGroup
{
private readonly ILogger _logger = logger.ForContext<InviteCommands>();
[Command("list")]
[Description("List this server's invites and their names.")]
public async Task<IResult> ListInvitesAsync()
{
var (userId, guildId) = contextInjection.GetUserAndGuild();
var guildInvites = await inviteCache.TryGetAsync(guildId);
if (!guildCache.TryGet(guildId, out var guild))
throw new CataloggerError("Guild not in cache");
var dbInvites = await db.Invites.Where(i => i.GuildId == guildId.Value).ToListAsync();
var fields = guildInvites
.Select(i => new PartialNamedInvite(
i.Code,
dbInvites.FirstOrDefault(dbI => dbI.Code == i.Code)?.Name,
i.Inviter,
i.CreatedAt
))
.OrderByDescending(i => i.Name != null)
.ThenBy(i => i.Name)
.ThenBy(i => i.Code)
.Select(i => new EmbedField(
Name: i.Code,
Value: $"""
**Name:** {i.Name ?? "*(unnamed)*"}
**Created at:** <t:{i.CreatedAt.ToUnixTimeSeconds()}>
**Created by:** {i.CreatedBy.OrDefault()?.Tag() ?? "*(unknown)*"}
""",
IsInline: true
));
return await feedbackService.SendContextualPaginatedMessageAsync(
userId,
DiscordUtils.PaginateFields(fields, title: $"Invites for {guild.Name}")
);
}
private record struct PartialNamedInvite(
string Code,
string? Name,
Optional<IUser> CreatedBy,
DateTimeOffset CreatedAt
);
[Command("create")]
[Description("Create a new invite.`")]
public async Task<IResult> CreateInviteAsync(
[Description("The channel to create the invite in")]
[ChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)]
IChannel channel,
[Description("What to name the new invite")] [Optional] string? name
)
{
var (userId, guildId) = contextInjection.GetUserAndGuild();
var inviteResult = await channelApi.CreateChannelInviteAsync(
channel.ID,
maxAge: TimeSpan.Zero,
isUnique: true,
reason: $"Create invite command by {userId}"
);
if (inviteResult.Error != null)
{
_logger.Error(
"Could not create an invite in channel {ChannelId} in {GuildId}: {Error}",
channel.ID,
guildId,
inviteResult.Error
);
return await feedbackService.ReplyAsync(
$"Could not create an invite in <#{channel.ID}>."
+ "Are you sure I have the \"Create Invite\" permission in that channel?"
);
}
if (name == null)
return await feedbackService.ReplyAsync(
$"Created a new invite in <#{channel.ID}>!"
+ $"\nLink: https://discord.gg/{inviteResult.Entity.Code}"
);
var dbInvite = new Invite
{
GuildId = guildId.Value,
Code = inviteResult.Entity.Code,
Name = name,
};
db.Add(dbInvite);
await db.SaveChangesAsync();
return await feedbackService.ReplyAsync(
$"Created a new invite in <#{channel.ID}> with the name **{name}**!"
+ $"\nLink: https://discord.gg/{inviteResult.Entity.Code}"
);
}
[Command("name")]
[Description("Name or rename an invite.")]
public async Task<IResult> NameInviteAsync(
[Description("The invite to rename")]
[AutocompleteProvider("autocomplete:invite-provider")]
string invite,
[Description("The invite's new name")] [Optional] string? name
)
{
var (_, guildId) = contextInjection.GetUserAndGuild();
var guildInvites = await inviteCache.TryGetAsync(guildId);
if (guildInvites.All(i => i.Code != invite))
{
return await feedbackService.ReplyAsync(
$"No invite with the code `{invite}` found.",
isEphemeral: true
);
}
var namedInvite = await db
.Invites.Where(i => i.GuildId == guildId.Value && i.Code == invite)
.FirstOrDefaultAsync();
if (namedInvite == null)
{
if (name == null)
return await feedbackService.ReplyAsync($"Invite `{invite}` already has no name.");
namedInvite = new Invite
{
GuildId = guildId.Value,
Code = invite,
Name = name,
};
db.Add(namedInvite);
await db.SaveChangesAsync();
return await feedbackService.ReplyAsync(
$"New name set! The invite `{invite}` will now show up as **{name}** in logs."
);
}
if (name == null)
{
db.Invites.Remove(namedInvite);
await db.SaveChangesAsync();
return await feedbackService.ReplyAsync($"Removed the name for `{invite}`.");
}
namedInvite.Name = name;
db.Update(namedInvite);
await db.SaveChangesAsync();
return await feedbackService.ReplyAsync(
$"New name set! The invite `{invite}` will now show up as **{name}** in logs."
);
}
}
public class InviteAutocompleteProvider(
ILogger logger,
DatabaseContext db,
IInviteCache inviteCache,
ContextInjectionService contextInjection
) : IAutocompleteProvider
{
private readonly ILogger _logger = logger.ForContext<InviteAutocompleteProvider>();
public string Identity => "autocomplete:invite-provider";
[SuppressMessage(
"Performance",
"CA1862:Use the 'StringComparison' method overloads to perform case-insensitive string comparisons",
Justification = "StringComparison doesn't translate to SQL"
)]
public async ValueTask<IReadOnlyList<IApplicationCommandOptionChoice>> GetSuggestionsAsync(
IReadOnlyList<IApplicationCommandInteractionDataOption> options,
string userInput,
CancellationToken ct = default
)
{
if (contextInjection.Context == null)
{
_logger.Debug("GetSuggestionsAsync did not get a context at all");
return [];
}
if (!contextInjection.Context.TryGetGuildID(out var guildId))
{
_logger.Debug("GetSuggestionsAsync did not get a context with a guild ID");
return [];
}
var namedInvites = await db
.Invites.Where(i =>
i.GuildId == guildId.Value && i.Name.ToLower().StartsWith(userInput.ToLower())
)
.OrderBy(i => i.Name)
.Take(25)
.ToListAsync(ct);
if (namedInvites.Count != 0)
{
return namedInvites
.Select(i => new ApplicationCommandOptionChoice(Name: i.Name, Value: i.Code))
.ToList();
}
var invites = await inviteCache.TryGetAsync(guildId);
return invites
.Where(i => i.Code.StartsWith(userInput, StringComparison.InvariantCultureIgnoreCase))
.Select(i => new ApplicationCommandOptionChoice(Name: i.Code, Value: i.Code))
.ToList();
}
}