Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/Nellebot/BotOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ public class BotOptions

public ulong MemberRoleId { get; init; }

public ulong[] MemberRoleIds { get; init; } = [];
public ulong[] MemberActivatingRoleIds { get; init; } = [];

public ulong BeginnerRoleId { get; init; }

public ulong[] BeginnerActivatingRoleIds { get; init; } = [];

public ulong GhostRoleId { get; init; }

Expand Down
2 changes: 1 addition & 1 deletion src/Nellebot/CommandHandlers/QuarantineUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public async Task Handle(QuarantineUserCommand request, CancellationToken cancel
}

DiscordInteraction? modalInteraction = null;
string? quarantineReason = null;
string? quarantineReason = request.Reason;

if (ctx is SlashCommandContext slashCtx && request.Reason == null)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Nellebot/CommandHandlers/ValhallKickUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public async Task Handle(ValhallKickUserCommand request, CancellationToken cance
}

DiscordInteraction? modalInteraction = null;
string? kickReason = null;
string? kickReason = request.Reason;

if (ctx is SlashCommandContext slashCtx && request.Reason == null)
{
Expand Down
155 changes: 106 additions & 49 deletions src/Nellebot/Jobs/RoleMaintenanceJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ public async Task Execute(IJobExecutionContext context)
CancellationToken cancellationToken = context.CancellationToken;

ulong guildId = _options.GuildId;
ulong memberRoleId = _options.MemberRoleId;
ulong[] memberRoleIds = _options.MemberRoleIds;
ulong ghostRoleId = _options.GhostRoleId;
ulong quarantineRoleId = _options.QuarantineRoleId;

DiscordGuild guild = _client.Guilds[guildId];
Expand All @@ -55,18 +52,11 @@ public async Task Execute(IJobExecutionContext context)

_discordLogger.LogOperationMessage($"Downloaded {allMembers.Count} guild members.");

DiscordRole memberRole = guild.Roles[memberRoleId]
?? throw new Exception($"Could not find member role with id {memberRoleId}");
DiscordRole ghostRole = guild.Roles[ghostRoleId]
?? throw new Exception($"Could not find ghost role with id {ghostRoleId}");
await MaintainMemberRoles(guild, allMembers, quarantineRoleId, cancellationToken);

await AddMissingMemberRoles(allMembers, memberRoleIds, memberRole, quarantineRoleId, cancellationToken);
await MaintainBeginnerRoles(guild, allMembers, quarantineRoleId, cancellationToken);

await RemoveUnneededMemberRoles(allMembers, memberRoleIds, memberRole, quarantineRoleId, cancellationToken);

await AddMissingGhostRoles(allMembers, ghostRole, cancellationToken);

await RemoveUnneededGhostRoles(allMembers, ghostRole, cancellationToken);
await MaintainGhostRoles(guild, allMembers, cancellationToken);

_discordLogger.LogOperationMessage($"Job finished: {Key}");
}
Expand All @@ -77,42 +67,94 @@ public async Task Execute(IJobExecutionContext context)
}
}

private async Task AddMissingMemberRoles(
private async Task MaintainMemberRoles(
DiscordGuild guild,
List<DiscordMember> allMembers,
ulong quarantineRoleId,
CancellationToken cancellationToken)
{
ulong memberRoleId = _options.MemberRoleId;
ulong[] memberActivatingRoleIds = _options.MemberActivatingRoleIds;
DiscordRole memberRole = guild.Roles[memberRoleId]
?? throw new Exception($"Could not find Member role with id {memberRoleId}");

await AddMissingActivatableRoles(
allMembers,
memberRole,
memberActivatingRoleIds,
quarantineRoleId,
cancellationToken);

await RemoveUnneededActivatableRoles(
allMembers,
memberRole,
memberActivatingRoleIds,
quarantineRoleId,
cancellationToken);
}

private async Task MaintainBeginnerRoles(
DiscordGuild guild,
List<DiscordMember> allMembers,
ulong[] memberRoleIds,
DiscordRole memberRole,
ulong quarantineRoleId,
CancellationToken cancellationToken)
{
List<DiscordMember> missingMemberRoleMembers = allMembers
.Where(m => !HasMemberRole(m) && HasMandatoryRoles(m) && !HasQuarantineRole(m))
ulong beginnerRoleId = _options.BeginnerRoleId;
ulong[] beginnerActivatingRoleIds = _options.BeginnerActivatingRoleIds;
DiscordRole beginnerRole = guild.Roles[beginnerRoleId]
?? throw new Exception($"Could not find Beginner role with id {beginnerRoleId}");

await AddMissingActivatableRoles(
allMembers,
beginnerRole,
beginnerActivatingRoleIds,
quarantineRoleId,
cancellationToken);

await RemoveUnneededActivatableRoles(
allMembers,
beginnerRole,
beginnerActivatingRoleIds,
quarantineRoleId,
cancellationToken);
}

private async Task AddMissingActivatableRoles(
List<DiscordMember> allMembers,
DiscordRole activatableRole,
ulong[] activatingRoleIds,
ulong quarantineRoleId,
CancellationToken cancellationToken)
{
List<DiscordMember> missingRoleMembers = allMembers
.Where(m => !HasMemberRole(m) && HasActivatingRoles(m) && !HasQuarantineRole(m))
.ToList();

if (missingMemberRoleMembers.Count != 0)
{
int totalCount = missingMemberRoleMembers.Count;
if (missingRoleMembers.Count == 0) return;

_discordLogger.LogOperationMessage(
$"Found {missingMemberRoleMembers.Count} users which are missing the Member role.");
int totalCount = missingRoleMembers.Count;

int successCount = await ExecuteRoleChangeWithRetry(
missingMemberRoleMembers,
m => m.GrantRoleAsync(memberRole),
cancellationToken);
_discordLogger.LogOperationMessage(
$"Found {missingRoleMembers.Count} users which are missing the {activatableRole.Name} role.");

_discordLogger.LogOperationMessage($"Done adding Member role for {successCount}/{totalCount} users.");
}
int successCount = await ExecuteRoleChangeWithRetry(
missingRoleMembers,
m => m.GrantRoleAsync(activatableRole),
cancellationToken);

_discordLogger.LogOperationMessage(
$"Done adding {activatableRole.Name} role for {successCount}/{totalCount} users.");

return;

bool HasMandatoryRoles(DiscordMember m)
bool HasActivatingRoles(DiscordMember m)
{
return m.Roles.Any(r => memberRoleIds.Contains(r.Id));
return m.Roles.Any(r => activatingRoleIds.Contains(r.Id));
}

bool HasMemberRole(DiscordMember m)
{
return m.Roles.Any(r => r.Id == memberRole.Id);
return m.Roles.Any(r => r.Id == activatableRole.Id);
}

bool HasQuarantineRole(DiscordMember m)
Expand All @@ -121,41 +163,42 @@ bool HasQuarantineRole(DiscordMember m)
}
}

