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)
|
if (lease.Data.UserId != userId)
|
||||||
{
|
{
|
||||||
return (Result)
|
return (Result)
|
||||||
await feedbackService.SendContextualAsync(
|
await feedbackService.ReplyAsync(
|
||||||
"This is not your configuration menu.",
|
"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)
|
if (lease.Data.UserId != userId)
|
||||||
{
|
{
|
||||||
return (Result)
|
return (Result)
|
||||||
await feedbackService.SendContextualAsync(
|
await feedbackService.ReplyAsync(
|
||||||
"This is not your configuration menu.",
|
"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);
|
var guildConfig = await db.GetGuildAsync(guildId);
|
||||||
|
|
||||||
if (guildConfig.KeyRoles.Count == 0)
|
if (guildConfig.KeyRoles.Count == 0)
|
||||||
return await feedbackService.SendContextualAsync(
|
return await feedbackService.ReplyAsync(
|
||||||
"There are no key roles to list. Add some with `/key-roles add`."
|
"There are no key roles to list. Add some with `/key-roles add`.",
|
||||||
|
isEphemeral: true
|
||||||
);
|
);
|
||||||
|
|
||||||
var description = string.Join(
|
var description = string.Join(
|
||||||
|
|
@ -71,15 +72,16 @@ public class KeyRoleCommands(
|
||||||
|
|
||||||
var guildConfig = await db.GetGuildAsync(guildId);
|
var guildConfig = await db.GetGuildAsync(guildId);
|
||||||
if (guildConfig.KeyRoles.Any(id => role.ID == id))
|
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);
|
guildConfig.KeyRoles.Add(role.ID.Value);
|
||||||
db.Update(guildConfig);
|
db.Update(guildConfig);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return await feedbackService.SendContextualAsync(
|
return await feedbackService.ReplyAsync($"Added {role.Name} to this server's key roles!");
|
||||||
$"Added {role.Name} to this server's key roles!"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command("remove")]
|
[Command("remove")]
|
||||||
|
|
@ -95,15 +97,16 @@ public class KeyRoleCommands(
|
||||||
|
|
||||||
var guildConfig = await db.GetGuildAsync(guildId);
|
var guildConfig = await db.GetGuildAsync(guildId);
|
||||||
if (guildConfig.KeyRoles.All(id => role.ID != id))
|
if (guildConfig.KeyRoles.All(id => role.ID != id))
|
||||||
return await feedbackService.SendContextualAsync(
|
return await feedbackService.ReplyAsync(
|
||||||
$"{role.Name} is already not a key role."
|
$"{role.Name} is already not a key role.",
|
||||||
|
isEphemeral: true
|
||||||
);
|
);
|
||||||
|
|
||||||
guildConfig.KeyRoles.Remove(role.ID.Value);
|
guildConfig.KeyRoles.Remove(role.ID.Value);
|
||||||
db.Update(guildConfig);
|
db.Update(guildConfig);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return await feedbackService.SendContextualAsync(
|
return await feedbackService.ReplyAsync(
|
||||||
$"Removed {role.Name} from this server's key roles!"
|
$"Removed {role.Name} from this server's key roles!"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using Remora.Discord.API;
|
using Remora.Discord.API;
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
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;
|
using Remora.Rest.Core;
|
||||||
|
|
||||||
namespace Catalogger.Backend.Bot;
|
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 Green = Color.FromArgb(46, 204, 113);
|
||||||
public static readonly Color Blue = Color.FromArgb(52, 152, 219);
|
public static readonly Color Blue = Color.FromArgb(52, 152, 219);
|
||||||
public static readonly Color Orange = Color.FromArgb(230, 126, 34);
|
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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ using OneOf;
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
using Remora.Discord.API.Abstractions.Rest;
|
using Remora.Discord.API.Abstractions.Rest;
|
||||||
using Remora.Discord.API.Objects;
|
using Remora.Discord.API.Objects;
|
||||||
|
using Remora.Discord.Commands.Feedback.Messages;
|
||||||
|
using Remora.Discord.Commands.Feedback.Services;
|
||||||
using Remora.Rest.Core;
|
using Remora.Rest.Core;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
|
|
@ -28,4 +30,24 @@ public static class DiscordRestExtensions
|
||||||
>(data)
|
>(data)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public static async Task<Result<IMessage>> ReplyAsync(
|
||||||
|
this IFeedbackService feedbackService,
|
||||||
|
Optional<string> content = default,
|
||||||
|
IEnumerable<IEmbed>? embeds = null,
|
||||||
|
bool isEphemeral = false
|
||||||
|
) =>
|
||||||
|
await feedbackService.SendContextualAsync(
|
||||||
|
content,
|
||||||
|
embeds != null
|
||||||
|
? new Optional<IReadOnlyList<IEmbed>>(embeds.ToList())
|
||||||
|
: new Optional<IReadOnlyList<IEmbed>>(),
|
||||||
|
options: new FeedbackMessageOptions(
|
||||||
|
MessageFlags: isEphemeral ? MessageFlags.Ephemeral : 0,
|
||||||
|
AllowedMentions: new AllowedMentions(
|
||||||
|
Parse: new List<MentionType>(),
|
||||||
|
MentionRepliedUser: true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,11 +71,13 @@ builder
|
||||||
.WithCommandGroup<MetaCommands>()
|
.WithCommandGroup<MetaCommands>()
|
||||||
.WithCommandGroup<ChannelCommands>()
|
.WithCommandGroup<ChannelCommands>()
|
||||||
.WithCommandGroup<KeyRoleCommands>()
|
.WithCommandGroup<KeyRoleCommands>()
|
||||||
|
.WithCommandGroup<InviteCommands>()
|
||||||
// End command tree
|
// End command tree
|
||||||
.Finish()
|
.Finish()
|
||||||
.AddPagination()
|
.AddPagination()
|
||||||
.AddInteractivity()
|
.AddInteractivity()
|
||||||
.AddInteractionGroup<ChannelCommandsComponents>()
|
.AddInteractionGroup<ChannelCommandsComponents>()
|
||||||
|
.AddAutocompleteProvider<InviteAutocompleteProvider>()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add metric server
|
// Add metric server
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue