Skip to content

Commit 8ef0cdd

Browse files
Merge pull request #308 from NELLExchange/feature/quarantine
Feature/quarantine
2 parents 8b54c0e + 9725bea commit 8ef0cdd

32 files changed

+917
-158
lines changed

src/Nellebot.Common/Models/UserLogs/UserLogType.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ public enum UserLogType
55
Unknown = 0,
66
UsernameChange = 1,
77
NicknameChange = 2,
8-
AvatarHashChange = 3,
9-
GuildAvatarHashChange = 4,
108
JoinedServer = 5,
119
LeftServer = 6,
10+
Quarantined = 7,
11+
Approved = 8,
1212
}

src/Nellebot.Common/Models/UserLogs/UserLogTypesMap.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ public static class UserLogTypesMap
1010
{ UserLogType.Unknown, typeof(object) },
1111
{ UserLogType.UsernameChange, typeof(string) },
1212
{ UserLogType.NicknameChange, typeof(string) },
13-
{ UserLogType.AvatarHashChange, typeof(string) },
14-
{ UserLogType.GuildAvatarHashChange, typeof(string) },
1513
{ UserLogType.JoinedServer, typeof(DateTime) },
1614
{ UserLogType.LeftServer, typeof(DateTime) },
15+
{ UserLogType.Quarantined, typeof(string) },
16+
{ UserLogType.Approved, typeof(string) },
1717
};
1818
}

src/Nellebot/BotOptions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ public class BotOptions
5858

5959
public ulong GhostRoleId { get; init; }
6060

61+
public ulong QuarantineRoleId { get; init; }
62+
63+
public ulong QuarantineChannelId { get; init; }
64+
6165
/// <summary>
6266
/// Gets a value indicating whether feature flag for populating message refs on Ready event.
6367
/// </summary>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using DSharpPlus.Commands;
6+
using DSharpPlus.Commands.Processors.SlashCommands;
7+
using DSharpPlus.Entities;
8+
using MediatR;
9+
using Microsoft.Extensions.Options;
10+
using Nellebot.Services;
11+
12+
namespace Nellebot.CommandHandlers;
13+
14+
public record ApproveUserCommand(CommandContext Ctx, DiscordMember Member)
15+
: BotCommandCommand(Ctx);
16+
17+
public class ApproveUserHandler : IRequestHandler<ApproveUserCommand>
18+
{
19+
private readonly QuarantineService _quarantineService;
20+
private readonly BotOptions _options;
21+
22+
public ApproveUserHandler(IOptions<BotOptions> options, QuarantineService quarantineService)
23+
{
24+
_quarantineService = quarantineService;
25+
_options = options.Value;
26+
}
27+
28+
public async Task Handle(ApproveUserCommand request, CancellationToken cancellationToken)
29+
{
30+
CommandContext ctx = request.Ctx;
31+
DiscordMember currentMember = ctx.Member ?? throw new Exception("Member is null");
32+
DiscordMember targetMember = request.Member;
33+
34+
if (ctx.Member?.Id == targetMember.Id)
35+
{
36+
await TryRespondEphemeral(ctx, "Hmm");
37+
return;
38+
}
39+
40+
bool userIsQuarantined = targetMember.Roles.Any(r => r.Id == _options.QuarantineRoleId);
41+
42+
if (!userIsQuarantined)
43+
{
44+
await TryRespondEphemeral(ctx, "User is not quarantined");
45+
46+
return;
47+
}
48+
49+
await _quarantineService.ApproveMember(targetMember, currentMember);
50+
51+
await TryRespondEphemeral(ctx, "User approved successfully");
52+
}
53+
54+
private static async Task TryRespondEphemeral(CommandContext ctx, string successMessage)
55+
{
56+
if (ctx is SlashCommandContext slashCtx)
57+
await slashCtx.RespondAsync(successMessage, ephemeral: true);
58+
else
59+
await ctx.RespondAsync(successMessage);
60+
}
61+
}

