make warnings permanent, log retractions, but leave the records intact

This commit is contained in:
13xforever 2019-03-07 23:37:14 +05:00
parent a1f3bb3736
commit 981bc40712
7 changed files with 458 additions and 33 deletions

View File

@ -9,6 +9,7 @@ using CompatBot.Database.Providers;
using CompatBot.Utils;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.CommandsNext.Converters;
using DSharpPlus.Entities;
namespace CompatBot.Commands
@ -45,25 +46,27 @@ namespace CompatBot.Commands
[Description("List users with warnings, sorted from most warned to least")]
public async Task Users(CommandContext ctx, [Description("Optional number of items to show. Default is 10")] int number = 10)
{
var isMod = ctx.User.IsWhitelisted(ctx.Client, ctx.Guild);
if (number < 1)
number = 10;
var table = new AsciiTable(
new AsciiColumn("Username", maxWidth: 24),
new AsciiColumn("User ID", disabled: !ctx.Channel.IsPrivate, alignToRight: true),
new AsciiColumn("Count", alignToRight: true)
new AsciiColumn("Count", alignToRight: true),
new AsciiColumn("Including removed", disabled: !ctx.Channel.IsPrivate || !isMod, alignToRight: true)
);
using (var db = new BotDb())
{
var query = from warn in db.Warning
group warn by warn.DiscordId
into userGroup
let row = new {discordId = userGroup.Key, count = userGroup.Count()}
let row = new {discordId = userGroup.Key, count = userGroup.Count(w => !w.Retracted), total = userGroup.Count()}
orderby row.count descending
select row;
foreach (var row in query.Take(number))
{
var username = await ctx.GetUserNameAsync(row.discordId).ConfigureAwait(false);
table.Add(username, row.discordId.ToString(), row.count.ToString());
table.Add(username, row.discordId.ToString(), row.count.ToString(), row.total.ToString());
}
}
await ctx.SendAutosplitMessageAsync(new StringBuilder("Warning count per user:").Append(table)).ConfigureAwait(false);
@ -101,19 +104,24 @@ namespace CompatBot.Commands
}
[Command("by"), RequiresBotModRole]
public Task By(CommandContext ctx, DiscordUser moderator, [Description("Optional number of items to show. Default is 10")] int number = 10)
=> By(ctx, moderator.Id, number);
[Command("by"), RequiresBotModRole]
public Task By(CommandContext ctx, string me, [Description("Optional number of items to show. Default is 10")] int number = 10)
[Command("by"), Priority(1), RequiresBotModRole]
public async Task By(CommandContext ctx, string me, [Description("Optional number of items to show. Default is 10")] int number = 10)
{
if (me?.ToLowerInvariant() == "me")
return By(ctx, ctx.User.Id, number);
{
await By(ctx, ctx.User.Id, number).ConfigureAwait(false);
return;
}
return Task.CompletedTask;
var user = await new DiscordUserConverter().ConvertAsync(me, ctx).ConfigureAwait(false);
if (user.HasValue)
await By(ctx, user.Value, number).ConfigureAwait(false);
}
[Command("by"), Priority(10), RequiresBotModRole]
public Task By(CommandContext ctx, DiscordUser moderator, [Description("Optional number of items to show. Default is 10")] int number = 10)
=> By(ctx, moderator.Id, number);
[Command("recent"), Aliases("last", "all"), RequiresBotModRole]
[Description("Shows last issued warnings in chronological order")]
public async Task Last(CommandContext ctx, [Description("Optional number of items to show. Default is 10")] int number = 10)

View File