private async Task RemoveUnneededMemberRoles(
private async Task RemoveUnneededActivatableRoles(
List<DiscordMember> allMembers,
ulong[] memberRoleIds,
DiscordRole memberRole,
DiscordRole activatableRole,
ulong[] activatingRoleIds,
ulong quarantineRoleId,
CancellationToken cancellationToken)
{
List<DiscordMember> memberRoleCandidates = allMembers
.Where(m => HasMemberRole(m) && (!HasMandatoryRoles(m) || HasQuarantineRole(m)))
.Where(m => HasMemberRole(m) && (!HasActivatingRoles(m) || HasQuarantineRole(m)))
.ToList();

if (memberRoleCandidates.Count != 0)
{
int totalCount = memberRoleCandidates.Count;
if (memberRoleCandidates.Count == 0) return;

_discordLogger.LogOperationMessage($"Found {memberRoleCandidates.Count} users with unneeded Member role.");
int totalCount = memberRoleCandidates.Count;

int successCount = await ExecuteRoleChangeWithRetry(
memberRoleCandidates,
m => m.RevokeRoleAsync(memberRole),
cancellationToken);
_discordLogger.LogOperationMessage(
$"Found {memberRoleCandidates.Count} users with unneeded {activatableRole.Name} role.");

_discordLogger.LogOperationMessage($"Done removing Member role for {successCount}/{totalCount} users.");
}
int successCount = await ExecuteRoleChangeWithRetry(
memberRoleCandidates,
m => m.RevokeRoleAsync(activatableRole),
cancellationToken);

_discordLogger.LogOperationMessage(
$"Done removing {activatableRole.Name} role for {successCount}/{totalCount} users.");

return;

bool HasMandatoryRoles(DiscordMember m)
bool HasActivatingRoles(DiscordMember m)
{
return m.Roles.Any(r => memberRoleIds.Contains(r.Id));
return m.Roles.Any(r => activatingRoleIds.Contains(r.Id));
}

bool HasMemberRole(DiscordMember m)
{
return m.Roles.Any(r => r.Id == memberRole.Id);
return m.Roles.Any(r => r.Id == activatableRole.Id);
}

bool HasQuarantineRole(DiscordMember m)
Expand All @@ -164,6 +207,20 @@ bool HasQuarantineRole(DiscordMember m)
}
}

private async Task MaintainGhostRoles(
DiscordGuild guild,
List<DiscordMember> allMembers,
CancellationToken cancellationToken)
{
ulong ghostRoleId = _options.GhostRoleId;
DiscordRole ghostRole = guild.Roles[ghostRoleId]
?? throw new Exception($"Could not find ghost role with id {ghostRoleId}");

await AddMissingGhostRoles(allMembers, ghostRole, cancellationToken);

await RemoveUnneededGhostRoles(allMembers, ghostRole, cancellationToken);
}

private async Task AddMissingGhostRoles(
List<DiscordMember> allMembers,
DiscordRole ghostRole,
Expand Down
64 changes: 43 additions & 21 deletions src/Nellebot/NotificationHandlers/MemberRoleIntegrityHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ public async Task Handle(GuildMemberUpdatedNotification notification, Cancellati
DiscordMember member = args.Member ?? throw new Exception(nameof(member));
DiscordGuild guild = args.Guild;

ulong[] memberRoleIds = _options.MemberRoleIds;
ulong memberRoleId = _options.MemberRoleId;
ulong ghostRoleId = _options.GhostRoleId;
ulong quarantineRoleId = _options.QuarantineRoleId;

await QuarantineIfSpammer(member, addedRoles);
Expand All @@ -71,47 +68,72 @@ public async Task Handle(GuildMemberUpdatedNotification notification, Cancellati
return;
}

await MaintainMemberRole(guild, memberRoleId, member, memberRoleIds, quarantineRoleId);
await MaintainMemberRole(guild, member, quarantineRoleId);

await MaintainBeginnerRole(guild, member, quarantineRoleId);

await MaintainGhostRole(guild, ghostRoleId, member);
await MaintainGhostRole(guild, member);
}

/// <summary>
/// Ensures that the member role is added if the user has any of the member roles,
/// and removed if the user has none of the member roles
/// </summary>
private static async Task MaintainMemberRole(
private async Task MaintainMemberRole(
DiscordGuild guild,
ulong memberRoleId,
DiscordMember member,
ulong[] memberRoleIds,
ulong quarantineRoleId)
{
ulong memberRoleId = _options.MemberRoleId;
ulong[] memberActivatingRoleIds = _options.MemberActivatingRoleIds;
DiscordRole memberRole = guild.Roles[memberRoleId]
?? throw new Exception($"Could not find member role with id {memberRoleId}");
?? throw new Exception($"Could not find Member role with id {memberRoleId}");

bool userHasMandatoryRoles = member.Roles.Any(r => memberRoleIds.Contains(r.Id));
bool userHasMemberRole = member.Roles.Any(r => r.Id == memberRoleId);
await MaintainActivatableRole(member, memberRole, memberActivatingRoleIds, quarantineRoleId);
}

private async Task MaintainBeginnerRole(
DiscordGuild guild,
DiscordMember member,
ulong quarantineRoleId)
{
ulong beginnerRoleId = _options.BeginnerRoleId;
ulong[] beginnerActivatingRoleIds = _options.BeginnerActivatingRoleIds;
DiscordRole beginnerRole = guild.Roles[beginnerRoleId]
?? throw new Exception($"Could not find Beginner role with id {beginnerRoleId}");

await MaintainActivatableRole(member, beginnerRole, beginnerActivatingRoleIds, quarantineRoleId);
}

/// <summary>
/// Ensures that the activatable role is added if the user has any of the activating roles,
/// and removed if the user has none of the activating roles
/// </summary>
private static async Task MaintainActivatableRole(
DiscordMember member,
DiscordRole activatableRole,
ulong[] activatingRoleIds,
ulong quarantineRoleId)
{
bool userHasActivatableRole = member.Roles.Any(r => r.Id == activatableRole.Id);
bool userHasActivatingRoles = member.Roles.Any(r => activatingRoleIds.Contains(r.Id));
bool userHasQuarantineRole = member.Roles.Any(r => r.Id == quarantineRoleId);

bool userIsEligibleForMemberRole = userHasMandatoryRoles && !userHasQuarantineRole;
bool userIsEligibleForActivatableRole = userHasActivatingRoles && !userHasQuarantineRole;

if (!userHasMemberRole && userIsEligibleForMemberRole)
if (!userHasActivatableRole && userIsEligibleForActivatableRole)
{
await member.GrantRoleAsync(memberRole);
await member.GrantRoleAsync(activatableRole);
}
else if (userHasMemberRole && !userIsEligibleForMemberRole)
else if (userHasActivatableRole && !userIsEligibleForActivatableRole)
{
await member.RevokeRoleAsync(memberRole);
await member.RevokeRoleAsync(activatableRole);
}
}

/// <summary>
/// Ensures that the ghost role is added if the user has no roles,
/// and removed if the user has any other roles
/// </summary>
private static async Task MaintainGhostRole(DiscordGuild guild, ulong ghostRoleId, DiscordMember member)
private async Task MaintainGhostRole(DiscordGuild guild, DiscordMember member)
{
ulong ghostRoleId = _options.GhostRoleId;
DiscordRole ghostRole = guild.Roles[ghostRoleId]
?? throw new Exception($"Could not find ghost role with id {ghostRoleId}");

Expand Down
7 changes: 6 additions & 1 deletion src/Nellebot/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,18 @@
],
"RequiredAwardCount": 1,
"MemberRoleId": 990253471691309146,
"MemberRoleIds": [
"MemberActivatingRoleIds": [
825149115348287533,
825149115338981405,
825149115338981404,
825149115338981403,
825149115338981401
],
"BeginnerRoleId": 1425228938019344547,
"BeginnerActivatingRoleIds": [
825149115338981403,
825149115338981404
],
"GhostRoleId": 1267826295816060988,
"QuarantineRoleId": 1412826444152967389,
"QuarantineChannelId": 1412828639724175410,
Expand Down
7 changes: 6 additions & 1 deletion src/Nellebot/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,19 @@
],
"RequiredAwardCount": 3,
"MemberRoleId": 990251983887806585,
"MemberRoleIds": [
"MemberActivatingRoleIds": [
622142742478323727,
622144734533648405,
622145068438126604,
622143807483281457,
622143551827869696,
622788175491891231
],
"BeginnerRoleId": 1425238421051412572,
"BeginnerActivatingRoleIds": [
622143551827869696,
622143807483281457
],
"GhostRoleId": 1267940960126369833,
"QuarantineRoleId": 1412827243516006442,
"QuarantineChannelId": 1412829017903595662,
Expand Down
Loading