bot update, part 1

This commit is contained in:
13xforever
2020-11-12 00:11:36 +05:00
parent f7ce0c1938
commit d65444f0be
44 changed files with 899 additions and 925 deletions

View File

@@ -233,8 +233,11 @@ namespace PsnClient
}
}
public async Task<TitlePatch?> GetTitleUpdatesAsync(string productId, CancellationToken cancellationToken)
public async Task<TitlePatch?> GetTitleUpdatesAsync(string? productId, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(productId))
return null;
if (ResponseCache.TryGetValue(productId, out TitlePatch patchInfo))
return patchInfo;

View File

@@ -10,10 +10,10 @@ namespace CompatBot.Commands.Attributes
{
protected abstract Task<bool> IsAllowed(CommandContext ctx, bool help);
public DiscordEmoji ReactOnSuccess { get; }
public DiscordEmoji ReactOnFailure { get; }
public DiscordEmoji? ReactOnSuccess { get; }
public DiscordEmoji? ReactOnFailure { get; }
public CheckBaseAttributeWithReactions(DiscordEmoji reactOnSuccess = null, DiscordEmoji reactOnFailure = null)
public CheckBaseAttributeWithReactions(DiscordEmoji? reactOnSuccess = null, DiscordEmoji? reactOnFailure = null)
{
ReactOnSuccess = reactOnSuccess;
ReactOnFailure = reactOnFailure;

View File

@@ -80,7 +80,7 @@ namespace CompatBot.Commands
var timestamps = db.Warning
.Where(w => w.Timestamp.HasValue && !w.Retracted)
.OrderBy(w => w.Timestamp)
.Select(w => w.Timestamp.Value)
.Select(w => w.Timestamp!.Value)
.ToList();
var firstWarnTimestamp = timestamps.FirstOrDefault();
var previousTimestamp = firstWarnTimestamp;
@@ -122,7 +122,7 @@ namespace CompatBot.Commands
if (warnCount > mostWarnings)
{
mostWarnings = warnCount;
mostWarningsStart = last24hWarnings.Min(w => w.Timestamp.Value);
mostWarningsStart = last24hWarnings.Min(w => w.Timestamp!.Value);
mostWarningsEnd = utcNow.Ticks;
}
var lastWarn = timestamps.Any() ? timestamps.Last() : (long?)null;
@@ -150,7 +150,7 @@ namespace CompatBot.Commands
{
statsBuilder.Append($"Warnings in the last 24h: **{warnCount}**");
if (warnCount == 0)
statsBuilder.Append(" ").Append(BotReactionsHandler.RandomPositiveReaction);
statsBuilder.Append(' ').Append(BotReactionsHandler.RandomPositiveReaction);
statsBuilder.AppendLine();
}
if (lastWarn.HasValue)
@@ -173,15 +173,15 @@ namespace CompatBot.Commands
.ToList();
var totalCalls = sortedCommandStats.Sum(c => c.stat);
var top = sortedCommandStats.Take(5).ToList();
if (top.Any())
{
var statsBuilder = new StringBuilder();
var n = 1;
foreach (var cmdStat in top)
statsBuilder.AppendLine($"{n++}. {cmdStat.name} ({cmdStat.stat} call{(cmdStat.stat == 1 ? "" : "s")}, {cmdStat.stat * 100.0 / totalCalls:0.##}%)");
statsBuilder.AppendLine($"Total commands executed: {totalCalls}");
embed.AddField($"Top {top.Count} Recent Commands", statsBuilder.ToString().TrimEnd(), true);
}
if (top.Count == 0)
return;
var statsBuilder = new StringBuilder();
var n = 1;
foreach (var (name, stat) in top)
statsBuilder.AppendLine($"{n++}. {name} ({stat} call{(stat == 1 ? "" : "s")}, {stat * 100.0 / totalCalls:0.##}%)");
statsBuilder.AppendLine($"Total commands executed: {totalCalls}");
embed.AddField($"Top {top.Count} Recent Commands", statsBuilder.ToString().TrimEnd(), true);
}
private static void AppendExplainStats(DiscordEmbedBuilder embed)
@@ -194,15 +194,15 @@ namespace CompatBot.Commands
.ToList();
var totalExplains = sortedTerms.Sum(t => t.stat);
var top = sortedTerms.Take(5).ToList();
if (top.Any())
{
var statsBuilder = new StringBuilder();
var n = 1;
foreach (var explain in top)
statsBuilder.AppendLine($"{n++}. {explain.term} ({explain.stat} display{(explain.stat == 1 ? "" : "s")}, {explain.stat * 100.0 / totalExplains:0.##}%)");
statsBuilder.AppendLine($"Total explanations shown: {totalExplains}");
embed.AddField($"Top {top.Count} Recent Explanations", statsBuilder.ToString().TrimEnd(), true);
}
if (top.Count == 0)
return;
var statsBuilder = new StringBuilder();
var n = 1;
foreach (var (term, stat) in top)
statsBuilder.AppendLine($"{n++}. {term} ({stat} display{(stat == 1 ? "" : "s")}, {stat * 100.0 / totalExplains:0.##}%)");
statsBuilder.AppendLine($"Total explanations shown: {totalExplains}");
embed.AddField($"Top {top.Count} Recent Explanations", statsBuilder.ToString().TrimEnd(), true);
}
private static void AppendGameLookupStats(DiscordEmbedBuilder embed)
@@ -215,15 +215,15 @@ namespace CompatBot.Commands
.ToList();
var totalLookups = sortedTitles.Sum(t => t.stat);
var top = sortedTitles.Take(5).ToList();
if (top.Any())
{
var statsBuilder = new StringBuilder();
var n = 1;
foreach (var title in top)
statsBuilder.AppendLine($"{n++}. {title.title.Trim(40)} ({title.stat} search{(title.stat == 1 ? "" : "es")}, {title.stat * 100.0 / totalLookups:0.##}%)");
statsBuilder.AppendLine($"Total game lookups: {totalLookups}");
embed.AddField($"Top {top.Count} Recent Game Lookups", statsBuilder.ToString().TrimEnd(), true);
}
if (top.Count == 0)
return;
var statsBuilder = new StringBuilder();
var n = 1;
foreach (var (title, stat) in top)
statsBuilder.AppendLine($"{n++}. {title.Trim(40)} ({stat} search{(stat == 1 ? "" : "es")}, {stat * 100.0 / totalLookups:0.##}%)");
statsBuilder.AppendLine($"Total game lookups: {totalLookups}");
embed.AddField($"Top {top.Count} Recent Game Lookups", statsBuilder.ToString().TrimEnd(), true);
}
private void AppendSyscallsStats(DiscordEmbedBuilder embed)
@@ -259,7 +259,12 @@ namespace CompatBot.Commands
var diff = kots > doggos ? (double)kots / doggos - 1.0 : (double)doggos / kots - 1.0;
var sign = double.IsNaN(diff) || (double.IsFinite(diff) && !double.IsNegative(diff) && diff < 0.05) ? ":" : (kots > doggos ? ">" : "<");
var kot = sign == ">" ? GoodKot[new Random().Next(GoodKot.Length)] : sign == ":" ? "🐱" : MeanKot[new Random().Next(MeanKot.Length)];
var kot = sign switch
{
">" => GoodKot[new Random().Next(GoodKot.Length)],
":" => "🐱",
_ => MeanKot[new Random().Next(MeanKot.Length)]
};
embed.AddField("🐾 Stats", $"{kot} {kots - 1} {sign} {doggos - 1} 🐶", true);
}
catch (Exception e)

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -32,8 +33,9 @@ namespace CompatBot.Commands
[Command("disable"), Aliases("add")]
[Description("Disables the specified command")]
public async Task Disable(CommandContext ctx, [RemainingText, Description("Fully qualified command to disable, e.g. `explain add` or `sudo mod *`")] string command)
public async Task Disable(CommandContext ctx, [RemainingText, Description("Fully qualified command to disable, e.g. `explain add` or `sudo mod *`")] string? command)
{
command ??= "";
var isPrefix = command.EndsWith('*');
if (isPrefix)
command = command.TrimEnd('*', ' ');
@@ -94,7 +96,7 @@ namespace CompatBot.Commands
[Command("enable"), Aliases("reenable", "remove", "delete", "del", "clear")]
[Description("Enables the specified command")]
public async Task Enable(CommandContext ctx, [RemainingText, Description("Fully qualified command to enable, e.g. `explain add` or `sudo mod *`")] string command)
public async Task Enable(CommandContext ctx, [RemainingText, Description("Fully qualified command to enable, e.g. `explain add` or `sudo mod *`")] string? command)
{
if (command == "*")
{
@@ -103,6 +105,7 @@ namespace CompatBot.Commands
return;
}
command ??= "";
var isPrefix = command.EndsWith('*');
if (isPrefix)
command = command.TrimEnd('*', ' ');
@@ -148,13 +151,13 @@ namespace CompatBot.Commands
}
}
private static Command GetCommand(CommandContext ctx, string qualifiedName)
private static Command? GetCommand(CommandContext ctx, string qualifiedName)
{
if (string.IsNullOrEmpty(qualifiedName))
return null;
var groups = ctx.CommandsNext.RegisteredCommands.Values;
Command result = null;
var groups = (IReadOnlyList<Command>)ctx.CommandsNext.RegisteredCommands.Values.ToList();
Command? result = null;
foreach (var cmdPart in qualifiedName.Split(' ', StringSplitOptions.RemoveEmptyEntries))
{
if (groups.FirstOrDefault(g => g.Name == cmdPart || g.Aliases.Any(a => a == cmdPart)) is Command c)

View File

@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@@ -28,7 +29,6 @@ using DSharpPlus.Interactivity.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.TeamFoundation.Build.WebApi;
using Newtonsoft.Json;
namespace CompatBot.Commands
{
@@ -37,10 +37,10 @@ namespace CompatBot.Commands
private static readonly Client client = new Client();
private static readonly GithubClient.Client githubClient = new GithubClient.Client();
private static readonly SemaphoreSlim updateCheck = new SemaphoreSlim(1, 1);
private static string lastUpdateInfo = null, lastFullBuildNumber = null;
private static string? lastUpdateInfo = null, lastFullBuildNumber = null;
private const string Rpcs3UpdateStateKey = "Rpcs3UpdateState";
private const string Rpcs3UpdateBuildKey = "Rpcs3UpdateBuild";
private static UpdateInfo CachedUpdateInfo = null;
private static UpdateInfo? CachedUpdateInfo = null;
private static readonly Regex UpdateVersionRegex = new Regex(@"v(?<version>\d+\.\d+\.\d+)-(?<build>\d+)-(?<commit>[0-9a-f]+)\b", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture);
static CompatList()
@@ -68,7 +68,7 @@ namespace CompatBot.Commands
[Command("compat"), Aliases("c", "compatibility")]
[Description("Searches the compatibility database, USE: !compat search term")]
public async Task Compat(CommandContext ctx, [RemainingText, Description("Game title to look up")] string title)
public async Task Compat(CommandContext ctx, [RemainingText, Description("Game title to look up")] string? title)
{
title = title?.TrimEager().Truncate(40);
if (string.IsNullOrEmpty(title))
@@ -122,13 +122,13 @@ namespace CompatBot.Commands
scoreType = scoreType.ToLowerInvariant();
number = number.Clamp(1, 100);
bool exactStatus = status.EndsWith("only");
var exactStatus = status.EndsWith("only");
if (exactStatus)
status = status[..^4];
if (!Enum.TryParse(status, true, out CompatStatus s))
s = CompatStatus.Playable;
using var db = new ThumbnailDb();
await using var db = new ThumbnailDb();
var queryBase = db.Thumbnail.AsNoTracking();
if (exactStatus)
queryBase = queryBase.Where(g => g.CompatibilityStatus == s);
@@ -137,9 +137,9 @@ namespace CompatBot.Commands
queryBase = queryBase.Where(g => g.Metacritic != null).Include(t => t.Metacritic);
var query = scoreType switch
{
"critic" => queryBase.Where(t => t.Metacritic.CriticScore > 0).AsEnumerable().Select(t => (title: t.Metacritic.Title, score: t.Metacritic.CriticScore.Value, second: t.Metacritic.UserScore ?? t.Metacritic.CriticScore.Value)),
"user" => queryBase.Where(t => t.Metacritic.UserScore > 0).AsEnumerable().Select(t => (title: t.Metacritic.Title, score: t.Metacritic.UserScore.Value, second: t.Metacritic.CriticScore ?? t.Metacritic.UserScore.Value)),
_ => queryBase.AsEnumerable().Select(t => (title: t.Metacritic.Title, score: Math.Max(t.Metacritic.CriticScore ?? 0, t.Metacritic.UserScore ?? 0), second: (byte)0)),
"critic" => queryBase.Where(t => t.Metacritic!.CriticScore > 0).AsEnumerable().Select(t => (title: t.Metacritic!.Title, score: t.Metacritic!.CriticScore!.Value, second: t.Metacritic.UserScore ?? t.Metacritic.CriticScore.Value)),
"user" => queryBase.Where(t => t.Metacritic!.UserScore > 0).AsEnumerable().Select(t => (title: t.Metacritic!.Title, score: t.Metacritic!.UserScore!.Value, second: t.Metacritic.CriticScore ?? t.Metacritic.UserScore.Value)),
_ => queryBase.AsEnumerable().Select(t => (title: t.Metacritic!.Title, score: Math.Max(t.Metacritic.CriticScore ?? 0, t.Metacritic.UserScore ?? 0), second: (byte)0)),
};
var resultList = query.Where(i => i.score > 0)
.OrderByDescending(i => i.score)
@@ -161,7 +161,7 @@ namespace CompatBot.Commands
await ctx.SendAutosplitMessageAsync(result, blockStart: null, blockEnd: null).ConfigureAwait(false);
}
else
await ctx.RespondAsync("Failed to generate lilst").ConfigureAwait(false);
await ctx.RespondAsync("Failed to generate list").ConfigureAwait(false);
}
[Group("latest"), TriggersTyping]
@@ -200,7 +200,7 @@ namespace CompatBot.Commands
return CheckForRpcs3Updates(ctx.Client, null);
}
public static async Task<bool> CheckForRpcs3Updates(DiscordClient discordClient, DiscordChannel channel, string sinceCommit = null, DiscordMessage emptyBotMsg = null)
public static async Task<bool> CheckForRpcs3Updates(DiscordClient discordClient, DiscordChannel? channel, string? sinceCommit = null, DiscordMessage? emptyBotMsg = null)
{
var updateAnnouncement = channel is null;
var updateAnnouncementRestore = emptyBotMsg != null;
@@ -215,7 +215,7 @@ namespace CompatBot.Commands
{
if (updateAnnouncementRestore)
{
Config.Log.Debug($"Failed to get update info for commit {sinceCommit}: {JsonConvert.SerializeObject(info)}");
Config.Log.Debug($"Failed to get update info for commit {sinceCommit}: {JsonSerializer.Serialize(info)}");
return false;
}
@@ -232,7 +232,7 @@ namespace CompatBot.Commands
}
if (!updateAnnouncement)
{
await channel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
await channel!.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
return true;
}
if (updateAnnouncementRestore)
@@ -241,7 +241,7 @@ namespace CompatBot.Commands
return false;
Config.Log.Debug($"Restoring update announcement for build {sinceCommit}: {embed.Title}\n{embed.Description}");
await emptyBotMsg.ModifyAsync(embed: embed.Build()).ConfigureAwait(false);
await emptyBotMsg!.ModifyAsync(embed: embed.Build()).ConfigureAwait(false);
return true;
}
@@ -287,15 +287,15 @@ namespace CompatBot.Commands
await compatChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
lastUpdateInfo = latestUpdatePr;
lastFullBuildNumber = latestUpdateBuild;
using var db = new BotDb();
await using var db = new BotDb();
var currentState = await db.BotState.FirstOrDefaultAsync(k => k.Key == Rpcs3UpdateStateKey).ConfigureAwait(false);
if (currentState == null)
db.BotState.Add(new BotState {Key = Rpcs3UpdateStateKey, Value = latestUpdatePr});
await db.BotState.AddAsync(new BotState {Key = Rpcs3UpdateStateKey, Value = latestUpdatePr}).ConfigureAwait(false);
else
currentState.Value = latestUpdatePr;
var savedLastBuild = await db.BotState.FirstOrDefaultAsync(k => k.Key == Rpcs3UpdateBuildKey).ConfigureAwait(false);
if (savedLastBuild == null)
db.BotState.Add(new BotState {Key = Rpcs3UpdateBuildKey, Value = latestUpdateBuild});
await db.BotState.AddAsync(new BotState {Key = Rpcs3UpdateBuildKey, Value = latestUpdateBuild}).ConfigureAwait(false);
else
savedLastBuild.Value = latestUpdateBuild;
await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
@@ -313,7 +313,7 @@ namespace CompatBot.Commands
return false;
}
private static async Task CheckMissedBuildsBetween(DiscordClient discordClient, DiscordChannel compatChannel, string previousUpdatePr, string latestUpdatePr, CancellationToken cancellationToken)
private static async Task CheckMissedBuildsBetween(DiscordClient discordClient, DiscordChannel compatChannel, string? previousUpdatePr, string? latestUpdatePr, CancellationToken cancellationToken)
{
if (!int.TryParse(previousUpdatePr, out var oldestPr)
|| !int.TryParse(latestUpdatePr, out var newestPr))
@@ -322,16 +322,16 @@ namespace CompatBot.Commands
var mergedPrs = await githubClient.GetClosedPrsAsync(cancellationToken).ConfigureAwait(false); // this will cache 30 latest PRs
var newestPrCommit = await githubClient.GetPrInfoAsync(newestPr, cancellationToken).ConfigureAwait(false);
var oldestPrCommit = await githubClient.GetPrInfoAsync(oldestPr, cancellationToken).ConfigureAwait(false);
if (newestPrCommit.MergedAt == null || oldestPrCommit.MergedAt == null)
if (newestPrCommit?.MergedAt == null || oldestPrCommit?.MergedAt == null)
return;
mergedPrs = mergedPrs?.Where(pri => pri.MergedAt.HasValue)
.OrderBy(pri => pri.MergedAt.Value)
.OrderBy(pri => pri.MergedAt!.Value)
.SkipWhile(pri => pri.Number != oldestPr)
.Skip(1)
.TakeWhile(pri => pri.Number != newestPr)
.ToList();
if (mergedPrs is null || mergedPrs.Count == 0)
if (mergedPrs is null or {Count: 0})
return;
var failedBuilds = await Config.GetAzureDevOpsClient().GetMasterBuildsAsync(
@@ -353,7 +353,7 @@ namespace CompatBot.Commands
}
else if (updateInfo.ReturnCode == -1) // unknown build
{
var masterBuildInfo = failedBuilds.FirstOrDefault(b => b.Commit.Equals(mergedPr.MergeCommitSha, StringComparison.InvariantCultureIgnoreCase));
var masterBuildInfo = failedBuilds?.FirstOrDefault(b => b.Commit?.Equals(mergedPr.MergeCommitSha, StringComparison.InvariantCultureIgnoreCase) is true);
var buildTime = masterBuildInfo?.FinishTime;
if (masterBuildInfo != null)
{
@@ -406,14 +406,14 @@ namespace CompatBot.Commands
private async Task DoRequestAndRespond(CommandContext ctx, RequestBuilder requestBuilder)
{
Config.Log.Info(requestBuilder.Build());
CompatResult result = null;
CompatResult? result = null;
try
{
var remoteSearchTask = client.GetCompatResultAsync(requestBuilder, Config.Cts.Token);
var localResult = GetLocalCompatResult(requestBuilder);
result = localResult;
var remoteResult = await remoteSearchTask.ConfigureAwait(false);
result = remoteResult.Append(localResult);
result = remoteResult?.Append(localResult);
}
catch
{
@@ -428,9 +428,9 @@ namespace CompatBot.Commands
await Task.Delay(5_000).ConfigureAwait(false);
#endif
var channel = await ctx.GetChannelForSpamAsync().ConfigureAwait(false);
if (result.Results?.Count == 1)
if (result?.Results?.Count == 1)
await ProductCodeLookup.LookupAndPostProductCodeEmbedAsync(ctx.Client, ctx.Message, new List<string>(result.Results.Keys)).ConfigureAwait(false);
else
else if (result != null)
foreach (var msg in FormatSearchResults(ctx, result))
await channel.SendAutosplitMessageAsync(msg, blockStart: "", blockEnd: "").ConfigureAwait(false);
}
@@ -475,15 +475,15 @@ namespace CompatBot.Commands
{
var authorMention = ctx.Channel.IsPrivate ? "You" : ctx.Message.Author.Mention;
var result = new StringBuilder();
result.AppendLine($"{authorMention} searched for: ***{request.Search.Sanitize(replaceBackTicks: true)}***");
if (request.Search.Contains("persona", StringComparison.InvariantCultureIgnoreCase)
|| request.Search.Contains("p5", StringComparison.InvariantCultureIgnoreCase))
result.AppendLine($"{authorMention} searched for: ***{request.Search?.Sanitize(replaceBackTicks: true)}***");
if (request.Search?.Contains("persona", StringComparison.InvariantCultureIgnoreCase) is true
|| request.Search?.Contains("p5", StringComparison.InvariantCultureIgnoreCase) is true)
result.AppendLine("Did you try searching for **__Unnamed__** instead?");
else if (!ctx.Channel.IsPrivate
&& ctx.Message.Author.Id == 197163728867688448
&& (compatResult.Results.Values.Any(i =>
&& compatResult.Results.Values.Any(i =>
i.Title.Contains("afrika", StringComparison.InvariantCultureIgnoreCase)
|| i.Title.Contains("africa", StringComparison.InvariantCultureIgnoreCase)))
|| i.Title.Contains("africa", StringComparison.InvariantCultureIgnoreCase))
)
{
var sqvat = ctx.Client.GetEmoji(":sqvat:", Config.Reactions.No);
@@ -543,27 +543,34 @@ namespace CompatBot.Commands
public static async Task ImportCompatListAsync()
{
var list = await client.GetCompatListSnapshotAsync(Config.Cts.Token).ConfigureAwait(false);
using var db = new ThumbnailDb();
if (list is null)
return;
await using var db = new ThumbnailDb();
foreach (var kvp in list.Results)
{
var (productCode, info) = kvp;
var dbItem = await db.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productCode).ConfigureAwait(false);
if (dbItem == null)
if (dbItem is null
&& await client.GetCompatResultAsync(RequestBuilder.Start().SetSearch(productCode), Config.Cts.Token).ConfigureAwait(false) is {} compatItemSearchResult
&& compatItemSearchResult.Results.TryGetValue(productCode, out var compatItem))
{
var compatItemSearchResult = await client.GetCompatResultAsync(RequestBuilder.Start().SetSearch(productCode), Config.Cts.Token).ConfigureAwait(false);
if (compatItemSearchResult.Results.TryGetValue(productCode, out var compatItem))
dbItem = db.Thumbnail.Add(new Thumbnail
{
ProductCode = productCode,
Name = compatItem.Title,
}).Entity;
dbItem = (await db.Thumbnail.AddAsync(new Thumbnail
{
ProductCode = productCode,
Name = compatItem.Title,
}).ConfigureAwait(false)).Entity;
}
if (dbItem == null)
if (dbItem is null)
{
Config.Log.Debug($"Missing product code {productCode} in {nameof(ThumbnailDb)}");
dbItem = new();
}
if (Enum.TryParse(info.Status, out CompatStatus status))
{
dbItem.CompatibilityStatus = status;
if (info.Date is string d && DateTime.TryParseExact(d, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date))
if (info.Date is string d
&& DateTime.TryParseExact(d, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date))
dbItem.CompatibilityChangeDate = date.Ticks;
}
else
@@ -577,7 +584,7 @@ namespace CompatBot.Commands
var scoreJson = "metacritic_ps3.json";
string json;
if (File.Exists(scoreJson))
json = File.ReadAllText(scoreJson);
json = await File.ReadAllTextAsync(scoreJson).ConfigureAwait(false);
else
{
Config.Log.Warn($"Missing {scoreJson}, trying to get an online copy...");
@@ -593,7 +600,8 @@ namespace CompatBot.Commands
}
}
var scoreList = JsonConvert.DeserializeObject<List<Metacritic>>(json);
var scoreList = JsonSerializer.Deserialize<List<Metacritic>>(json) ?? new();
Config.Log.Debug($"Importing {scoreList.Count} Metacritic items");
var duplicates = new List<Metacritic>();
duplicates.AddRange(
@@ -602,7 +610,7 @@ namespace CompatBot.Commands
);
duplicates.AddRange(
scoreList.Where(i => i.Title.Contains("A Telltale Game"))
.Select(i => i.WithTitle(i.Title.Substring(0, i.Title.IndexOf("A Telltale Game") - 1).TrimEnd(' ', '-', ':')))
.Select(i => i.WithTitle(i.Title.Substring(0, i.Title.IndexOf("A Telltale Game", StringComparison.Ordinal) - 1).TrimEnd(' ', '-', ':')))
);
duplicates.AddRange(
scoreList.Where(i => i.Title.StartsWith("Ratchet & Clank Future"))
@@ -617,7 +625,7 @@ namespace CompatBot.Commands
.Select(i => i.WithTitle(i.Title.Replace("HAWX", "H.A.W.X")))
);
using var db = new ThumbnailDb();
await using var db = new ThumbnailDb();
foreach (var mcScore in scoreList.Where(s => s.CriticScore > 0 || s.UserScore > 0))
{
if (Config.Cts.IsCancellationRequested)
@@ -625,7 +633,7 @@ namespace CompatBot.Commands
var item = db.Metacritic.FirstOrDefault(i => i.Title == mcScore.Title);
if (item == null)
item = db.Metacritic.Add(mcScore).Entity;
item = (await db.Metacritic.AddAsync(mcScore).ConfigureAwait(false)).Entity;
else
{
item.CriticScore = mcScore.CriticScore;
@@ -656,11 +664,12 @@ namespace CompatBot.Commands
try
{
var searchResult = await client.GetCompatResultAsync(RequestBuilder.Start().SetSearch(title), Config.Cts.Token).ConfigureAwait(false);
var compatListMatches = searchResult.Results
var compatListMatches = searchResult?.Results
.Select(i => (productCode: i.Key, titleInfo: i.Value, coef: Math.Max(title.GetFuzzyCoefficientCached(i.Value.Title), title.GetFuzzyCoefficientCached(i.Value.AlternativeTitle))))
.Where(i => i.coef > 0.85)
.OrderByDescending(i => i.coef)
.ToList();
.ToList()
?? new();
if (compatListMatches.Any(i => i.coef > 0.99))
compatListMatches = compatListMatches.Where(i => i.coef > 0.99).ToList();
else if (compatListMatches.Any(i => i.coef > 0.95))
@@ -670,21 +679,21 @@ namespace CompatBot.Commands
foreach ((string productCode, TitleInfo titleInfo, double coef) in compatListMatches)
{
var dbItem = await db.Thumbnail.FirstOrDefaultAsync(i => i.ProductCode == productCode).ConfigureAwait(false);
if (dbItem == null)
dbItem = db.Thumbnail.Add(new Thumbnail
if (dbItem is null)
dbItem = (await db.Thumbnail.AddAsync(new Thumbnail
{
ProductCode = productCode,
Name = titleInfo.Title,
}).Entity;
if (dbItem != null)
{
dbItem.Name = titleInfo.Title;
if (Enum.TryParse(titleInfo.Status, out CompatStatus status))
dbItem.CompatibilityStatus = status;
if (DateTime.TryParseExact(titleInfo.Date, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date))
dbItem.CompatibilityChangeDate = date.Ticks;
matches.Add((dbItem, coef));
}
}).ConfigureAwait(false)).Entity;
if (dbItem is null)
continue;
dbItem.Name = titleInfo.Title;
if (Enum.TryParse(titleInfo.Status, out CompatStatus status))
dbItem.CompatibilityStatus = status;
if (DateTime.TryParseExact(titleInfo.Date, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date))
dbItem.CompatibilityChangeDate = date.Ticks;
matches.Add((dbItem, coef));
}
await db.SaveChangesAsync().ConfigureAwait(false);
}
@@ -693,7 +702,7 @@ namespace CompatBot.Commands
Config.Log.Warn(e);
}
}
matches = matches.Where(i => !Regex.IsMatch(i.thumb.Name, @"\b(demo|trial)\b", RegexOptions.IgnoreCase | RegexOptions.Singleline)).ToList();
matches = matches.Where(i => !Regex.IsMatch(i.thumb.Name ?? "", @"\b(demo|trial)\b", RegexOptions.IgnoreCase | RegexOptions.Singleline)).ToList();
//var bestMatch = matches.FirstOrDefault();
//Config.Log.Trace($"Best title match for [{item.Title}] is [{bestMatch.thumb.Name}] with score {bestMatch.coef:0.0000}");
if (matches.Count > 0)

View File

@@ -39,47 +39,45 @@ namespace CompatBot.Commands
new AsciiColumn("Actions"),
new AsciiColumn("Custom message")
);
using (var db = new BotDb())
await using var db = new BotDb();
var duplicates = new Dictionary<string, FilterContext>(StringComparer.InvariantCultureIgnoreCase);
var filters = db.Piracystring.Where(ps => !ps.Disabled).AsNoTracking().AsEnumerable().OrderBy(ps => ps.String.ToUpperInvariant()).ToList();
var nonUniqueTriggers = (
from f in filters
group f by f.String.ToUpperInvariant()
into g
where g.Count() > 1
select g.Key
).ToList();
foreach (var t in nonUniqueTriggers)
{
var duplicates = new Dictionary<string, FilterContext>(StringComparer.InvariantCultureIgnoreCase);
var filters = db.Piracystring.Where(ps => !ps.Disabled).AsNoTracking().AsEnumerable().OrderBy(ps => ps.String.ToUpperInvariant()).ToList();
var nonUniqueTriggers = (
from f in filters
group f by f.String.ToUpperInvariant()
into g
where g.Count() > 1
select g.Key
).ToList();
foreach (var t in nonUniqueTriggers)
var duplicateFilters = filters.Where(ps => ps.String.Equals(t, StringComparison.InvariantCultureIgnoreCase)).ToList();
foreach (FilterContext fctx in Enum.GetValues(typeof(FilterContext)))
{
var duplicateFilters = filters.Where(ps => ps.String.Equals(t, StringComparison.InvariantCultureIgnoreCase)).ToList();
foreach (FilterContext fctx in Enum.GetValues(typeof(FilterContext)))
if (duplicateFilters.Count(f => (f.Context & fctx) == fctx) > 1)
{
if (duplicateFilters.Count(f => (f.Context & fctx) == fctx) > 1)
{
if (duplicates.TryGetValue(t, out var fctxs))
duplicates[t] = fctxs | fctx;
else
duplicates[t] = fctx;
}
if (duplicates.TryGetValue(t, out var fctxDup))
duplicates[t] = fctxDup | fctx;
else
duplicates[t] = fctx;
}
}
foreach (var item in filters)
{
var ctxl = item.Context.ToString();
if (duplicates.Count > 0
&& duplicates.TryGetValue(item.String, out var fctx)
&& (item.Context & fctx) != 0)
ctxl = "❗ " + ctxl;
table.Add(
item.Id.ToString(),
item.String.Sanitize(),
item.ValidatingRegex,
ctxl,
item.Actions.ToFlagsString(),
string.IsNullOrEmpty(item.CustomMessage) ? "" : "✅"
);
}
}
foreach (var item in filters)
{
var ctxl = item.Context.ToString();
if (duplicates.Count > 0
&& duplicates.TryGetValue(item.String, out var fctx)
&& (item.Context & fctx) != 0)
ctxl = "❗ " + ctxl;
table.Add(
item.Id.ToString(),
item.String.Sanitize(),
item.ValidatingRegex ?? "",
ctxl,
item.Actions.ToFlagsString(),
string.IsNullOrEmpty(item.CustomMessage) ? "" : "✅"
);
}
await ctx.SendAutosplitMessageAsync(table.ToString()).ConfigureAwait(false);
await ctx.RespondAsync(FilterActionExtensions.GetLegend()).ConfigureAwait(false);
@@ -89,7 +87,7 @@ namespace CompatBot.Commands
[Description("Adds a new content filter")]
public async Task Add(CommandContext ctx, [RemainingText, Description("A plain string to match")] string trigger)
{
using var db = new BotDb();
await using var db = new BotDb();
Piracystring filter;
if (string.IsNullOrEmpty(trigger))
filter = new Piracystring();
@@ -116,7 +114,7 @@ namespace CompatBot.Commands
await db.SaveChangesAsync().ConfigureAwait(false);
await msg.UpdateOrCreateMessageAsync(ctx.Channel, embed: FormatFilter(filter).WithTitle("Created a new content filter #" + filter.Id)).ConfigureAwait(false);
var member = ctx.Member ?? ctx.Client.GetMember(ctx.User);
var reportMsg = $"{member.GetMentionWithNickname()} added a new content filter: `{filter.String.Sanitize()}`";
var reportMsg = $"{member?.GetMentionWithNickname()} added a new content filter: `{filter.String.Sanitize()}`";
if (!string.IsNullOrEmpty(filter.ValidatingRegex))
reportMsg += $"\nValidation: `{filter.ValidatingRegex}`";
await ctx.Client.ReportAsync("🆕 Content filter created", reportMsg, null, ReportSeverity.Low).ConfigureAwait(false);
@@ -130,9 +128,9 @@ namespace CompatBot.Commands
[Description("Modifies the specified content filter")]
public async Task Edit(CommandContext ctx, [Description("Filter ID")] int id)
{
using var db = new BotDb();
await using var db = new BotDb();
var filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.Id == id && !ps.Disabled).ConfigureAwait(false);
if (filter == null)
if (filter is null)
{
await ctx.RespondAsync("Specified filter does not exist").ConfigureAwait(false);
return;
@@ -144,9 +142,9 @@ namespace CompatBot.Commands
[Command("edit")]
public async Task Edit(CommandContext ctx, [Description("Trigger to edit"), RemainingText] string trigger)
{
using var db = new BotDb();
await using var db = new BotDb();
var filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.String.Equals(trigger, StringComparison.InvariantCultureIgnoreCase) && !ps.Disabled).ConfigureAwait(false);
if (filter == null)
if (filter is null)
{
await ctx.RespondAsync("Specified filter does not exist").ConfigureAwait(false);
return;
@@ -161,7 +159,7 @@ namespace CompatBot.Commands
{
int removedFilters;
var removedTriggers = new StringBuilder();
using (var db = new BotDb())
await using (var db = new BotDb())
{
foreach (var f in db.Piracystring.Where(ps => ids.Contains(ps.Id) && !ps.Disabled))
{
@@ -181,7 +179,7 @@ namespace CompatBot.Commands
var filterList = removedTriggers.ToString();
if (removedFilters == 1)
filterList = filterList.TrimStart();
await ctx.Client.ReportAsync($"📴 Content filter{s} removed", $"{member.GetMentionWithNickname()} removed {removedFilters} content filter{s}: {filterList}".Trim(EmbedPager.MaxDescriptionLength), null, ReportSeverity.Medium).ConfigureAwait(false);
await ctx.Client.ReportAsync($"📴 Content filter{s} removed", $"{member?.GetMentionWithNickname()} removed {removedFilters} content filter{s}: {filterList}".Trim(EmbedPager.MaxDescriptionLength), null, ReportSeverity.Medium).ConfigureAwait(false);
}
ContentFilter.RebuildMatcher();
}
@@ -195,10 +193,10 @@ namespace CompatBot.Commands
return;
}
using (var db = new BotDb())
await using (var db = new BotDb())
{
var f = await db.Piracystring.FirstOrDefaultAsync(ps => ps.String.Equals(trigger, StringComparison.InvariantCultureIgnoreCase) && !ps.Disabled).ConfigureAwait(false);
if (f == null)
if (f is null)
{
await ctx.ReactWithAsync(Config.Reactions.Failure, "Specified filter does not exist").ConfigureAwait(false);
return;
@@ -210,7 +208,7 @@ namespace CompatBot.Commands
await ctx.ReactWithAsync(Config.Reactions.Success, "Trigger was removed").ConfigureAwait(false);
var member = ctx.Member ?? ctx.Client.GetMember(ctx.User);
await ctx.Client.ReportAsync("📴 Content filter removed", $"{member.GetMentionWithNickname()} removed 1 content filter: `{trigger.Sanitize()}`", null, ReportSeverity.Medium).ConfigureAwait(false);
await ctx.Client.ReportAsync("📴 Content filter removed", $"{member?.GetMentionWithNickname()} removed 1 content filter: `{trigger.Sanitize()}`", null, ReportSeverity.Medium).ConfigureAwait(false);
ContentFilter.RebuildMatcher();
}
@@ -222,7 +220,7 @@ namespace CompatBot.Commands
await db.SaveChangesAsync().ConfigureAwait(false);
await msg.UpdateOrCreateMessageAsync(ctx.Channel, embed: FormatFilter(filter).WithTitle("Updated content filter")).ConfigureAwait(false);
var member = ctx.Member ?? ctx.Client.GetMember(ctx.User);
var reportMsg = $"{member.GetMentionWithNickname()} changed content filter #{filter.Id} (`{filter.Actions.ToFlagsString()}`): `{filter.String.Sanitize()}`";
var reportMsg = $"{member?.GetMentionWithNickname()} changed content filter #{filter.Id} (`{filter.Actions.ToFlagsString()}`): `{filter.String.Sanitize()}`";
if (!string.IsNullOrEmpty(filter.ValidatingRegex))
reportMsg += $"\nValidation: `{filter.ValidatingRegex}`";
await ctx.Client.ReportAsync("🆙 Content filter updated", reportMsg, null, ReportSeverity.Low).ConfigureAwait(false);
@@ -232,7 +230,19 @@ namespace CompatBot.Commands
await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Content filter update aborted").ConfigureAwait(false);
}
private async Task<(bool success, DiscordMessage message)> EditFilterPropertiesAsync(CommandContext ctx, BotDb db, Piracystring filter)
private static async Task<(bool success, DiscordMessage? message)> EditFilterPropertiesAsync(CommandContext ctx, BotDb db, Piracystring filter)
{
try
{
return await EditFilterPropertiesInternalAsync(ctx, db, filter).ConfigureAwait(false);
}
catch (Exception e)
{
Config.Log.Error(e, "Failed to edit content filter");
return (false, null);
}
}
private static async Task<(bool success, DiscordMessage? message)> EditFilterPropertiesInternalAsync(CommandContext ctx, BotDb db, Piracystring filter)
{
var interact = ctx.Client.GetInteractivity();
var abort = DiscordEmoji.FromUnicode("🛑");
@@ -251,10 +261,10 @@ namespace CompatBot.Commands
var letterE = DiscordEmoji.FromUnicode("🇪");
var letterU = DiscordEmoji.FromUnicode("🇺");
DiscordMessage msg = null;
string errorMsg = null;
DiscordMessage txt;
MessageReactionAddEventArgs emoji;
DiscordMessage? msg = null;
string? errorMsg = null;
DiscordMessage? txt;
MessageReactionAddEventArgs? emoji;
step1:
// step 1: define trigger string
@@ -612,7 +622,8 @@ namespace CompatBot.Commands
filter.ExplainTerm = null;
else
{
var existingTerm = await db.Explanation.FirstOrDefaultAsync(exp => exp.Keyword == txt.Content.ToLowerInvariant()).ConfigureAwait(false);
var term = txt.Content.ToLowerInvariant();
var existingTerm = await db.Explanation.FirstOrDefaultAsync(exp => exp.Keyword == term).ConfigureAwait(false);
if (existingTerm == null)
{
errorMsg = $"Term `{txt.Content.ToLowerInvariant().Sanitize()}` is not defined.";
@@ -694,7 +705,7 @@ namespace CompatBot.Commands
return (false, msg);
}
private static DiscordEmbedBuilder FormatFilter(Piracystring filter, string error = null, int highlight = -1)
private static DiscordEmbedBuilder FormatFilter(Piracystring filter, string? error = null, int highlight = -1)
{
var field = 1;
var result = new DiscordEmbedBuilder
@@ -709,14 +720,14 @@ namespace CompatBot.Commands
result.AddFieldEx(validTrigger + "Trigger", filter.String, highlight == field++, true)
.AddFieldEx("Context", filter.Context.ToString(), highlight == field++, true)
.AddFieldEx("Actions", filter.Actions.ToFlagsString(), highlight == field++, true)
.AddFieldEx("Validation", filter.ValidatingRegex, highlight == field++, true);
.AddFieldEx("Validation", filter.ValidatingRegex ?? "", highlight == field++, true);
if (filter.Actions.HasFlag(FilterAction.SendMessage))
result.AddFieldEx("Message", filter.CustomMessage, highlight == field, true);
result.AddFieldEx("Message", filter.CustomMessage ?? "", highlight == field, true);
field++;
if (filter.Actions.HasFlag(FilterAction.ShowExplain))
{
var validExplainTerm = string.IsNullOrEmpty(filter.ExplainTerm) ? "⚠ " : "";
result.AddFieldEx(validExplainTerm + "Explain", filter.ExplainTerm, highlight == field, true);
result.AddFieldEx(validExplainTerm + "Explain", filter.ExplainTerm ?? "", highlight == field, true);
}
#if DEBUG
result.WithFooter("Test bot instance");

View File

@@ -31,7 +31,6 @@ namespace CompatBot.Commands
[GroupCommand]
public async Task ShowExplanation(CommandContext ctx, [RemainingText, Description("Term to explain")] string term)
{
var sourceTerm = term;
if (string.IsNullOrEmpty(term))
{
var lastBotMessages = await ctx.Channel.GetMessagesBeforeCachedAsync(ctx.Message.Id, 10).ConfigureAwait(false);
@@ -55,8 +54,6 @@ namespace CompatBot.Commands
await ctx.ReactWithAsync(Config.Reactions.Failure).ConfigureAwait(false);
return;
}
sourceTerm = term = newMessage.Result.Content;
}
if (!await DiscordInviteFilter.CheckMessageForInvitesAsync(ctx.Client, ctx.Message).ConfigureAwait(false))
@@ -70,7 +67,7 @@ namespace CompatBot.Commands
if (result.explanation == null || !string.IsNullOrEmpty(result.fuzzyMatch))
{
term = term.StripQuotes();
var idx = term.LastIndexOf(" to ");
var idx = term.LastIndexOf(" to ", StringComparison.Ordinal);
if (idx > 0)
{
var potentialUserId = term[(idx + 4)..].Trim();
@@ -95,7 +92,7 @@ namespace CompatBot.Commands
if (await SendExplanation(result, term, ctx.Message).ConfigureAwait(false))
return;
string inSpecificLocation = null;
string? inSpecificLocation = null;
if (!LimitedToSpamChannel.IsSpamChannel(ctx.Channel))
{
var spamChannel = await ctx.Client.GetChannelAsync(Config.BotSpamId).ConfigureAwait(false);
@@ -114,8 +111,8 @@ namespace CompatBot.Commands
try
{
term = term.ToLowerInvariant().StripQuotes();
byte[] attachment = null;
string attachmentFilename = null;
byte[]? attachment = null;
string? attachmentFilename = null;
if (ctx.Message.Attachments.FirstOrDefault() is DiscordAttachment att)
{
attachmentFilename = att.FileName;
@@ -134,7 +131,7 @@ namespace CompatBot.Commands
await ctx.ReactWithAsync(Config.Reactions.Failure, "An explanation for the term must be provided").ConfigureAwait(false);
else
{
using var db = new BotDb();
await using var db = new BotDb();
if (await db.Explanation.AnyAsync(e => e.Keyword == term).ConfigureAwait(false))
await ctx.ReactWithAsync(Config.Reactions.Failure, $"`{term}` is already defined. Use `update` to update an existing term.").ConfigureAwait(false);
else
@@ -163,8 +160,8 @@ namespace CompatBot.Commands
[RemainingText, Description("New explanation text")] string explanation)
{
term = term.ToLowerInvariant().StripQuotes();
byte[] attachment = null;
string attachmentFilename = null;
byte[]? attachment = null;
string? attachmentFilename = null;
if (ctx.Message.Attachments.FirstOrDefault() is DiscordAttachment att)
{
attachmentFilename = att.FileName;
@@ -178,7 +175,7 @@ namespace CompatBot.Commands
Config.Log.Warn(e, "Failed to download explanation attachment " + ctx);
}
}
using var db = new BotDb();
await using var db = new BotDb();
var item = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
if (item == null)
await ctx.ReactWithAsync(Config.Reactions.Failure, $"Term `{term}` is not defined").ConfigureAwait(false);
@@ -203,7 +200,7 @@ namespace CompatBot.Commands
{
oldTerm = oldTerm.ToLowerInvariant().StripQuotes();
newTerm = newTerm.ToLowerInvariant().StripQuotes();
using var db = new BotDb();
await using var db = new BotDb();
var item = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == oldTerm).ConfigureAwait(false);
if (item == null)
await ctx.ReactWithAsync(Config.Reactions.Failure, $"Term `{oldTerm}` is not defined").ConfigureAwait(false);
@@ -235,7 +232,7 @@ namespace CompatBot.Commands
public async Task List(CommandContext ctx)
{
var responseChannel = await ctx.GetChannelForSpamAsync().ConfigureAwait(false);
using var db = new BotDb();
await using var db = new BotDb();
var keywords = await db.Explanation.Select(e => e.Keyword).OrderBy(t => t).ToListAsync().ConfigureAwait(false);
if (keywords.Count == 0)
await ctx.RespondAsync("Nothing has been defined yet").ConfigureAwait(false);
@@ -259,7 +256,7 @@ namespace CompatBot.Commands
public async Task RemoveExplanation(CommandContext ctx, [RemainingText, Description("Term to remove")] string term)
{
term = term.ToLowerInvariant().StripQuotes();
using var db = new BotDb();
await using var db = new BotDb();
var item = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
if (item == null)
await ctx.ReactWithAsync(Config.Reactions.Failure, $"Term `{term}` is not defined").ConfigureAwait(false);
@@ -276,7 +273,7 @@ namespace CompatBot.Commands
public async Task Attachment(CommandContext ctx, [RemainingText, Description("Term to remove")] string term)
{
term = term.ToLowerInvariant().StripQuotes();
using var db = new BotDb();
await using var db = new BotDb();
var item = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
if (item == null)
await ctx.ReactWithAsync(Config.Reactions.Failure, $"Term `{term}` is not defined").ConfigureAwait(false);
@@ -298,7 +295,7 @@ namespace CompatBot.Commands
public async Task Text(CommandContext ctx, [RemainingText, Description("Term to remove")] string term)
{
term = term.ToLowerInvariant().StripQuotes();
using var db = new BotDb();
await using var db = new BotDb();
var item = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
if (item == null)
await ctx.ReactWithAsync(Config.Reactions.Failure, $"Term `{term}` is not defined").ConfigureAwait(false);
@@ -337,7 +334,7 @@ namespace CompatBot.Commands
return;
}
using var db = new BotDb();
await using var db = new BotDb();
var item = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == termOrLink).ConfigureAwait(false);
if (item == null)
{
@@ -348,21 +345,21 @@ namespace CompatBot.Commands
{
if (!string.IsNullOrEmpty(item.Text))
{
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(item.Text));
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(item.Text));
await ctx.Channel.SendFileAsync($"{termOrLink}.txt", stream).ConfigureAwait(false);
}
if (!string.IsNullOrEmpty(item.AttachmentFilename))
if (!string.IsNullOrEmpty(item.AttachmentFilename) && item.Attachment?.Length > 0)
{
using var stream = new MemoryStream(item.Attachment);
await using var stream = new MemoryStream(item.Attachment);
await ctx.Channel.SendFileAsync(item.AttachmentFilename, stream).ConfigureAwait(false);
}
}
}
internal static async Task<(Explanation explanation, string fuzzyMatch, double score)> LookupTerm(string term)
internal static async Task<(Explanation? explanation, string? fuzzyMatch, double score)> LookupTerm(string term)
{
using var db = new BotDb();
string fuzzyMatch = null;
await using var db = new BotDb();
string? fuzzyMatch = null;
double coefficient;
var explanation = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
if (explanation == null)
@@ -378,7 +375,7 @@ namespace CompatBot.Commands
return (explanation, fuzzyMatch, coefficient);
}
internal static async Task<bool> SendExplanation((Explanation explanation, string fuzzyMatch, double score) termLookupResult, string term, DiscordMessage sourceMessage)
internal static async Task<bool> SendExplanation((Explanation? explanation, string? fuzzyMatch, double score) termLookupResult, string term, DiscordMessage sourceMessage)
{
try
{
@@ -410,8 +407,8 @@ namespace CompatBot.Commands
private static async Task DumpLink(CommandContext ctx, string messageLink)
{
string explanation = null;
DiscordMessage msg = null;
string? explanation = null;
DiscordMessage? msg = null;
try { msg = await ctx.GetMessageAsync(messageLink).ConfigureAwait(false); } catch {}
if (msg != null)
{
@@ -425,7 +422,7 @@ namespace CompatBot.Commands
await ctx.ReactWithAsync(Config.Reactions.Failure, "Couldn't find any text in the specified message").ConfigureAwait(false);
else
{
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(explanation));
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(explanation));
await ctx.Channel.SendFileAsync("explanation.txt", stream).ConfigureAwait(false);
}
}

View File

@@ -28,7 +28,7 @@ namespace CompatBot.Commands
public Task List(CommandContext ctx, [Description("Get information for specific PR number")] int pr) => LinkPrBuild(ctx.Client, ctx.Message, pr);
[GroupCommand]
public async Task List(CommandContext ctx, [Description("Get information for PRs with specified text in description. First word might be an author"), RemainingText] string searchStr = null)
public async Task List(CommandContext ctx, [Description("Get information for PRs with specified text in description. First word might be an author"), RemainingText] string? searchStr = null)
{
var openPrList = await githubClient.GetOpenPrsAsync(Config.Cts.Token).ConfigureAwait(false);
if (openPrList == null)
@@ -45,7 +45,10 @@ namespace CompatBot.Commands
if (!string.IsNullOrEmpty(searchStr))
{
var filteredList = openPrList.Where(pr => pr.Title.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase) || pr.User.Login.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase)).ToList();
var filteredList = openPrList.Where(
pr => pr.Title?.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase) is true
|| pr.User?.Login?.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase) is true
).ToList();
if (filteredList.Count == 0)
{
var searchParts = searchStr.Split(' ', 2);
@@ -53,7 +56,10 @@ namespace CompatBot.Commands
{
var author = searchParts[0].Trim();
var substr = searchParts[1].Trim();
openPrList = openPrList.Where(pr => pr.User.Login.Contains(author, StringComparison.InvariantCultureIgnoreCase) && pr.Title.Contains(substr, StringComparison.InvariantCultureIgnoreCase)).ToList();
openPrList = openPrList.Where(
pr => pr.User?.Login?.Contains(author, StringComparison.InvariantCultureIgnoreCase) is true
&& pr.Title?.Contains(substr, StringComparison.InvariantCultureIgnoreCase) is true
).ToList();
}
else
openPrList = filteredList;
@@ -77,20 +83,20 @@ namespace CompatBot.Commands
var responseChannel = await ctx.GetChannelForSpamAsync().ConfigureAwait(false);
const int maxTitleLength = 80;
var maxNum = openPrList.Max(pr => pr.Number).ToString().Length + 1;
var maxAuthor = openPrList.Max(pr => pr.User.Login.GetVisibleLength());
var maxAuthor = openPrList.Max(pr => (pr.User?.Login).GetVisibleLength());
var maxTitle = Math.Min(openPrList.Max(pr => pr.Title.GetVisibleLength()), maxTitleLength);
var result = new StringBuilder($"There are {openPrList.Count} open pull requests:\n");
foreach (var pr in openPrList)
result.Append("`").Append($"{("#" + pr.Number).PadLeft(maxNum)} by {pr.User.Login.PadRightVisible(maxAuthor)}: {pr.Title.Trim(maxTitleLength).PadRightVisible(maxTitle)}".FixSpaces()).AppendLine($"` <{pr.HtmlUrl}>");
result.Append('`').Append($"{("#" + pr.Number).PadLeft(maxNum)} by {pr.User?.Login?.PadRightVisible(maxAuthor)}: {pr.Title?.Trim(maxTitleLength).PadRightVisible(maxTitle)}".FixSpaces()).AppendLine($"` <{pr.HtmlUrl}>");
await responseChannel.SendAutosplitMessageAsync(result, blockStart: null, blockEnd: null).ConfigureAwait(false);
}
public static async Task LinkPrBuild(DiscordClient client, DiscordMessage message, int pr)
{
var prInfo = await githubClient.GetPrInfoAsync(pr, Config.Cts.Token).ConfigureAwait(false);
if (prInfo.Number == 0)
if (prInfo is null or {Number: 0})
{
await message.ReactWithAsync(Config.Reactions.Failure, prInfo.Message ?? "PR not found").ConfigureAwait(false);
await message.ReactWithAsync(Config.Reactions.Failure, prInfo?.Message ?? "PR not found").ConfigureAwait(false);
return;
}
@@ -100,9 +106,9 @@ namespace CompatBot.Commands
{
var windowsDownloadHeader = "Windows PR Build";
var linuxDownloadHeader = "Linux PR Build";
string windowsDownloadText = null;
string linuxDownloadText = null;
string buildTime = null;
string? windowsDownloadText = null;
string? linuxDownloadText = null;
string? buildTime = null;
var azureClient = Config.GetAzureDevOpsClient();
if (azureClient != null && prInfo.Head?.Sha is string commit)
@@ -204,7 +210,7 @@ namespace CompatBot.Commands
public static async Task LinkIssue(DiscordClient client, DiscordMessage message, int issue)
{
var issueInfo = await githubClient.GetIssueInfoAsync(issue, Config.Cts.Token).ConfigureAwait(false);
if (issueInfo.Number == 0)
if (issueInfo is null or {Number: 0})
return;
if (issueInfo.PullRequest != null)

View File

@@ -154,7 +154,7 @@ namespace CompatBot.Commands
await ctx.RespondAsync($"Fixed {changed} title{(changed == 1 ? "" : "s")}").ConfigureAwait(false);
}
public static async Task<string> FixChannelMentionAsync(CommandContext ctx, string msg)
public static async Task<string?> FixChannelMentionAsync(CommandContext ctx, string? msg)
{
if (string.IsNullOrEmpty(msg))
return msg;

View File

@@ -75,7 +75,7 @@ namespace CompatBot.Commands
[Command("describe"), TriggersTyping]
[Description("Generates an image description from the attached image, or from the url")]
public Task Describe(CommandContext ctx, [RemainingText] string imageUrl = null)
public Task Describe(CommandContext ctx, [RemainingText] string? imageUrl = null)
{
if (imageUrl?.StartsWith("tag") ?? false)
return Tag(ctx, imageUrl[3..].TrimStart());
@@ -84,7 +84,7 @@ namespace CompatBot.Commands
[Command("tag"), TriggersTyping]
[Description("Tags recognized objects in the image")]
public async Task Tag(CommandContext ctx, string imageUrl = null)
public async Task Tag(CommandContext ctx, string? imageUrl = null)
{
try
{
@@ -92,9 +92,9 @@ namespace CompatBot.Commands
if (string.IsNullOrEmpty(imageUrl))
return;
using var imageStream = Config.MemoryStreamManager.GetStream();
await using var imageStream = Config.MemoryStreamManager.GetStream();
using (var httpClient = HttpClientFactory.Create())
using (var stream = await httpClient.GetStreamAsync(imageUrl).ConfigureAwait(false))
await using (var stream = await httpClient.GetStreamAsync(imageUrl).ConfigureAwait(false))
await stream.CopyToAsync(imageStream).ConfigureAwait(false);
imageStream.Seek(0, SeekOrigin.Begin);
#pragma warning disable VSTHRD103
@@ -114,7 +114,7 @@ namespace CompatBot.Commands
if (resized || imgFormat.Name != JpegFormat.Instance.Name)
{
imageStream.SetLength(0);
img.Save(imageStream, new JpegEncoder { Quality = 90 });
await img.SaveAsync(imageStream, new JpegEncoder { Quality = 90 }).ConfigureAwait(false);
imageStream.Seek(0, SeekOrigin.Begin);
}
else
@@ -132,7 +132,7 @@ namespace CompatBot.Commands
{
quality -= 5;
imageStream.SetLength(0);
img.Save(imageStream, new JpegEncoder {Quality = quality});
await img.SaveAsync(imageStream, new JpegEncoder {Quality = quality}).ConfigureAwait(false);
imageStream.Seek(0, SeekOrigin.Begin);
}
@@ -159,7 +159,7 @@ namespace CompatBot.Commands
foreach (var obj in objects)
{
var r = obj.Rectangle;
using var tmpStream = Config.MemoryStreamManager.GetStream();
await using var tmpStream = Config.MemoryStreamManager.GetStream();
using var boxCopy = img.Clone(i => i.Crop(new Rectangle(r.X, r.Y, r.W, r.H)));
await boxCopy.SaveAsBmpAsync(tmpStream).ConfigureAwait(false);
tmpStream.Seek(0, SeekOrigin.Begin);
@@ -274,11 +274,11 @@ namespace CompatBot.Commands
if (bgBox.Y + bgBox.Height > img.Height)
bgBox.Y = img.Height - bgBox.Height;
drawnBoxes.Add(bgBox);
var textboxColor = complementaryColor;
var textBoxColor = complementaryColor;
var textColor = color;
try
{
img.Mutate(i => i.Fill(bgSgo, textboxColor, bgBox));
img.Mutate(i => i.Fill(bgSgo, textBoxColor, bgBox));
img.Mutate(i => i.GaussianBlur(10 * scale, Rectangle.Round(bgBox)));
}
catch (Exception ex)
@@ -297,12 +297,12 @@ namespace CompatBot.Commands
Config.Log.Error(ex, "Failed to generate tag label");
}
}
using var resultStream = Config.MemoryStreamManager.GetStream();
await using var resultStream = Config.MemoryStreamManager.GetStream();
quality = 95;
do
{
resultStream.SetLength(0);
img.Save(resultStream, new JpegEncoder {Quality = 95});
await img.SaveAsync(resultStream, new JpegEncoder {Quality = 95}).ConfigureAwait(false);
resultStream.Seek(0, SeekOrigin.Begin);
quality--;
} while (resultStream.Length > Config.AttachmentSizeLimit);
@@ -367,12 +367,12 @@ namespace CompatBot.Commands
await reactMsg.ReactWithAsync(DiscordEmoji.FromUnicode(emojiList[new Random().Next(emojiList.Length)])).ConfigureAwait(false);
}
private static async Task<string> GetImageUrlAsync(CommandContext ctx, string imageUrl)
private static async Task<string?> GetImageUrlAsync(CommandContext ctx, string? imageUrl)
{
var reactMsg = ctx.Message;
if (GetImageAttachment(reactMsg).FirstOrDefault() is DiscordAttachment attachment)
imageUrl = attachment.Url;
imageUrl = imageUrl?.Trim();
imageUrl = imageUrl?.Trim() ?? "";
if (!string.IsNullOrEmpty(imageUrl)
&& imageUrl.StartsWith('<')
&& imageUrl.EndsWith('>'))
@@ -388,28 +388,22 @@ namespace CompatBot.Commands
&& ctx.Channel.PermissionsFor(ctx.Client.GetMember(ctx.Guild, ctx.Client.CurrentUser)).HasPermission(Permissions.ReadMessageHistory))
try
{
var previousMessages = await ctx.Channel.GetMessagesBeforeCachedAsync(ctx.Message.Id, 10).ConfigureAwait(false);
var (selectedMsg, selectedAttachment) = (
var previousMessages = (await ctx.Channel.GetMessagesBeforeCachedAsync(ctx.Message.Id, 10).ConfigureAwait(false))!;
imageUrl = (
from m in previousMessages
where m.Attachments?.Count > 0
from a in GetImageAttachment(m)
select (m, a)
select a.Url
).FirstOrDefault();
if (selectedMsg != null)
reactMsg = selectedMsg;
imageUrl = selectedAttachment?.Url;
if (string.IsNullOrEmpty(imageUrl))
{
var (selectedMsg2, selectedUrl) = (
var selectedUrl = (
from m in previousMessages
where m.Embeds?.Count > 0
from e in m.Embeds
let url = e.Image?.Url ?? e.Image?.ProxyUrl ?? e.Thumbnail?.Url ?? e.Thumbnail?.ProxyUrl
select (m, url)
select url
).FirstOrDefault();
if (selectedMsg2 != null)
reactMsg = selectedMsg2;
imageUrl = selectedUrl?.ToString();
}
}

View File

@@ -38,9 +38,7 @@ namespace CompatBot.Commands
[GroupCommand]
[Description("List your own warning list")]
public async Task List(CommandContext ctx)
{
await List(ctx, ctx.Message.Author).ConfigureAwait(false);
}
=> await List(ctx, ctx.Message.Author).ConfigureAwait(false);
[Command("users"), Aliases("top"), RequiresBotModRole, TriggersTyping]
[Description("List users with warnings, sorted from most warned to least")]
@@ -57,19 +55,17 @@ namespace CompatBot.Commands
new AsciiColumn("Count", alignToRight: true),
new AsciiColumn("All time", alignToRight: true)
);
using (var db = new BotDb())
await using var db = new BotDb();
var query = from warn in db.Warning.AsEnumerable()
group warn by warn.DiscordId
into userGroup
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 query = from warn in db.Warning.AsEnumerable()
group warn by warn.DiscordId
into userGroup
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(), row.total.ToString());
}
var username = await ctx.GetUserNameAsync(row.discordId).ConfigureAwait(false);
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);
}
@@ -103,7 +99,7 @@ namespace CompatBot.Commands
foreach (var row in query.Take(number))
{
var username = await ctx.GetUserNameAsync(row.DiscordId).ConfigureAwait(false);
var timestamp = row.Timestamp.HasValue ? new DateTime(row.Timestamp.Value, DateTimeKind.Utc).ToString("u") : null;
var timestamp = row.Timestamp.HasValue ? new DateTime(row.Timestamp.Value, DateTimeKind.Utc).ToString("u") : "";
table.Add(row.Id.ToString(), username, row.DiscordId.ToString(), timestamp, row.Reason, row.FullReason);
}
}
@@ -115,7 +111,7 @@ namespace CompatBot.Commands
[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")
if (me.ToLowerInvariant() == "me")
{
await By(ctx, ctx.User.Id, number).ConfigureAwait(false);
return;
@@ -148,33 +144,31 @@ namespace CompatBot.Commands
new AsciiColumn("Reason"),
new AsciiColumn("Context", disabled: !ctx.Channel.IsPrivate)
);
using (var db = new BotDb())
await using var db = new BotDb();
IOrderedQueryable<Warning> query;
if (showRetractions)
query = from warn in db.Warning
orderby warn.Id descending
select warn;
else
query = from warn in db.Warning
where !warn.Retracted
orderby warn.Id descending
select warn;
foreach (var row in query.Take(number))
{
IOrderedQueryable<Warning> query;
if (showRetractions)
query = from warn in db.Warning
orderby warn.Id descending
select warn;
else
query = from warn in db.Warning
where !warn.Retracted
orderby warn.Id descending
select warn;
foreach (var row in query.Take(number))
var username = await ctx.GetUserNameAsync(row.DiscordId).ConfigureAwait(false);
var modname = await ctx.GetUserNameAsync(row.IssuerId, defaultName: "Unknown mod").ConfigureAwait(false);
var timestamp = row.Timestamp.HasValue ? new DateTime(row.Timestamp.Value, DateTimeKind.Utc).ToString("u") : "";
if (row.Retracted)
{
var username = await ctx.GetUserNameAsync(row.DiscordId).ConfigureAwait(false);
var modname = await ctx.GetUserNameAsync(row.IssuerId, defaultName: "Unknown mod").ConfigureAwait(false);
var timestamp = row.Timestamp.HasValue ? new DateTime(row.Timestamp.Value, DateTimeKind.Utc).ToString("u") : null;
if (row.Retracted)
{
var modnameRetracted = row.RetractedBy.HasValue ? await ctx.GetUserNameAsync(row.RetractedBy.Value, defaultName: "Unknown mod").ConfigureAwait(false) : "";
var timestampRetracted = row.RetractionTimestamp.HasValue ? new DateTime(row.RetractionTimestamp.Value, DateTimeKind.Utc).ToString("u") : null;
table.Add(row.Id.ToString(), "-", username, row.DiscordId.ToString(), modnameRetracted, timestampRetracted, row.RetractionReason, "");
table.Add(row.Id.ToString(), "+", username.StrikeThrough(), row.DiscordId.ToString().StrikeThrough(), modname.StrikeThrough(), timestamp.StrikeThrough(), row.Reason.StrikeThrough(), row.FullReason.StrikeThrough());
}
else
table.Add(row.Id.ToString(), "+", username, row.DiscordId.ToString(), modname, timestamp, row.Reason, row.FullReason);
var modnameRetracted = row.RetractedBy.HasValue ? await ctx.GetUserNameAsync(row.RetractedBy.Value, defaultName: "Unknown mod").ConfigureAwait(false) : "";
var timestampRetracted = row.RetractionTimestamp.HasValue ? new DateTime(row.RetractionTimestamp.Value, DateTimeKind.Utc).ToString("u") : "";
table.Add(row.Id.ToString(), "-", username, row.DiscordId.ToString(), modnameRetracted, timestampRetracted, row.RetractionReason ?? "", "");
table.Add(row.Id.ToString(), "+", username.StrikeThrough(), row.DiscordId.ToString().StrikeThrough(), modname.StrikeThrough(), timestamp.StrikeThrough(), row.Reason.StrikeThrough(), row.FullReason.StrikeThrough());
}
else
table.Add(row.Id.ToString(), "+", username, row.DiscordId.ToString(), modname, timestamp, row.Reason, row.FullReason);
}
await ctx.SendAutosplitMessageAsync(new StringBuilder("Recent warnings:").Append(table)).ConfigureAwait(false);
}

View File

@@ -51,7 +51,11 @@ namespace CompatBot.Commands
{
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);
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);
@@ -59,19 +63,16 @@ namespace CompatBot.Commands
}
await msg.DeleteAsync().ConfigureAwait(false);
int removedCount;
using (var db = new BotDb())
await using var db = new BotDb();
var warningsToRemove = await db.Warning.Where(w => ids.Contains(w.Id)).ToListAsync().ConfigureAwait(false);
foreach (var w in warningsToRemove)
{
var warningsToRemove = await db.Warning.Where(w => ids.Contains(w.Id)).ToListAsync().ConfigureAwait(false);
foreach (var w in warningsToRemove)
{
w.Retracted = true;
w.RetractedBy = ctx.User.Id;
w.RetractionReason = response.Result.Content;
w.RetractionTimestamp = DateTime.UtcNow.Ticks;
}
removedCount = await db.SaveChangesAsync().ConfigureAwait(false);
w.Retracted = true;
w.RetractedBy = ctx.User.Id;
w.RetractionReason = response.Result.Content;
w.RetractionTimestamp = DateTime.UtcNow.Ticks;
}
var removedCount = await db.SaveChangesAsync().ConfigureAwait(false);
if (removedCount == ids.Length)
await ctx.RespondAsync($"Warning{StringUtils.GetSuffix(ids.Length)} successfully removed!").ConfigureAwait(false);
else
@@ -81,9 +82,7 @@ namespace CompatBot.Commands
[Command("clear"), RequiresBotModRole]
[Description("Removes **all** warnings for a user")]
public Task Clear(CommandContext ctx, [Description("User to clear warnings for")] DiscordUser user)
{
return Clear(ctx, user.Id);
}
=> Clear(ctx, user.Id);
[Command("clear"), RequiresBotModRole]
public async Task Clear(CommandContext ctx, [Description("User ID to clear warnings for")] ulong userId)
@@ -100,19 +99,16 @@ namespace CompatBot.Commands
await msg.DeleteAsync().ConfigureAwait(false);
try
{
int removed;
using (var db = new BotDb())
await using var db = new BotDb();
var warningsToRemove = await db.Warning.Where(w => w.DiscordId == userId).ToListAsync().ConfigureAwait(false);
foreach (var w in warningsToRemove)
{
var warningsToRemove = await db.Warning.Where(w => w.DiscordId == userId).ToListAsync().ConfigureAwait(false);
foreach (var w in warningsToRemove)
{
w.Retracted = true;
w.RetractedBy = ctx.User.Id;
w.RetractionReason = response.Result.Content;
w.RetractionTimestamp = DateTime.UtcNow.Ticks;
}
removed = await db.SaveChangesAsync().ConfigureAwait(false);
w.Retracted = true;
w.RetractedBy = ctx.User.Id;
w.RetractionReason = response.Result.Content;
w.RetractionTimestamp = DateTime.UtcNow.Ticks;
}
var removed = await db.SaveChangesAsync().ConfigureAwait(false);
await ctx.RespondAsync($"{removed} warning{StringUtils.GetSuffix(removed)} successfully removed!").ConfigureAwait(false);
}
catch (Exception e)
@@ -125,7 +121,7 @@ namespace CompatBot.Commands
[Description("Changes the state of the warning status")]
public async Task Revert(CommandContext ctx, [Description("Warning ID to change")] int id)
{
using var db = new BotDb();
await using var db = new BotDb();
var warn = await db.Warning.FirstOrDefaultAsync(w => w.Id == id).ConfigureAwait(false);
if (warn.Retracted)
{
@@ -140,45 +136,47 @@ namespace CompatBot.Commands
await Remove(ctx, id).ConfigureAwait(false);
}
internal static async Task<bool> AddAsync(CommandContext ctx, ulong userId, string userName, DiscordUser issuer, string reason, string fullReason = null)
internal static async Task<bool> AddAsync(CommandContext ctx, ulong userId, string userName, 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)
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);
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
{
int totalCount;
using (var db = new BotDb())
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.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)
{
await db.Warning.AddAsync(new Warning { 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;
}
totalCount = db.Warning.Count(w => w.DiscordId == userId && !w.Retracted);
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.RespondAsync($"User warning saved! User currently has {totalCount} warning{StringUtils.GetSuffix(totalCount)}!").ConfigureAwait(false);
if (totalCount > 1)
await ListUserWarningsAsync(client, message, userId, userName).ConfigureAwait(false);
@@ -196,7 +194,7 @@ namespace CompatBot.Commands
{
try
{
var isWhitelisted = client.GetMember(message.Author)?.IsWhitelisted() ?? false;
var isWhitelisted = client.GetMember(message.Author)?.IsWhitelisted() is true;
if (message.Author.Id != userId && !isWhitelisted)
{
Config.Log.Error($"Somehow {message.Author.Username} ({message.Author.Id}) triggered warning list for {userId}");
@@ -207,13 +205,11 @@ namespace CompatBot.Commands
var isPrivate = channel.IsPrivate;
int count, removed;
bool isKot, isDoggo;
using (var db = new BotDb())
{
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);
isKot = db.Kot.Any(k => k.UserId == userId);
isDoggo = db.Doggo.Any(d => d.UserId == userId);
}
await using var db = new BotDb();
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);
isKot = db.Kot.Any(k => k.UserId == userId);
isDoggo = db.Doggo.Any(d => d.UserId == userId);
if (count == 0)
{
if (isKot && isDoggo)
@@ -225,12 +221,12 @@ namespace CompatBot.Commands
}
var msg = (removed, isPrivate, 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",
(0, _, _, _) => $"{userName} has no warnings, is an upstanding citizen, and a pillar of this community",
(_, true, _, _) => $"{userName} has no warnings ({removed} retracted warning{(removed == 1 ? "" : "s")})",
(_, _, true, false) => $"{userName} has no warnings, but are they a good kot?",
(_, _, false, true) => $"{userName} has no warnings, but are they a good boy?",
(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",
(0, _, _, _) => $"{userName} has no warnings, is an upstanding citizen, and a pillar of this community",
(_, true, _, _) => $"{userName} has no warnings ({removed} retracted warning{(removed == 1 ? "" : "s")})",
(_, _, true, false) => $"{userName} has no warnings, but are they a good kot?",
(_, _, false, true) => $"{userName} has no warnings, but are they a good boy?",
_ => $"{userName} has no warnings",
};
await message.RespondAsync(msg).ConfigureAwait(false);
@@ -242,62 +238,59 @@ namespace CompatBot.Commands
return;
const int maxWarningsInPublicChannel = 3;
using (var db = new BotDb())
var showCount = Math.Min(maxWarningsInPublicChannel, count);
var table = new AsciiTable(
new AsciiColumn("ID", alignToRight: true),
new AsciiColumn("±", disabled: !isPrivate || !isWhitelisted),
new AsciiColumn("By", maxWidth: 15),
new AsciiColumn("On date (UTC)"),
new AsciiColumn("Reason"),
new AsciiColumn("Context", disabled: !isPrivate, maxWidth: 4096)
);
IQueryable<Warning> query = db.Warning.Where(w => w.DiscordId == userId).OrderByDescending(w => w.Id);
if (!isPrivate || !isWhitelisted)
query = query.Where(w => !w.Retracted);
if (!isPrivate && !isWhitelisted)
query = query.Take(maxWarningsInPublicChannel);
foreach (var warning in await query.ToListAsync().ConfigureAwait(false))
{
var showCount = Math.Min(maxWarningsInPublicChannel, count);
var table = new AsciiTable(
new AsciiColumn("ID", alignToRight: true),
new AsciiColumn("±", disabled: !isPrivate || !isWhitelisted),
new AsciiColumn("By", maxWidth: 15),
new AsciiColumn("On date (UTC)"),
new AsciiColumn("Reason"),
new AsciiColumn("Context", disabled: !isPrivate, maxWidth: 4096)
);
IQueryable<Warning> query = db.Warning.Where(w => w.DiscordId == userId).OrderByDescending(w => w.Id);
if (!isPrivate || !isWhitelisted)
query = query.Where(w => !w.Retracted);
if (!isPrivate && !isWhitelisted)
query = query.Take(maxWarningsInPublicChannel);
foreach (var warning in await query.ToListAsync().ConfigureAwait(false))
if (warning.Retracted)
{
if (warning.Retracted)
if (isWhitelisted && isPrivate)
{
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 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")
: "";
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);
: "";
table.Add(warning.Id.ToString().StrikeThrough(), "+", issuerName.StrikeThrough(), timestamp.StrikeThrough(), warning.Reason.StrikeThrough(), warning.FullReason.StrikeThrough());
}
}
var result = new StringBuilder("Warning list for ").Append(userName);
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);
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")
: "";
table.Add(warning.Id.ToString(), "+", issuerName, timestamp, warning.Reason, warning.FullReason);
}
}
var result = new StringBuilder("Warning list for ").Append(userName);
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);
}
catch (Exception e)
{

View File

@@ -10,6 +10,7 @@
<NoWarn>1701;1702;VSTHRD200</NoWarn>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@@ -58,7 +59,6 @@
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.8" />
<PackageReference Include="Microsoft.VisualStudio.Services.Client" Version="16.153.0" />
<PackageReference Include="Nerdbank.Streams" Version="2.6.81" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NLog" Version="4.7.5" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.6.5" />
<PackageReference Include="NReco.Text.AhoCorasickDoubleArrayTrie" Version="1.0.2" />

View File

@@ -30,8 +30,8 @@ namespace CompatBot
{
internal static class Config
{
private static IConfigurationRoot config;
private static TelemetryClient telemetryClient;
private static IConfigurationRoot config = null!;
private static TelemetryClient? telemetryClient;
private static readonly DependencyTrackingTelemetryModule dependencyTrackingTelemetryModule = new DependencyTrackingTelemetryModule();
private static readonly PerformanceCollectorModule performanceCollectorModule = new PerformanceCollectorModule();
@@ -101,9 +101,9 @@ namespace CompatBot
if (SandboxDetector.Detect() == SandboxType.Docker)
return "/bot-config/credentials.json";
if (Assembly.GetEntryAssembly().GetCustomAttribute<UserSecretsIdAttribute>() is UserSecretsIdAttribute attribute)
if (Assembly.GetEntryAssembly()?.GetCustomAttribute<UserSecretsIdAttribute>() is UserSecretsIdAttribute attribute
&& Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(attribute.UserSecretsId)) is string path)
{
var path = Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(attribute.UserSecretsId));
path = Path.Combine(path, "credentials.json");
if (File.Exists(path))
return path;
@@ -217,6 +217,7 @@ namespace CompatBot
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Error initializing settings: " + e.Message);
Console.ResetColor();
throw;
}
}
@@ -231,7 +232,7 @@ namespace CompatBot
private static ILogger GetLog()
{
var config = new NLog.Config.LoggingConfiguration();
var loggingConfig = new NLog.Config.LoggingConfiguration();
var fileTarget = new FileTarget("logfile") {
FileName = CurrentLogPath,
ArchiveEvery = FileArchivePeriod.Day,
@@ -266,25 +267,25 @@ namespace CompatBot
new MethodCallParameter("${message}"),
});
#if DEBUG
config.AddRule(LogLevel.Trace, LogLevel.Fatal, consoleTarget, "default"); // only echo messages from default logger to the console
loggingConfig.AddRule(LogLevel.Trace, LogLevel.Fatal, consoleTarget, "default"); // only echo messages from default logger to the console
#else
config.AddRule(LogLevel.Info, LogLevel.Fatal, consoleTarget, "default");
loggingConfig.AddRule(LogLevel.Info, LogLevel.Fatal, consoleTarget, "default");
#endif
config.AddRule(LogLevel.Debug, LogLevel.Fatal, asyncFileTarget);
config.AddRule(LogLevel.Info, LogLevel.Fatal, watchdogTarget);
loggingConfig.AddRule(LogLevel.Debug, LogLevel.Fatal, asyncFileTarget);
loggingConfig.AddRule(LogLevel.Info, LogLevel.Fatal, watchdogTarget);
var ignoreFilter1 = new ConditionBasedFilter { Condition = "contains('${message}','TaskCanceledException')", Action = FilterResult.Ignore, };
var ignoreFilter2 = new ConditionBasedFilter { Condition = "contains('${message}','One or more pre-execution checks failed')", Action = FilterResult.Ignore, };
foreach (var rule in config.LoggingRules)
foreach (var rule in loggingConfig.LoggingRules)
{
rule.Filters.Add(ignoreFilter1);
rule.Filters.Add(ignoreFilter2);
}
LogManager.Configuration = config;
LogManager.Configuration = loggingConfig;
return LogManager.GetLogger("default");
}
public static BuildHttpClient GetAzureDevOpsClient()
public static BuildHttpClient? GetAzureDevOpsClient()
{
if (string.IsNullOrEmpty(AzureDevOpsToken))
return null;
@@ -294,14 +295,14 @@ namespace CompatBot
return azureConnection.GetClient<BuildHttpClient>();
}
public static TelemetryClient TelemetryClient
public static TelemetryClient? TelemetryClient
{
get
{
if (string.IsNullOrEmpty(AzureAppInsightsKey))
return null;
if (telemetryClient != null && telemetryClient.InstrumentationKey == AzureAppInsightsKey)
if (telemetryClient?.InstrumentationKey == AzureAppInsightsKey)
return telemetryClient;
var telemetryConfig = TelemetryConfiguration.CreateDefault();

View File

@@ -8,18 +8,18 @@ namespace CompatBot.Database
{
internal class BotDb: DbContext
{
public DbSet<BotState> BotState { get; set; }
public DbSet<Moderator> Moderator { get; set; }
public DbSet<Piracystring> Piracystring { get; set; }
public DbSet<Warning> Warning { get; set; }
public DbSet<Explanation> Explanation { get; set; }
public DbSet<DisabledCommand> DisabledCommands { get; set; }
public DbSet<WhitelistedInvite> WhitelistedInvites { get; set; }
public DbSet<EventSchedule> EventSchedule { get; set; }
public DbSet<Stats> Stats { get; set; }
public DbSet<Kot> Kot { get; set; }
public DbSet<Doggo> Doggo { get; set; }
public DbSet<ForcedNickname> ForcedNicknames { get; set; }
public DbSet<BotState> BotState { get; set; } = null!;
public DbSet<Moderator> Moderator { get; set; } = null!;
public DbSet<Piracystring> Piracystring { get; set; } = null!;
public DbSet<Warning> Warning { get; set; } = null!;
public DbSet<Explanation> Explanation { get; set; } = null!;
public DbSet<DisabledCommand> DisabledCommands { get; set; } = null!;
public DbSet<WhitelistedInvite> WhitelistedInvites { get; set; } = null!;
public DbSet<EventSchedule> EventSchedule { get; set; } = null!;
public DbSet<Stats> Stats { get; set; } = null!;
public DbSet<Kot> Kot { get; set; } = null!;
public DbSet<Doggo> Doggo { get; set; } = null!;
public DbSet<ForcedNickname> ForcedNicknames { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var dbPath = DbImporter.GetDbPath("bot.db", Environment.SpecialFolder.ApplicationData);
@@ -58,8 +58,8 @@ namespace CompatBot.Database
internal class BotState
{
public int Id { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public string? Key { get; set; }
public string? Value { get; set; }
}
internal class Moderator
@@ -73,12 +73,12 @@ namespace CompatBot.Database
{
public int Id { get; set; }
[Required, Column(TypeName = "varchar(255)")]
public string String { get; set; }
public string ValidatingRegex { get; set; }
public string String { get; set; } = null!;
public string? ValidatingRegex { get; set; }
public FilterContext Context { get; set; }
public FilterAction Actions { get; set; }
public string ExplainTerm { get; set; }
public string CustomMessage { get; set; }
public string? ExplainTerm { get; set; }
public string? CustomMessage { get; set; }
public bool Disabled { get; set; }
}
@@ -106,13 +106,13 @@ namespace CompatBot.Database
public ulong DiscordId { get; set; }
public ulong IssuerId { get; set; }
[Required]
public string Reason { get; set; }
public string Reason { get; set; } = null!;
[Required]
public string FullReason { get; set; }
public string FullReason { get; set; } = null!;
public long? Timestamp { get; set; }
public bool Retracted { get; set; }
public ulong? RetractedBy { get; set; }
public string RetractionReason { get; set; }
public string? RetractionReason { get; set; }
public long? RetractionTimestamp { get; set; }
}
@@ -120,27 +120,27 @@ namespace CompatBot.Database
{
public int Id { get; set; }
[Required]
public string Keyword { get; set; }
public string Keyword { get; set; } = null!;
[Required]
public string Text { get; set; }
public string Text { get; set; } = null!;
[MaxLength(7*1024*1024)]
public byte[] Attachment { get; set; }
public string AttachmentFilename { get; set; }
public byte[]? Attachment { get; set; }
public string? AttachmentFilename { get; set; }
}
internal class DisabledCommand
{
public int Id { get; set; }
[Required]
public string Command { get; set; }
public string Command { get; set; } = null!;
}
internal class WhitelistedInvite
{
public int Id { get; set; }
public ulong GuildId { get; set; }
public string Name { get; set; }
public string InviteCode { get; set; }
public string? Name { get; set; }
public string? InviteCode { get; set; }
}
internal class EventSchedule
@@ -149,17 +149,17 @@ namespace CompatBot.Database
public int Year { get; set; }
public long Start { get; set; }
public long End { get; set; }
public string Name { get; set; }
public string EventName { get; set; }
public string? Name { get; set; }
public string? EventName { get; set; }
}
internal class Stats
{
public int Id { get; set; }
[Required]
public string Category { get; set; }
public string Category { get; set; } = null!;
[Required]
public string Key { get; set; }
public string Key { get; set; } = null!;
public int Value { get; set; }
public long ExpirationTimestamp { get; set; }
}
@@ -182,6 +182,6 @@ namespace CompatBot.Database
public ulong GuildId { set; get; }
public ulong UserId { set; get; }
[Required]
public string Nickname { get; set; }
public string Nickname { get; set; } = null!;
}
}

View File

@@ -9,7 +9,9 @@ using CompatBot.Utils;
using DSharpPlus;
using DSharpPlus.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Caching.Memory;
using PsnClient.POCOs;
namespace CompatBot.Database.Providers
{
@@ -19,8 +21,11 @@ namespace CompatBot.Database.Providers
private static readonly PsnClient.Client PsnClient = new PsnClient.Client();
private static readonly MemoryCache ColorCache = new MemoryCache(new MemoryCacheOptions{ ExpirationScanFrequency = TimeSpan.FromDays(1) });
public static async Task<string> GetThumbnailUrlAsync(this DiscordClient client, string productCode)
public static async Task<string?> GetThumbnailUrlAsync(this DiscordClient client, string? productCode)
{
if (string.IsNullOrEmpty(productCode))
return null;
productCode = productCode.ToUpperInvariant();
var tmdbInfo = await PsnClient.GetTitleMetaAsync(productCode, Config.Cts.Token).ConfigureAwait(false);
if (tmdbInfo?.Icon.Url is string tmdbIconUrl)
@@ -38,11 +43,8 @@ namespace CompatBot.Database.Providers
var gameTdbCoverUrl = await GameTdbScraper.GetThumbAsync(productCode).ConfigureAwait(false);
if (!string.IsNullOrEmpty(gameTdbCoverUrl))
{
if (thumb == null)
{
var addResult = await db.Thumbnail.AddAsync(new Thumbnail {ProductCode = productCode, Url = gameTdbCoverUrl}).ConfigureAwait(false);
thumb = addResult.Entity;
}
if (thumb is null)
thumb = (await db.Thumbnail.AddAsync(new Thumbnail {ProductCode = productCode, Url = gameTdbCoverUrl}).ConfigureAwait(false)).Entity;
else
thumb.Url = gameTdbCoverUrl;
thumb.Timestamp = DateTime.UtcNow.Ticks;
@@ -53,47 +55,43 @@ namespace CompatBot.Database.Providers
if (thumb?.Url is string url && !string.IsNullOrEmpty(url))
{
var contentName = (thumb.ContentId ?? thumb.ProductCode);
var embed = await GetEmbeddableUrlAsync(client, contentName, url).ConfigureAwait(false);
if (embed.url != null)
var (embedUrl, _) = await GetEmbeddableUrlAsync(client, contentName, url).ConfigureAwait(false);
if (embedUrl != null)
{
thumb.EmbeddableUrl = embed.url;
thumb.EmbeddableUrl = embedUrl;
await db.SaveChangesAsync().ConfigureAwait(false);
return embed.url;
return embedUrl;
}
}
}
return null;
}
public static async Task<string> GetTitleNameAsync(string productCode, CancellationToken cancellationToken)
public static async Task<string?> GetTitleNameAsync(string? productCode, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(productCode))
return null;
productCode = productCode.ToUpperInvariant();
using var db = new ThumbnailDb();
await using var db = new ThumbnailDb();
var thumb = await db.Thumbnail.FirstOrDefaultAsync(
t => t.ProductCode == productCode,
cancellationToken: cancellationToken
).ConfigureAwait(false);
if (thumb?.Name is string title)
return title;
if (thumb?.Name is string result)
return result;
var meta = await PsnClient.GetTitleMetaAsync(productCode, cancellationToken).ConfigureAwait(false);
title = meta?.Name;
var title = (await PsnClient.GetTitleMetaAsync(productCode, cancellationToken).ConfigureAwait(false))?.Name;
try
{
if (!string.IsNullOrEmpty(title))
{
if (thumb == null)
thumb = (
await db.Thumbnail.AddAsync(new Thumbnail
{
ProductCode = productCode,
Name = title,
}, cancellationToken).ConfigureAwait(false)
).Entity;
await db.Thumbnail.AddAsync(new Thumbnail
{
ProductCode = productCode,
Name = title,
}, cancellationToken).ConfigureAwait(false);
else
thumb.Name = title;
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
@@ -107,38 +105,38 @@ namespace CompatBot.Database.Providers
return title;
}
public static async Task<(string url, DiscordColor color)> GetThumbnailUrlWithColorAsync(DiscordClient client, string contentId, DiscordColor defaultColor, string url = null)
public static async Task<(string? url, DiscordColor color)> GetThumbnailUrlWithColorAsync(DiscordClient client, string contentId, DiscordColor defaultColor, string? url = null)
{
if (string.IsNullOrEmpty(contentId))
throw new ArgumentException("ContentID can't be empty", nameof(contentId));
contentId = contentId.ToUpperInvariant();
using var db = new ThumbnailDb();
await using var db = new ThumbnailDb();
var info = await db.Thumbnail.FirstOrDefaultAsync(ti => ti.ContentId == contentId, Config.Cts.Token).ConfigureAwait(false);
info ??= new Thumbnail{Url = url};
if (info.Url == null)
if (info.Url is null)
return (null, defaultColor);
DiscordColor? analyzedColor = null;
if (string.IsNullOrEmpty(info.EmbeddableUrl))
{
var em = await GetEmbeddableUrlAsync(client, contentId, info.Url).ConfigureAwait(false);
if (em.url is string eUrl)
var (embedUrl, image) = await GetEmbeddableUrlAsync(client, contentId, info.Url).ConfigureAwait(false);
if (embedUrl is string eUrl)
{
info.EmbeddableUrl = eUrl;
if (em.image is byte[] jpg)
if (image is byte[] jpg)
{
Config.Log.Trace("Getting dominant color for " + eUrl);
analyzedColor = ColorGetter.Analyze(jpg, defaultColor);
var c = analyzedColor.Value.Value;
if (c != defaultColor.Value)
info.EmbedColor = c;
if (analyzedColor.HasValue
&& analyzedColor.Value.Value != defaultColor.Value)
info.EmbedColor = analyzedColor.Value.Value;
}
await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
}
}
if ((!info.EmbedColor.HasValue && !analyzedColor.HasValue)
|| (info.EmbedColor.HasValue && info.EmbedColor.Value == defaultColor.Value))
if (!info.EmbedColor.HasValue && !analyzedColor.HasValue
|| info.EmbedColor.HasValue && info.EmbedColor.Value == defaultColor.Value)
{
var c = await GetImageColorAsync(info.EmbeddableUrl, defaultColor).ConfigureAwait(false);
if (c.HasValue && c.Value.Value != defaultColor.Value)
@@ -151,15 +149,15 @@ namespace CompatBot.Database.Providers
return (info.EmbeddableUrl, color);
}
public static async Task<(string url, byte[] image)> GetEmbeddableUrlAsync(DiscordClient client, string contentId, string url)
public static async Task<(string? url, byte[]? image)> GetEmbeddableUrlAsync(DiscordClient client, string contentId, string url)
{
try
{
if (!string.IsNullOrEmpty(Path.GetExtension(url)))
return (url, null);
using var imgStream = await HttpClient.GetStreamAsync(url).ConfigureAwait(false);
using var memStream = Config.MemoryStreamManager.GetStream();
await using var imgStream = await HttpClient.GetStreamAsync(url).ConfigureAwait(false);
await using var memStream = Config.MemoryStreamManager.GetStream();
await imgStream.CopyToAsync(memStream).ConfigureAwait(false);
// minimum jpg size is 119 bytes, png is 67 bytes
if (memStream.Length < 64)
@@ -168,7 +166,7 @@ namespace CompatBot.Database.Providers
memStream.Seek(0, SeekOrigin.Begin);
var spam = await client.GetChannelAsync(Config.ThumbnailSpamId).ConfigureAwait(false);
var message = await spam.SendFileAsync(contentId + ".jpg", memStream, contentId).ConfigureAwait(false);
url = message.Attachments.First().Url;
url = message.Attachments[0].Url;
return (url, memStream.ToArray());
}
catch (Exception e)
@@ -178,7 +176,7 @@ namespace CompatBot.Database.Providers
return (null, null);
}
public static async Task<DiscordColor?> GetImageColorAsync(string url, DiscordColor? defaultColor = null)
public static async Task<DiscordColor?> GetImageColorAsync(string? url, DiscordColor? defaultColor = null)
{
try
{
@@ -188,8 +186,8 @@ namespace CompatBot.Database.Providers
if (ColorCache.TryGetValue(url, out DiscordColor? result))
return result;
using var imgStream = await HttpClient.GetStreamAsync(url).ConfigureAwait(false);
using var memStream = Config.MemoryStreamManager.GetStream();
await using var imgStream = await HttpClient.GetStreamAsync(url).ConfigureAwait(false);
await using var memStream = Config.MemoryStreamManager.GetStream();
await imgStream.CopyToAsync(memStream).ConfigureAwait(false);
// minimum jpg size is 119 bytes, png is 67 bytes
if (memStream.Length < 64)

View File

@@ -8,11 +8,11 @@ namespace CompatBot.Database
{
internal class ThumbnailDb: DbContext
{
public DbSet<State> State { get; set; }
public DbSet<Thumbnail> Thumbnail { get; set; }
public DbSet<SyscallInfo> SyscallInfo { get; set; }
public DbSet<SyscallToProductMap> SyscallToProductMap { get; set; }
public DbSet<Metacritic> Metacritic { get; set; }
public DbSet<State> State { get; set; } = null!;
public DbSet<Thumbnail> Thumbnail { get; set; } = null!;
public DbSet<SyscallInfo> SyscallInfo { get; set; } = null!;
public DbSet<SyscallToProductMap> SyscallToProductMap { get; set; } = null!;
public DbSet<Metacritic> Metacritic { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
@@ -45,7 +45,7 @@ namespace CompatBot.Database
internal class State
{
public int Id { get; set; }
public string Locale { get; set; }
public string? Locale { get; set; }
public long Timestamp { get; set; }
}
@@ -53,20 +53,20 @@ namespace CompatBot.Database
{
public int Id { get; set; }
[Required]
public string ProductCode { get; set; }
public string ContentId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public string EmbeddableUrl { get; set; }
public string ProductCode { get; set; } = null!;
public string? ContentId { get; set; }
public string? Name { get; set; }
public string? Url { get; set; }
public string? EmbeddableUrl { get; set; }
public long Timestamp { get; set; }
public int? EmbedColor { get; set; }
public CompatStatus? CompatibilityStatus { get; set; }
public long? CompatibilityChangeDate { get; set; }
public int? MetacriticId { get; set; }
public Metacritic Metacritic { get; set; }
public Metacritic? Metacritic { get; set; }
public List<SyscallToProductMap> SyscallToProductMap { get; set; }
public List<SyscallToProductMap> SyscallToProductMap { get; set; } = null!;
}
public enum CompatStatus : byte
@@ -83,28 +83,28 @@ namespace CompatBot.Database
{
public int Id { get; set; }
[Required]
public string Function { get; set; }
public string Function { get; set; } = null!;
public List<SyscallToProductMap> SyscallToProductMap { get; set; }
public List<SyscallToProductMap> SyscallToProductMap { get; set; } = null!;
}
internal class SyscallToProductMap
{
public int ProductId { get; set; }
public Thumbnail Product { get; set; }
public Thumbnail Product { get; set; } = null!;
public int SyscallInfoId { get; set; }
public SyscallInfo SyscallInfo { get; set; }
public SyscallInfo SyscallInfo { get; set; } = null!;
}
internal class Metacritic
{
public int Id { get; set; }
[Required]
public string Title { get; set; }
public string Title { get; set; } = null!;
public byte? CriticScore { get; set; }
public byte? UserScore { get; set; }
public string Notes { get; set; }
public string? Notes { get; set; }
public Metacritic WithTitle(string title)
{

View File

@@ -127,7 +127,7 @@ namespace CompatBot.EventHandlers
if (!string.IsNullOrEmpty(args.Message.Content) && Paws.Matches(args.Message.Content) is MatchCollection mc)
{
using var db = new BotDb();
await using var db = new BotDb();
var matchedGroups = (from m in mc
from Group g in m.Groups
where g.Success && !string.IsNullOrEmpty(g.Value)
@@ -138,7 +138,7 @@ namespace CompatBot.EventHandlers
{
if (!db.Kot.Any(k => k.UserId == args.Author.Id))
{
db.Kot.Add(new Kot {UserId = args.Author.Id});
await db.Kot.AddAsync(new Kot {UserId = args.Author.Id}).ConfigureAwait(false);
await db.SaveChangesAsync().ConfigureAwait(false);
}
}
@@ -146,7 +146,7 @@ namespace CompatBot.EventHandlers
{
if (!db.Doggo.Any(d => d.UserId == args.Author.Id))
{
db.Doggo.Add(new Doggo {UserId = args.Author.Id});
await db.Doggo.AddAsync(new Doggo {UserId = args.Author.Id}).ConfigureAwait(false);
await db.SaveChangesAsync().ConfigureAwait(false);
}
}
@@ -159,7 +159,7 @@ namespace CompatBot.EventHandlers
if (needToThank)
{
DiscordEmoji emoji;
string thankYouMessage;
string? thankYouMessage;
lock (theDoor)
{
emoji = ThankYouReactions[rng.Next(ThankYouReactions.Length)];
@@ -198,7 +198,7 @@ namespace CompatBot.EventHandlers
internal static (bool needToChill, bool needToThank) NeedToSilence(DiscordMessage msg)
{
if (string.IsNullOrEmpty(msg?.Content))
if (string.IsNullOrEmpty(msg.Content))
return (false, false);
var needToChill = false;
@@ -211,7 +211,7 @@ namespace CompatBot.EventHandlers
else
needToThank = true;
});
var mentionsBot = msgContent.Contains("bot") || (msg.MentionedUsers?.Any(u => { try { return u.IsCurrent; } catch { return false; }}) ?? false);
var mentionsBot = msgContent.Contains("bot") || msg.MentionedUsers?.Any(u => { try { return u.IsCurrent; } catch { return false; }}) is true;
return (needToChill && mentionsBot, needToThank && mentionsBot);
}
}

View File

@@ -183,7 +183,7 @@ namespace CompatBot.EventHandlers
if (string.IsNullOrEmpty(message))
return (false, false, new List<DiscordInvite>(0));
var inviteCodes = new HashSet<string>(InviteLink.Matches(message).Select(m => m.Groups["invite_id"]?.Value).Where(s => !string.IsNullOrEmpty(s)));
var inviteCodes = new HashSet<string>(InviteLink.Matches(message).Select(m => m.Groups["invite_id"].Value).Where(s => !string.IsNullOrEmpty(s)));
var discordMeLinks = InviteLink.Matches(message).Select(m => m.Groups["me_id"]?.Value).Distinct().Where(s => !string.IsNullOrEmpty(s)).ToList();
var attemptedWorkaround = false;
if (author != null && InviteCodeCache.TryGetValue(author.Id, out HashSet<string> recentInvites))
@@ -236,25 +236,34 @@ namespace CompatBot.EventHandlers
{
["_token"] = csrfTokenMatch.Groups["csrf_token"].Value,
["serverEid"] = serverEidMatch.Groups["server_eid"].Value,
}),
}!),
};
postRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));
postRequest.Headers.UserAgent.Add(new ProductInfoHeaderValue("RPCS3CompatibilityBot", "2.0"));
using var postResponse = await httpClient.SendAsync(postRequest).ConfigureAwait(false);
if (postResponse.StatusCode == HttpStatusCode.Redirect)
{
var redirectId = postResponse.Headers.Location.Segments.Last();
using var getDiscordRequest = new HttpRequestMessage(HttpMethod.Get, "https://discord.me/server/join/redirect/" + redirectId);
getDiscordRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
getDiscordRequest.Headers.UserAgent.Add(new ProductInfoHeaderValue("RPCS3CompatibilityBot", "2.0"));
using var discordRedirect = await httpClient.SendAsync(getDiscordRequest).ConfigureAwait(false);
if (discordRedirect.StatusCode == HttpStatusCode.Redirect)
var redirectId = postResponse.Headers.Location?.Segments.Last();
if (redirectId != null)
{
inviteCodes.Add(discordRedirect.Headers.Location.Segments.Last());
hasInvalidInvites = false;
using var getDiscordRequest = new HttpRequestMessage(HttpMethod.Get, "https://discord.me/server/join/redirect/" + redirectId);
getDiscordRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
getDiscordRequest.Headers.UserAgent.Add(new ProductInfoHeaderValue("RPCS3CompatibilityBot", "2.0"));
using var discordRedirect = await httpClient.SendAsync(getDiscordRequest).ConfigureAwait(false);
if (discordRedirect.StatusCode == HttpStatusCode.Redirect)
{
var inviteCodeSegment = discordRedirect.Headers.Location?.Segments.Last();
if (inviteCodeSegment != null)
{
inviteCodes.Add(inviteCodeSegment);
hasInvalidInvites = false;
}
}
else
Config.Log.Warn($"Unexpected response code from GET discord redirect: {discordRedirect.StatusCode}");
}
else
Config.Log.Warn($"Unexpected response code from GET discord redirect: {discordRedirect.StatusCode}");
Config.Log.Warn($"Failed to get redirection URL from {postResponse.RequestMessage?.RequestUri}");
}
else
Config.Log.Warn($"Unexpected response code from POST: {postResponse.StatusCode}");

View File

@@ -34,7 +34,7 @@ namespace CompatBot.EventHandlers
return;
lastBotMessages = await args.Channel.GetMessagesBeforeCachedAsync(args.Message.Id, Config.ProductCodeLookupHistoryThrottle).ConfigureAwait(false);
StringBuilder previousRepliesBuilder = null;
StringBuilder? previousRepliesBuilder = null;
foreach (var msg in lastBotMessages)
{
if (msg.Author.IsCurrent)

View File

@@ -32,7 +32,7 @@ namespace CompatBot.EventHandlers
public static async Task OnMessageCreated(DiscordClient c, MessageCreateEventArgs args)
{
if (DefaultHandlerFilter.IsFluff(args?.Message))
if (DefaultHandlerFilter.IsFluff(args.Message))
return;
#if !DEBUG
@@ -92,7 +92,7 @@ namespace CompatBot.EventHandlers
CooldownBuckets[args.Channel.Id] = DateTime.UtcNow;
}
public static async Task<(string productCode, TitleInfo info)> LookupGameAsync(DiscordChannel channel, DiscordMessage message, string gameTitle)
public static async Task<(string? productCode, TitleInfo? info)> LookupGameAsync(DiscordChannel channel, DiscordMessage message, string gameTitle)
{
var lastBotMessages = await channel.GetMessagesBeforeAsync(message.Id, 20, DateTime.UtcNow.AddSeconds(-30)).ConfigureAwait(false);
foreach (var msg in lastBotMessages)
@@ -105,36 +105,38 @@ namespace CompatBot.EventHandlers
var searchCompatListTask = Client.GetCompatResultAsync(requestBuilder, Config.Cts.Token);
var localList = CompatList.GetLocalCompatResult(requestBuilder);
var status = await searchCompatListTask.ConfigureAwait(false);
status = status.Append(localList);
if ((status.ReturnCode == 0 || status.ReturnCode == 2) && status.Results.Any())
status = status?.Append(localList);
if (status is null
|| status.ReturnCode != 0 && status.ReturnCode != 2
|| !status.Results.Any())
return (null, null);
var sortedList = status.GetSortedList();
var bestMatch = sortedList.First();
var listWithStatus = sortedList
.TakeWhile(i => Math.Abs(i.score - bestMatch.score) < double.Epsilon)
.Where(i => !string.IsNullOrEmpty(i.info.Status) && i.info.Status != "Unknown")
.ToList();
if (listWithStatus.Count > 0)
bestMatch = listWithStatus.First();
var (code, info, score) = bestMatch;
Config.Log.Debug($"Looked up \"{gameTitle}\", got \"{info?.Title}\" with score {score}");
if (score < Config.GameTitleMatchThreshold)
return (null, null);
if (!string.IsNullOrEmpty(info?.Title))
{
var sortedList = status.GetSortedList();
var bestMatch = sortedList.First();
var listWithStatus = sortedList
.TakeWhile(i => Math.Abs(i.score - bestMatch.score) < double.Epsilon)
.Where(i => !string.IsNullOrEmpty(i.info.Status) && i.info.Status != "Unknown")
.ToList();
if (listWithStatus.Count > 0)
bestMatch = listWithStatus.First();
var (code, info, score) = bestMatch;
Config.Log.Debug($"Looked up \"{gameTitle}\", got \"{info?.Title}\" with score {score}");
if (score < Config.GameTitleMatchThreshold)
return (null, null);
if (!string.IsNullOrEmpty(info?.Title))
{
StatsStorage.GameStatCache.TryGetValue(info.Title, out int stat);
StatsStorage.GameStatCache.Set(info.Title, ++stat, StatsStorage.CacheTime);
}
return (code, info);
StatsStorage.GameStatCache.TryGetValue(info.Title, out int stat);
StatsStorage.GameStatCache.Set(info.Title, ++stat, StatsStorage.CacheTime);
}
return (code, info);
}
catch (Exception e)
{
Config.Log.Warn(e);
return (null, null);
}
return (null, null);
}
}
}

View File

@@ -27,28 +27,24 @@ namespace CompatBot.EventHandlers
return;
var lastBotMessages = await args.Channel.GetMessagesBeforeAsync(args.Message.Id, 20, DateTime.UtcNow.AddSeconds(-30)).ConfigureAwait(false);
foreach (var msg in lastBotMessages)
if (BotReactionsHandler.NeedToSilence(msg).needToChill)
return;
if (lastBotMessages.Any(msg => BotReactionsHandler.NeedToSilence(msg).needToChill))
return;
lastBotMessages = await args.Channel.GetMessagesBeforeCachedAsync(args.Message.Id, Config.ProductCodeLookupHistoryThrottle).ConfigureAwait(false);
StringBuilder previousRepliesBuilder = null;
foreach (var msg in lastBotMessages)
StringBuilder? previousRepliesBuilder = null;
foreach (var msg in lastBotMessages.Where(m => m.Author.IsCurrent))
{
if (msg.Author.IsCurrent)
{
previousRepliesBuilder ??= new StringBuilder();
previousRepliesBuilder.AppendLine(msg.Content);
var embeds = msg.Embeds;
if (embeds?.Count > 0)
foreach (var embed in embeds)
previousRepliesBuilder.AppendLine(embed.Title).AppendLine(embed.Description);
}
previousRepliesBuilder ??= new StringBuilder();
previousRepliesBuilder.AppendLine(msg.Content);
var embeds = msg.Embeds;
if (embeds?.Count > 0)
foreach (var embed in embeds)
previousRepliesBuilder.AppendLine(embed.Title).AppendLine(embed.Description);
}
var previousReplies = previousRepliesBuilder?.ToString() ?? "";
var codesToLookup = GetProductIds(args.Message.Content)
.Where(c => !previousReplies.Contains(c, StringComparison.InvariantCultureIgnoreCase))
.Where(pc => !previousReplies.Contains(pc, StringComparison.InvariantCultureIgnoreCase))
.Take(args.Channel.IsPrivate ? 50 : 5)
.ToList();
if (codesToLookup.Count == 0)
@@ -95,7 +91,7 @@ namespace CompatBot.EventHandlers
}
}
public static List<string> GetProductIds(string input)
public static List<string> GetProductIds(string? input)
{
if (string.IsNullOrEmpty(input))
return new List<string>(0);
@@ -106,7 +102,7 @@ namespace CompatBot.EventHandlers
.ToList();
}
public static async Task<DiscordEmbedBuilder> LookupGameInfoAsync(this DiscordClient client, string code, string gameTitle = null, bool forLog = false, string category = null)
public static async Task<DiscordEmbedBuilder> LookupGameInfoAsync(this DiscordClient client, string? code, string? gameTitle = null, bool forLog = false, string? category = null)
{
if (string.IsNullOrEmpty(code))
return TitleInfo.Unknown.AsEmbed(code, gameTitle, forLog);
@@ -167,7 +163,7 @@ namespace CompatBot.EventHandlers
titleInfoEmbed.Title.Contains("afrika", StringComparison.InvariantCultureIgnoreCase)
))
{
var sqvat = client.GetEmoji(":sqvat:", Config.Reactions.No);
var sqvat = client.GetEmoji(":sqvat:", Config.Reactions.No)!;
titleInfoEmbed.Title = "How about no (๑•ิཬ•ั๑)";
if (!string.IsNullOrEmpty(titleInfoEmbed.Thumbnail?.Url))
titleInfoEmbed.WithThumbnail("https://cdn.discordapp.com/attachments/417347469521715210/516340151589535745/onionoff.png");

View File

@@ -54,11 +54,12 @@ namespace CompatBot.EventHandlers
if (cnfe.CommandName.Length < 3)
return;
var pos = e.Context.Message?.Content?.IndexOf(cnfe.CommandName) ?? -1;
var content = e.Context.Message.Content;
var pos = content?.IndexOf(cnfe.CommandName) ?? -1;
if (pos < 0)
return;
var gameTitle = e.Context.Message.Content[pos..].TrimEager().Trim(40);
var gameTitle = content![pos..].TrimEager().Trim(40);
if (string.IsNullOrEmpty(gameTitle) || char.IsPunctuation(gameTitle[0]))
return;
@@ -123,7 +124,6 @@ namespace CompatBot.EventHandlers
return;
}
var ch = await e.Context.GetChannelForSpamAsync().ConfigureAwait(false);
await ch.SendMessageAsync(
$"I am not sure what you wanted me to do, please use one of the following commands:\n" +
@@ -165,6 +165,6 @@ namespace CompatBot.EventHandlers
return allKnownBotCommands;
}
private static List<string> allKnownBotCommands;
private static List<string>? allKnownBotCommands;
}
}

View File

@@ -26,14 +26,17 @@ namespace CompatBot.EventHandlers
if (args.UserBefore.Username == args.UserAfter.Username)
return;
var potentialTargets = GetPotentialVictims(c, c.GetMember(args.UserAfter), true, false);
var m = c.GetMember(args.UserAfter);
if (m is null)
return;
var potentialTargets = GetPotentialVictims(c, m, true, false);
if (!potentialTargets.Any())
return;
if (await IsFlashmobAsync(c, potentialTargets).ConfigureAwait(false))
return;
var m = c.GetMember(args.UserAfter);
await c.ReportAsync("🕵️ Potential user impersonation",
$"User {m.GetMentionWithNickname()} has changed their __username__ from " +
$"**{args.UserBefore.Username.Sanitize()}#{args.UserBefore.Discriminator}** to " +
@@ -77,7 +80,7 @@ namespace CompatBot.EventHandlers
ReportSeverity.Medium);
}
internal static List<DiscordMember> GetPotentialVictims(DiscordClient client, DiscordMember newMember, bool checkUsername, bool checkNickname, List<DiscordMember> listToCheckAgainst = null)
internal static List<DiscordMember> GetPotentialVictims(DiscordClient client, DiscordMember newMember, bool checkUsername, bool checkNickname, List<DiscordMember>? listToCheckAgainst = null)
{
var membersWithRoles = listToCheckAgainst ??
client.Guilds.SelectMany(guild => guild.Value.Members.Values)
@@ -125,28 +128,27 @@ namespace CompatBot.EventHandlers
private static string GetCanonical(string name)
{
string result;
if (UsernameLock.Wait(0))
try
{
if (UsernameMapping.TryGetValue(name, out result))
if (UsernameMapping.TryGetValue(name, out var result))
return result;
}
finally
{
UsernameLock.Release();
}
result = name.ToCanonicalForm();
var canonicalName = name.ToCanonicalForm();
if (UsernameLock.Wait(0))
try
{
UsernameMapping[name] = result;
UsernameMapping[name] = canonicalName;
}
finally
{
UsernameLock.Release();
}
return result;
return canonicalName;
}
}
}

View File

@@ -24,10 +24,10 @@ namespace CompatBot.EventHandlers
if (guildMember.IsWhitelisted())
return;
if (!(guild.Permissions?.HasFlag(Permissions.ChangeNickname) ?? true))
if (guild.Permissions?.HasFlag(Permissions.ChangeNickname) is false)
return;
using var db = new BotDb();
await using var db = new BotDb();
var forcedNickname = await db.ForcedNicknames.AsNoTracking().FirstOrDefaultAsync(x => x.UserId == guildMember.Id && x.GuildId == guildMember.Guild.Id).ConfigureAwait(false);
if (forcedNickname is null)
return;
@@ -51,46 +51,48 @@ namespace CompatBot.EventHandlers
{
if (!once)
await Task.Delay(Config.ForcedNicknamesRecheckTimeInHours, Config.Cts.Token).ConfigureAwait(false);
if (await Moderation.Audit.CheckLock.WaitAsync(0).ConfigureAwait(false))
try
{
foreach (var guild in client.Guilds.Values)
if (!await Moderation.Audit.CheckLock.WaitAsync(0).ConfigureAwait(false))
continue;
try
{
foreach (var guild in client.Guilds.Values)
try
{
try
if (guild.Permissions?.HasFlag(Permissions.ChangeNickname) is false)
continue;
await using var db = new BotDb();
var forcedNicknames = await db.ForcedNicknames
.Where(mem => mem.GuildId == guild.Id)
.ToListAsync()
.ConfigureAwait(false);
if (forcedNicknames.Count == 0)
continue;
foreach (var forced in forcedNicknames)
{
if (!(guild.Permissions?.HasFlag(Permissions.ChangeNickname) ?? true))
var member = client.GetMember(guild, forced.UserId);
if (member is null || member.DisplayName == forced.Nickname)
continue;
using var db = new BotDb();
var forcedNicknames = await db.ForcedNicknames
.Where(mem => mem.GuildId == guild.Id)
.ToListAsync()
.ConfigureAwait(false);
if (forcedNicknames.Count == 0)
continue;
foreach (var forced in forcedNicknames)
try
{
var member = client.GetMember(guild, forced.UserId);
if (member?.DisplayName != forced.Nickname)
try
{
await member.ModifyAsync(mem => mem.Nickname = forced.Nickname).ConfigureAwait(false);
Config.Log.Info($"Enforced nickname {forced.Nickname} for user {member.Id} ({member.Username}#{member.Discriminator})");
}
catch {}
await member.ModifyAsync(mem => mem.Nickname = forced.Nickname).ConfigureAwait(false);
Config.Log.Info($"Enforced nickname {forced.Nickname} for user {member.Id} ({member.Username}#{member.Discriminator})");
}
}
catch (Exception e)
{
Config.Log.Error(e);
catch { }
}
}
}
finally
{
Moderation.Audit.CheckLock.Release();
}
catch (Exception e)
{
Config.Log.Error(e);
}
}
finally
{
Moderation.Audit.CheckLock.Release();
}
} while (!Config.Cts.IsCancellationRequested && !once);
}
}

View File

@@ -22,15 +22,15 @@ namespace CompatBot.EventHandlers
{
try
{
var m = c.GetMember(args.UserAfter);
if (NeedsRename(m.DisplayName))
if (c.GetMember(args.UserAfter) is DiscordMember m
&& NeedsRename(m.DisplayName))
{
var suggestedName = StripZalgo(m.DisplayName, m.Id).Sanitize();
await c.ReportAsync("🔣 Potential display name issue",
$"User {m.GetMentionWithNickname()} has changed their __username__ and is now shown as **{m.DisplayName.Sanitize()}**\nAutomatically renamed to: **{suggestedName}**",
null,
ReportSeverity.Medium);
await DmAndRenameUserAsync(c, c.GetMember(args.UserAfter), suggestedName).ConfigureAwait(false);
await DmAndRenameUserAsync(c, m, suggestedName).ConfigureAwait(false);
}
}
catch (Exception e)
@@ -85,7 +85,7 @@ namespace CompatBot.EventHandlers
public static bool NeedsRename(string displayName)
{
displayName = displayName?.Normalize().TrimEager();
displayName = displayName.Normalize().TrimEager();
return displayName != StripZalgo(displayName, 0ul, NormalizationForm.FormC, 3);
}
@@ -111,7 +111,7 @@ namespace CompatBot.EventHandlers
public static string StripZalgo(string displayName, ulong userId, NormalizationForm normalizationForm = NormalizationForm.FormD, int level = 0)
{
displayName = displayName?.Normalize(normalizationForm).TrimEager();
displayName = displayName.Normalize(normalizationForm).TrimEager();
if (string.IsNullOrEmpty(displayName))
return "Rule #7 Breaker #" + userId.GetHashCode().ToString("x8");

View File

@@ -43,7 +43,7 @@ namespace CompatBot.ThumbScrapper
} while (!cancellationToken.IsCancellationRequested);
}
public static async Task<string> GetThumbAsync(string productCode)
public static async Task<string?> GetThumbAsync(string productCode)
{
try
{
@@ -72,63 +72,58 @@ namespace CompatBot.ThumbScrapper
return;
Config.Log.Debug("Scraping GameTDB for game titles...");
using (var fileStream = new FileStream(Path.GetTempFileName(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 16384, FileOptions.Asynchronous | FileOptions.RandomAccess | FileOptions.DeleteOnClose))
await using var fileStream = new FileStream(Path.GetTempFileName(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 16384, FileOptions.Asynchronous | FileOptions.RandomAccess | FileOptions.DeleteOnClose);
await using (var downloadStream = await HttpClient.GetStreamAsync(TitleDownloadLink, cancellationToken).ConfigureAwait(false))
await downloadStream.CopyToAsync(fileStream, 16384, cancellationToken).ConfigureAwait(false);
fileStream.Seek(0, SeekOrigin.Begin);
using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read);
var logEntry = zipArchive.Entries.FirstOrDefault(e => e.Name.EndsWith(".xml", StringComparison.InvariantCultureIgnoreCase));
if (logEntry == null)
throw new InvalidOperationException("No zip entries that match the .xml criteria");
await using var zipStream = logEntry.Open();
using var xmlReader = XmlReader.Create(zipStream, new XmlReaderSettings { Async = true });
xmlReader.ReadToFollowing("PS3TDB");
var version = xmlReader.GetAttribute("version");
if (!DateTime.TryParseExact(version, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var timestamp))
return;
if (ScrapeStateProvider.IsFresh("PS3TDB", timestamp))
{
using (var downloadStream = await HttpClient.GetStreamAsync(TitleDownloadLink).ConfigureAwait(false))
await downloadStream.CopyToAsync(fileStream, 16384, cancellationToken).ConfigureAwait(false);
fileStream.Seek(0, SeekOrigin.Begin);
using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read);
var logEntry = zipArchive.Entries.FirstOrDefault(e => e.Name.EndsWith(".xml", StringComparison.InvariantCultureIgnoreCase));
if (logEntry == null)
throw new InvalidOperationException("No zip entries that match the .xml criteria");
using var zipStream = logEntry.Open();
using var xmlReader = XmlReader.Create(zipStream, new XmlReaderSettings { Async = true });
xmlReader.ReadToFollowing("PS3TDB");
var version = xmlReader.GetAttribute("version");
if (!DateTime.TryParseExact(version, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var timestamp))
return;
if (ScrapeStateProvider.IsFresh("PS3TDB", timestamp))
{
await ScrapeStateProvider.SetLastRunTimestampAsync("PS3TDB").ConfigureAwait(false);
return;
}
while (!cancellationToken.IsCancellationRequested && xmlReader.ReadToFollowing("game"))
{
if (xmlReader.ReadToFollowing("id"))
{
var productId = (await xmlReader.ReadElementContentAsStringAsync().ConfigureAwait(false)).ToUpperInvariant();
if (!ProductCodeLookup.ProductCode.IsMatch(productId))
continue;
string title = null;
if (xmlReader.ReadToFollowing("locale") && xmlReader.ReadToFollowing("title"))
title = await xmlReader.ReadElementContentAsStringAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(title))
{
using var db = new ThumbnailDb();
var item = await db.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productId, cancellationToken).ConfigureAwait(false);
if (item == null)
{
await db.Thumbnail.AddAsync(new Thumbnail {ProductCode = productId, Name = title}, cancellationToken).ConfigureAwait(false);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
else
{
if (item.Name != title && item.Timestamp == 0)
{
item.Name = title;
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
}
}
}
}
await ScrapeStateProvider.SetLastRunTimestampAsync("PS3TDB").ConfigureAwait(false);
return;
}
while (!cancellationToken.IsCancellationRequested && xmlReader.ReadToFollowing("game"))
{
if (!xmlReader.ReadToFollowing("id"))
continue;
var productId = (await xmlReader.ReadElementContentAsStringAsync().ConfigureAwait(false)).ToUpperInvariant();
if (!ProductCodeLookup.ProductCode.IsMatch(productId))
continue;
string? title = null;
if (xmlReader.ReadToFollowing("locale") && xmlReader.ReadToFollowing("title"))
title = await xmlReader.ReadElementContentAsStringAsync().ConfigureAwait(false);
if (string.IsNullOrEmpty(title))
continue;
await using var db = new ThumbnailDb();
var item = await db.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productId, cancellationToken).ConfigureAwait(false);
if (item is null)
{
await db.Thumbnail.AddAsync(new Thumbnail {ProductCode = productId, Name = title}, cancellationToken).ConfigureAwait(false);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
else if (item.Name != title && item.Timestamp == 0)
{
item.Name = title;
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
}
await ScrapeStateProvider.SetLastRunTimestampAsync("PS3TDB").ConfigureAwait(false);
await ScrapeStateProvider.SetLastRunTimestampAsync(container).ConfigureAwait(false);
}
catch (Exception e)

View File

@@ -2,7 +2,7 @@
{
public sealed class AsciiColumn
{
public AsciiColumn(string name = null, bool disabled = false, bool alignToRight = false, int maxWidth = 80)
public AsciiColumn(string? name = null, bool disabled = false, bool alignToRight = false, int maxWidth = 80)
{
Name = name;
Disabled = disabled;
@@ -10,7 +10,7 @@
MaxWidth = maxWidth;
}
public string Name;
public string? Name;
public bool Disabled;
public bool AlignToRight;
public int MaxWidth;

View File

@@ -48,7 +48,7 @@ namespace CompatBot.Utils
width = new int[columns.Length];
for (var i = 0; i < columns.Length; i++)
{
this.columns[i] = columns[i].Name;
this.columns[i] = columns[i].Name ?? "";
disabled[i] = columns[i].Disabled;
maxWidth[i] = columns[i].MaxWidth;
width[i] = columns[i].Name.GetVisibleLength();

View File

@@ -31,7 +31,7 @@ namespace CompatBot.Utils
return sortedList;
}
public static double GetScore(string search, TitleInfo titleInfo)
public static double GetScore(string? search, TitleInfo titleInfo)
{
var score = Math.Max(
search.GetFuzzyCoefficientCached(titleInfo.Title),

View File

@@ -10,29 +10,17 @@ namespace CompatBot.Utils
{
public static class AutosplitResponseHelper
{
public static Task SendAutosplitMessageAsync(this CommandContext ctx, StringBuilder message, int blockSize = 2000, string blockEnd = "\n```", string blockStart = "```\n")
public static Task SendAutosplitMessageAsync(this CommandContext ctx, StringBuilder message, int blockSize = 2000, string? blockEnd = "\n```", string? blockStart = "```\n")
=> ctx.Channel.SendAutosplitMessageAsync(message, blockSize, blockEnd, blockStart);
public static Task SendAutosplitMessageAsync(this CommandContext ctx, string message, int blockSize = 2000, string? blockEnd = "\n```", string? blockStart = "```\n")
=> ctx.Channel.SendAutosplitMessageAsync(message, blockSize, blockEnd, blockStart);
public static async Task SendAutosplitMessageAsync(this DiscordChannel channel, StringBuilder message, int blockSize = 2000, string? blockEnd = "\n```", string? blockStart = "```\n")
=> await SendAutosplitMessageAsync(channel, message.ToString(), blockSize, blockEnd, blockStart).ConfigureAwait(false);
public static async Task SendAutosplitMessageAsync(this DiscordChannel channel, string message, int blockSize = 2000, string? blockEnd = "\n```", string? blockStart = "```\n")
{
return ctx.Channel.SendAutosplitMessageAsync(message, blockSize, blockEnd, blockStart);
}
public static Task SendAutosplitMessageAsync(this CommandContext ctx, string message, int blockSize = 2000, string blockEnd = "\n```", string blockStart = "```\n")
{
return ctx.Channel.SendAutosplitMessageAsync(message, blockSize, blockEnd, blockStart);
}
public static async Task SendAutosplitMessageAsync(this DiscordChannel channel, StringBuilder message, int blockSize = 2000, string blockEnd = "\n```", string blockStart = "```\n")
{
if (message == null)
throw new ArgumentNullException(nameof(message));
await SendAutosplitMessageAsync(channel, message.ToString(), blockSize, blockEnd, blockStart).ConfigureAwait(false);
}
public static async Task SendAutosplitMessageAsync(this DiscordChannel channel, string message, int blockSize = 2000, string blockEnd = "\n```", string blockStart = "```\n")
{
if (channel == null)
throw new ArgumentNullException(nameof(channel));
if (string.IsNullOrEmpty(message))
return;

View File

@@ -17,18 +17,18 @@ namespace CompatBot.Utils.Extensions
public class BuildInfo
{
public string Commit;
public string WindowsFilename;
public string LinuxFilename;
public string WindowsBuildDownloadLink;
public string LinuxBuildDownloadLink;
public string? Commit;
public string? WindowsFilename;
public string? LinuxFilename;
public string? WindowsBuildDownloadLink;
public string? LinuxBuildDownloadLink;
public DateTime? StartTime;
public DateTime? FinishTime;
public BuildStatus? Status;
public BuildResult? Result { get; set; }
}
public static async Task<List<BuildInfo>> GetMasterBuildsAsync(this BuildHttpClient azureDevOpsClient, string oldestMergeCommit, string newestMergeCommit, DateTime? oldestTimestamp, CancellationToken cancellationToken)
public static async Task<List<BuildInfo>?> GetMasterBuildsAsync(this BuildHttpClient? azureDevOpsClient, string? oldestMergeCommit, string? newestMergeCommit, DateTime? oldestTimestamp, CancellationToken cancellationToken)
{
if (azureDevOpsClient == null || string.IsNullOrEmpty(oldestMergeCommit) || string.IsNullOrEmpty(newestMergeCommit))
return null;
@@ -57,7 +57,7 @@ namespace CompatBot.Utils.Extensions
return builds.Select(b => azureDevOpsClient.GetArtifactsInfoAsync(b.SourceVersion, b, cancellationToken).GetAwaiter().GetResult()).ToList();
}
public static async Task<BuildInfo> GetMasterBuildInfoAsync(this BuildHttpClient azureDevOpsClient, string commit, DateTime? oldestTimestamp, CancellationToken cancellationToken)
public static async Task<BuildInfo?> GetMasterBuildInfoAsync(this BuildHttpClient? azureDevOpsClient, string? commit, DateTime? oldestTimestamp, CancellationToken cancellationToken)
{
if (azureDevOpsClient == null || string.IsNullOrEmpty(commit))
return null;
@@ -91,7 +91,7 @@ namespace CompatBot.Utils.Extensions
return result;
}
public static async Task<BuildInfo> GetPrBuildInfoAsync(this BuildHttpClient azureDevOpsClient, string commit, DateTime? oldestTimestamp, int pr, CancellationToken cancellationToken)
public static async Task<BuildInfo?> GetPrBuildInfoAsync(this BuildHttpClient? azureDevOpsClient, string? commit, DateTime? oldestTimestamp, int pr, CancellationToken cancellationToken)
{
if (azureDevOpsClient == null || string.IsNullOrEmpty(commit))
return null;
@@ -127,9 +127,6 @@ namespace CompatBot.Utils.Extensions
public static async Task<BuildInfo> GetArtifactsInfoAsync(this BuildHttpClient azureDevOpsClient, string commit, Build build, CancellationToken cancellationToken)
{
if (azureDevOpsClient == null)
return null;
var result = new BuildInfo
{
Commit = commit,
@@ -149,7 +146,7 @@ namespace CompatBot.Utils.Extensions
try
{
using var httpClient = HttpClientFactory.Create();
using var stream = await httpClient.GetStreamAsync(winDownloadUrl).ConfigureAwait(false);
await using var stream = await httpClient.GetStreamAsync(winDownloadUrl, cancellationToken).ConfigureAwait(false);
using var zipStream = ReaderFactory.Open(stream);
while (zipStream.MoveToNextEntry() && !cancellationToken.IsCancellationRequested)
{
@@ -176,7 +173,7 @@ namespace CompatBot.Utils.Extensions
try
{
using var httpClient = HttpClientFactory.Create();
using var stream = await httpClient.GetStreamAsync(linDownloadUrl).ConfigureAwait(false);
await using var stream = await httpClient.GetStreamAsync(linDownloadUrl, cancellationToken).ConfigureAwait(false);
using var zipStream = ReaderFactory.Open(stream);
while (zipStream.MoveToNextEntry() && !cancellationToken.IsCancellationRequested)
{

View File

@@ -14,37 +14,33 @@ namespace CompatBot.Utils
{
public static class DiscordClientExtensions
{
public static DiscordMember GetMember(this DiscordClient client, DiscordGuild guild, ulong userId)
public static DiscordMember? GetMember(this DiscordClient client, DiscordGuild? guild, ulong userId)
{
if (guild == null)
if (guild is null)
return GetMember(client, userId);
return GetMember(client, guild.Id, userId);
}
public static DiscordMember GetMember(this DiscordClient client, ulong guildId, ulong userId)
{
return (from g in client.Guilds
public static DiscordMember? GetMember(this DiscordClient client, ulong guildId, ulong userId)
=> (from g in client.Guilds
where g.Key == guildId
from u in g.Value.Members.Values
where u.Id == userId
select u
).FirstOrDefault();
}
public static DiscordMember GetMember(this DiscordClient client, ulong guildId, DiscordUser user) => GetMember(client, guildId, user.Id);
public static DiscordMember GetMember(this DiscordClient client, DiscordGuild guild, DiscordUser user) => GetMember(client, guild, user.Id);
public static DiscordMember? GetMember(this DiscordClient client, ulong guildId, DiscordUser user) => GetMember(client, guildId, user.Id);
public static DiscordMember? GetMember(this DiscordClient client, DiscordGuild? guild, DiscordUser user) => GetMember(client, guild, user.Id);
public static DiscordMember GetMember(this DiscordClient client, ulong userId)
{
return (from g in client.Guilds
public static DiscordMember? GetMember(this DiscordClient client, ulong userId)
=> (from g in client.Guilds
from u in g.Value.Members.Values
where u.Id == userId
select u
).FirstOrDefault();
}
public static DiscordMember GetMember(this DiscordClient client, DiscordUser user) => GetMember(client, user.Id);
public static DiscordMember? GetMember(this DiscordClient client, DiscordUser user) => GetMember(client, user.Id);
public static async Task<string> GetUserNameAsync(this DiscordClient client, DiscordChannel channel, ulong userId, bool? forDmPurposes = null, string defaultName = "Unknown user")
{
@@ -74,7 +70,7 @@ namespace CompatBot.Utils
}
}
public static async Task ReactWithAsync(this DiscordMessage message, DiscordEmoji emoji, string fallbackMessage = null, bool? showBoth = null)
public static async Task ReactWithAsync(this DiscordMessage message, DiscordEmoji emoji, string? fallbackMessage = null, bool? showBoth = null)
{
try
{
@@ -96,7 +92,7 @@ namespace CompatBot.Utils
return RemoveReactionAsync(ctx.Message, emoji);
}
public static Task ReactWithAsync(this CommandContext ctx, DiscordEmoji emoji, string fallbackMessage = null, bool? showBoth = null)
public static Task ReactWithAsync(this CommandContext ctx, DiscordEmoji emoji, string? fallbackMessage = null, bool? showBoth = null)
{
return ReactWithAsync(ctx.Message, emoji, fallbackMessage, showBoth ?? (ctx.Prefix == Config.AutoRemoveCommandPrefix));
}
@@ -104,17 +100,17 @@ namespace CompatBot.Utils
public static async Task<IReadOnlyCollection<DiscordMessage>> GetMessagesBeforeAsync(this DiscordChannel channel, ulong beforeMessageId, int limit = 100, DateTime? timeLimit = null)
{
if (timeLimit > DateTime.UtcNow)
throw new ArgumentException(nameof(timeLimit));
throw new ArgumentException("Time limit can't be set in the future", nameof(timeLimit));
var afterTime = timeLimit ?? DateTime.UtcNow.AddSeconds(-30);
var messages = await channel.GetMessagesBeforeCachedAsync(beforeMessageId, limit).ConfigureAwait(false);
return messages.TakeWhile(m => m.CreationTimestamp > afterTime).ToList().AsReadOnly();
}
public static async Task<DiscordMessage> ReportAsync(this DiscordClient client, string infraction, DiscordMessage message, string trigger, string context, ReportSeverity severity, string actionList = null)
public static async Task<DiscordMessage?> ReportAsync(this DiscordClient client, string infraction, DiscordMessage message, string trigger, string? context, ReportSeverity severity, string? actionList = null)
{
var logChannel = await client.GetChannelAsync(Config.BotLogId).ConfigureAwait(false);
if (logChannel == null)
if (logChannel is null)
return null;
var embedBuilder = MakeReportTemplate(client, infraction, message, severity, actionList);
@@ -134,13 +130,11 @@ namespace CompatBot.Utils
{
if (conents?.Count > 0)
foreach (var f in conents.Values)
#pragma warning disable VSTHRD103
f.Dispose();
#pragma warning restore VSTHRD103
await f.DisposeAsync();
}
}
public static async Task<DiscordMessage> ReportAsync(this DiscordClient client, string infraction, DiscordMessage message, IEnumerable<DiscordMember> reporters, string comment, ReportSeverity severity)
public static async Task<DiscordMessage> ReportAsync(this DiscordClient client, string infraction, DiscordMessage message, IEnumerable<DiscordMember> reporters, string? comment, ReportSeverity severity)
{
var getLogChannelTask = client.GetChannelAsync(Config.BotLogId);
var embedBuilder = MakeReportTemplate(client, infraction, message, severity);
@@ -152,7 +146,7 @@ namespace CompatBot.Utils
return await logChannel.SendMessageAsync(embed: embedBuilder.Build(), mentions: Config.AllowedMentions.Nothing).ConfigureAwait(false);
}
public static async Task<DiscordMessage> ReportAsync(this DiscordClient client, string infraction, string description, ICollection<DiscordMember> potentialVictims, ReportSeverity severity)
public static async Task<DiscordMessage> ReportAsync(this DiscordClient client, string infraction, string description, ICollection<DiscordMember>? potentialVictims, ReportSeverity severity)
{
var result = new DiscordEmbedBuilder
{
@@ -167,23 +161,15 @@ namespace CompatBot.Utils
}
public static string GetMentionWithNickname(this DiscordMember member)
=> string.IsNullOrEmpty(member.Nickname) ? $"<@{member.Id}> (`{member.Username.Sanitize()}#{member.Discriminator}`)" : $"<@{member.Id}> (`{member.Username.Sanitize()}#{member.Discriminator}`, shown as `{member.Nickname.Sanitize()}`)";
public static string GetUsernameWithNickname(this DiscordUser user, DiscordClient client, DiscordGuild? guild = null)
{
if (member == null)
return null;
return string.IsNullOrEmpty(member.Nickname) ? $"<@{member.Id}> (`{member.Username.Sanitize()}#{member.Discriminator}`)" : $"<@{member.Id}> (`{member.Username.Sanitize()}#{member.Discriminator}`, shown as `{member.Nickname.Sanitize()}`)";
}
public static string GetUsernameWithNickname(this DiscordUser user, DiscordClient client, DiscordGuild guild = null)
{
if (user == null)
return null;
return client.GetMember(guild, user).GetUsernameWithNickname()
?? $"`{user.Username.Sanitize()}#{user.Discriminator}`";
?? $"`{user.Username.Sanitize()}#{user.Discriminator}`";
}
public static string GetUsernameWithNickname(this DiscordMember member)
public static string? GetUsernameWithNickname(this DiscordMember? member)
{
if (member == null)
return null;
@@ -191,12 +177,10 @@ namespace CompatBot.Utils
return string.IsNullOrEmpty(member.Nickname) ? $"`{member.Username.Sanitize()}#{member.Discriminator}`" : $"`{member.Username.Sanitize()}#{member.Discriminator}` (shown as `{member.Nickname.Sanitize()}`)";
}
public static DiscordEmoji GetEmoji(this DiscordClient client, string emojiName, string fallbackEmoji = null)
{
return GetEmoji(client, emojiName, fallbackEmoji == null ? null : DiscordEmoji.FromUnicode(fallbackEmoji));
}
public static DiscordEmoji? GetEmoji(this DiscordClient client, string? emojiName, string? fallbackEmoji = null)
=> GetEmoji(client, emojiName, fallbackEmoji == null ? null : DiscordEmoji.FromUnicode(fallbackEmoji));
public static DiscordEmoji GetEmoji(this DiscordClient client, string emojiName, DiscordEmoji fallbackEmoji)
public static DiscordEmoji? GetEmoji(this DiscordClient client, string? emojiName, DiscordEmoji? fallbackEmoji)
{
if (string.IsNullOrEmpty(emojiName))
return fallbackEmoji;
@@ -215,15 +199,14 @@ namespace CompatBot.Utils
}
}
public static Task SendMessageAsync(this DiscordChannel channel, string message, byte[] attachment, string filename)
public static Task SendMessageAsync(this DiscordChannel channel, string message, byte[]? attachment, string? filename)
{
if (!string.IsNullOrEmpty(filename) && attachment?.Length > 0)
return channel.SendFileAsync(filename, new MemoryStream(attachment), message);
else
return channel.SendMessageAsync(message);
return channel.SendMessageAsync(message);
}
private static DiscordEmbedBuilder MakeReportTemplate(DiscordClient client, string infraction, DiscordMessage message, ReportSeverity severity, string actionList = null)
private static DiscordEmbedBuilder MakeReportTemplate(DiscordClient client, string infraction, DiscordMessage message, ReportSeverity severity, string? actionList = null)
{
var content = message.Content;
if (message.Channel.IsPrivate)
@@ -248,7 +231,7 @@ namespace CompatBot.Utils
if (string.IsNullOrEmpty(content))
content = "🤔 something fishy is going on here, there was no message or attachment";
DiscordMember author = null;
DiscordMember? author = null;
try
{
author = client.GetMember(message.Author);
@@ -261,7 +244,7 @@ namespace CompatBot.Utils
{
Title = infraction,
Color = GetColor(severity),
}.AddField("Violator", author == null ? message.Author.Mention : GetMentionWithNickname(author), true)
}.AddField("Violator", author is null ? message.Author.Mention : GetMentionWithNickname(author), true)
.AddField("Channel", message.Channel.IsPrivate ? "Bot's DM" : message.Channel.Mention, true)
//.AddField("Time (UTC)", message.CreationTimestamp.ToString("yyyy-MM-dd HH:mm:ss"), true)
.AddField("Content of the offending item", content.Trim(EmbedPager.MaxFieldLength));

View File

@@ -11,8 +11,9 @@ namespace CompatBot.Utils
{
public static class DiscordMessageExtensions
{
public static Task<DiscordMessage> UpdateOrCreateMessageAsync(this DiscordMessage message, DiscordChannel channel, string content = null, bool tts = false, DiscordEmbed embed = null)
public static Task<DiscordMessage> UpdateOrCreateMessageAsync(this DiscordMessage? message, DiscordChannel channel, string? content = null, bool tts = false, DiscordEmbed? embed = null)
{
Exception? lastException = null;
for (var i = 0; i<3; i++)
try
{
@@ -22,44 +23,43 @@ namespace CompatBot.Utils
}
catch (Exception e)
{
lastException = e;
if (i == 2)
Config.Log.Error(e);
else
Task.Delay(100).ConfigureAwait(false).GetAwaiter().GetResult();
}
return Task.FromResult((DiscordMessage)null);
throw lastException ?? new InvalidOperationException("Something gone horribly wrong");
}
public static async Task<(Dictionary<string, Stream> attachmentContent, List<string> failedFilenames)> DownloadAttachmentsAsync(this DiscordMessage msg)
public static async Task<(Dictionary<string, Stream>? attachmentContent, List<string>? failedFilenames)> DownloadAttachmentsAsync(this DiscordMessage msg)
{
Dictionary<string, Stream> attachmentContent = null;
List<string> attachmentFilenames = null;
if (msg.Attachments.Any())
{
attachmentContent = new Dictionary<string, Stream>(msg.Attachments.Count);
attachmentFilenames = new List<string>();
using var httpClient = HttpClientFactory.Create(new CompressionMessageHandler());
foreach (var att in msg.Attachments)
{
if (att.FileSize > Config.AttachmentSizeLimit)
{
attachmentFilenames.Add(att.FileName);
continue;
}
if (msg.Attachments.Count == 0)
return (null, null);
try
{
using var sourceStream = await httpClient.GetStreamAsync(att.Url).ConfigureAwait(false);
var fileStream = new FileStream(Path.GetTempFileName(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 16384, FileOptions.Asynchronous | FileOptions.RandomAccess | FileOptions.DeleteOnClose);
await sourceStream.CopyToAsync(fileStream, 16384, Config.Cts.Token).ConfigureAwait(false);
fileStream.Seek(0, SeekOrigin.Begin);
attachmentContent[att.FileName] = fileStream;
}
catch (Exception ex)
{
Config.Log.Warn(ex, $"Failed to download attachment {att.FileName} from deleted message {msg.JumpLink}");
attachmentFilenames.Add(att.FileName);
}
var attachmentContent = new Dictionary<string, Stream>(msg.Attachments.Count);
var attachmentFilenames = new List<string>();
using var httpClient = HttpClientFactory.Create(new CompressionMessageHandler());
foreach (var att in msg.Attachments)
{
if (att.FileSize > Config.AttachmentSizeLimit)
{
attachmentFilenames.Add(att.FileName);
continue;
}
try
{
await using var sourceStream = await httpClient.GetStreamAsync(att.Url).ConfigureAwait(false);
var fileStream = new FileStream(Path.GetTempFileName(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 16384, FileOptions.Asynchronous | FileOptions.RandomAccess | FileOptions.DeleteOnClose);
await sourceStream.CopyToAsync(fileStream, 16384, Config.Cts.Token).ConfigureAwait(false);
fileStream.Seek(0, SeekOrigin.Begin);
attachmentContent[att.FileName] = fileStream;
}
catch (Exception ex)
{
Config.Log.Warn(ex, $"Failed to download attachment {att.FileName} from deleted message {msg.JumpLink}");
attachmentFilenames.Add(att.FileName);
}
}
return (attachmentContent: attachmentContent, failedFilenames: attachmentFilenames);

View File

@@ -11,31 +11,29 @@ namespace CompatBot.Utils
{
public static class InteractivityExtensions
{
public static Task<(DiscordMessage message, DiscordMessage text, MessageReactionAddEventArgs reaction)> WaitForMessageOrReactionAsync(
public static Task<(DiscordMessage? message, DiscordMessage? text, MessageReactionAddEventArgs? reaction)> WaitForMessageOrReactionAsync(
this InteractivityExtension interactivity,
DiscordMessage message,
DiscordUser user,
params DiscordEmoji[] reactions)
=> WaitForMessageOrReactionAsync(interactivity, message, user, null, reactions);
public static async Task<(DiscordMessage message, DiscordMessage text, MessageReactionAddEventArgs reaction)> WaitForMessageOrReactionAsync(
public static async Task<(DiscordMessage? message, DiscordMessage? text, MessageReactionAddEventArgs? reaction)> WaitForMessageOrReactionAsync(
this InteractivityExtension interactivity,
DiscordMessage message,
DiscordUser user,
TimeSpan? timeout,
params DiscordEmoji[] reactions)
params DiscordEmoji?[] reactions)
{
if (message == null)
throw new ArgumentNullException(nameof(message));
if (reactions.Length == 0)
throw new ArgumentException("At least one reaction must be specified", nameof(reactions));
DiscordMessage? result = message;
try
{
reactions = reactions.Where(r => r != null).ToArray();
foreach (var emoji in reactions)
await message.ReactWithAsync(emoji).ConfigureAwait(false);
await message.ReactWithAsync(emoji!).ConfigureAwait(false);
var expectedChannel = message.Channel;
var waitTextResponseTask = interactivity.WaitForMessageAsync(m => m.Author == user && m.Channel == expectedChannel && !string.IsNullOrEmpty(m.Content), timeout);
var waitReactionResponse = interactivity.WaitForReactionAsync(arg => reactions.Contains(arg.Emoji), message, user, timeout);
@@ -50,10 +48,10 @@ namespace CompatBot.Utils
catch
{
await message.DeleteAsync().ConfigureAwait(false);
message = null;
result = null;
}
DiscordMessage text = null;
MessageReactionAddEventArgs reaction = null;
DiscordMessage? text = null;
MessageReactionAddEventArgs? reaction = null;
if (waitTextResponseTask.IsCompletedSuccessfully)
text = (await waitTextResponseTask).Result;
if (waitReactionResponse.IsCompletedSuccessfully)
@@ -65,12 +63,12 @@ namespace CompatBot.Utils
await text.DeleteAsync().ConfigureAwait(false);
}
catch {}
return (message, text, reaction);
return (result, text, reaction);
}
catch (Exception e)
{
Config.Log.Warn(e, "Failed to get interactive reaction");
return (message, null, null);
return (result, null, null);
}
}
}

View File

@@ -10,42 +10,37 @@ namespace CompatBot.Utils
{
public static List<T> GetCacheKeys<T>(this MemoryCache memoryCache)
{
if (memoryCache == null)
return null;
var field = memoryCache.GetType()
.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
.FirstOrDefault(fi => fi.Name == "_entries");
if (field == null)
if (field is null)
{
Config.Log.Error($"Looks like {nameof(MemoryCache)} internals have changed");
return new List<T>(0);
return new List<T>();
}
var value = (IDictionary)field.GetValue(memoryCache);
return value.Keys.OfType<T>().ToList();
var value = (IDictionary?)field.GetValue(memoryCache);
return value?.Keys.OfType<T>().ToList() ?? new List<T>();
}
public static Dictionary<TKey, ICacheEntry> GetCacheEntries<TKey>(this MemoryCache memoryCache)
public static Dictionary<TKey, ICacheEntry?> GetCacheEntries<TKey>(this MemoryCache memoryCache)
where TKey: notnull
{
if (memoryCache == null)
return null;
var field = memoryCache.GetType()
.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
.FirstOrDefault(fi => fi.Name == "_entries");
if (field == null)
var cacheEntries = (IDictionary?)field?.GetValue(memoryCache);
if (cacheEntries is null)
{
Config.Log.Error($"Looks like {nameof(MemoryCache)} internals have changed");
return new Dictionary<TKey, ICacheEntry>(0);
return new Dictionary<TKey, ICacheEntry?>(0);
}
var cacheEntries = (IDictionary)field.GetValue(memoryCache);
var result = new Dictionary<TKey, ICacheEntry>(cacheEntries.Count);
var result = new Dictionary<TKey, ICacheEntry?>(cacheEntries.Count);
foreach (DictionaryEntry e in cacheEntries)
result.Add((TKey)e.Key, (ICacheEntry)e.Value);
result.Add((TKey)e.Key, (ICacheEntry?)e.Value);
return result;
}
}

View File

@@ -41,7 +41,7 @@ namespace CompatBot.Utils
public static string StripMarks(this string str)
{
return str?.Replace("(R)", " ", StringComparison.InvariantCultureIgnoreCase)
return str.Replace("(R)", " ", StringComparison.InvariantCultureIgnoreCase)
.Replace("®", " ")
.Replace("(TM)", " ", StringComparison.InvariantCultureIgnoreCase)
.Replace("™", " ")
@@ -52,7 +52,7 @@ namespace CompatBot.Utils
public static string StripQuotes(this string str)
{
if (str == null || str.Length < 2)
if (str.Length < 2)
return str;
if (str.StartsWith('"') && str.EndsWith('"'))
@@ -117,7 +117,7 @@ namespace CompatBot.Utils
return CreateTrimmedString(str, start, end);
}
public static string AsString(this ReadOnlySequence<byte> buffer, Encoding encoding = null)
public static string AsString(this ReadOnlySequence<byte> buffer, Encoding? encoding = null)
{
encoding ??= Latin8BitEncoding;
if (buffer.IsSingleSegment)
@@ -135,9 +135,7 @@ namespace CompatBot.Utils
}
public static string ToUtf8(this string str)
{
return Utf8.GetString(Latin8BitEncoding.GetBytes(str));
}
=> Utf8.GetString(Latin8BitEncoding.GetBytes(str));
public static string ToLatin8BitEncoding(this string str)
{
@@ -154,11 +152,12 @@ namespace CompatBot.Utils
public static string GetSuffix(long num) => num == 1 ? "" : "s";
public static string FixSpaces(this string text) => text?.Replace(" ", " " + InvisibleSpacer)
.Replace("`", InvisibleSpacer + "`")
.Replace(Environment.NewLine, "\n");
public static string FixSpaces(this string text)
=> text.Replace(" ", " " + InvisibleSpacer)
.Replace("`", InvisibleSpacer + "`")
.Replace(Environment.NewLine, "\n");
public static int GetVisibleLength(this string s)
public static int GetVisibleLength(this string? s)
{
if (string.IsNullOrEmpty(s))
return 0;
@@ -217,9 +216,6 @@ namespace CompatBot.Utils
public static string TrimVisible(this string s, int maxLength)
{
if (string.IsNullOrEmpty(s))
return s;
if (maxLength < 1)
throw new ArgumentException("Max length can't be less than 1", nameof(maxLength));
@@ -238,12 +234,11 @@ namespace CompatBot.Utils
c++;
}
return result.Append("…").ToString();
return result.Append('…').ToString();
}
public static string PadLeftVisible(this string s, int totalWidth, char padding = ' ')
{
s ??= "";
var valueWidth = s.GetVisibleLength();
var diff = s.Length - valueWidth;
totalWidth += diff;
@@ -252,7 +247,6 @@ namespace CompatBot.Utils
public static string PadRightVisible(this string s, int totalWidth, char padding = ' ')
{
s ??= "";
var valueWidth = s.GetVisibleLength();
var diff = s.Length - valueWidth;
totalWidth += diff;
@@ -278,7 +272,7 @@ namespace CompatBot.Utils
public static string GetMoons(decimal? stars, bool haveFun = true)
{
if (!stars.HasValue)
return null;
return "";
var fullStars = (int)stars;
var halfStar = (int)Math.Round((stars.Value - fullStars)*4, MidpointRounding.ToEven);
@@ -314,7 +308,7 @@ namespace CompatBot.Utils
public static string GetStars(decimal? stars)
{
if (!stars.HasValue)
return null;
return "";
var fullStars = (int)Math.Round(stars.Value, MidpointRounding.ToEven);
var noStars = 5 - fullStars;
@@ -355,7 +349,7 @@ namespace CompatBot.Utils
return result;
}
internal static double GetFuzzyCoefficientCached(this string strA, string strB)
internal static double GetFuzzyCoefficientCached(this string? strA, string? strB)
{
strA = strA?.ToLowerInvariant() ?? "";
strB = strB?.ToLowerInvariant() ?? "";
@@ -396,8 +390,8 @@ namespace CompatBot.Utils
private class FuzzyCacheValue
{
public string StrA;
public string StrB;
public string? StrA;
public string? StrB;
public double Coefficient;
}
}

View File

@@ -1,17 +1,19 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace CompatBot.Utils
{
internal class FixedLengthBuffer<TKey, TValue>: IList<TValue>
where TKey: notnull
{
internal readonly object syncObj = new object();
public FixedLengthBuffer(Func<TValue, TKey> keyGenerator)
{
makeKey = keyGenerator ?? throw new ArgumentNullException(nameof(keyGenerator));
makeKey = keyGenerator;
}
public FixedLengthBuffer<TKey, TValue> CloneShallow()
@@ -19,8 +21,8 @@ namespace CompatBot.Utils
var result = new FixedLengthBuffer<TKey, TValue>(makeKey);
foreach (var key in keyList)
result.keyList.Add(key);
foreach (var kvp in lookup)
result.lookup[kvp.Key] = kvp.Value;
foreach (var (key, value) in lookup)
result.lookup[key] = value;
return result;
}
@@ -29,7 +31,7 @@ namespace CompatBot.Utils
public void Add(TValue item)
{
TKey key = makeKey(item);
var key = makeKey(item);
if (!lookup.ContainsKey(key))
keyList.Add(key);
lookup[key] = item;
@@ -72,7 +74,7 @@ namespace CompatBot.Utils
public TValue Evict(TKey key)
{
if (!lookup.TryGetValue(key, out var result))
return default;
return result;
lookup.Remove(key);
keyList.Remove(key);

View File

@@ -316,7 +316,7 @@ namespace CompatBot.Utils.ResultFormatters
items["gpu_info"] = items["driver_manuf"];
if (!string.IsNullOrEmpty(items["gpu_info"]))
{
items["gpu_info"] = items["gpu_info"].StripMarks();
items["gpu_info"] = items["gpu_info"]?.StripMarks();
items["driver_version_info"] = GetVulkanDriverVersion(items["vulkan_initialized_device"], multiItems["vulkan_found_device"]) ??
GetVulkanDriverVersion(items["gpu_info"], multiItems["vulkan_found_device"]) ??
GetOpenglDriverVersion(items["gpu_info"], items["driver_version_new"] ?? items["driver_version"]) ??
@@ -389,10 +389,10 @@ namespace CompatBot.Utils.ResultFormatters
var interpreter = sm.Contains("interpreter", StringComparison.InvariantCultureIgnoreCase);
items["shader_mode"] = (async, recompiler, interpreter) switch
{
(true, true, false) => "Async",
(true, _, true) => "Async+Interpreter",
( true, true, false) => "Async",
( true, _, true) => "Async+Interpreter",
(false, true, false) => "Recompiler only",
(false, _, true) => "Interpreter only",
(false, _, true) => "Interpreter only",
_ => items["shader_mode"],
};
}
@@ -401,7 +401,7 @@ namespace CompatBot.Utils.ResultFormatters
else
items["shader_mode"] = "Recompiler only";
static string reformatDecoder(string dec)
static string? reformatDecoder(string? dec)
{
if (string.IsNullOrEmpty(dec))
return dec;
@@ -418,7 +418,7 @@ namespace CompatBot.Utils.ResultFormatters
items["os_type"] = "Windows";
else if (items["lin_path"] != null)
items["os_type"] = "Linux";
if (items["os_type"] == "Windows" && GetWindowsVersion((items["driver_version_new"] ?? items["driver_version"])) is string winVersion)
if (items["os_type"] == "Windows" && GetWindowsVersion(items["driver_version_new"] ?? items["driver_version"]) is string winVersion)
items["os_windows_version"] = winVersion;
if (items["library_list"] is string libs)
{
@@ -451,7 +451,7 @@ namespace CompatBot.Utils.ResultFormatters
value = EnabledMark;
else if ("false".Equals(value, StringComparison.CurrentCultureIgnoreCase))
value = DisabledMark;
items[key] = value.Sanitize(false);
items[key] = value?.Sanitize(false);
}
}
@@ -468,7 +468,7 @@ namespace CompatBot.Utils.ResultFormatters
}
}
private static async Task<UpdateInfo> CheckForUpdateAsync(NameValueCollection items)
private static async Task<UpdateInfo?> CheckForUpdateAsync(NameValueCollection items)
{
if (string.IsNullOrEmpty(items["build_and_specs"]))
return null;
@@ -490,17 +490,20 @@ namespace CompatBot.Utils.ResultFormatters
return null;
}
private static bool VersionIsTooOld(NameValueCollection items, Match update, UpdateInfo updateInfo)
private static bool VersionIsTooOld(NameValueCollection items, Match update, UpdateInfo? updateInfo)
{
if ((updateInfo.GetUpdateDelta() is TimeSpan updateTimeDelta) && (updateTimeDelta < Config.BuildTimeDifferenceForOutdatedBuildsInDays))
if (updateInfo.GetUpdateDelta() is TimeSpan updateTimeDelta
&& updateTimeDelta < Config.BuildTimeDifferenceForOutdatedBuildsInDays)
return false;
if (Version.TryParse(items["build_version"], out var logVersion) && Version.TryParse(update.Groups["version"].Value, out var updateVersion))
if (Version.TryParse(items["build_version"], out var logVersion)
&& Version.TryParse(update.Groups["version"].Value, out var updateVersion))
{
if (logVersion < updateVersion)
return true;
if (int.TryParse(items["build_number"], out var logBuild) && int.TryParse(update.Groups["build"].Value, out var updateBuild))
if (int.TryParse(items["build_number"], out var logBuild)
&& int.TryParse(update.Groups["build"].Value, out var updateBuild))
{
if (logBuild + Config.BuildNumberDifferenceForOutdatedBuilds < updateBuild)
return true;
@@ -510,7 +513,7 @@ namespace CompatBot.Utils.ResultFormatters
return !SameCommits(items["build_commit"], update.Groups["commit"].Value);
}
private static bool SameCommits(string commitA, string commitB)
private static bool SameCommits(string? commitA, string? commitB)
{
if (string.IsNullOrEmpty(commitA) && string.IsNullOrEmpty(commitB))
return true;
@@ -522,20 +525,21 @@ namespace CompatBot.Utils.ResultFormatters
return commitA[..len] == commitB[..len];
}
private static string GetOpenglDriverVersion(string gpuInfo, string version)
private static string? GetOpenglDriverVersion(string? gpuInfo, string? version)
{
if (string.IsNullOrEmpty(version))
return null;
if (gpuInfo.Contains("Radeon", StringComparison.InvariantCultureIgnoreCase) ||
gpuInfo.Contains("AMD", StringComparison.InvariantCultureIgnoreCase) ||
gpuInfo.Contains("ATI ", StringComparison.InvariantCultureIgnoreCase))
gpuInfo ??= "";
if (gpuInfo.Contains("Radeon", StringComparison.InvariantCultureIgnoreCase)
|| gpuInfo.Contains("AMD", StringComparison.InvariantCultureIgnoreCase)
|| gpuInfo.Contains("ATI ", StringComparison.InvariantCultureIgnoreCase))
return AmdDriverVersionProvider.GetFromOpenglAsync(version).ConfigureAwait(false).GetAwaiter().GetResult();
return version;
}
private static string GetVulkanDriverVersion(string gpu, UniqueList<string> foundDevices)
private static string? GetVulkanDriverVersion(string? gpu, UniqueList<string> foundDevices)
{
if (string.IsNullOrEmpty(gpu) || !foundDevices.Any())
return null;
@@ -585,11 +589,12 @@ namespace CompatBot.Utils.ResultFormatters
return result;
}
private static string GetVulkanDriverVersionRaw(string gpuInfo, string version)
private static string? GetVulkanDriverVersionRaw(string? gpuInfo, string? version)
{
if (string.IsNullOrEmpty(version))
return null;
gpuInfo ??= "";
var ver = int.Parse(version);
if (IsAmd(gpuInfo))
{
@@ -621,7 +626,7 @@ namespace CompatBot.Utils.ResultFormatters
}
}
private static string GetWindowsVersion(string driverVersionString)
private static string? GetWindowsVersion(string? driverVersionString)
{
// see https://docs.microsoft.com/en-us/windows-hardware/drivers/display/wddm-2-1-features#driver-versioning
if (string.IsNullOrEmpty(driverVersionString) || !Version.TryParse(driverVersionString, out var driverVer))
@@ -651,7 +656,7 @@ namespace CompatBot.Utils.ResultFormatters
};
}
private static string GetWindowsVersion(Version windowsVersion) =>
private static string? GetWindowsVersion(Version windowsVersion) =>
windowsVersion.Major switch
{
5 => windowsVersion.Minor switch
@@ -747,9 +752,9 @@ namespace CompatBot.Utils.ResultFormatters
return $"{microseconds / 1_000_000.0:0.##} s";
}
private static List<string> SortLines(List<string> notes, DiscordEmoji piracyEmoji = null)
private static List<string> SortLines(List<string> notes, DiscordEmoji? piracyEmoji = null)
{
if (notes == null || notes.Count < 2)
if (notes.Count < 2)
return notes;
var priorityList = new List<string>(EmojiPriority);
@@ -795,36 +800,33 @@ namespace CompatBot.Utils.ResultFormatters
return new HashSet<string>(hashList.Split(Environment.NewLine), StringComparer.InvariantCultureIgnoreCase);
}
internal static DiscordEmbedBuilder AddAuthor(this DiscordEmbedBuilder builder, DiscordClient client, DiscordMessage message, ISource source, LogParseState state = null)
internal static DiscordEmbedBuilder AddAuthor(this DiscordEmbedBuilder builder, DiscordClient client, DiscordMessage? message, ISource source, LogParseState? state = null)
{
if (state?.Error == LogParseState.ErrorCode.PiracyDetected)
if (message == null || state?.Error == LogParseState.ErrorCode.PiracyDetected)
return builder;
if (message != null)
{
var author = message.Author;
var member = client.GetMember(message.Channel?.Guild, author);
string msg;
if (member == null)
msg = $"Log from {author.Username.Sanitize()} | {author.Id}\n";
else
msg = $"Log from {member.DisplayName.Sanitize()} | {member.Id}\n";
msg += " | " + (source?.SourceType ?? "Unknown source");
if (state?.ReadBytes > 0 && source?.LogFileSize > 0 && source.LogFileSize < 2L*1024*1024*1024 && state.ReadBytes <= source.LogFileSize)
msg += $" | Parsed {state.ReadBytes * 100.0 / source.LogFileSize:0.##}%";
else if (source?.SourceFilePosition > 0 && source.SourceFileSize > 0 && source.SourceFilePosition <= source.SourceFileSize)
msg += $" | Read {source.SourceFilePosition * 100.0 / source.SourceFileSize:0.##}%";
else if (state?.ReadBytes > 0)
msg += $" | Parsed {state.ReadBytes} byte{(state.ReadBytes == 1 ? "" : "s")}";
else if (source?.LogFileSize > 0)
msg += $" | {source.LogFileSize} byte{(source.LogFileSize == 1 ? "" : "s")}";
var author = message.Author;
var member = client.GetMember(message.Channel?.Guild, author);
string msg;
if (member == null)
msg = $"Log from {author.Username.Sanitize()} | {author.Id}\n";
else
msg = $"Log from {member.DisplayName.Sanitize()} | {member.Id}\n";
msg += " | " + (source?.SourceType ?? "Unknown source");
if (state?.ReadBytes > 0 && source?.LogFileSize > 0 && source.LogFileSize < 2L*1024*1024*1024 && state.ReadBytes <= source.LogFileSize)
msg += $" | Parsed {state.ReadBytes * 100.0 / source.LogFileSize:0.##}%";
else if (source?.SourceFilePosition > 0 && source.SourceFileSize > 0 && source.SourceFilePosition <= source.SourceFileSize)
msg += $" | Read {source.SourceFilePosition * 100.0 / source.SourceFileSize:0.##}%";
else if (state?.ReadBytes > 0)
msg += $" | Parsed {state.ReadBytes} byte{(state.ReadBytes == 1 ? "" : "s")}";
else if (source?.LogFileSize > 0)
msg += $" | {source.LogFileSize} byte{(source.LogFileSize == 1 ? "" : "s")}";
#if DEBUG
if (state?.ParsingTime.TotalMilliseconds > 0)
msg += $" | {state.ParsingTime.TotalSeconds:0.###}s";
msg += " | Test Bot Instance";
if (state?.ParsingTime.TotalMilliseconds > 0)
msg += $" | {state.ParsingTime.TotalSeconds:0.###}s";
msg += " | Test Bot Instance";
#endif
builder.WithFooter(msg);
}
builder.WithFooter(msg);
return builder;
}
@@ -864,8 +866,8 @@ namespace CompatBot.Utils.ResultFormatters
foreach (var error in fatalErrors[1..])
{
int idx = -1;
double similarity = 0.0;
var idx = -1;
var similarity = 0.0;
for (var i = 0; i < result.Count; i++)
{
similarity = result[i].fatalError.GetFuzzyCoefficientCached(error);

View File

@@ -21,7 +21,7 @@ namespace CompatBot.Utils.ResultFormatters
return new DiscordEmbedBuilder {Title = title, Url = issueInfo.HtmlUrl, Description = issueInfo.Title, Color = state.color};
}
public static (string state, DiscordColor color) GetState(this PrInfo prInfo)
public static (string? state, DiscordColor color) GetState(this PrInfo prInfo)
{
if (prInfo.State == "open")
return ("Open", Config.Colors.PrOpen);
@@ -37,7 +37,7 @@ namespace CompatBot.Utils.ResultFormatters
return (null, Config.Colors.DownloadLinks);
}
public static (string state, DiscordColor color) GetState(this IssueInfo issueInfo)
public static (string? state, DiscordColor color) GetState(this IssueInfo issueInfo)
{
if (issueInfo.State == "open")
return ("Open", Config.Colors.PrOpen);

View File

@@ -24,10 +24,12 @@ namespace CompatBot.Utils.ResultFormatters
{"Playable", Config.Colors.CompatStatusPlayable},
};
public static string ToUpdated(this TitleInfo info)
=> DateTime.TryParseExact(info.Date, ApiConfig.DateInputFormat, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out var date) ? date.ToString(ApiConfig.DateOutputFormat) : null;
public static string? ToUpdated(this TitleInfo info)
=> DateTime.TryParseExact(info.Date, ApiConfig.DateInputFormat, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out var date)
? date.ToString(ApiConfig.DateOutputFormat)
: null;
private static string ToPrString(this TitleInfo info, string defaultString, bool link = false)
private static string? ToPrString(this TitleInfo info, string? defaultString, bool link = false)
{
if ((info.Pr ?? 0) == 0)
return defaultString;
@@ -63,7 +65,7 @@ namespace CompatBot.Utils.ResultFormatters
return $"Product code {titleId} was not found in compatibility database";
}
public static DiscordEmbedBuilder AsEmbed(this TitleInfo info, string titleId, string gameTitle = null, bool forLog = false, string thumbnailUrl = null)
public static DiscordEmbedBuilder AsEmbed(this TitleInfo info, string? titleId, string? gameTitle = null, bool forLog = false, string? thumbnailUrl = null)
{
if (string.IsNullOrWhiteSpace(gameTitle))
gameTitle = null;
@@ -82,10 +84,9 @@ namespace CompatBot.Utils.ResultFormatters
Title = thumb.Name,
UsingLocalCache = true,
};
}
}
if (StatusColors.TryGetValue(info.Status, out var color))
if (info.Status is string status && StatusColors.TryGetValue(status, out var color))
{
// apparently there's no formatting in the footer, but you need to escape everything in description; ugh
var onlineOnlypart = info.Network == 1 ? " 🌐" : "";
@@ -105,7 +106,7 @@ namespace CompatBot.Utils.ResultFormatters
StatsStorage.GameStatCache.TryGetValue(cacheTitle, out int stat);
StatsStorage.GameStatCache.Set(cacheTitle, ++stat, StatsStorage.CacheTime);
}
var title = $"{productCodePart}{cacheTitle.Trim(200)}{onlineOnlypart}";
var title = $"{productCodePart}{cacheTitle?.Trim(200)}{onlineOnlypart}";
if (string.IsNullOrEmpty(title))
desc = "";
return new DiscordEmbedBuilder
@@ -151,18 +152,12 @@ namespace CompatBot.Utils.ResultFormatters
}
public static string AsString(this (string code, TitleInfo info, double score) resultInfo)
{
return resultInfo.info.AsString(resultInfo.code);
}
=> resultInfo.info.AsString(resultInfo.code);
public static string AsString(this KeyValuePair<string, TitleInfo> resultInfo)
{
return resultInfo.Value.AsString(resultInfo.Key);
}
=> resultInfo.Value.AsString(resultInfo.Key);
public static DiscordEmbed AsEmbed(this KeyValuePair<string, TitleInfo> resultInfo)
{
return resultInfo.Value.AsEmbed(resultInfo.Key);
}
=> resultInfo.Value.AsEmbed(resultInfo.Key);
}
}

View File

@@ -18,17 +18,17 @@ namespace CompatBot.Utils.ResultFormatters
{
private static readonly GithubClient.Client githubClient = new GithubClient.Client();
public static async Task<DiscordEmbedBuilder> AsEmbedAsync(this UpdateInfo info, DiscordClient client, bool includePrBody = false, DiscordEmbedBuilder builder = null, PrInfo currentPrInfo = null)
public static async Task<DiscordEmbedBuilder> AsEmbedAsync(this UpdateInfo? info, DiscordClient client, bool includePrBody = false, DiscordEmbedBuilder? builder = null, PrInfo? currentPrInfo = null)
{
if ((info?.LatestBuild?.Windows?.Download ?? info?.LatestBuild?.Linux?.Download) == null)
if ((info?.LatestBuild?.Windows?.Download ?? info?.LatestBuild?.Linux?.Download) is null)
return builder ?? new DiscordEmbedBuilder {Title = "Error", Description = "Error communicating with the update API. Try again later.", Color = Config.Colors.Maintenance};
var justAppend = builder != null;
var latestBuild = info.LatestBuild;
var latestBuild = info!.LatestBuild;
var latestPr = latestBuild?.Pr;
var currentPr = info.CurrentBuild?.Pr;
string url = null;
PrInfo latestPrInfo = null;
string? url = null;
PrInfo? latestPrInfo = null;
string prDesc = "";
if (!justAppend)
@@ -38,8 +38,7 @@ namespace CompatBot.Utils.ResultFormatters
latestPrInfo = await githubClient.GetPrInfoAsync(latestPr.Value, Config.Cts.Token).ConfigureAwait(false);
url = latestPrInfo?.HtmlUrl ?? "https://github.com/RPCS3/rpcs3/pull/" + latestPr;
var userName = latestPrInfo?.User?.Login ?? "???";
var emoji = GetUserNameEmoji(client, userName);
if (emoji != null)
if (GetUserNameEmoji(client, userName) is DiscordEmoji emoji)
userName += " " + emoji;
prDesc = $"PR #{latestPr} by {userName}";
}
@@ -53,7 +52,7 @@ namespace CompatBot.Utils.ResultFormatters
if (includePrBody
&& latestPrInfo?.Body is string prInfoBody
&& !string.IsNullOrEmpty(prInfoBody))
desc = $"**{desc.TrimEnd()}**\n\n{prInfoBody}";
desc = $"**{desc?.TrimEnd()}**\n\n{prInfoBody}";
desc = desc?.Trim();
if (!string.IsNullOrEmpty(desc))
{
@@ -87,7 +86,7 @@ namespace CompatBot.Utils.ResultFormatters
var uniqueLinks = new HashSet<string>(2);
foreach (Match m in commitMatches)
{
if (m.Groups["commit_mention"]?.Value is string lnk
if (m.Groups["commit_mention"].Value is string lnk
&& !string.IsNullOrEmpty(lnk)
&& uniqueLinks.Add(lnk))
{
@@ -109,7 +108,7 @@ namespace CompatBot.Utils.ResultFormatters
var uniqueLinks = new HashSet<string>(10);
foreach (Match m in imgMatches)
{
if (m.Groups["img_markup"]?.Value is string str
if (m.Groups["img_markup"].Value is string str
&& !string.IsNullOrEmpty(str)
&& uniqueLinks.Add(str))
{
@@ -121,20 +120,23 @@ namespace CompatBot.Utils.ResultFormatters
}
}
}
desc = desc.Trim(EmbedPager.MaxDescriptionLength);
desc = desc?.Trim(EmbedPager.MaxDescriptionLength);
builder ??= new DiscordEmbedBuilder {Title = prDesc, Url = url, Description = desc, Color = Config.Colors.DownloadLinks};
var currentCommit = currentPrInfo?.MergeCommitSha;
var latestCommit = latestPrInfo?.MergeCommitSha;
var buildTimestampKind = "Built";
var azureClient = Config.GetAzureDevOpsClient();
var currentAppveyorBuild = await azureClient.GetMasterBuildInfoAsync(currentCommit, currentPrInfo?.MergedAt, Config.Cts.Token).ConfigureAwait(false);
var latestAppveyorBuild = await azureClient.GetMasterBuildInfoAsync(latestCommit, latestPrInfo?.MergedAt, Config.Cts.Token).ConfigureAwait(false);
var latestBuildTimestamp = latestAppveyorBuild?.FinishTime;
var currentBuildTimestamp = currentAppveyorBuild?.FinishTime;
if (!latestBuildTimestamp.HasValue)
DateTime? latestBuildTimestamp = null, currentBuildTimestamp = null;
if (Config.GetAzureDevOpsClient() is {} azureClient)
{
buildTimestampKind = "Merged";
latestBuildTimestamp = currentPrInfo?.MergedAt;
var currentAppveyorBuild = await azureClient.GetMasterBuildInfoAsync(currentCommit, currentPrInfo?.MergedAt, Config.Cts.Token).ConfigureAwait(false);
var latestAppveyorBuild = await azureClient.GetMasterBuildInfoAsync(latestCommit, latestPrInfo?.MergedAt, Config.Cts.Token).ConfigureAwait(false);
latestBuildTimestamp = latestAppveyorBuild?.FinishTime;
currentBuildTimestamp = currentAppveyorBuild?.FinishTime;
if (!latestBuildTimestamp.HasValue)
{
buildTimestampKind = "Merged";
latestBuildTimestamp = currentPrInfo?.MergedAt;
}
}
if (!string.IsNullOrEmpty(latestBuild?.Datetime))
@@ -158,7 +160,7 @@ namespace CompatBot.Utils.ResultFormatters
.AddField("Linux download", GetLinkMessage(latestBuild?.Linux?.Download, true), true);
}
private static string GetLinkMessage(string link, bool simpleName)
private static string GetLinkMessage(string? link, bool simpleName)
{
if (string.IsNullOrEmpty(link))
return "No link available";
@@ -172,7 +174,7 @@ namespace CompatBot.Utils.ResultFormatters
return $"[⏬ {text}]({link}){" ".FixSpaces()}";
}
private static DiscordEmoji GetUserNameEmoji(DiscordClient client, string githubLogin)
private static DiscordEmoji? GetUserNameEmoji(DiscordClient client, string githubLogin)
=> client.GetEmoji(githubLogin switch
{
#if DEBUG
@@ -206,7 +208,7 @@ namespace CompatBot.Utils.ResultFormatters
return null;
}
public static TimeSpan? GetUpdateDelta(this UpdateInfo updateInfo)
public static TimeSpan? GetUpdateDelta(this UpdateInfo? updateInfo)
{
if (updateInfo?.LatestBuild?.Datetime is string latestDateTimeStr
&& DateTime.TryParse(latestDateTimeStr, out var latestDateTime)

View File

@@ -10,23 +10,23 @@ namespace CompatBot.Utils
private readonly List<T> list;
private readonly HashSet<T> set;
public UniqueList(IEqualityComparer<T> comparer = null)
public UniqueList(IEqualityComparer<T>? comparer = null)
{
list = new List<T>();
set = new HashSet<T>(comparer);
this.Comparer = comparer;
Comparer = comparer;
}
public UniqueList(int count, IEqualityComparer<T> comparer = null)
public UniqueList(int count, IEqualityComparer<T>? comparer = null)
{
list = new List<T>(count);
set = new HashSet<T>(count, comparer);
this.Comparer = comparer;
Comparer = comparer;
}
public UniqueList(IEnumerable<T> collection, IEqualityComparer<T> comparer = null)
public UniqueList(IEnumerable<T> collection, IEqualityComparer<T>? comparer = null)
{
this.Comparer = comparer;
Comparer = comparer;
if (collection is ICollection c)
{
list = new List<T>(c.Count);
@@ -110,7 +110,7 @@ namespace CompatBot.Utils
}
public int Length => list.Count;
public IEqualityComparer<T> Comparer { get; }
public IEqualityComparer<T>? Comparer { get; }
T GetAt(int index) => list[index];
}

View File

@@ -19,7 +19,7 @@ namespace CompatBot
public static readonly ConcurrentQueue<DateTime> DisconnectTimestamps = new ConcurrentQueue<DateTime>();
public static readonly Stopwatch TimeSinceLastIncomingMessage = Stopwatch.StartNew();
private static bool IsOk => DisconnectTimestamps.IsEmpty && TimeSinceLastIncomingMessage.Elapsed < Config.IncomingMessageCheckIntervalInMin;
private static DiscordClient discordClient = null;
private static DiscordClient? discordClient = null;
public static async Task Watch(DiscordClient client)
{
@@ -71,20 +71,21 @@ namespace CompatBot
{
if (level == nameof(LogLevel.Info))
{
if (message?.Contains("Session resumed") ?? false)
if (message.Contains("Session resumed"))
DisconnectTimestamps.Clear();
}
else if (level == nameof(LogLevel.Warn))
{
if (message?.Contains("Dispatch:PRESENCES_REPLACE") ?? false)
if (message.Contains("Dispatch:PRESENCES_REPLACE")
&& discordClient != null)
BotStatusMonitor.RefreshAsync(discordClient).ConfigureAwait(false).GetAwaiter().GetResult();
else if (message?.Contains("Pre-emptive ratelimit triggered") ?? false)
else if (message.Contains("Pre-emptive ratelimit triggered"))
Config.TelemetryClient?.TrackEvent("preemptive-rate-limit");
}
else if (level == nameof(LogLevel.Fatal))
{
if ((message?.Contains("Socket connection terminated") ?? false)
|| (message?.Contains("heartbeats were skipped. Issuing reconnect.") ?? false))
if ((message.Contains("Socket connection terminated"))
|| (message.Contains("heartbeats were skipped. Issuing reconnect.")))
DisconnectTimestamps.Enqueue(DateTime.UtcNow);
}
}
@@ -94,12 +95,15 @@ namespace CompatBot
do
{
await Task.Delay(Config.MetricsIntervalInSec).ConfigureAwait(false);
var gcMemInfo = GC.GetGCMemoryInfo();
using var process = Process.GetCurrentProcess();
if (Config.TelemetryClient is TelemetryClient tc)
{
tc.TrackMetric("gw-latency", client.Ping);
tc.TrackMetric("time-since-last-incoming-message", TimeSinceLastIncomingMessage.ElapsedMilliseconds);
tc.TrackMetric("memory-gc-total", GC.GetTotalMemory(false));
tc.TrackMetric("memory-gc-total", gcMemInfo.HeapSizeBytes);
tc.TrackMetric("memory-gc-load", gcMemInfo.MemoryLoadBytes);
tc.TrackMetric("memory-gc-commited", gcMemInfo.TotalCommittedBytes);
tc.TrackMetric("memory-process-private", process.PrivateMemorySize64);
tc.TrackMetric("memory-process-ws", process.WorkingSet64);
tc.TrackMetric("github-limit-remaining", GithubClient.Client.RateLimitRemaining);
@@ -112,22 +116,16 @@ namespace CompatBot
{
do
{
var gcMemInfo = GC.GetGCMemoryInfo();
using var process = Process.GetCurrentProcess();
Config.Log.Info($"Process memory stats:\n" +
$"GC: {GC.GetTotalMemory(false)}\n" +
$"GC Heap: {gcMemInfo.HeapSizeBytes}\n" +
$"Private: {process.PrivateMemorySize64}\n" +
$"Working set: {process.WorkingSet64}\n" +
$"Virtual: {process.VirtualMemorySize64}\n" +
$"Paged: {process.PagedMemorySize64}\n" +
$"Paged sytem: {process.PagedSystemMemorySize64}\n" +
$"Non-pated system: {process.NonpagedSystemMemorySize64}");
var processMemory = process.PrivateMemorySize64;
var gcMemory = GC.GetTotalMemory(false);
if (processMemory / (double)gcMemory > 2)
{
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(2, GCCollectionMode.Optimized, true, true); // force LOH compaction
}
await Task.Delay(TimeSpan.FromHours(1)).ConfigureAwait(false);
} while (!Config.Cts.IsCancellationRequested);
}