feat: invite management commands
This commit is contained in:
parent
32732d74d0
commit
5a2bd7388c
6 changed files with 310 additions and 13 deletions
|
|
@ -54,9 +54,9 @@ public class ChannelCommandsComponents(
|
|||
if (lease.Data.UserId != userId)
|
||||
{
|
||||
return (Result)
|
||||
await feedbackService.SendContextualAsync(
|
||||
await feedbackService.ReplyAsync(
|
||||
"This is not your configuration menu.",
|
||||
options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral)
|
||||
isEphemeral: true
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -253,9 +253,9 @@ public class ChannelCommandsComponents(
|
|||
if (lease.Data.UserId != userId)
|
||||
{
|
||||
return (Result)
|
||||
await feedbackService.SendContextualAsync(
|
||||
await feedbackService.ReplyAsync(
|
||||
"This is not your configuration menu.",
|
||||
options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral)
|
||||
isEphemeral: true
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
253
Catalogger.Backend/Bot/Commands/InviteCommands.cs
Normal file
253
Catalogger.Backend/Bot/Commands/InviteCommands.cs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -36,8 +36,9 @@ public class KeyRoleCommands(
|
|||
var guildConfig = await db.GetGuildAsync(guildId);
|
||||
|
||||
if (guildConfig.KeyRoles.Count == 0)
|
||||
return await feedbackService.SendContextualAsync(
|
||||
"There are no key roles to list. Add some with `/key-roles add`."
|
||||
return await feedbackService.ReplyAsync(
|
||||
"There are no key roles to list. Add some with `/key-roles add`.",
|
||||
isEphemeral: true
|
||||
);
|
||||
|
||||
var description = string.Join(
|
||||
|
|
@ -71,15 +72,16 @@ public class KeyRoleCommands(
|
|||
|
||||
var guildConfig = await db.GetGuildAsync(guildId);
|
||||
if (guildConfig.KeyRoles.Any(id => role.ID == id))
|
||||
return await feedbackService.SendContextualAsync($"{role.Name} is already a key role.");
|
||||
return await feedbackService.ReplyAsync(
|
||||
$"{role.Name} is already a key role.",
|
||||
isEphemeral: true
|
||||
);
|
||||
|
||||
guildConfig.KeyRoles.Add(role.ID.Value);
|
||||
db.Update(guildConfig);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return await feedbackService.SendContextualAsync(
|
||||
$"Added {role.Name} to this server's key roles!"
|
||||
);
|
||||
return await feedbackService.ReplyAsync($"Added {role.Name} to this server's key roles!");
|
||||
}
|
||||
|
||||
[Command("remove")]
|
||||
|
|
@ -95,15 +97,16 @@ public class KeyRoleCommands(
|
|||
|
||||
var guildConfig = await db.GetGuildAsync(guildId);
|
||||
if (guildConfig.KeyRoles.All(id => role.ID != id))
|
||||
return await feedbackService.SendContextualAsync(
|
||||
$"{role.Name} is already not a key role."
|
||||
return await feedbackService.ReplyAsync(
|
||||
$"{role.Name} is already not a key role.",
|
||||
isEphemeral: true
|
||||
);
|
||||
|
||||
guildConfig.KeyRoles.Remove(role.ID.Value);
|
||||
db.Update(guildConfig);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return await feedbackService.SendContextualAsync(
|
||||
return await feedbackService.ReplyAsync(
|
||||
$"Removed {role.Name} from this server's key roles!"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
using System.Drawing;
|
||||
using Remora.Discord.API;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.API.Objects;
|
||||
using Remora.Discord.Commands.Feedback.Services;
|
||||
using Remora.Discord.Pagination;
|
||||
using Remora.Discord.Pagination.Extensions;
|
||||
using Remora.Rest.Core;
|
||||
|
||||
namespace Catalogger.Backend.Bot;
|
||||
|
|
@ -14,4 +18,17 @@ public static class DiscordUtils
|
|||
public static readonly Color Green = Color.FromArgb(46, 204, 113);
|
||||
public static readonly Color Blue = Color.FromArgb(52, 152, 219);
|
||||
public static readonly Color Orange = Color.FromArgb(230, 126, 34);
|
||||
|
||||
public static List<Embed> PaginateFields(
|
||||
IEnumerable<IEmbedField> fields,
|
||||
Optional<string> title = default,
|
||||
string description = "",
|
||||
uint fieldsPerPage = 6
|
||||
) =>
|
||||
PageFactory.FromFields(
|
||||
fields,
|
||||
fieldsPerPage,
|
||||
description,
|
||||
new Embed(Title: title, Colour: Purple)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue