mirror of
https://github.com/RPCS3/discord-bot.git
synced 2026-01-31 01:25:22 +01:00
fix warns
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
using System.Linq.Expressions;
|
||||
using CompatBot.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CompatBot.Commands.AutoCompleteProviders;
|
||||
|
||||
public class WarningAutoCompleteProvider: IAutoCompleteProvider
|
||||
{
|
||||
public async ValueTask<IEnumerable<DiscordAutoCompleteChoice>> AutoCompleteAsync(AutoCompleteContext context)
|
||||
{
|
||||
var authorized = await context.User.IsWhitelistedAsync(context.Client, context.Guild).ConfigureAwait(false);
|
||||
if (!authorized)
|
||||
return [new($"{Config.Reactions.Denied} You are not authorized to use this command.", -1)];
|
||||
|
||||
//var user = context.Arguments.FirstOrDefault(kvp => kvp.Key.Name.Equals("user") && kvp.Value is DiscordUser).Value as DiscordUser;
|
||||
Expression<Func<Warning, bool>> filter = context.Command.Name is nameof(Warnings.Revert)
|
||||
? w => w.Retracted
|
||||
: w => !w.Retracted;
|
||||
await using var db = new BotDb();
|
||||
IEnumerable<Warning> result;
|
||||
if (context.UserInput is not { Length: > 0 } prefix)
|
||||
result = db.Warning
|
||||
.OrderByDescending(w => w.Id)
|
||||
.Where(filter)
|
||||
.Take(25)
|
||||
.AsNoTracking()
|
||||
.AsEnumerable();
|
||||
else
|
||||
{
|
||||
prefix = prefix.ToLowerInvariant();
|
||||
var prefixMatches = db.Warning
|
||||
.Where(filter)
|
||||
.Where(w => w.Id.ToString().StartsWith(prefix) || w.Reason.StartsWith(prefix))
|
||||
.Take(25);
|
||||
var substringMatches= db.Warning
|
||||
.Where(filter)
|
||||
.Where(w => w.Id.ToString().Contains(prefix) || w.Reason.Contains(prefix))
|
||||
.Take(50);
|
||||
result = prefixMatches
|
||||
.Concat(substringMatches)
|
||||
.Distinct()
|
||||
.OrderByDescending(i => i.Id)
|
||||
.Take(25)
|
||||
.AsNoTracking()
|
||||
.AsEnumerable();
|
||||
}
|
||||
return result.Select(
|
||||
w => new DiscordAutoCompleteChoice($"{w.Id}: {w.Timestamp?.AsUtc():O}: {w.Reason}", w.Id)
|
||||
).ToList();
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ internal static class Mod
|
||||
await ctx.RespondAsync($"{Config.Reactions.Failure} {moderator.Mention} is not a moderator (yet)", ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("💔 Remove bot admin permissions")]
|
||||
[Command("💔 Remove bot admin permissions"), SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)]
|
||||
public static async ValueTask Unsudo(UserCommandContext ctx, DiscordUser sudoer)
|
||||
{
|
||||
if (ctx.Client.CurrentApplication.Owners?.Any(u => u.Id == sudoer.Id) ?? false)
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace CompatBot.Commands;
|
||||
internal static class MessageMenuCommands
|
||||
{
|
||||
/*
|
||||
[Command("🗨️ message")]
|
||||
[Command("🗨️ message command with very long name")]
|
||||
[Description("12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901")]
|
||||
public async ValueTask AnalyzerTest(){}
|
||||
*/
|
||||
|
||||
151
CompatBot/Commands/Warnings.UserMenu.cs
Normal file
151
CompatBot/Commands/Warnings.UserMenu.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using CompatApiClient.Utils;
|
||||
using DSharpPlus.Commands.Processors.MessageCommands;
|
||||
using DSharpPlus.Commands.Processors.UserCommands;
|
||||
using DSharpPlus.Interactivity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CompatBot.Commands;
|
||||
|
||||
internal static class WarningsContextMenus
|
||||
{
|
||||
[Command("❗ Warn user"), RequiresBotModRole, SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)]
|
||||
[Description("Give user a warning")]
|
||||
public static async ValueTask Warn(UserCommandContext ctx, DiscordUser user)
|
||||
{
|
||||
var interactivity = ctx.Extension.ServiceProvider.GetService<InteractivityExtension>();
|
||||
if (interactivity is null)
|
||||
{
|
||||
await ctx.RespondAsync($"{Config.Reactions.Failure} Couldn't get interactivity extension").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var interaction = ctx.Interaction;
|
||||
var modal = new DiscordInteractionResponseBuilder()
|
||||
.AsEphemeral()
|
||||
.WithCustomId($"modal:warn:{Guid.NewGuid():n}")
|
||||
.WithTitle("Issue new warning")
|
||||
.AddComponents(
|
||||
new DiscordTextInputComponent(
|
||||
"Warning reason",
|
||||
"warning",
|
||||
"Rule #2",
|
||||
min_length: 2
|
||||
)
|
||||
);
|
||||
await ctx.RespondWithModalAsync(modal).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
InteractivityResult<ModalSubmittedEventArgs> modalResult;
|
||||
string reason;
|
||||
do
|
||||
{
|
||||
modalResult = await interactivity.WaitForModalAsync(modal.CustomId, ctx.User).ConfigureAwait(false);
|
||||
if (modalResult.TimedOut)
|
||||
return;
|
||||
} while (!modalResult.Result.Values.TryGetValue("warning", out reason!));
|
||||
|
||||
interaction = modalResult.Result.Interaction;
|
||||
await interaction.CreateResponseAsync(
|
||||
DiscordInteractionResponseType.DeferredChannelMessageWithSource,
|
||||
new DiscordInteractionResponseBuilder().AsEphemeral()
|
||||
).ConfigureAwait(false);
|
||||
var (saved, suppress, recent, total) = await Warnings.AddAsync(user.Id, ctx.User, reason).ConfigureAwait(false);
|
||||
if (!saved)
|
||||
{
|
||||
await ctx.RespondAsync($"{Config.Reactions.Failure} Couldn't save the warning, please try again", ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!suppress)
|
||||
{
|
||||
var userMsgContent = $"{Config.Reactions.Success} User warning saved, {user.Mention} has {recent} recent warning{StringUtils.GetSuffix(recent)} ({total} total)";
|
||||
var userMsg = new DiscordMessageBuilder()
|
||||
.WithContent(userMsgContent)
|
||||
.AddMention(UserMention.All);
|
||||
await ctx.Channel.SendMessageAsync(userMsg).ConfigureAwait(false);
|
||||
}
|
||||
await Warnings.ListUserWarningsAsync(ctx.Client, ctx.Interaction, user.Id, user.Username.Sanitize()).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Config.Log.Error(e);
|
||||
var msg = new DiscordInteractionResponseBuilder()
|
||||
.AsEphemeral()
|
||||
.WithContent($"{Config.Reactions.Failure} Failed to change nickname, check bot's permissions");
|
||||
await interaction.EditOriginalResponseAsync(new(msg)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("❗ Warn user"), RequiresBotModRole, SlashCommandTypes(DiscordApplicationCommandType.MessageContextMenu)]
|
||||
[Description("Give user a warning")]
|
||||
public static async ValueTask Warn(MessageCommandContext ctx, DiscordMessage message)
|
||||
{
|
||||
var interactivity = ctx.Extension.ServiceProvider.GetService<InteractivityExtension>();
|
||||
if (interactivity is null)
|
||||
{
|
||||
await ctx.RespondAsync($"{Config.Reactions.Failure} Couldn't get interactivity extension").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var interaction = ctx.Interaction;
|
||||
var modal = new DiscordInteractionResponseBuilder()
|
||||
.AsEphemeral()
|
||||
.WithCustomId($"modal:warn:{Guid.NewGuid():n}")
|
||||
.WithTitle("Issue new warning")
|
||||
.AddComponents(
|
||||
new DiscordTextInputComponent(
|
||||
"Warning reason",
|
||||
"warning",
|
||||
"Rule #2",
|
||||
min_length: 2
|
||||
)
|
||||
);
|
||||
await ctx.RespondWithModalAsync(modal).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
InteractivityResult<ModalSubmittedEventArgs> modalResult;
|
||||
string reason;
|
||||
do
|
||||
{
|
||||
modalResult = await interactivity.WaitForModalAsync(modal.CustomId, ctx.User).ConfigureAwait(false);
|
||||
if (modalResult.TimedOut)
|
||||
return;
|
||||
} while (!modalResult.Result.Values.TryGetValue("warning", out reason!));
|
||||
|
||||
interaction = modalResult.Result.Interaction;
|
||||
await interaction.CreateResponseAsync(
|
||||
DiscordInteractionResponseType.DeferredChannelMessageWithSource,
|
||||
new DiscordInteractionResponseBuilder().AsEphemeral()
|
||||
).ConfigureAwait(false);
|
||||
var user = message.Author!;
|
||||
var (saved, suppress, recent, total) = await Warnings.AddAsync(user.Id, ctx.User, reason, message.Content.Sanitize()).ConfigureAwait(false);
|
||||
if (!saved)
|
||||
{
|
||||
await ctx.RespondAsync($"{Config.Reactions.Failure} Couldn't save the warning, please try again", ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!suppress)
|
||||
{
|
||||
var userMsgContent = $"{Config.Reactions.Success} User warning saved, {user.Mention} has {recent} recent warning{StringUtils.GetSuffix(recent)} ({total} total)";
|
||||
var userMsg = new DiscordMessageBuilder()
|
||||
.WithContent(userMsgContent)
|
||||
.AddMention(UserMention.All);
|
||||
await ctx.Channel.SendMessageAsync(userMsg).ConfigureAwait(false);
|
||||
}
|
||||
await Warnings.ListUserWarningsAsync(ctx.Client, ctx.Interaction, user.Id, user.Username.Sanitize()).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Config.Log.Error(e);
|
||||
var msg = new DiscordInteractionResponseBuilder()
|
||||
.AsEphemeral()
|
||||
.WithContent($"{Config.Reactions.Failure} Failed to change nickname, check bot's permissions");
|
||||
await interaction.EditOriginalResponseAsync(new(msg)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +1,131 @@
|
||||
using CompatApiClient.Utils;
|
||||
using CompatBot.Commands.AutoCompleteProviders;
|
||||
using CompatBot.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CompatBot.Commands;
|
||||
|
||||
//[Command("warn")]
|
||||
[Command("warning"), RequiresBotModRole]
|
||||
[Description("Command used to manage warnings")]
|
||||
internal sealed partial class Warnings
|
||||
internal static partial class Warnings
|
||||
{
|
||||
/*
|
||||
[DefaultGroupCommand] //attributes on overloads do not work, so no easy permission checks
|
||||
[Description("Command used to issue a new warning")]
|
||||
public async Task Warn(CommandContext ctx, [Description("User to warn. Can also use @id")] DiscordUser user, [RemainingText, Description("Warning explanation")] string reason)
|
||||
[Command("give")]
|
||||
[Description("Issue a new warning to a user")]
|
||||
public static async ValueTask Warn(
|
||||
SlashCommandContext ctx,
|
||||
[Description("User to warn")]
|
||||
DiscordUser user,
|
||||
[Description("Warning explanation")]
|
||||
string reason
|
||||
)
|
||||
{
|
||||
//need to do manual check of the attribute in all GroupCommand overloads :(
|
||||
if (!await new RequiresBotModRoleAttribute().ExecuteCheckAsync(ctx, false).ConfigureAwait(false))
|
||||
await ctx.DeferResponseAsync(ephemeral: true).ConfigureAwait(false);
|
||||
var (saved, suppress, recent, total) = await AddAsync(user.Id, ctx.User, reason).ConfigureAwait(false);
|
||||
if (!saved)
|
||||
{
|
||||
await ctx.RespondAsync($"{Config.Reactions.Failure} Couldn't save the warning, please try again", ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await AddAsync(ctx, user.Id, user.Username.Sanitize(), ctx.Message.Author, reason).ConfigureAwait(false))
|
||||
await ctx.ReactWithAsync(Config.Reactions.Success).ConfigureAwait(false);
|
||||
else
|
||||
await ctx.ReactWithAsync(Config.Reactions.Failure, "Couldn't save the warning, please try again").ConfigureAwait(false);
|
||||
if (!suppress)
|
||||
{
|
||||
var userMsgContent = $"{Config.Reactions.Success} User warning saved, {user.Mention} has {recent} recent warning{StringUtils.GetSuffix(recent)} ({total} total)";
|
||||
var userMsg = new DiscordMessageBuilder()
|
||||
.WithContent(userMsgContent)
|
||||
.AddMention(UserMention.All);
|
||||
await ctx.Channel.SendMessageAsync(userMsg).ConfigureAwait(false);
|
||||
}
|
||||
await ListUserWarningsAsync(ctx.Client, ctx.Interaction, user.Id, user.Username.Sanitize()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[DefaultGroupCommand]
|
||||
public async Task Warn(CommandContext ctx, [Description("ID of a user to warn")] ulong userId, [RemainingText, Description("Warning explanation")] string reason)
|
||||
[Command("update")]
|
||||
[Description("Change warning details")]
|
||||
public static async ValueTask Edit(
|
||||
SlashCommandContext ctx,
|
||||
[Description("Warning ID to edit"), SlashAutoCompleteProvider<WarningAutoCompleteProvider>]
|
||||
int id,
|
||||
[Description("Updated warning explanation")]
|
||||
string reason,
|
||||
[Description("User to filter autocomplete results")]
|
||||
DiscordUser? user = null
|
||||
)
|
||||
{
|
||||
if (!await new RequiresBotModRoleAttribute().ExecuteCheckAsync(ctx, false).ConfigureAwait(false))
|
||||
return;
|
||||
|
||||
if (await AddAsync(ctx, userId, $"<@{userId}>", ctx.Message.Author, reason).ConfigureAwait(false))
|
||||
await ctx.ReactWithAsync(Config.Reactions.Success).ConfigureAwait(false);
|
||||
else
|
||||
await ctx.ReactWithAsync(Config.Reactions.Failure, "Couldn't save the warning, please try again").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("edit"), RequiresBotModRole]
|
||||
[Description("Edit specified warning")]
|
||||
public async Task Edit(CommandContext ctx, [Description("Warning ID to edit")] int id)
|
||||
{
|
||||
var interact = ctx.Client.GetInteractivity();
|
||||
await using var db = new BotDb();
|
||||
var warnings = await db.Warning.Where(w => id.Equals(w.Id)).ToListAsync().ConfigureAwait(false);
|
||||
if (warnings.Count == 0)
|
||||
if (warnings.Count is 0)
|
||||
{
|
||||
await ctx.ReactWithAsync(Config.Reactions.Denied, $"{ctx.Message.Author.Mention} Warn not found", true);
|
||||
await ctx.RespondAsync($"{Config.Reactions.Failure} Warning not found", ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var warningToEdit = warnings.First();
|
||||
if (warningToEdit.IssuerId != ctx.User.Id)
|
||||
{
|
||||
await ctx.ReactWithAsync(Config.Reactions.Denied, $"{ctx.Message.Author.Mention} This warn wasn't issued by you :(", true);
|
||||
await ctx.RespondAsync($"{Config.Reactions.Denied} This warning wasn't issued by you", ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = await ctx.Channel.SendMessageAsync("Updated warn reason?").ConfigureAwait(false);
|
||||
var response = await interact.WaitForMessageAsync(
|
||||
m => m.Author == ctx.User
|
||||
&& m.Channel == ctx.Channel
|
||||
&& !string.IsNullOrEmpty(m.Content)
|
||||
).ConfigureAwait(false);
|
||||
|
||||
await msg.DeleteAsync().ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(response.Result?.Content))
|
||||
{
|
||||
await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Can't edit warning without a new reason").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
warningToEdit.Reason = response.Result.Content;
|
||||
warningToEdit.Reason = reason;
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
await ctx.Channel.SendMessageAsync($"Warning successfully edited!").ConfigureAwait(false);
|
||||
await ctx.RespondAsync("Warning successfully updated", ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("remove"), TextAlias("delete", "del"), RequiresBotModRole]
|
||||
[Command("remove")]
|
||||
[Description("Removes specified warnings")]
|
||||
public async Task Remove(CommandContext ctx, [Description("Warning IDs to remove separated with space")] params int[] ids)
|
||||
public static async ValueTask Remove(
|
||||
SlashCommandContext ctx,
|
||||
[Description("Warning ID to remove"), SlashAutoCompleteProvider<WarningAutoCompleteProvider>]
|
||||
int id,
|
||||
[Description("Reason for warning removal")]
|
||||
string reason,
|
||||
[Description("User to filter autocomplete results")]
|
||||
DiscordUser? user = null
|
||||
)
|
||||
{
|
||||
var interact = ctx.Client.GetInteractivity();
|
||||
var msg = await ctx.Channel.SendMessageAsync("What is the reason for removal?").ConfigureAwait(false);
|
||||
var response = await interact.WaitForMessageAsync(
|
||||
m => m.Author == ctx.User
|
||||
&& m.Channel == ctx.Channel
|
||||
&& !string.IsNullOrEmpty(m.Content)
|
||||
).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(response.Result?.Content))
|
||||
{
|
||||
await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Can't remove warnings without a reason").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await msg.DeleteAsync().ConfigureAwait(false);
|
||||
await using var db = new BotDb();
|
||||
var warningsToRemove = await db.Warning.Where(w => ids.Contains(w.Id)).ToListAsync().ConfigureAwait(false);
|
||||
var warningsToRemove = await db.Warning.Where(w => w.Id == id).ToListAsync().ConfigureAwait(false);
|
||||
foreach (var w in warningsToRemove)
|
||||
{
|
||||
w.Retracted = true;
|
||||
w.RetractedBy = ctx.User.Id;
|
||||
w.RetractionReason = response.Result.Content;
|
||||
w.RetractionReason = reason;
|
||||
w.RetractionTimestamp = DateTime.UtcNow.Ticks;
|
||||
}
|
||||
var removedCount = await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
if (removedCount == ids.Length)
|
||||
await ctx.Channel.SendMessageAsync($"Warning{StringUtils.GetSuffix(ids.Length)} successfully removed!").ConfigureAwait(false);
|
||||
if (removedCount is 0)
|
||||
await ctx.RespondAsync($"{Config.Reactions.Failure} Failed to remove warning").ConfigureAwait(false);
|
||||
else
|
||||
await ctx.Channel.SendMessageAsync($"Removed {removedCount} items, but was asked to remove {ids.Length}").ConfigureAwait(false);
|
||||
{
|
||||
await ctx.Channel.SendMessageAsync("Warning successfully removed").ConfigureAwait(false);
|
||||
user ??= await ctx.Client.GetUserAsync(warningsToRemove[0].DiscordId).ConfigureAwait(false);
|
||||
await ListUserWarningsAsync(ctx.Client, ctx.Interaction, user.Id, user.Username.Sanitize(), false).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("clear"), RequiresBotModRole]
|
||||
[Command("clear")]
|
||||
[Description("Removes **all** warnings for a user")]
|
||||
public Task Clear(CommandContext ctx, [Description("User to clear warnings for")] DiscordUser user)
|
||||
=> Clear(ctx, user.Id);
|
||||
|
||||
[Command("clear"), RequiresBotModRole]
|
||||
public async Task Clear(CommandContext ctx, [Description("User ID to clear warnings for")] ulong userId)
|
||||
public static async ValueTask Clear(
|
||||
SlashCommandContext ctx,
|
||||
[Description("User to clear warnings for")]
|
||||
DiscordUser user,
|
||||
[Description("Reason for clear warning removal")]
|
||||
string reason
|
||||
)
|
||||
{
|
||||
var interact = ctx.Client.GetInteractivity();
|
||||
var msg = await ctx.Channel.SendMessageAsync("What is the reason for removing all the warnings?").ConfigureAwait(false);
|
||||
var response = await interact.WaitForMessageAsync(m => m.Author == ctx.User && m.Channel == ctx.Channel && !string.IsNullOrEmpty(m.Content)).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(response.Result?.Content))
|
||||
{
|
||||
await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Can't remove warnings without a reason").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await msg.DeleteAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await using var db = new BotDb();
|
||||
var warningsToRemove = await db.Warning.Where(w => w.DiscordId == userId && !w.Retracted).ToListAsync().ConfigureAwait(false);
|
||||
var warningsToRemove = await db.Warning.Where(w => w.DiscordId == user.Id && !w.Retracted).ToListAsync().ConfigureAwait(false);
|
||||
foreach (var w in warningsToRemove)
|
||||
{
|
||||
w.Retracted = true;
|
||||
w.RetractedBy = ctx.User.Id;
|
||||
w.RetractionReason = response.Result.Content;
|
||||
w.RetractionReason = reason;
|
||||
w.RetractionTimestamp = DateTime.UtcNow.Ticks;
|
||||
}
|
||||
var removed = await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
await ctx.Channel.SendMessageAsync($"{removed} warning{StringUtils.GetSuffix(removed)} successfully removed!").ConfigureAwait(false);
|
||||
await ListUserWarningsAsync(ctx.Client, ctx.Interaction, user.Id, user.Username.Sanitize()).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -147,9 +133,15 @@ internal sealed partial class Warnings
|
||||
}
|
||||
}
|
||||
|
||||
[Command("revert"), RequiresBotModRole]
|
||||
[Description("Changes the state of the warning status")]
|
||||
public async Task Revert(CommandContext ctx, [Description("Warning ID to change")] int id)
|
||||
[Command("revert")]
|
||||
[Description("Bring back warning that's been removed before")]
|
||||
public static async ValueTask Revert(
|
||||
SlashCommandContext ctx,
|
||||
[Description("Warning ID to change"), SlashAutoCompleteProvider<WarningAutoCompleteProvider>]
|
||||
int id,
|
||||
[Description("User to filter autocomplete results")]
|
||||
DiscordUser? user = null
|
||||
)
|
||||
{
|
||||
await using var db = new BotDb();
|
||||
var warn = await db.Warning.FirstOrDefaultAsync(w => w.Id == id).ConfigureAwait(false);
|
||||
@@ -160,82 +152,60 @@ internal sealed partial class Warnings
|
||||
warn.RetractionReason = null;
|
||||
warn.RetractionTimestamp = null;
|
||||
await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
|
||||
await ctx.ReactWithAsync(Config.Reactions.Success, "Reissued the warning", true).ConfigureAwait(false);
|
||||
await ctx.RespondAsync($"{Config.Reactions.Success} Reissued the warning", ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
await Remove(ctx, id).ConfigureAwait(false);
|
||||
await ctx.RespondAsync($"{Config.Reactions.Failure} Warning is not retracted", ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static async Task<bool> AddAsync(CommandContext ctx, ulong userId, string userName, DiscordUser issuer, string? reason, string? fullReason = null)
|
||||
internal static async ValueTask<(bool saved, bool suppress, int recentCount, int totalCount)>
|
||||
AddAsync(ulong userId, DiscordUser issuer, string reason, string? fullReason = null)
|
||||
{
|
||||
reason = await Sudo.Fix.FixChannelMentionAsync(ctx, reason).ConfigureAwait(false);
|
||||
return await AddAsync(ctx.Client, ctx.Message, userId, userName, issuer, reason, fullReason);
|
||||
}
|
||||
*/
|
||||
|
||||
internal static async Task<bool> AddAsync(DiscordClient client, DiscordMessage message, ulong userId, string userName, DiscordUser issuer, string? reason, string? fullReason = null)
|
||||
{
|
||||
/*
|
||||
if (string.IsNullOrEmpty(reason))
|
||||
{
|
||||
var interact = client.GetInteractivity();
|
||||
var msg = await message.Channel.SendMessageAsync("What is the reason for this warning?").ConfigureAwait(false);
|
||||
var response = await interact.WaitForMessageAsync(
|
||||
m => m.Author == message.Author
|
||||
&& m.Channel == message.Channel
|
||||
&& !string.IsNullOrEmpty(m.Content)
|
||||
).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(response.Result.Content))
|
||||
{
|
||||
await msg.UpdateOrCreateMessageAsync(message.Channel, "A reason needs to be provided").ConfigureAwait(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
await msg.DeleteAsync().ConfigureAwait(false);
|
||||
reason = response.Result.Content;
|
||||
}
|
||||
*/
|
||||
try
|
||||
{
|
||||
await using var db = new BotDb();
|
||||
await db.Warning.AddAsync(new Warning { DiscordId = userId, IssuerId = issuer.Id, Reason = reason, FullReason = fullReason ?? "", Timestamp = DateTime.UtcNow.Ticks }).ConfigureAwait(false);
|
||||
await db.Warning.AddAsync(
|
||||
new()
|
||||
{
|
||||
DiscordId = userId,
|
||||
IssuerId = issuer.Id,
|
||||
Reason = reason,
|
||||
FullReason = fullReason ?? "",
|
||||
Timestamp = DateTime.UtcNow.Ticks
|
||||
}
|
||||
).ConfigureAwait(false);
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var threshold = DateTime.UtcNow.AddMinutes(-15).Ticks;
|
||||
var recentCount = db.Warning.Count(w => w.DiscordId == userId && !w.Retracted && w.Timestamp > threshold);
|
||||
if (recentCount > 3)
|
||||
{
|
||||
Config.Log.Debug("Suicide behavior detected, not spamming with warning responses");
|
||||
return true;
|
||||
}
|
||||
|
||||
var totalCount = db.Warning.Count(w => w.DiscordId == userId && !w.Retracted);
|
||||
await message.Channel.SendMessageAsync($"User warning saved! User currently has {totalCount} warning{StringUtils.GetSuffix(totalCount)}!").ConfigureAwait(false);
|
||||
if (totalCount > 1)
|
||||
await ListUserWarningsAsync(client, message, userId, userName).ConfigureAwait(false);
|
||||
return true;
|
||||
var recentCount = db.Warning.Count(w => w.DiscordId == userId && !w.Retracted && w.Timestamp > threshold);
|
||||
if (recentCount < 4)
|
||||
return (true, false, recentCount, totalCount);
|
||||
|
||||
Config.Log.Debug("Suicide behavior detected, not spamming with warning responses");
|
||||
return (true, true, recentCount, totalCount);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Config.Log.Error(e, "Couldn't save the warning");
|
||||
return false;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
//note: be sure to pass a sanitized userName
|
||||
private static async Task ListUserWarningsAsync(DiscordClient client, DiscordMessage message, ulong userId, string userName, bool skipIfOne = true)
|
||||
//note2: itneraction must be deferred
|
||||
internal static async ValueTask ListUserWarningsAsync(DiscordClient client, DiscordInteraction interaction, ulong userId, string userName, bool skipIfOne = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isWhitelisted = (await client.GetMemberAsync(message.Author).ConfigureAwait(false))?.IsWhitelisted() is true;
|
||||
if (message.Author.Id != userId && !isWhitelisted)
|
||||
var isWhitelisted = await interaction.User.IsWhitelistedAsync(client, interaction.Guild).ConfigureAwait(false);
|
||||
if (interaction.User.Id != userId && !isWhitelisted)
|
||||
{
|
||||
Config.Log.Error($"Somehow {message.Author.Username} ({message.Author.Id}) triggered warning list for {userId}");
|
||||
Config.Log.Error($"Somehow {interaction.User.Username} ({interaction.User.Id}) triggered warning list for {userId}");
|
||||
return;
|
||||
}
|
||||
|
||||
var channel = message.Channel;
|
||||
var isPrivate = channel.IsPrivate;
|
||||
const bool ephemeral = true;
|
||||
int count, removed;
|
||||
bool isKot, isDoggo;
|
||||
await using var db = new BotDb();
|
||||
@@ -243,7 +213,8 @@ internal sealed partial class Warnings
|
||||
removed = await db.Warning.CountAsync(w => w.DiscordId == userId && w.Retracted).ConfigureAwait(false);
|
||||
isKot = db.Kot.Any(k => k.UserId == userId);
|
||||
isDoggo = db.Doggo.Any(d => d.UserId == userId);
|
||||
if (count == 0)
|
||||
var response = new DiscordInteractionResponseBuilder().AsEphemeral(ephemeral);
|
||||
if (count is 0)
|
||||
{
|
||||
if (isKot && isDoggo)
|
||||
{
|
||||
@@ -252,7 +223,7 @@ internal sealed partial class Warnings
|
||||
else
|
||||
isDoggo = false;
|
||||
}
|
||||
var msg = (removed, isPrivate, isKot, isDoggo) switch
|
||||
var msg = (removed, ephemeral, isKot, isDoggo) switch
|
||||
{
|
||||
(0, _, true, false) => $"{userName} has no warnings, is an upstanding kot, and a paw bean of this community",
|
||||
(0, _, false, true) => $"{userName} has no warnings, is a good boy, and a wiggling tail of this community",
|
||||
@@ -262,37 +233,41 @@ internal sealed partial class Warnings
|
||||
(_, _, false, true) => $"{userName} has no warnings, but are they a good boy?",
|
||||
_ => $"{userName} has no warnings",
|
||||
};
|
||||
await message.Channel.SendMessageAsync(msg).ConfigureAwait(false);
|
||||
if (!isPrivate || removed == 0)
|
||||
;
|
||||
await interaction.EditOriginalResponseAsync(new(response.WithContent(msg))).ConfigureAwait(false);
|
||||
if (!ephemeral || removed is 0)
|
||||
return;
|
||||
}
|
||||
|
||||
if (count == 1 && skipIfOne)
|
||||
if (count is 1 && skipIfOne)
|
||||
{
|
||||
await interaction.EditOriginalResponseAsync(new(response.WithContent("No additional warnings on record"))).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const int maxWarningsInPublicChannel = 3;
|
||||
var showCount = Math.Min(maxWarningsInPublicChannel, count);
|
||||
var table = new AsciiTable(
|
||||
new AsciiColumn("ID", alignToRight: true),
|
||||
new AsciiColumn("±", disabled: !isPrivate || !isWhitelisted),
|
||||
new AsciiColumn("±", disabled: !ephemeral || !isWhitelisted),
|
||||
new AsciiColumn("By", maxWidth: 15),
|
||||
new AsciiColumn("On date (UTC)"),
|
||||
new AsciiColumn("Reason"),
|
||||
new AsciiColumn("Context", disabled: !isPrivate, maxWidth: 4096)
|
||||
new AsciiColumn("Context", disabled: !ephemeral, maxWidth: 4096)
|
||||
);
|
||||
IQueryable<Warning> query = db.Warning.Where(w => w.DiscordId == userId).OrderByDescending(w => w.Id);
|
||||
if (!isPrivate || !isWhitelisted)
|
||||
if (!ephemeral || !isWhitelisted)
|
||||
query = query.Where(w => !w.Retracted);
|
||||
if (!isPrivate && !isWhitelisted)
|
||||
if (!ephemeral && !isWhitelisted)
|
||||
query = query.Take(maxWarningsInPublicChannel);
|
||||
foreach (var warning in await query.ToListAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (warning.Retracted)
|
||||
{
|
||||
if (isWhitelisted && isPrivate)
|
||||
if (isWhitelisted && ephemeral)
|
||||
{
|
||||
var retractedByName = warning.RetractedBy.HasValue
|
||||
? await client.GetUserNameAsync(channel, warning.RetractedBy.Value, isPrivate, "unknown mod").ConfigureAwait(false)
|
||||
? await client.GetUserNameAsync(interaction.Channel, warning.RetractedBy.Value, ephemeral, "unknown mod").ConfigureAwait(false)
|
||||
: "";
|
||||
var retractionTimestamp = warning.RetractionTimestamp.HasValue
|
||||
? new DateTime(warning.RetractionTimestamp.Value, DateTimeKind.Utc).ToString("u")
|
||||
@@ -301,7 +276,7 @@ internal sealed partial class Warnings
|
||||
|
||||
var issuerName = warning.IssuerId == 0
|
||||
? ""
|
||||
: await client.GetUserNameAsync(channel, warning.IssuerId, isPrivate, "unknown mod").ConfigureAwait(false);
|
||||
: await client.GetUserNameAsync(interaction.Channel, warning.IssuerId, ephemeral, "unknown mod").ConfigureAwait(false);
|
||||
var timestamp = warning.Timestamp.HasValue
|
||||
? new DateTime(warning.Timestamp.Value, DateTimeKind.Utc).ToString("u")
|
||||
: "";
|
||||
@@ -312,7 +287,7 @@ internal sealed partial class Warnings
|
||||
{
|
||||
var issuerName = warning.IssuerId == 0
|
||||
? ""
|
||||
: await client.GetUserNameAsync(channel, warning.IssuerId, isPrivate, "unknown mod").ConfigureAwait(false);
|
||||
: await client.GetUserNameAsync(interaction.Channel, warning.IssuerId, ephemeral, "unknown mod").ConfigureAwait(false);
|
||||
var timestamp = warning.Timestamp.HasValue
|
||||
? new DateTime(warning.Timestamp.Value, DateTimeKind.Utc).ToString("u")
|
||||
: "";
|
||||
@@ -321,14 +296,22 @@ internal sealed partial class Warnings
|
||||
}
|
||||
|
||||
var result = new StringBuilder("Warning list for ").Append(Formatter.Sanitize(userName));
|
||||
if (!isPrivate && !isWhitelisted && count > maxWarningsInPublicChannel)
|
||||
if (!ephemeral && !isWhitelisted && count > maxWarningsInPublicChannel)
|
||||
result.Append($" (last {showCount} of {count}, full list in DMs)");
|
||||
result.AppendLine(":").Append(table);
|
||||
await channel.SendAutosplitMessageAsync(result).ConfigureAwait(false);
|
||||
var pages = AutosplitResponseHelper.AutosplitMessage(result.ToString());
|
||||
await interaction.EditOriginalResponseAsync(new(response.WithContent(pages[0]))).ConfigureAwait(false);
|
||||
foreach (var page in pages.Skip(1).Take(4))
|
||||
{
|
||||
var followupMsg = new DiscordFollowupMessageBuilder()
|
||||
.AsEphemeral(ephemeral)
|
||||
.WithContent(page);
|
||||
await interaction.CreateFollowupMessageAsync(followupMsg).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Config.Log.Warn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,7 +224,14 @@ internal static class ContentFilter
|
||||
{
|
||||
try
|
||||
{
|
||||
await Warnings.AddAsync(client, message, message.Author.Id, message.Author.Username, client.CurrentUser, warningReason ?? "Mention of piracy", message.Content.Sanitize()).ConfigureAwait(false);
|
||||
var (saved, suppress, recent, total) = await Warnings.AddAsync(
|
||||
message.Author!.Id,
|
||||
client.CurrentUser,
|
||||
warningReason ?? "Mention of piracy",
|
||||
message.Content.Sanitize()
|
||||
).ConfigureAwait(false);
|
||||
if (saved && !suppress && message.Channel is not null)
|
||||
await message.Channel.SendMessageAsync($"User warning saved, {message.Author.Mention} has {recent} recent warning{StringUtils.GetSuffix(recent)} ({total} total)").ConfigureAwait(false);
|
||||
completedActions.Add(FilterAction.IssueWarning);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@@ -120,7 +120,16 @@ internal static partial class DiscordInviteFilter
|
||||
|
||||
await message.Channel.SendMessageAsync(userMsg).ConfigureAwait(false);
|
||||
if (circumventionAttempt)
|
||||
await Warnings.AddAsync(client, message, message.Author.Id, message.Author.Username, client.CurrentUser, "Attempted to circumvent discord invite filter", codeResolveMsg);
|
||||
{
|
||||
var (saved, suppress, recent, total) = await Warnings.AddAsync(
|
||||
message.Author.Id,
|
||||
client.CurrentUser,
|
||||
"Attempted to circumvent discord invite filter",
|
||||
codeResolveMsg
|
||||
).ConfigureAwait(false);
|
||||
if (saved && !suppress)
|
||||
await message.Channel.SendMessageAsync($"User warning saved, {message.Author.Mention} has {recent} recent warning{StringUtils.GetSuffix(recent)} ({total} total)").ConfigureAwait(false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,17 @@ public static class LogParsingHandler
|
||||
{
|
||||
Config.Log.Error(e, "Failed to send piracy report");
|
||||
}
|
||||
if (!(message.Channel.IsPrivate || (message.Channel.Name?.Contains("spam") ?? true)))
|
||||
await Warnings.AddAsync(client, message, message.Author.Id, message.Author.Username, client.CurrentUser, "Pirated Release", $"{result.SelectedFilter.String} - {result.SelectedFilterContext?.Sanitize()}");
|
||||
if (!(message.Channel!.IsPrivate || message.Channel.Name.Contains("spam")))
|
||||
{
|
||||
var (saved, suppress, recent, total) = await Warnings.AddAsync(
|
||||
message.Author.Id,
|
||||
client.CurrentUser,
|
||||
"Pirated Release",
|
||||
$"{result.SelectedFilter.String} - {result.SelectedFilterContext?.Sanitize()}"
|
||||
);
|
||||
if (saved && !suppress)
|
||||
await message.Channel.SendMessageAsync($"User warning saved, {message.Author.Mention} has {recent} recent warning{StringUtils.GetSuffix(recent)} ({total} total)").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -53,9 +53,6 @@ public static partial class CommandContextExtensions
|
||||
public static Task<DiscordChannel> GetChannelForSpamAsync(this CommandContext ctx)
|
||||
=> ctx.Channel.IsSpamChannel() ? Task.FromResult(ctx.Channel) : ctx.CreateDmAsync();
|
||||
|
||||
public static Task<string> GetUserNameAsync(this CommandContext ctx, ulong userId, bool? forDmPurposes = null, string defaultName = "Unknown user")
|
||||
=> ctx.Client.GetUserNameAsync(ctx.Channel, userId, forDmPurposes, defaultName);
|
||||
|
||||
public static async Task<DiscordMessage?> GetMessageAsync(this CommandContext ctx, string messageLink)
|
||||
{
|
||||
if (MessageLinkPattern().Match(messageLink) is Match m
|
||||
|
||||
@@ -43,7 +43,10 @@ public static class DiscordClientExtensions
|
||||
public static Task<DiscordMember?> GetMemberAsync(this DiscordClient client, DiscordGuild? guild, ulong userId)
|
||||
=> guild is null ? GetMemberAsync(client, userId) : GetMemberAsync(client, guild.Id, userId);
|
||||
|
||||
public static async Task<string> GetUserNameAsync(this DiscordClient client, DiscordChannel channel, ulong userId, bool? forDmPurposes = null, string defaultName = "Unknown user")
|
||||
public static ValueTask<string> GetUserNameAsync(this CommandContext ctx, ulong userId, bool? forDmPurposes = null, string defaultName = "Unknown user")
|
||||
=> GetUserNameAsync(ctx.Client, ctx.Channel, userId, forDmPurposes, defaultName);
|
||||
|
||||
public static async ValueTask<string> GetUserNameAsync(this DiscordClient client, DiscordChannel channel, ulong userId, bool? forDmPurposes = null, string defaultName = "Unknown user")
|
||||
{
|
||||
var isPrivate = forDmPurposes ?? channel.IsPrivate;
|
||||
if (userId == 0)
|
||||
|
||||
@@ -6,46 +6,46 @@ internal static class RolesExtensions
|
||||
{
|
||||
public static async ValueTask<bool> IsModeratorAsync(this DiscordUser? user, DiscordClient client, DiscordGuild? guild = null)
|
||||
{
|
||||
if (user == null)
|
||||
if (user is null)
|
||||
return false;
|
||||
|
||||
if (ModProvider.IsSudoer(user.Id))
|
||||
return true;
|
||||
|
||||
var member = await (guild == null ? client.GetMemberAsync(user) : client.GetMemberAsync(guild, user)).ConfigureAwait(false);
|
||||
var member = await (guild is null ? client.GetMemberAsync(user) : client.GetMemberAsync(guild, user)).ConfigureAwait(false);
|
||||
return member?.Roles.IsModerator() is true;
|
||||
}
|
||||
|
||||
public static async ValueTask<bool> IsWhitelistedAsync(this DiscordUser? user, DiscordClient client, DiscordGuild? guild = null)
|
||||
{
|
||||
if (user == null)
|
||||
if (user is null)
|
||||
return false;
|
||||
|
||||
if (ModProvider.IsMod(user.Id))
|
||||
return true;
|
||||
|
||||
var member = await (guild == null ? client.GetMemberAsync(user) : client.GetMemberAsync(guild, user)).ConfigureAwait(false);
|
||||
var member = await (guild is null ? client.GetMemberAsync(user) : client.GetMemberAsync(guild, user)).ConfigureAwait(false);
|
||||
return member?.Roles.IsWhitelisted() is true;
|
||||
}
|
||||
|
||||
public static async ValueTask<bool> IsSmartlistedAsync(this DiscordUser? user, DiscordClient client, DiscordGuild? guild = null)
|
||||
{
|
||||
if (user == null)
|
||||
if (user is null)
|
||||
return false;
|
||||
|
||||
if (ModProvider.IsMod(user.Id))
|
||||
return true;
|
||||
|
||||
var member = await (guild == null ? client.GetMemberAsync(user) : client.GetMemberAsync(guild, user)).ConfigureAwait(false);
|
||||
var member = await (guild is null ? client.GetMemberAsync(user) : client.GetMemberAsync(guild, user)).ConfigureAwait(false);
|
||||
return member?.Roles.IsSmartlisted() is true;
|
||||
}
|
||||
|
||||
public static async ValueTask<bool> IsSupporterAsync(this DiscordUser? user, DiscordClient client, DiscordGuild? guild = null)
|
||||
{
|
||||
if (user == null)
|
||||
if (user is null)
|
||||
return false;
|
||||
|
||||
var member = await (guild == null ? client.GetMemberAsync(user) : client.GetMemberAsync(guild, user)).ConfigureAwait(false);
|
||||
var member = await (guild is null ? client.GetMemberAsync(user) : client.GetMemberAsync(guild, user)).ConfigureAwait(false);
|
||||
return member?.Roles.IsSupporter() is true;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public class AttributeUsageAnalyzer : DiagnosticAnalyzer
|
||||
isEnabledByDefault: true,
|
||||
description: "Description must be less than or equal to 100 characters."
|
||||
);
|
||||
private static readonly DiagnosticDescriptor CommandWithEmojiVariationSelector = new(
|
||||
private static readonly DiagnosticDescriptor CommandWithEmojiVariationSelectorRule = new(
|
||||
"DSP0003",
|
||||
"Emoji with variation selector",
|
||||
"Command name has an emoji character with VS{0} ({1}), which may not work as a command name",
|
||||
@@ -41,11 +41,21 @@ public class AttributeUsageAnalyzer : DiagnosticAnalyzer
|
||||
isEnabledByDefault: true,
|
||||
description: "Commands should avoid using variation selectors for emoji characters in command names."
|
||||
);
|
||||
private static readonly DiagnosticDescriptor CommandNameLengthRule = new(
|
||||
"DSP0004",
|
||||
"Command name length is too long",
|
||||
"Command name is {0} characters long, which is {1} characters longer than allowed",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "Command name must be between 1 and 32 characters long."
|
||||
);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [
|
||||
AccessCheckAttributeOnGroupCommandRule,
|
||||
DescriptionLengthRule,
|
||||
CommandWithEmojiVariationSelector,
|
||||
CommandWithEmojiVariationSelectorRule,
|
||||
CommandNameLengthRule,
|
||||
];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
@@ -197,19 +207,31 @@ public class AttributeUsageAnalyzer : DiagnosticAnalyzer
|
||||
if (actualName is not {Length: >0})
|
||||
return;
|
||||
|
||||
if (actualName.Length > 32)
|
||||
{
|
||||
context.ReportDiagnostic(
|
||||
Diagnostic.Create(CommandNameLengthRule,
|
||||
// The highlighted area in the analyzed source code. Keep it as specific as possible.
|
||||
attributeSyntax.GetLocation(),
|
||||
// The value is passed to the 'MessageFormat' argument of your rule.
|
||||
actualName.Length, actualName.Length - 32
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
var vs = actualName.ToCharArray().FirstOrDefault(VariationSelectors.Contains);
|
||||
if (vs is default(char))
|
||||
return;
|
||||
|
||||
var diagnostic = Diagnostic.Create(CommandWithEmojiVariationSelector,
|
||||
// The highlighted area in the analyzed source code. Keep it as specific as possible.
|
||||
attributeSyntax.GetLocation(),
|
||||
// The value is passed to the 'MessageFormat' argument of your rule.
|
||||
vs - 0xFE00 + 1, $"0x{(int)vs:X4}"
|
||||
);
|
||||
|
||||
// Reporting a diagnostic is the primary outcome of analyzers.
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
context.ReportDiagnostic(
|
||||
Diagnostic.Create(CommandWithEmojiVariationSelectorRule,
|
||||
// The highlighted area in the analyzed source code. Keep it as specific as possible.
|
||||
attributeSyntax.GetLocation(),
|
||||
// The value is passed to the 'MessageFormat' argument of your rule.
|
||||
vs - 0xFE00 + 1, $"0x{(int)vs:X4}"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static bool IsDescendantOfAttribute(AttributeData attributeData, string baseAttributeClassNameWithNamespace)
|
||||
|
||||
Reference in New Issue
Block a user