src/Nellebot/CommandHandlers/MessageTemplates/SetGreetingMessage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public async Task Handle(SetGreetingMessageCommand request, CancellationToken ca
3636

3737
string previewMemberMention = ctx.Member?.Mention ?? string.Empty;
3838

39-
string? messagePreview = await _botSettingsService.GetGreetingsMessage(previewMemberMention);
39+
string? messagePreview = await _botSettingsService.GetGreetingMessage(previewMemberMention);
4040

4141
var sb = new StringBuilder("Greeting message updated successfully. Here's a preview:");
4242
sb.AppendLine();
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System.Text;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using DSharpPlus.Commands;
5+
using MediatR;
6+
using Nellebot.Services;
7+
8+
namespace Nellebot.CommandHandlers.MessageTemplates;
9+
10+
public record SetQuarantineMessageCommand : BotCommandCommand
11+
{
12+
public SetQuarantineMessageCommand(CommandContext ctx, string quarantineMessage)
13+
: base(ctx)
14+
{
15+
QuarantineMessage = quarantineMessage;
16+
}
17+
18+
public string QuarantineMessage { get; }
19+
}
20+
21+
public class SetQuarantineMessageHandler : IRequestHandler<SetQuarantineMessageCommand>
22+
{
23+
private readonly BotSettingsService _botSettingsService;
24+
25+
public SetQuarantineMessageHandler(BotSettingsService botSettingsService)
26+
{
27+
_botSettingsService = botSettingsService;
28+
}
29+
30+
public async Task Handle(SetQuarantineMessageCommand request, CancellationToken cancellationToken)
31+
{
32+
CommandContext ctx = request.Ctx;
33+
string message = request.QuarantineMessage;
34+
35+
await _botSettingsService.SetQuarantineMessage(message);
36+
37+
string previewMemberMention = ctx.Member?.Mention ?? string.Empty;
38+
const string previewReason = "Sussy";
39+
40+
string? messagePreview = await _botSettingsService.GetQuarantineMessage(previewMemberMention, previewReason);
41+
42+
var sb = new StringBuilder("Quarantine message updated successfully. Here's a preview:");
43+
sb.AppendLine();
44+
sb.AppendLine();
45+
sb.AppendLine(messagePreview);
46+
47+
await ctx.RespondAsync(sb.ToString());
48+
}
49+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using DSharpPlus.Commands;
6+
using DSharpPlus.Commands.Processors.SlashCommands;
7+
using DSharpPlus.Entities;
8+
using DSharpPlus.EventArgs;
9+
using DSharpPlus.Interactivity;
10+
using MediatR;
11+
using Microsoft.Extensions.Options;
12+
using Nellebot.Services;
13+
using Nellebot.Utils;
14+
15+
namespace Nellebot.CommandHandlers;
16+
17+
public record QuarantineUserCommand(CommandContext Ctx, DiscordMember Member, string? Reason)
18+
: BotCommandCommand(Ctx);
19+
20+
public class QuarantineUserHandler : IRequestHandler<QuarantineUserCommand>
21+
{
22+
private const string ModalTextInputId = "modal-text-input";
23+
private readonly InteractivityExtension _interactivityExtension;
24+
private readonly BotOptions _options;
25+
private readonly QuarantineService _quarantineService;
26+
27+
public QuarantineUserHandler(
28+
IOptions<BotOptions> options,
29+
QuarantineService quarantineService,
30+
InteractivityExtension interactivityExtension)
31+
{
32+
_quarantineService = quarantineService;
33+
_interactivityExtension = interactivityExtension;
34+
_options = options.Value;
35+
}
36+
37+
public async Task Handle(QuarantineUserCommand request, CancellationToken cancellationToken)
38+
{
39+
CommandContext ctx = request.Ctx;
40+
DiscordMember currentMember = ctx.Member ?? throw new Exception("Member is null");
41+
DiscordMember targetMember = request.Member;
42+
43+
if (ctx.Member?.Id == targetMember.Id)
44+
{
45+
await ctx.TryRespondEphemeral("Hmm");
46+
return;
47+
}
48+
49+
TimeSpan guildAge = DateTimeOffset.UtcNow - targetMember.JoinedAt;
50+
51+
int maxAgeHours = _options.ValhallKickMaxMemberAgeInHours;
52+
53+
if (guildAge.TotalHours >= maxAgeHours)
54+
{
55+
var content =
56+
$"You cannot quarantine this user. They have been a member of the server for more than {maxAgeHours} hours.";
57+
58+
await ctx.TryRespondEphemeral(content);
59+
60+
return;
61+
}
62+
63+
bool userAlreadyQuarantined = targetMember.Roles.Any(r => r.Id == _options.QuarantineRoleId);
64+
65+
if (userAlreadyQuarantined)
66+
{
67+
await ctx.TryRespondEphemeral("User is already quarantined");
68+
}
69+
70+
DiscordInteraction? modalInteraction = null;
71+
string? quarantineReason = null;
72+
73+
if (ctx is SlashCommandContext slashCtx && request.Reason == null)
74+
{
75+
ModalSubmittedEventArgs modalSubmissionResult = await ShowGetReasonModal(slashCtx);
76+
77+
modalInteraction = modalSubmissionResult.Interaction;
78+
79+
quarantineReason = modalSubmissionResult.Values[ModalTextInputId];
80+
81+
await modalInteraction.DeferAsync(ephemeral: true);
82+
}
83+
84+
quarantineReason = quarantineReason.NullOrWhiteSpaceTo("/shrug");
85+
86+
await _quarantineService.QuarantineMember(targetMember, currentMember, quarantineReason);
87+
88+
await ctx.TryRespondEphemeral("User quarantined successfully", modalInteraction);
89+
}
90+
91+
private async Task<ModalSubmittedEventArgs> ShowGetReasonModal(SlashCommandContext ctx)
92+
{
93+
var modalId = $"get-reason-modal-{Guid.NewGuid()}";
94+
95+
DiscordInteractionResponseBuilder interactionBuilder = new DiscordInteractionResponseBuilder()
96+
.WithTitle("Quarantine user")
97+
.WithCustomId(modalId)
98+
.AddTextInputComponent(
99+
new DiscordTextInputComponent(
100+
"Reason",
101+
ModalTextInputId,
102+
"Write a reason for quarantining",
103+
string.Empty,
104+
required: true,
105+
DiscordTextInputStyle.Paragraph,
106+
min_length: 0,
107+
DiscordConstants.MaxAuditReasonLength));
108+
109+
await ctx.RespondWithModalAsync(interactionBuilder);
110+
111+
InteractivityResult<ModalSubmittedEventArgs> modalSubmission =
112+
await _interactivityExtension.WaitForModalAsync(modalId, DiscordConstants.MaxDeferredInteractionWait);
113+
114+
return modalSubmission.Result;
115+
}
116+
}

src/Nellebot/CommandHandlers/ValhallKickUser.cs

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,26 @@
44
using DSharpPlus.Commands;
55
using DSharpPlus.Commands.Processors.SlashCommands;
66
using DSharpPlus.Entities;
7+
using DSharpPlus.EventArgs;
8+
using DSharpPlus.Interactivity;
79
using MediatR;
810
using Microsoft.Extensions.Options;
911
using Nellebot.Utils;
1012

1113
namespace Nellebot.CommandHandlers;
1214

13-
public record ValhallKickUserCommand(CommandContext Ctx, DiscordMember Member, string Reason)
15+
public record ValhallKickUserCommand(CommandContext Ctx, DiscordMember Member, string? Reason)
1416
: BotCommandCommand(Ctx);
1517

1618
public class ValhallKickUserHandler : IRequestHandler<ValhallKickUserCommand>
1719
{
20+
private readonly InteractivityExtension _interactivityExtension;
21+
private const string ModalTextInputId = "modal-text-input";
1822
private readonly BotOptions _options;
1923

20-
public ValhallKickUserHandler(IOptions<BotOptions> options)
24+
public ValhallKickUserHandler(IOptions<BotOptions> options, InteractivityExtension interactivityExtension)
2125
{
26+
_interactivityExtension = interactivityExtension;
2227
_options = options.Value;
2328
}
2429

@@ -30,7 +35,7 @@ public async Task Handle(ValhallKickUserCommand request, CancellationToken cance
3035

3136
if (ctx.Member?.Id == targetMember.Id)
3237
{
33-
await TryRespondEphemeral(ctx, "Hmm");
38+
await ctx.TryRespondEphemeral("Hmm");
3439
return;
3540
}
3641

@@ -43,24 +48,58 @@ public async Task Handle(ValhallKickUserCommand request, CancellationToken cance
4348
var content =
4449
$"You cannot vkick this user. They have been a member of the server for more than {maxAgeHours} hours.";
4550

46-
await TryRespondEphemeral(ctx, content);
51+
await ctx.TryRespondEphemeral(content);
4752

4853
return;
4954
}
5055

51-
var kickReason =
52-
$"Kicked on behalf of {currentMember.DisplayName}. Reason: {request.Reason.NullOrWhiteSpaceTo("/shrug")}";
56+
DiscordInteraction? modalInteraction = null;
57+
string? kickReason = null;
5358

54-
await targetMember.RemoveAsync(kickReason);
59+
if (ctx is SlashCommandContext slashCtx && request.Reason == null)
60+
{
61+
ModalSubmittedEventArgs modalSubmissionResult = await ShowGetReasonModal(slashCtx);
62+
63+
modalInteraction = modalSubmissionResult.Interaction;
64+
65+
kickReason = modalSubmissionResult.Values[ModalTextInputId];
66+
67+
await modalInteraction.DeferAsync(ephemeral: true);
68+
}
69+
70+
kickReason = kickReason.NullOrWhiteSpaceTo("/shrug");
5571

56-
await TryRespondEphemeral(ctx, "User vkicked successfully");
72+
var onBehalfOfReason =
73+
$"Kicked on behalf of {currentMember.DisplayName}. Reason: {kickReason}";
74+
75+
await targetMember.RemoveAsync(onBehalfOfReason);
76+
77+
await ctx.TryRespondEphemeral("User vkicked successfully", modalInteraction);
5778
}
5879

59-
private static async Task TryRespondEphemeral(CommandContext ctx, string successMessage)
80+
private async Task<ModalSubmittedEventArgs> ShowGetReasonModal(SlashCommandContext ctx)
6081
{
61-
if (ctx is SlashCommandContext slashCtx)
62-
await slashCtx.RespondAsync(successMessage, true);
63-
else
64-
await ctx.RespondAsync(successMessage);
82+
var modalId = $"get-reason-modal-{Guid.NewGuid()}";
83+
84+
DiscordInteractionResponseBuilder interactionBuilder = new DiscordInteractionResponseBuilder()
85+
.WithTitle("Valhall kick user")
86+
.WithCustomId(modalId)
87+
.AddTextInputComponent(
88+
new DiscordTextInputComponent(
89+
"Reason",
90+
ModalTextInputId,
91+
"Write a reason for kicking",
92+
string.Empty,
93+
required: true,
94+
DiscordTextInputStyle.Paragraph,
95+
min_length: 0,
96+
DiscordConstants.MaxAuditReasonLength));
97+
98+
await ctx.RespondWithModalAsync(interactionBuilder);
99+
100+
InteractivityResult<ModalSubmittedEventArgs> modalSubmission =
101+
await _interactivityExtension.WaitForModalAsync(modalId, DiscordConstants.MaxDeferredInteractionWait);
102+
103+
return modalSubmission.Result;
65104
}
66105
}

src/Nellebot/CommandModules/AdminModule.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ public Task SetGreetingMessage(CommandContext ctx, [RemainingText] string messag
4949
return _commandQueue.Writer.WriteAsync(new SetGreetingMessageCommand(ctx, message)).AsTask();
5050
}
5151

52+
[Command("set-quarantine-message")]
53+
public Task SetQuarantineMessage(CommandContext ctx, [RemainingText] string message)
54+
{
55+
return _commandQueue.Writer.WriteAsync(new SetQuarantineMessageCommand(ctx, message)).AsTask();
56+
}
57+
5258
[Command("populate-messages")]
5359
public Task PopulateMessages(CommandContext ctx)
5460
{

0 commit comments

Comments
 (0)