mirror of
https://github.com/RPCS3/discord-bot.git
synced 2026-01-31 01:25:22 +01:00
bot update, part 1
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user