@ -10,6 +10,7 @@ using DSharpPlus;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.Entities;
using DSharpPlus.Interactivity;
using Microsoft.EntityFrameworkCore;
namespace CompatBot.Commands
@ -48,11 +49,26 @@ namespace CompatBot.Commands
[Description("Removes specified warnings")]
public async Task Remove(CommandContext ctx, [Description("Warning IDs to remove separated with space")] params int[] ids)
{
var interact = ctx.Client.GetInteractivity();
var msg = await ctx.RespondAsync("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?.Message?.Content))
{
await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Can't remove warnings without a reason").ConfigureAwait(false);
return;
}
int removedCount;
using (var db = new BotDb())
{
var warningsToRemove = await db.Warning.Where(w => ids.Contains(w.Id)).ToListAsync().ConfigureAwait(false);
db.Warning.RemoveRange(warningsToRemove);
foreach (var w in warningsToRemove)
{
w.Retracted = true;
w.RetractedBy = ctx.User.Id;
w.RetractionReason = response.Message.Content;
w.RetractionTimestamp = DateTime.UtcNow.Ticks;
}
removedCount = await db.SaveChangesAsync().ConfigureAwait(false);
}
if (removedCount == ids.Length)
@ -71,14 +87,28 @@ namespace CompatBot.Commands
[Command("clear"), RequiresBotModRole]
public async Task Clear(CommandContext ctx, [Description("User ID to clear warnings for")] ulong userId)
{
var interact = ctx.Client.GetInteractivity();
var msg = await ctx.RespondAsync("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?.Message?.Content))
{
await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Can't remove warnings without a reason").ConfigureAwait(false);
return;
}
try
{
//var removed = await BotDb.Instance.Database.ExecuteSqlCommandAsync($"DELETE FROM `warning` WHERE `discord_id`={userId}").ConfigureAwait(false);
int removed;
using (var db = new BotDb())
{
var warningsToRemove = await db.Warning.Where(w => w.DiscordId == userId).ToListAsync().ConfigureAwait(false);
db.Warning.RemoveRange(warningsToRemove);
foreach (var w in warningsToRemove)
{
w.Retracted = true;
w.RetractedBy = ctx.User.Id;
w.RetractionReason = response.Message.Content;
w.RetractionTimestamp = DateTime.UtcNow.Ticks;
}
removed = await db.SaveChangesAsync().ConfigureAwait(false);
}
await ctx.RespondAsync($"{removed} warning{StringUtils.GetSuffix(removed)} successfully removed!").ConfigureAwait(false);
@ -137,13 +167,27 @@ namespace CompatBot.Commands
{
try
{
var isWhitelisted = client.GetMember(message.Author)?.IsWhitelisted() ?? false;
if (message.Author.Id != userId && !isWhitelisted)
{
Config.Log.Error($"Somehow {message.Author.Username} ({message.Author.Id}) triggered warning list for {userId}");
return;
}
var channel = message.Channel;
int count;
var isPrivate = channel.IsPrivate;
int count, removed;
using (var db = new BotDb())
count = await db.Warning.CountAsync(w => w.DiscordId == userId).ConfigureAwait(false);
{
count = await db.Warning.CountAsync(w => w.DiscordId == userId && !w.Retracted).ConfigureAwait(false);
removed = await db.Warning.CountAsync(w => w.DiscordId == userId && w.Retracted).ConfigureAwait(false);
}
if (count == 0)
{
await message.RespondAsync(userName + " has no warnings, is a standup citizen, and a pillar of this community").ConfigureAwait(false);
if (removed == 0)
await message.RespondAsync(userName + " has no warnings, is a standup citizen, and a pillar of this community").ConfigureAwait(false);
else
await message.RespondAsync(userName + " has no warnings" + (isPrivate ? $" ({removed} retracted warning{(removed == 1 ? "" : "s")})" : "")).ConfigureAwait(false);
return;
}
@ -151,15 +195,13 @@ namespace CompatBot.Commands
return;
const int maxWarningsInPublicChannel = 3;
var isPrivate = channel.IsPrivate;
var isWhitelisted = client.GetMember(message.Author)?.IsWhitelisted() ?? false;
using (var db = new BotDb())
{
var totalWarningCount = db.Warning.Count(w => w.DiscordId == userId);
var showCount = Math.Min(maxWarningsInPublicChannel, totalWarningCount);
var showCount = Math.Min(maxWarningsInPublicChannel, count);
var table = new AsciiTable(
new AsciiColumn("ID", alignToRight: true),
new AsciiColumn("Issued by", maxWidth: 15),
new AsciiColumn("±", disabled: !isPrivate || !isWhitelisted),
new AsciiColumn("By", maxWidth: 15),
new AsciiColumn("On date (UTC)"),
new AsciiColumn("Reason"),
new AsciiColumn("Context", disabled: !isPrivate)
@ -169,17 +211,41 @@ namespace CompatBot.Commands
query = query.Take(maxWarningsInPublicChannel);
foreach (var warning in await query.ToListAsync().ConfigureAwait(false))
{
var issuerName = warning.IssuerId == 0
? ""
: await client.GetUserNameAsync(channel, warning.IssuerId, isPrivate, "unknown mod").ConfigureAwait(false);
var timestamp = warning.Timestamp.HasValue
? new DateTime(warning.Timestamp.Value, DateTimeKind.Utc).ToString("u")
: null;
table.Add(warning.Id.ToString(), issuerName, timestamp, warning.Reason, warning.FullReason);
if (warning.Retracted)
{
if (isWhitelisted && isPrivate)
{
var retractedByName = !warning.RetractedBy.HasValue
? ""
: await client.GetUserNameAsync(channel, warning.RetractedBy.Value, isPrivate, "unknown mod").ConfigureAwait(false);
var retractionTimestamp = warning.RetractionTimestamp.HasValue
? new DateTime(warning.RetractionTimestamp.Value, DateTimeKind.Utc).ToString("u")
: null;
table.Add(warning.Id.ToString(), "-", retractedByName, retractionTimestamp, warning.RetractionReason, "");
var issuerName = warning.IssuerId == 0
? ""
: await client.GetUserNameAsync(channel, warning.IssuerId, isPrivate, "unknown mod").ConfigureAwait(false);
var timestamp = warning.Timestamp.HasValue
? new DateTime(warning.Timestamp.Value, DateTimeKind.Utc).ToString("u")
: null;
table.Add(warning.Id.ToString().StrikeThrough(), "+", issuerName.StrikeThrough(), timestamp.StrikeThrough(), warning.Reason.StrikeThrough(), warning.FullReason.StrikeThrough());
}
}
else
{
var issuerName = warning.IssuerId == 0
? ""
: await client.GetUserNameAsync(channel, warning.IssuerId, isPrivate, "unknown mod").ConfigureAwait(false);
var timestamp = warning.Timestamp.HasValue
? new DateTime(warning.Timestamp.Value, DateTimeKind.Utc).ToString("u")
: null;
table.Add(warning.Id.ToString(), "+", issuerName, timestamp, warning.Reason, warning.FullReason);
}
}
var result = new StringBuilder("Warning list for ").Append(userName);
if (!isPrivate && !isWhitelisted && totalWarningCount > maxWarningsInPublicChannel)
result.Append($" (last {showCount} of {totalWarningCount}, full list in DMs)");
if (!isPrivate && !isWhitelisted && count > maxWarningsInPublicChannel)
result.Append($" (last {showCount} of {count}, full list in DMs)");
result.AppendLine(":").Append(table);
await channel.SendAutosplitMessageAsync(result).ConfigureAwait(false);
}

View File

@ -76,6 +76,10 @@ namespace CompatBot.Database
[Required]
public string FullReason { get; set; }
public long? Timestamp { get; set; }
public bool Retracted { get; set; }
public ulong? RetractedBy { get; set; }
public string RetractionReason { get; set; }
public long? RetractionTimestamp { get; set; }
}
internal class Explanation

View File

@ -0,0 +1,268 @@
// <auto-generated />
using System;
using CompatBot.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace CompatBot.Database.Migrations
{
[DbContext(typeof(BotDb))]
[Migration("20190307173026_PermanentWarnings")]
partial class PermanentWarnings
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.2-servicing-10034");
modelBuilder.Entity("CompatBot.Database.BotState", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<string>("Key")
.HasColumnName("key");
b.Property<string>("Value")
.HasColumnName("value");
b.HasKey("Id")
.HasName("id");
b.HasIndex("Key")
.IsUnique()
.HasName("bot_state_key");
b.ToTable("bot_state");
});
modelBuilder.Entity("CompatBot.Database.DisabledCommand", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<string>("Command")
.IsRequired()
.HasColumnName("command");
b.HasKey("Id")
.HasName("id");
b.HasIndex("Command")
.IsUnique()
.HasName("disabled_command_command");
b.ToTable("disabled_commands");
});
modelBuilder.Entity("CompatBot.Database.EventSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<long>("End")
.HasColumnName("end");
b.Property<string>("EventName")
.HasColumnName("event_name");
b.Property<string>("Name")
.HasColumnName("name");
b.Property<long>("Start")
.HasColumnName("start");
b.Property<int>("Year")
.HasColumnName("year");
b.HasKey("Id")
.HasName("id");
b.HasIndex("Year", "EventName")
.HasName("event_schedule_year_event_name");
b.ToTable("event_schedule");
});
modelBuilder.Entity("CompatBot.Database.Explanation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<byte[]>("Attachment")
.HasColumnName("attachment")
.HasMaxLength(7340032);
b.Property<string>("AttachmentFilename")
.HasColumnName("attachment_filename");
b.Property<string>("Keyword")
.IsRequired()
.HasColumnName("keyword");
b.Property<string>("Text")
.IsRequired()
.HasColumnName("text");
b.HasKey("Id")
.HasName("id");
b.HasIndex("Keyword")
.IsUnique()
.HasName("explanation_keyword");
b.ToTable("explanation");
});
modelBuilder.Entity("CompatBot.Database.Moderator", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<ulong>("DiscordId")
.HasColumnName("discord_id");
b.Property<bool>("Sudoer")
.HasColumnName("sudoer");
b.HasKey("Id")
.HasName("id");
b.HasIndex("DiscordId")
.IsUnique()
.HasName("moderator_discord_id");
b.ToTable("moderator");
});
modelBuilder.Entity("CompatBot.Database.Piracystring", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<string>("String")
.IsRequired()
.HasColumnName("string")
.HasColumnType("varchar(255)");
b.HasKey("Id")
.HasName("id");
b.HasIndex("String")
.IsUnique()
.HasName("piracystring_string");
b.ToTable("piracystring");
});
modelBuilder.Entity("CompatBot.Database.Stats", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<string>("Category")
.IsRequired()
.HasColumnName("category");
b.Property<long>("ExpirationTimestamp")
.HasColumnName("expiration_timestamp");
b.Property<string>("Key")
.IsRequired()
.HasColumnName("key");
b.Property<int>("Value")
.HasColumnName("value");
b.HasKey("Id")
.HasName("id");
b.HasIndex("Category", "Key")
.IsUnique()
.HasName("stats_category_key");
b.ToTable("stats");
});
modelBuilder.Entity("CompatBot.Database.Warning", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<ulong>("DiscordId")
.HasColumnName("discord_id");
b.Property<string>("FullReason")
.IsRequired()
.HasColumnName("full_reason");
b.Property<ulong>("IssuerId")
.HasColumnName("issuer_id");
b.Property<string>("Reason")
.IsRequired()
.HasColumnName("reason");
b.Property<bool>("Retracted")
.HasColumnName("retracted");
b.Property<ulong?>("RetractedBy")
.HasColumnName("retracted_by");
b.Property<string>("RetractionReason")
.HasColumnName("retraction_reason");
b.Property<long?>("RetractionTimestamp")
.HasColumnName("retraction_timestamp");
b.Property<long?>("Timestamp")
.HasColumnName("timestamp");
b.HasKey("Id")
.HasName("id");
b.HasIndex("DiscordId")
.HasName("warning_discord_id");
b.ToTable("warning");
});
modelBuilder.Entity("CompatBot.Database.WhitelistedInvite", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<ulong>("GuildId")
.HasColumnName("guild_id");
b.Property<string>("InviteCode")
.HasColumnName("invite_code");
b.Property<string>("Name")
.HasColumnName("name");
b.HasKey("Id")
.HasName("id");
b.HasIndex("GuildId")
.IsUnique()
.HasName("whitelisted_invite_guild_id");
b.ToTable("whitelisted_invites");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace CompatBot.Database.Migrations
{
public partial class PermanentWarnings : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "retracted",
table: "warning",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<ulong>(
name: "retracted_by",
table: "warning",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "retraction_reason",
table: "warning",
nullable: true);
migrationBuilder.AddColumn<long>(
name: "retraction_timestamp",
table: "warning",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "retracted",
table: "warning");
migrationBuilder.DropColumn(
name: "retracted_by",
table: "warning");
migrationBuilder.DropColumn(
name: "retraction_reason",
table: "warning");
migrationBuilder.DropColumn(
name: "retraction_timestamp",
table: "warning");
}
}
}

View File

@ -212,6 +212,18 @@ namespace CompatBot.Database.Migrations
.IsRequired()
.HasColumnName("reason");
b.Property<bool>("Retracted")
.HasColumnName("retracted");
b.Property<ulong?>("RetractedBy")
.HasColumnName("retracted_by");
b.Property<string>("RetractionReason")
.HasColumnName("retraction_reason");
b.Property<long?>("RetractionTimestamp")
.HasColumnName("retraction_timestamp");
b.Property<long?>("Timestamp")
.HasColumnName("timestamp");

View File

@ -18,6 +18,7 @@ namespace CompatBot.Utils
?? Encoding.ASCII;
private static readonly Encoding Utf8 = new UTF8Encoding(false);
private static readonly MemoryCache FuzzyPairCache = new MemoryCache(new MemoryCacheOptions {ExpirationScanFrequency = TimeSpan.FromMinutes(10)});
private const char StrikeThroughChar = '\u0336'; // 0x0335 = short dash, 0x0336 = long dash, 0x0337 = short slash, 0x0338 = long slash
private static readonly HashSet<char> SpaceCharacters = new HashSet<char>
{
@ -118,7 +119,7 @@ namespace CompatBot.Utils
while (e.MoveNext())
{
var strEl = e.GetTextElement();
if (char.IsControl(strEl[0]) || char.GetUnicodeCategory(strEl[0]) == UnicodeCategory.Format)
if (char.IsControl(strEl[0]) || char.GetUnicodeCategory(strEl[0]) == UnicodeCategory.Format || strEl[0] == StrikeThroughChar)
continue;
c++;
@ -144,7 +145,7 @@ namespace CompatBot.Utils
{
var strEl = e.GetTextElement();
result.Append(strEl);
if (char.IsControl(strEl[0]) || char.GetUnicodeCategory(strEl[0]) == UnicodeCategory.Format)
if (char.IsControl(strEl[0]) || char.GetUnicodeCategory(strEl[0]) == UnicodeCategory.Format || strEl[0] == StrikeThroughChar)
continue;
c++;
@ -170,6 +171,22 @@ namespace CompatBot.Utils
return s.PadRight(totalWidth, padding);
}
public static string StrikeThrough(this string str)
{
if (string.IsNullOrEmpty(str))
return str;
var result = new StringBuilder(str.Length*2);
result.Append(StrikeThroughChar);
foreach (var c in str)
{
result.Append(c);
if (char.IsLetterOrDigit(c) || char.IsLowSurrogate(c))
result.Append(StrikeThroughChar);
}
return result.ToString(0, result.Length-1);
}
public static string GetMoons(decimal? stars)
{
if (!stars.HasValue)