update code with new language and api features

This commit is contained in:
13xforever 2023-04-20 21:22:50 +05:00
parent 0d99046a0b
commit d957936ede
No known key found for this signature in database
GPG Key ID: 2B2A36B482FE70C5
99 changed files with 719 additions and 768 deletions

View File

@ -23,8 +23,7 @@ public static class CirrusCi
static CirrusCi() static CirrusCi()
{ {
var collection = new ServiceCollection(); var collection = new ServiceCollection();
collection.AddClient(ExecutionStrategy.CacheAndNetwork) collection.AddClient(ExecutionStrategy.CacheAndNetwork).ConfigureHttpClient(c => c.BaseAddress = new("https://api.cirrus-ci.com/graphql"));
.ConfigureHttpClient(c => c.BaseAddress = new("https://api.cirrus-ci.com/graphql"));
ServiceProvider = collection.BuildServiceProvider(); ServiceProvider = collection.BuildServiceProvider();
} }

View File

@ -90,8 +90,8 @@ public static class ApiConfig
catch (Exception e) catch (Exception e)
{ {
Log.Fatal(e); Log.Fatal(e);
ReverseDirections = new Dictionary<string, char>(); ReverseDirections = new();
ReverseReleaseTypes = new Dictionary<string, char>(); ReverseReleaseTypes = new();
} }
} }
} }

View File

@ -31,8 +31,7 @@ public class CompressionMessageHandler : DelegatingHandler
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{ {
if (isServer if (isServer
&& request.Content?.Headers.ContentEncoding != null && request.Content?.Headers.ContentEncoding.FirstOrDefault() is string serverEncoding
&& request.Content.Headers.ContentEncoding.FirstOrDefault() is string serverEncoding
&& Compressors.FirstOrDefault(c => c.EncodingType.Equals(serverEncoding, StringComparison.OrdinalIgnoreCase)) is ICompressor serverDecompressor) && Compressors.FirstOrDefault(c => c.EncodingType.Equals(serverEncoding, StringComparison.OrdinalIgnoreCase)) is ICompressor serverDecompressor)
{ {
request.Content = new DecompressedContent(request.Content, serverDecompressor); request.Content = new DecompressedContent(request.Content, serverDecompressor);

View File

@ -8,16 +8,11 @@ public sealed class CompatApiCommitHashConverter : JsonConverter<string>
{ {
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
if (reader.TokenType == JsonTokenType.Number if (reader is not { TokenType: JsonTokenType.Number, HasValueSequence: false, ValueSpan: [(byte)'0'] })
&& !reader.HasValueSequence return reader.GetString();
&& reader.ValueSpan.Length == 1
&& reader.ValueSpan[0] == (byte)'0') _ = reader.GetInt32();
{ return null;
_ = reader.GetInt32();
return null;
}
return reader.GetString();
} }
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)

View File

@ -18,15 +18,8 @@ public static class NamingStyles
return value; return value;
} }
public static string Dashed(string value) public static string Dashed(string value) => Delimitied(value, '-');
{ public static string Underscore(string value) => Delimitied(value, '_');
return Delimitied(value, '-');
}
public static string Underscore(string value)
{
return Delimitied(value, '_');
}
private static string Delimitied(string value, char separator) private static string Delimitied(string value, char separator)
{ {

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics;
namespace CompatApiClient.Utils; namespace CompatApiClient.Utils;
@ -7,7 +8,7 @@ public static class Statistics
{ {
public static long Mean(this IEnumerable<long> data) public static long Mean(this IEnumerable<long> data)
{ {
System.Numerics.BigInteger sum = 0; BigInteger sum = 0;
var itemCount = 0; var itemCount = 0;
foreach (var value in data) foreach (var value in data)
{ {
@ -22,12 +23,12 @@ public static class Statistics
public static double StdDev(this IEnumerable<long> data) public static double StdDev(this IEnumerable<long> data)
{ {
System.Numerics.BigInteger σx = 0, σx2 = 0; BigInteger σx = 0, σx2 = 0;
var n = 0; var n = 0;
foreach (var value in data) foreach (var value in data)
{ {
σx += value; σx += value;
σx2 += (System.Numerics.BigInteger)value * value; σx2 += (BigInteger)value * value;
n++; n++;
} }
if (n == 0) if (n == 0)

View File

@ -14,21 +14,15 @@ public static class UriExtensions
public static NameValueCollection ParseQueryString(Uri uri) public static NameValueCollection ParseQueryString(Uri uri)
{ {
if (!uri.IsAbsoluteUri) if (!uri.IsAbsoluteUri)
uri = new Uri(FakeHost, uri); uri = new(FakeHost, uri);
return uri.ParseQueryString(); return uri.ParseQueryString();
} }
public static string? GetQueryParameter(this Uri uri, string name) public static string? GetQueryParameter(this Uri uri, string name)
{ => ParseQueryString(uri)[name];
var parameters = ParseQueryString(uri);
return parameters[name];
}
public static Uri AddQueryParameter(this Uri uri, string name, string value) public static Uri AddQueryParameter(this Uri uri, string name, string value)
{ => AddQueryValue(uri, WebUtility.UrlEncode(name) + "=" + WebUtility.UrlEncode(value));
var queryValue = WebUtility.UrlEncode(name) + "=" + WebUtility.UrlEncode(value);
return AddQueryValue(uri, queryValue);
}
public static Uri AddQueryParameters(Uri uri, IEnumerable<KeyValuePair<string, string>> parameters) public static Uri AddQueryParameters(Uri uri, IEnumerable<KeyValuePair<string, string>> parameters)
{ {
@ -105,16 +99,16 @@ public static class UriExtensions
if (isAbsolute) if (isAbsolute)
{ {
var builder = new UriBuilder(uri) { Query = value }; var builder = new UriBuilder(uri) { Query = value };
return new Uri(builder.ToString()); return new(builder.ToString());
} }
else else
{ {
var startWithSlash = uri.OriginalString.StartsWith("/"); var startWithSlash = uri.OriginalString.StartsWith("/");
uri = new Uri(FakeHost, uri); uri = new(FakeHost, uri);
var builder = new UriBuilder(uri) { Query = value }; var builder = new UriBuilder(uri) { Query = value };
var additionalStrip = startWithSlash ? 0 : 1; var additionalStrip = startWithSlash ? 0 : 1;
var newUri = builder.ToString()[(FakeHost.OriginalString.Length + additionalStrip)..]; var newUri = builder.ToString()[(FakeHost.OriginalString.Length + additionalStrip)..];
return new Uri(newUri, UriKind.Relative); return new(newUri, UriKind.Relative);
} }
} }
} }

View File

@ -50,13 +50,11 @@ public static class Utils
=> AsStorageUnit((long)bytes); => AsStorageUnit((long)bytes);
public static string AsStorageUnit(this long bytes) public static string AsStorageUnit(this long bytes)
{ => bytes switch
if (bytes < UnderKB) {
return $"{bytes} byte{(bytes == 1 ? "" : "s")}"; < UnderKB => $"{bytes} byte{(bytes == 1 ? "" : "s")}",
if (bytes < UnderMB) < UnderMB => $"{bytes / 1024.0:0.##} KB",
return $"{bytes / 1024.0:0.##} KB"; < UnderGB => $"{bytes / (1024.0 * 1024):0.##} MB",
if (bytes < UnderGB) _ => $"{bytes / (1024.0 * 1024 * 1024):0.##} GB"
return $"{bytes / 1024.0 / 1024:0.##} MB"; };
return $"{bytes / 1024.0 / 1024 / 1024:0.##} GB";
}
} }

View File

@ -4,12 +4,13 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CompatApiClient; using CompatApiClient;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Octokit;
namespace GithubClient; namespace GithubClient;
public class Client public class Client
{ {
private readonly Octokit.GitHubClient client; private readonly GitHubClient client;
private static readonly TimeSpan PrStatusCacheTime = TimeSpan.FromMinutes(3); private static readonly TimeSpan PrStatusCacheTime = TimeSpan.FromMinutes(3);
private static readonly TimeSpan IssueStatusCacheTime = TimeSpan.FromMinutes(30); private static readonly TimeSpan IssueStatusCacheTime = TimeSpan.FromMinutes(30);
@ -22,26 +23,22 @@ public class Client
public Client(string? githubToken) public Client(string? githubToken)
{ {
client = new Octokit.GitHubClient(new Octokit.ProductHeaderValue(ApiConfig.ProductName, ApiConfig.ProductVersion)); client = new(new ProductHeaderValue(ApiConfig.ProductName, ApiConfig.ProductVersion));
if (!string.IsNullOrEmpty(githubToken)) if (githubToken is {Length: >0})
{ client.Credentials = new(githubToken);
client.Credentials = new Octokit.Credentials(githubToken);
}
} }
public async Task<Octokit.PullRequest?> GetPrInfoAsync(int pr, CancellationToken cancellationToken) public async Task<PullRequest?> GetPrInfoAsync(int pr, CancellationToken cancellationToken)
{ {
if (StatusesCache.TryGetValue(pr, out Octokit.PullRequest? result)) if (StatusesCache.TryGetValue(pr, out PullRequest? result))
{ {
ApiConfig.Log.Debug($"Returned {nameof(Octokit.PullRequest)} for {pr} from cache"); ApiConfig.Log.Debug($"Returned {nameof(PullRequest)} for {pr} from cache");
return result; return result;
} }
try try
{ {
var request = client.PullRequest.Get("RPCS3", "rpcs3", pr); result = await client.PullRequest.Get("RPCS3", "rpcs3", pr).WaitAsync(cancellationToken).ConfigureAwait(false);
request.Wait(cancellationToken);
result = (await request.ConfigureAwait(false));
UpdateRateLimitStats(); UpdateRateLimitStats();
} }
catch (Exception e) catch (Exception e)
@ -50,61 +47,53 @@ public class Client
} }
if (result == null) if (result == null)
{ {
ApiConfig.Log.Debug($"Failed to get {nameof(Octokit.PullRequest)}, returning empty result"); ApiConfig.Log.Debug($"Failed to get {nameof(PullRequest)}, returning empty result");
return new(pr); return new(pr);
} }
StatusesCache.Set(pr, result, PrStatusCacheTime); StatusesCache.Set(pr, result, PrStatusCacheTime);
ApiConfig.Log.Debug($"Cached {nameof(Octokit.PullRequest)} for {pr} for {PrStatusCacheTime}"); ApiConfig.Log.Debug($"Cached {nameof(PullRequest)} for {pr} for {PrStatusCacheTime}");
return result; return result;
} }
public async Task<Octokit.Issue?> GetIssueInfoAsync(int issue, CancellationToken cancellationToken) public async Task<Issue?> GetIssueInfoAsync(int issue, CancellationToken cancellationToken)
{ {
if (IssuesCache.TryGetValue(issue, out Octokit.Issue? result)) if (IssuesCache.TryGetValue(issue, out Issue? result))
{ {
ApiConfig.Log.Debug($"Returned {nameof(Octokit.Issue)} for {issue} from cache"); ApiConfig.Log.Debug($"Returned {nameof(Issue)} for {issue} from cache");
return result; return result;
} }
try try
{ {
var request = client.Issue.Get("RPCS3", "rpcs3", issue); result = await client.Issue.Get("RPCS3", "rpcs3", issue).WaitAsync(cancellationToken).ConfigureAwait(false);
request.Wait(cancellationToken);
result = (await request.ConfigureAwait(false));
UpdateRateLimitStats(); UpdateRateLimitStats();
IssuesCache.Set(issue, result, IssueStatusCacheTime);
ApiConfig.Log.Debug($"Cached {nameof(Issue)} for {issue} for {IssueStatusCacheTime}");
return result;
} }
catch (Exception e) catch (Exception e)
{ {
ApiConfig.Log.Error(e); ApiConfig.Log.Error(e);
} }
if (result == null) ApiConfig.Log.Debug($"Failed to get {nameof(Issue)}, returning empty result");
{ return new();
ApiConfig.Log.Debug($"Failed to get {nameof(Octokit.Issue)}, returning empty result");
return new() { };
}
IssuesCache.Set(issue, result, IssueStatusCacheTime);
ApiConfig.Log.Debug($"Cached {nameof(Octokit.Issue)} for {issue} for {IssueStatusCacheTime}");
return result;
} }
public Task<IReadOnlyList<Octokit.PullRequest>?> GetOpenPrsAsync(CancellationToken cancellationToken) => GetPrsWithStatusAsync(new Octokit.PullRequestRequest public Task<IReadOnlyList<PullRequest>?> GetOpenPrsAsync(CancellationToken cancellationToken)
=> GetPrsWithStatusAsync(new() { State = ItemStateFilter.Open }, cancellationToken);
public Task<IReadOnlyList<PullRequest>?> GetClosedPrsAsync(CancellationToken cancellationToken) => GetPrsWithStatusAsync(new()
{ {
State = Octokit.ItemStateFilter.Open State = ItemStateFilter.Closed,
SortProperty = PullRequestSort.Updated,
SortDirection = SortDirection.Descending
}, cancellationToken); }, cancellationToken);
public Task<IReadOnlyList<Octokit.PullRequest>?> GetClosedPrsAsync(CancellationToken cancellationToken) => GetPrsWithStatusAsync(new Octokit.PullRequestRequest private async Task<IReadOnlyList<PullRequest>?> GetPrsWithStatusAsync(PullRequestRequest filter, CancellationToken cancellationToken)
{ {
State = Octokit.ItemStateFilter.Closed, var statusUri = "https://api.github.com/repos/RPCS3/rpcs3/pulls?state=" + filter;
SortProperty = Octokit.PullRequestSort.Updated, if (StatusesCache.TryGetValue(statusUri, out IReadOnlyList<PullRequest>? result))
SortDirection = Octokit.SortDirection.Descending
}, cancellationToken);
private async Task<IReadOnlyList<Octokit.PullRequest>?> GetPrsWithStatusAsync(Octokit.PullRequestRequest filter, CancellationToken cancellationToken)
{
var statusURI = "https://api.github.com/repos/RPCS3/rpcs3/pulls?state=" + filter.ToString();
if (StatusesCache.TryGetValue(statusURI, out IReadOnlyList<Octokit.PullRequest>? result))
{ {
ApiConfig.Log.Debug("Returned list of opened PRs from cache"); ApiConfig.Log.Debug("Returned list of opened PRs from cache");
return result; return result;
@ -112,23 +101,17 @@ public class Client
try try
{ {
var request = client.PullRequest.GetAllForRepository("RPCS3", "rpcs3", filter); result = await client.PullRequest.GetAllForRepository("RPCS3", "rpcs3", filter).WaitAsync(cancellationToken).ConfigureAwait(false);
request.Wait(cancellationToken);
result = (await request.ConfigureAwait(false));
UpdateRateLimitStats(); UpdateRateLimitStats();
StatusesCache.Set(statusUri, result, PrStatusCacheTime);
foreach (var prInfo in result)
StatusesCache.Set(prInfo.Number, prInfo, PrStatusCacheTime);
ApiConfig.Log.Debug($"Cached list of open PRs for {PrStatusCacheTime}");
} }
catch (Exception e) catch (Exception e)
{ {
ApiConfig.Log.Error(e); ApiConfig.Log.Error(e);
} }
if (result != null)
{
StatusesCache.Set(statusURI, result, PrStatusCacheTime);
foreach (var prInfo in result)
StatusesCache.Set(prInfo.Number, prInfo, PrStatusCacheTime);
ApiConfig.Log.Debug($"Cached list of open PRs for {PrStatusCacheTime}");
}
return result; return result;
} }
@ -136,16 +119,12 @@ public class Client
{ {
var apiInfo = client.GetLastApiInfo(); var apiInfo = client.GetLastApiInfo();
if (apiInfo == null) if (apiInfo == null)
{
return; return;
}
RateLimit = apiInfo.RateLimit.Limit; RateLimit = apiInfo.RateLimit.Limit;
RateLimitRemaining = apiInfo.RateLimit.Remaining; RateLimitRemaining = apiInfo.RateLimit.Remaining;
RateLimitResetTime = DateTimeOffset.FromUnixTimeSeconds(apiInfo.RateLimit.ResetAsUtcEpochSeconds).UtcDateTime; RateLimitResetTime = DateTimeOffset.FromUnixTimeSeconds(apiInfo.RateLimit.ResetAsUtcEpochSeconds).UtcDateTime;
if (RateLimitRemaining < 10) if (RateLimitRemaining < 10)
ApiConfig.Log.Warn($"Github rate limit is low: {RateLimitRemaining} out of {RateLimit}, will be reset on {RateLimitResetTime:u}"); ApiConfig.Log.Warn($"Github rate limit is low: {RateLimitRemaining} out of {RateLimit}, will be reset on {RateLimitResetTime:u}");
} }
} }

View File

@ -29,7 +29,7 @@ public class IrdClient
public IrdClient() public IrdClient()
{ {
client = HttpClientFactory.Create(new CompressionMessageHandler()); client = HttpClientFactory.Create(new CompressionMessageHandler());
jsonOptions = new JsonSerializerOptions jsonOptions = new()
{ {
PropertyNamingPolicy = SpecialJsonNamingPolicy.SnakeCase, PropertyNamingPolicy = SpecialJsonNamingPolicy.SnakeCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
@ -209,9 +209,8 @@ public class IrdClient
var idx = html.LastIndexOf("</span>", StringComparison.Ordinal); var idx = html.LastIndexOf("</span>", StringComparison.Ordinal);
var result = html[(idx + 7)..].Trim(); var result = html[(idx + 7)..].Trim();
if (string.IsNullOrEmpty(result)) if (result is {Length: >0})
return null; return result;
return null;
return result;
} }
} }

View File

@ -38,8 +38,10 @@ public class Client
"pl-PL", "pt-BR", "pt-PT", "ru-RU", "ru-UA", "sv-SE", "tr-TR", "zh-Hans-CN", "zh-Hans-HK", "zh-Hant-HK", "zh-Hant-TW", "pl-PL", "pt-BR", "pt-PT", "ru-RU", "ru-UA", "sv-SE", "tr-TR", "zh-Hans-CN", "zh-Hans-HK", "zh-Hant-HK", "zh-Hant-TW",
}; };
// Dest=87;ImageVersion=0001091d;SystemSoftwareVersion=4.8500;CDN=http://duk01.ps3.update.playstation.net/update/ps3/image/uk/2019_0828_c975768e5d70e105a72656f498cc9be9/PS3UPDAT.PUP;CDN_Timeout=30; // Dest=87;ImageVersion=0001091d;SystemSoftwareVersion=4.8500;CDN=http://duk01.ps3.update.playstation.net/update/ps3/image/uk/2019_0828_c975768e5d70e105a72656f498cc9be9/PS3UPDAT.PUP;CDN_Timeout=30;
private static readonly Regex FwVersionInfo = new(@"Dest=(?<dest>\d+);ImageVersion=(?<image>[0-9a-f]+);SystemSoftwareVersion=(?<version>\d+\.\d+);CDN=(?<url>http[^;]+);CDN_Timeout=(?<timeout>\d+)", private static readonly Regex FwVersionInfo = new(
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline | RegexOptions.IgnoreCase); @"Dest=(?<dest>\d+);ImageVersion=(?<image>[0-9a-f]+);SystemSoftwareVersion=(?<version>\d+\.\d+);CDN=(?<url>http[^;]+);CDN_Timeout=(?<timeout>\d+)",
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline | RegexOptions.IgnoreCase
);
// directly from vsh.self // directly from vsh.self
private static readonly string[] KnownFwLocales = { "jp", "us", "eu", "kr", "uk", "mx", "au", "sa", "tw", "ru", "cn", "br", }; private static readonly string[] KnownFwLocales = { "jp", "us", "eu", "kr", "uk", "mx", "au", "sa", "tw", "ru", "cn", "br", };
@ -47,19 +49,19 @@ public class Client
public Client() public Client()
{ {
client = HttpClientFactory.Create(new CustomTlsCertificatesHandler(), new CompressionMessageHandler()); client = HttpClientFactory.Create(new CustomTlsCertificatesHandler(), new CompressionMessageHandler());
dashedJson = new JsonSerializerOptions dashedJson = new()
{ {
PropertyNamingPolicy = SpecialJsonNamingPolicy.Dashed, PropertyNamingPolicy = SpecialJsonNamingPolicy.Dashed,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
IncludeFields = true, IncludeFields = true,
}; };
snakeJson = new JsonSerializerOptions snakeJson = new()
{ {
PropertyNamingPolicy = SpecialJsonNamingPolicy.SnakeCase, PropertyNamingPolicy = SpecialJsonNamingPolicy.SnakeCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
IncludeFields = true, IncludeFields = true,
}; };
xmlFormatters = new MediaTypeFormatterCollection(new[] {new XmlMediaTypeFormatter {UseXmlSerializer = true}}); xmlFormatters = new(new[] {new XmlMediaTypeFormatter {UseXmlSerializer = true}});
} }
public static string[] GetLocales() => KnownStoreLocales; // Sony removed the ability to get the full store list, now relying on geolocation service instead public static string[] GetLocales() => KnownStoreLocales; // Sony removed the ability to get the full store list, now relying on geolocation service instead
@ -101,7 +103,6 @@ public class Client
{ {
message.Headers.Add("Cookie", sessionCookies); message.Headers.Add("Cookie", sessionCookies);
response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false); response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false);
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false); await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
var tries = 0; var tries = 0;
while (response.StatusCode == HttpStatusCode.Redirect && tries < 10 && !cancellationToken.IsCancellationRequested) while (response.StatusCode == HttpStatusCode.Redirect && tries < 10 && !cancellationToken.IsCancellationRequested)
@ -116,7 +117,7 @@ public class Client
tries++; tries++;
} }
if (response.StatusCode == HttpStatusCode.Redirect) if (response.StatusCode == HttpStatusCode.Redirect)
return new List<string>(0); return new(0);
} }
using (response) using (response)
@ -127,7 +128,7 @@ public class Client
var matches = ContainerIdLink.Matches(html); var matches = ContainerIdLink.Matches(html);
var result = new List<string>(); var result = new List<string>();
foreach (Match m in matches) foreach (Match m in matches)
if (m.Groups["id"].Value is string id && !string.IsNullOrEmpty(id)) if (m.Groups["id"].Value is {Length: >0} id)
result.Add(id); result.Add(id);
return result; return result;
} }
@ -344,7 +345,7 @@ public class Client
allVersions = allVersions.OrderByDescending(fwi => fwi.Version).ToList(); allVersions = allVersions.OrderByDescending(fwi => fwi.Version).ToList();
if (allVersions.Count == 0) if (allVersions.Count == 0)
return new List<FirmwareInfo>(0); return new(0);
var maxFw = allVersions.First(); var maxFw = allVersions.First();
var result = allVersions.Where(fwi => fwi.Version == maxFw.Version).ToList(); var result = allVersions.Where(fwi => fwi.Version == maxFw.Version).ToList();
@ -372,7 +373,7 @@ public class Client
{ {
["country_code"] = country, ["country_code"] = country,
["language_code"] = language, ["language_code"] = language,
}!) })
}; };
using (authMessage) using (authMessage)
using (response = await client.SendAsync(authMessage, cancellationToken).ConfigureAwait(false)) using (response = await client.SendAsync(authMessage, cancellationToken).ConfigureAwait(false))
@ -418,7 +419,7 @@ public class Client
if (string.IsNullOrEmpty(data)) if (string.IsNullOrEmpty(data))
return null; return null;
if (FwVersionInfo.Match(data) is not Match m || !m.Success) if (FwVersionInfo.Match(data) is not { Success: true } m)
return null; return null;
var ver = m.Groups["version"].Value; var ver = m.Groups["version"].Value;
@ -429,7 +430,7 @@ public class Client
else else
ver = ver[..4] + "." + ver[4..].TrimEnd('0'); //4.851 -> 4.85.1 ver = ver[..4] + "." + ver[4..].TrimEnd('0'); //4.851 -> 4.85.1
} }
return new FirmwareInfo { Version = ver, DownloadUrl = m.Groups["url"].Value, Locale = fwLocale}; return new() { Version = ver, DownloadUrl = m.Groups["url"].Value, Locale = fwLocale};
} }
catch (Exception e) catch (Exception e)

View File

@ -21,7 +21,7 @@ public sealed class Client
public Client() public Client()
{ {
client = HttpClientFactory.Create(new CompressionMessageHandler()); client = HttpClientFactory.Create(new CompressionMessageHandler());
jsonOptions = new JsonSerializerOptions jsonOptions = new()
{ {
PropertyNamingPolicy = SpecialJsonNamingPolicy.SnakeCase, PropertyNamingPolicy = SpecialJsonNamingPolicy.SnakeCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
@ -36,7 +36,7 @@ public sealed class Client
{ {
try try
{ {
var uri = new Uri($"https://cloud-api.yandex.net/v1/disk/public/resources").SetQueryParameters( var uri = new Uri("https://cloud-api.yandex.net/v1/disk/public/resources").SetQueryParameters(
("public_key", publicUri.ToString()), ("public_key", publicUri.ToString()),
("fields", "size,name,file") ("fields", "size,name,file")
); );

View File

@ -23,8 +23,7 @@ internal class LimitedToSpamChannel: CheckBaseAttribute
try try
{ {
var msgList = await ctx.Channel.GetMessagesCachedAsync(10).ConfigureAwait(false); var msgList = await ctx.Channel.GetMessagesCachedAsync(10).ConfigureAwait(false);
if (msgList.Any(m => m.Author.IsCurrent if (msgList.Any(m => m is {Author.IsCurrent: true, Content: {Length: >0} s }
&& m.Content is string s
&& s.Contains(ctx.Command.QualifiedName, StringComparison.OrdinalIgnoreCase))) && s.Contains(ctx.Command.QualifiedName, StringComparison.OrdinalIgnoreCase)))
{ {
await ctx.ReactWithAsync(Config.Reactions.Failure).ConfigureAwait(false); await ctx.ReactWithAsync(Config.Reactions.Failure).ConfigureAwait(false);

View File

@ -11,7 +11,5 @@ internal class RequiresBotModRole: CheckBaseAttributeWithReactions
public RequiresBotModRole() : base(reactOnFailure: Config.Reactions.Denied) { } public RequiresBotModRole() : base(reactOnFailure: Config.Reactions.Denied) { }
protected override Task<bool> IsAllowed(CommandContext ctx, bool help) protected override Task<bool> IsAllowed(CommandContext ctx, bool help)
{ => Task.FromResult(ModProvider.IsMod(ctx.User.Id));
return Task.FromResult(ModProvider.IsMod(ctx.User.Id));
}
} }

View File

@ -9,7 +9,5 @@ namespace CompatBot.Commands.Attributes;
internal class RequiresNotMedia: CheckBaseAttribute internal class RequiresNotMedia: CheckBaseAttribute
{ {
public override Task<bool> ExecuteCheckAsync(CommandContext ctx, bool help) public override Task<bool> ExecuteCheckAsync(CommandContext ctx, bool help)
{ => Task.FromResult(ctx.Channel.Name != "media");
return Task.FromResult(ctx.Channel.Name != "media");
}
} }

View File

@ -11,7 +11,5 @@ internal class RequiresSupporterRole: CheckBaseAttributeWithReactions
public RequiresSupporterRole() : base(reactOnFailure: Config.Reactions.Denied) { } public RequiresSupporterRole() : base(reactOnFailure: Config.Reactions.Denied) { }
protected override Task<bool> IsAllowed(CommandContext ctx, bool help) protected override Task<bool> IsAllowed(CommandContext ctx, bool help)
{ => Task.FromResult(ctx.User.IsWhitelisted(ctx.Client, ctx.Guild) || ctx.User.IsSupporter(ctx.Client, ctx.Guild));
return Task.FromResult(ctx.User.IsWhitelisted(ctx.Client, ctx.Guild) || ctx.User.IsSupporter(ctx.Client, ctx.Guild));
}
} }

View File

@ -11,7 +11,5 @@ internal class RequiresWhitelistedRole: CheckBaseAttributeWithReactions
public RequiresWhitelistedRole() : base(reactOnFailure: Config.Reactions.Denied) { } public RequiresWhitelistedRole() : base(reactOnFailure: Config.Reactions.Denied) { }
protected override Task<bool> IsAllowed(CommandContext ctx, bool help) protected override Task<bool> IsAllowed(CommandContext ctx, bool help)
{ => Task.FromResult(ctx.User.IsWhitelisted(ctx.Client, ctx.Guild));
return Task.FromResult(ctx.User.IsWhitelisted(ctx.Client, ctx.Guild));
}
} }

View File

@ -9,7 +9,5 @@ internal class TriggersTyping: Attribute
public bool InDmOnly { get; set; } public bool InDmOnly { get; set; }
public bool ExecuteCheck(CommandContext ctx) public bool ExecuteCheck(CommandContext ctx)
{ => !InDmOnly || ctx.Channel.IsPrivate;
return !InDmOnly || ctx.Channel.IsPrivate;
}
} }

View File

@ -15,8 +15,7 @@ internal class BaseApplicationCommandModuleCustom : ApplicationCommandModule
public override async Task<bool> BeforeSlashExecutionAsync(InteractionContext ctx) public override async Task<bool> BeforeSlashExecutionAsync(InteractionContext ctx)
{ {
executionStart = DateTimeOffset.UtcNow; executionStart = DateTimeOffset.UtcNow;
if (ctx is {Channel.Name: "media", Interaction: { Type: InteractionType.ApplicationCommand, Data.Name: not ("warn" or "report") } })
if (ctx.Channel.Name == "media" && ctx.Interaction is { Type: InteractionType.ApplicationCommand, Data.Name: not ("warn" or "report") })
{ {
//todo: look what's available in data //todo: look what's available in data
Config.Log.Info($"Ignoring slash command from {ctx.User.Username} (<@{ctx.User.Id}>) in #media: {ctx.Interaction.Data}"); Config.Log.Info($"Ignoring slash command from {ctx.User.Username} (<@{ctx.User.Id}>) in #media: {ctx.Interaction.Data}");

View File

@ -29,7 +29,10 @@ internal sealed class BotMath : BaseCommandModuleCustom
return; return;
} }
var result = @"Something went wrong ¯\\_(ツ)\_/¯" + "\nMath is hard, yo"; var result = """
Something went wrong ¯\\\_()\_/¯
Math is hard, yo
""";
try try
{ {
var expr = new Expression(expression); var expr = new Expression(expression);
@ -45,5 +48,5 @@ internal sealed class BotMath : BaseCommandModuleCustom
[Command("help"), LimitedToSpamChannel, Cooldown(1, 5, CooldownBucketType.Channel)] [Command("help"), LimitedToSpamChannel, Cooldown(1, 5, CooldownBucketType.Channel)]
[Description("General math expression help, or description of specific math word")] [Description("General math expression help, or description of specific math word")]
public Task Help(CommandContext ctx) public Task Help(CommandContext ctx)
=> ctx.Channel.SendMessageAsync("Help for all the features and built-in constants and functions could be found at <https://mathparser.org/mxparser-math-collection/>"); => ctx.Channel.SendMessageAsync("Help for all the features and built-in constants and functions could be found at [mXparser website](<https://mathparser.org/mxparser-math-collection/>)");
} }

View File

@ -37,19 +37,33 @@ internal sealed class BotStats: BaseCommandModuleCustom
var osInfo = RuntimeInformation.OSDescription; var osInfo = RuntimeInformation.OSDescription;
if (Environment.OSVersion.Platform is PlatformID.Unix or PlatformID.MacOSX) if (Environment.OSVersion.Platform is PlatformID.Unix or PlatformID.MacOSX)
osInfo = RuntimeInformation.RuntimeIdentifier; osInfo = RuntimeInformation.RuntimeIdentifier;
var gcMemInfo = GC.GetGCMemoryInfo();
var apiMsm = ApiConfig.MemoryStreamManager;
var botMsm = Config.MemoryStreamManager;
var apiLpsTotal = apiMsm.LargePoolInUseSize + apiMsm.LargePoolFreeSize;
var apiSpsTotal = apiMsm.SmallPoolInUseSize + apiMsm.SmallPoolFreeSize;
var botLpsTotal = botMsm.LargePoolInUseSize + botMsm.LargePoolFreeSize;
var botSpsTotal = botMsm.SmallPoolInUseSize + botMsm.SmallPoolFreeSize;
embed.AddField("API Tokens", GetConfiguredApiStats(), true) embed.AddField("API Tokens", GetConfiguredApiStats(), true)
.AddField("Memory Usage", $"GC: {GC.GetGCMemoryInfo().HeapSizeBytes.AsStorageUnit()}/{GC.GetGCMemoryInfo().TotalAvailableMemoryBytes.AsStorageUnit()}\n" + .AddField("Memory Usage", $"""
$"API pools: L: {ApiConfig.MemoryStreamManager.LargePoolInUseSize.AsStorageUnit()}/{(ApiConfig.MemoryStreamManager.LargePoolInUseSize + ApiConfig.MemoryStreamManager.LargePoolFreeSize).AsStorageUnit()}" + GC: {gcMemInfo.HeapSizeBytes.AsStorageUnit()}/{gcMemInfo.TotalAvailableMemoryBytes.AsStorageUnit()}
$" S: {ApiConfig.MemoryStreamManager.SmallPoolInUseSize.AsStorageUnit()}/{(ApiConfig.MemoryStreamManager.SmallPoolInUseSize + ApiConfig.MemoryStreamManager.SmallPoolFreeSize).AsStorageUnit()}\n" + API pools: L: {apiMsm.LargePoolInUseSize.AsStorageUnit()}/{apiLpsTotal.AsStorageUnit()} S: {apiMsm.SmallPoolInUseSize.AsStorageUnit()}/{apiSpsTotal.AsStorageUnit()}
$"Bot pools: L: {Config.MemoryStreamManager.LargePoolInUseSize.AsStorageUnit()}/{(Config.MemoryStreamManager.LargePoolInUseSize + Config.MemoryStreamManager.LargePoolFreeSize).AsStorageUnit()}" + Bot pools: L: {botMsm.LargePoolInUseSize.AsStorageUnit()}/{botLpsTotal.AsStorageUnit()} S: {botMsm.SmallPoolInUseSize.AsStorageUnit()}/{botSpsTotal.AsStorageUnit()}
$" S: {Config.MemoryStreamManager.SmallPoolInUseSize.AsStorageUnit()}/{(Config.MemoryStreamManager.SmallPoolInUseSize + Config.MemoryStreamManager.SmallPoolFreeSize).AsStorageUnit()}", true) """, true)
.AddField("GitHub Rate Limit", $"{GithubClient.Client.RateLimitRemaining} out of {GithubClient.Client.RateLimit} calls available\nReset in {(GithubClient.Client.RateLimitResetTime - DateTime.UtcNow).AsShortTimespan()}", true) .AddField("GitHub Rate Limit", $"""
.AddField(".NET Info", $"{RuntimeInformation.FrameworkDescription}\n" + {GithubClient.Client.RateLimitRemaining} out of {GithubClient.Client.RateLimit} calls available
$"{(System.Runtime.GCSettings.IsServerGC ? "Server" : "Workstation")} GC Mode", true) Reset in {(GithubClient.Client.RateLimitResetTime - DateTime.UtcNow).AsShortTimespan()}
.AddField("Runtime Info", $"Confinement: {SandboxDetector.Detect()}\n" + """, true)
$"OS: {osInfo}\n" + .AddField(".NET Info", $"""
$"CPUs: {Environment.ProcessorCount}\n" + {RuntimeInformation.FrameworkDescription}
$"Time zones: {TimeParser.TimeZoneMap.Count} out of {TimeParser.TimeZoneAcronyms.Count} resolved, {TimeZoneInfo.GetSystemTimeZones().Count} total", true); {(System.Runtime.GCSettings.IsServerGC ? "Server" : "Workstation")} GC Mode
""", true)
.AddField("Runtime Info", $"""
Confinement: {SandboxDetector.Detect()}
OS: {osInfo}
CPUs: {Environment.ProcessorCount}
Time zones: {TimeParser.TimeZoneMap.Count} out of {TimeParser.TimeZoneAcronyms.Count} resolved, {TimeZoneInfo.GetSystemTimeZones().Count} total
""", true);
AppendPiracyStats(embed); AppendPiracyStats(embed);
AppendCmdStats(embed); AppendCmdStats(embed);
AppendExplainStats(embed); AppendExplainStats(embed);
@ -70,16 +84,13 @@ internal sealed class BotStats: BaseCommandModuleCustom
public Task Hardware(CommandContext ctx, [Description("Desired period in days, default is 30")] int period = 30) => Commands.Hardware.ShowStats(ctx, period); public Task Hardware(CommandContext ctx, [Description("Desired period in days, default is 30")] int period = 30) => Commands.Hardware.ShowStats(ctx, period);
private static string GetConfiguredApiStats() private static string GetConfiguredApiStats()
{ => $"""
return new StringBuilder() {(GoogleDriveHandler.ValidateCredentials() ? "✅" : "❌")} Google Drive
.Append(GoogleDriveHandler.ValidateCredentials() ? "✅" : "❌").AppendLine(" Google Drive") {(string.IsNullOrEmpty(Config.AzureDevOpsToken) ? "❌" : "✅")} Azure DevOps
.Append(string.IsNullOrEmpty(Config.AzureDevOpsToken) ? "❌" : "✅").AppendLine(" Azure DevOps") {(string.IsNullOrEmpty(Config.AzureComputerVisionKey) ? "❌" : "✅")} Computer Vision
.Append(string.IsNullOrEmpty(Config.AzureComputerVisionKey) ? "❌" : "✅").AppendLine(" Computer Vision") {(string.IsNullOrEmpty(Config.AzureAppInsightsConnectionString) ? "❌" : "✅")} AppInsights
.Append(string.IsNullOrEmpty(Config.AzureAppInsightsConnectionString) ? "❌" : "✅").AppendLine(" AppInsights") {(string.IsNullOrEmpty(Config.GithubToken) ? "❌" : "✅")} GitHub
.Append(string.IsNullOrEmpty(Config.GithubToken) ? "❌" : "✅").AppendLine(" Github") """;
.ToString()
.Trim();
}
private static void AppendPiracyStats(DiscordEmbedBuilder embed) private static void AppendPiracyStats(DiscordEmbedBuilder embed)
{ {
@ -228,11 +239,11 @@ internal sealed class BotStats: BaseCommandModuleCustom
var totalFuncCount = db.SyscallInfo.AsNoTracking().Select(sci => sci.Function).Distinct().Count(); var totalFuncCount = db.SyscallInfo.AsNoTracking().Select(sci => sci.Function).Distinct().Count();
var fwCallCount = totalFuncCount - syscallCount; var fwCallCount = totalFuncCount - syscallCount;
var gameCount = db.SyscallToProductMap.AsNoTracking().Select(m => m.ProductId).Distinct().Count(); var gameCount = db.SyscallToProductMap.AsNoTracking().Select(m => m.ProductId).Distinct().Count();
embed.AddField("SceCall Stats", embed.AddField("SceCall Stats", $"""
$"Tracked game IDs: {gameCount}\n" + Tracked game IDs: {gameCount}
$"Tracked syscalls: {syscallCount} function{(syscallCount == 1 ? "" : "s")}\n" + Tracked syscalls: {syscallCount} function{(syscallCount == 1 ? "" : "s")}
$"Tracked fw calls: {fwCallCount} function{(fwCallCount == 1 ? "" : "s")}\n", Tracked fw calls: {fwCallCount} function{(fwCallCount == 1 ? "" : "s")}
true); """, true);
} }
catch (Exception e) catch (Exception e)
{ {
@ -258,15 +269,12 @@ internal sealed class BotStats: BaseCommandModuleCustom
.OrderByDescending(s => s.count) .OrderByDescending(s => s.count)
.FirstOrDefault(); .FirstOrDefault();
var cpuInfo = ""; var cpuInfo = cpu is null ? "" : $"Popular CPU: {cpu.maker} {cpu.name} ({cpu.count * 100.0 / monthCount:0.##}%)";
if (cpu is not null) embed.AddField("Hardware Stats", $"""
cpuInfo = $"\nPopular CPU: {cpu.maker} {cpu.name} ({cpu.count*100.0/monthCount:0.##}%)"; Total: {totalCount} system{(totalCount == 1 ? "" : "s")}
embed.AddField("Hardware Stats", Last 30 days: {monthCount} system{(monthCount == 1 ? "" : "s")}
$"Total: {totalCount} system{(totalCount == 1 ? "" : "s")}\n" + {cpuInfo}
$"Last 30 days: {monthCount} system{(monthCount == 1 ? "" : "s")}" + """.TrimEnd(), true);
cpuInfo,
true);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -22,7 +22,10 @@ public sealed class CommandsManagement : BaseCommandModule
var list = DisabledCommandsProvider.Get(); var list = DisabledCommandsProvider.Get();
if (list.Count > 0) if (list.Count > 0)
{ {
var result = new StringBuilder("Currently disabled commands:").AppendLine().AppendLine("```"); var result = new StringBuilder("""
Currently disabled commands:
```
""");
foreach (var cmd in list) foreach (var cmd in list)
result.AppendLine(cmd); result.AppendLine(cmd);
await ctx.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false); await ctx.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false);

View File

@ -41,7 +41,10 @@ internal sealed class CompatList : BaseCommandModuleCustom
private const string Rpcs3UpdateStateKey = "Rpcs3UpdateState"; private const string Rpcs3UpdateStateKey = "Rpcs3UpdateState";
private const string Rpcs3UpdateBuildKey = "Rpcs3UpdateBuild"; private const string Rpcs3UpdateBuildKey = "Rpcs3UpdateBuild";
private static UpdateInfo? cachedUpdateInfo; private static UpdateInfo? cachedUpdateInfo;
private static readonly Regex UpdateVersionRegex = new(@"v(?<version>\d+\.\d+\.\d+)-(?<build>\d+)-(?<commit>[0-9a-f]+)\b", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture); private static readonly Regex UpdateVersionRegex = new(
@"v(?<version>\d+\.\d+\.\d+)-(?<build>\d+)-(?<commit>[0-9a-f]+)\b",
RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture
);
static CompatList() static CompatList()
{ {
@ -153,7 +156,7 @@ internal sealed class CompatList : BaseCommandModuleCustom
if (exactStatus) if (exactStatus)
result.Append(" only"); result.Append(" only");
result.Append(" games"); result.Append(" games");
if (scoreType == "critic" || scoreType == "user") if (scoreType is "critic" or "user")
result.Append($" according to {scoreType}s"); result.Append($" according to {scoreType}s");
result.AppendLine(":"); result.AppendLine(":");
foreach (var (title, score, _) in resultList) foreach (var (title, score, _) in resultList)
@ -170,17 +173,12 @@ internal sealed class CompatList : BaseCommandModuleCustom
public sealed class UpdatesCheck: BaseCommandModuleCustom public sealed class UpdatesCheck: BaseCommandModuleCustom
{ {
[GroupCommand] [GroupCommand]
public Task Latest(CommandContext ctx) public Task Latest(CommandContext ctx) => CheckForRpcs3Updates(ctx.Client, ctx.Channel);
{
return CheckForRpcs3Updates(ctx.Client, ctx.Channel);
}
[Command("since")] [Command("since")]
[Description("Show additional info about changes since specified update")] [Description("Show additional info about changes since specified update")]
public Task Since(CommandContext ctx, [Description("Commit hash of the update, such as `46abe0f31`")] string commit) public Task Since(CommandContext ctx, [Description("Commit hash of the update, such as `46abe0f31`")] string commit)
{ => CheckForRpcs3Updates(ctx.Client, ctx.Channel, commit);
return CheckForRpcs3Updates(ctx.Client, ctx.Channel, commit);
}
[Command("clear"), RequiresBotModRole] [Command("clear"), RequiresBotModRole]
[Description("Clears update info cache that is used to post new build announcements")] [Description("Clears update info cache that is used to post new build announcements")]
@ -345,7 +343,7 @@ internal sealed class CompatList : BaseCommandModuleCustom
{ {
var updateInfo = await Client.GetUpdateAsync(cancellationToken, mergedPr.MergeCommitSha).ConfigureAwait(false) var updateInfo = await Client.GetUpdateAsync(cancellationToken, mergedPr.MergeCommitSha).ConfigureAwait(false)
?? new UpdateInfo {ReturnCode = -1}; ?? new UpdateInfo {ReturnCode = -1};
if (updateInfo.ReturnCode == 0 || updateInfo.ReturnCode == 1) // latest or known build if (updateInfo.ReturnCode is 0 or 1) // latest or known build
{ {
updateInfo.LatestBuild = updateInfo.CurrentBuild; updateInfo.LatestBuild = updateInfo.CurrentBuild;
updateInfo.CurrentBuild = null; updateInfo.CurrentBuild = null;
@ -503,7 +501,7 @@ internal sealed class CompatList : BaseCommandModuleCustom
if (trimmedList.Count > 0) if (trimmedList.Count > 0)
sortedList = trimmedList; sortedList = trimmedList;
var searchTerm = request.Search ?? @"¯\_(ツ)_/¯"; var searchTerm = request.Search ?? @"¯\\\_(ツ)\_/¯";
var searchHits = sortedList.Where(t => t.score > 0.5 var searchHits = sortedList.Where(t => t.score > 0.5
|| (t.info.Title?.StartsWith(searchTerm, StringComparison.InvariantCultureIgnoreCase) ?? false) || (t.info.Title?.StartsWith(searchTerm, StringComparison.InvariantCultureIgnoreCase) ?? false)
|| (t.info.AlternativeTitle?.StartsWith(searchTerm, StringComparison.InvariantCultureIgnoreCase) ?? false)); || (t.info.AlternativeTitle?.StartsWith(searchTerm, StringComparison.InvariantCultureIgnoreCase) ?? false));
@ -684,8 +682,6 @@ internal sealed class CompatList : BaseCommandModuleCustom
ProductCode = productCode, ProductCode = productCode,
Name = titleInfo.Title, Name = titleInfo.Title,
}).ConfigureAwait(false)).Entity; }).ConfigureAwait(false)).Entity;
if (dbItem is null)
continue;
dbItem.Name = titleInfo.Title; dbItem.Name = titleInfo.Title;
if (Enum.TryParse(titleInfo.Status, out CompatStatus status)) if (Enum.TryParse(titleInfo.Status, out CompatStatus status))

View File

@ -406,10 +406,10 @@ internal sealed class ContentFilters: BaseCommandModuleCustom
step1: step1:
// step 1: define trigger string // step 1: define trigger string
var embed = FormatFilter(filter, errorMsg, 1) var embed = FormatFilter(filter, errorMsg, 1)
.WithDescription( .WithDescription("""
"Any simple string that is used to flag potential content for a check using Validation regex.\n" + Any simple string that is used to flag potential content for a check using Validation regex.
"**Must** be sufficiently long to reduce the number of checks." **Must** be sufficiently long to reduce the number of checks.
); """);
saveEdit.SetEnabled(filter.IsComplete()); saveEdit.SetEnabled(filter.IsComplete());
var messageBuilder = new DiscordMessageBuilder() var messageBuilder = new DiscordMessageBuilder()
.WithContent("Please specify a new **trigger**") .WithContent("Please specify a new **trigger**")
@ -453,12 +453,12 @@ internal sealed class ContentFilters: BaseCommandModuleCustom
step2: step2:
// step 2: context of the filter where it is applicable // step 2: context of the filter where it is applicable
embed = FormatFilter(filter, errorMsg, 2) embed = FormatFilter(filter, errorMsg, 2)
.WithDescription( .WithDescription($"""
"Context of the filter indicates where it is applicable.\n" + Context of the filter indicates where it is applicable.
$"**`C`** = **`{FilterContext.Chat}`** will apply it in filtering discord messages.\n" + **`C`** = **`{FilterContext.Chat}`** will apply it in filtering discord messages.
$"**`L`** = **`{FilterContext.Log}`** will apply it during log parsing.\n" + **`L`** = **`{FilterContext.Log}`** will apply it during log parsing.
"Reactions will toggle the context, text message will set the specified flags." Reactions will toggle the context, text message will set the specified flags.
); """);
saveEdit.SetEnabled(filter.IsComplete()); saveEdit.SetEnabled(filter.IsComplete());
contextChat.SetEmoji(filter.Context.HasFlag(FilterContext.Chat) ? minus : plus); contextChat.SetEmoji(filter.Context.HasFlag(FilterContext.Chat) ? minus : plus);
contextLog.SetEmoji(filter.Context.HasFlag(FilterContext.Log) ? minus : plus); contextLog.SetEmoji(filter.Context.HasFlag(FilterContext.Log) ? minus : plus);
@ -529,16 +529,16 @@ internal sealed class ContentFilters: BaseCommandModuleCustom
step3: step3:
// step 3: actions that should be performed on match // step 3: actions that should be performed on match
embed = FormatFilter(filter, errorMsg, 3) embed = FormatFilter(filter, errorMsg, 3)
.WithDescription( .WithDescription($"""
"Actions that will be executed on positive match.\n" + Actions that will be executed on positive match.
$"**`R`** = **`{FilterAction.RemoveContent}`** will remove the message / log.\n" + **`R`** = **`{FilterAction.RemoveContent}`** will remove the message / log.
$"**`W`** = **`{FilterAction.IssueWarning}`** will issue a warning to the user.\n" + **`W`** = **`{FilterAction.IssueWarning}`** will issue a warning to the user.
$"**`M`** = **`{FilterAction.SendMessage}`** send _a_ message with an explanation of why it was removed.\n" + **`M`** = **`{FilterAction.SendMessage}`** send _a_ message with an explanation of why it was removed.
$"**`E`** = **`{FilterAction.ShowExplain}`** show `explain` for the specified term (**not implemented**).\n" + **`E`** = **`{FilterAction.ShowExplain}`** show `explain` for the specified term (**not implemented**).
$"**`U`** = **`{FilterAction.MuteModQueue}`** mute mod queue reporting for this action.\n" + **`U`** = **`{FilterAction.MuteModQueue}`** mute mod queue reporting for this action.
$"**`K`** = **`{FilterAction.Kick}`** kick user from server.\n" + **`K`** = **`{FilterAction.Kick}`** kick user from server.
"Buttons will toggle the action, text message will set the specified flags." Buttons will toggle the action, text message will set the specified flags.
); """);
actionR.SetEmoji(filter.Actions.HasFlag(FilterAction.RemoveContent) ? minus : plus); actionR.SetEmoji(filter.Actions.HasFlag(FilterAction.RemoveContent) ? minus : plus);
actionW.SetEmoji(filter.Actions.HasFlag(FilterAction.IssueWarning) ? minus : plus); actionW.SetEmoji(filter.Actions.HasFlag(FilterAction.IssueWarning) ? minus : plus);
actionM.SetEmoji(filter.Actions.HasFlag(FilterAction.SendMessage) ? minus : plus); actionM.SetEmoji(filter.Actions.HasFlag(FilterAction.SendMessage) ? minus : plus);
@ -666,11 +666,11 @@ internal sealed class ContentFilters: BaseCommandModuleCustom
step4: step4:
// step 4: validation regex to filter out false positives of the plaintext triggers // step 4: validation regex to filter out false positives of the plaintext triggers
embed = FormatFilter(filter, errorMsg, 4) embed = FormatFilter(filter, errorMsg, 4)
.WithDescription( .WithDescription("""
"Validation [regex](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference) to optionally perform more strict trigger check.\n" + Validation [regex](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference) to optionally perform more strict trigger check.
"**Please [test](https://regex101.com/) your regex**. Following flags are enabled: Multiline, IgnoreCase.\n" + **Please [test](https://regex101.com/) your regex**. Following flags are enabled: Multiline, IgnoreCase.
"Additional validation can help reduce false positives of a plaintext trigger match." Additional validation can help reduce false positives of a plaintext trigger match.
); """);
var next = (filter.Actions & (FilterAction.SendMessage | FilterAction.ShowExplain)) == 0 ? firstPage : nextPage; var next = (filter.Actions & (FilterAction.SendMessage | FilterAction.ShowExplain)) == 0 ? firstPage : nextPage;
trash.SetDisabled(string.IsNullOrEmpty(filter.ValidatingRegex)); trash.SetDisabled(string.IsNullOrEmpty(filter.ValidatingRegex));
saveEdit.SetEnabled(filter.IsComplete()); saveEdit.SetEnabled(filter.IsComplete());
@ -775,10 +775,10 @@ internal sealed class ContentFilters: BaseCommandModuleCustom
step6: step6:
// step 6: show explanation for the term // step 6: show explanation for the term
embed = FormatFilter(filter, errorMsg, 6) embed = FormatFilter(filter, errorMsg, 6)
.WithDescription( .WithDescription("""
"Explanation term that is used to show an explanation.\n" + Explanation term that is used to show an explanation.
"**__Currently not implemented__**." **__Currently not implemented__**.
); """);
saveEdit.SetEnabled(filter.IsComplete()); saveEdit.SetEnabled(filter.IsComplete());
messageBuilder = new DiscordMessageBuilder() messageBuilder = new DiscordMessageBuilder()
.WithContent("Please specify filter **explanation term**") .WithContent("Please specify filter **explanation term**")

View File

@ -25,12 +25,12 @@ internal sealed class DevOnly : BaseCommandModuleCustom
#pragma warning disable 8321 #pragma warning disable 8321
static void addRandomStuff(DiscordEmbedBuilder emb) static void addRandomStuff(DiscordEmbedBuilder emb)
{ {
var txt = "😾 lasjdf wqoieyr osdf `Vreoh Sdab` wohe `270`\n" + emb.AddField("Random section", """
"🤔 salfhiosfhsero hskfh shufwei oufhwehw e wkihrwe h\n" + 😾 lasjdf wqoieyr osdf `Vreoh Sdab` wohe `270`
" sakfjas f hs `ASfhewighehw safds` asfw\n" + 🤔 salfhiosfhsero hskfh shufwei oufhwehw e wkihrwe h
"🔮 ¯\\\\\\_(ツ)\\_/¯"; sakfjas f hs `ASfhewighehw safds` asfw
🔮 ¯\\\_()\_/¯
emb.AddField("Random section", txt, false); """, false);
} }
#pragma warning restore 8321 #pragma warning restore 8321
var embed = new DiscordEmbedBuilder() var embed = new DiscordEmbedBuilder()

View File

@ -81,7 +81,7 @@ internal class EventsBaseCommand: BaseCommandModuleCustom
var noEventMsg = $"No information about the upcoming {eventName?.Sanitize(replaceBackTicks: true)} at the moment"; var noEventMsg = $"No information about the upcoming {eventName?.Sanitize(replaceBackTicks: true)} at the moment";
if (eventName?.Length > 10) if (eventName?.Length > 10)
noEventMsg = "No information about such event at the moment"; noEventMsg = "No information about such event at the moment";
else if (ctx.User.Id == 259997001880436737ul || ctx.User.Id == 377190919327318018ul) else if (ctx.User.Id is 259997001880436737ul or 377190919327318018ul)
{ {
noEventMsg = $"Haha, very funny, {ctx.User.Mention}. So original. Never saw this joke before."; noEventMsg = $"Haha, very funny, {ctx.User.Mention}. So original. Never saw this joke before.";
promo = null; promo = null;
@ -114,7 +114,7 @@ internal class EventsBaseCommand: BaseCommandModuleCustom
var noEventMsg = $"No information about the upcoming {eventName?.Sanitize(replaceBackTicks: true)} at the moment"; var noEventMsg = $"No information about the upcoming {eventName?.Sanitize(replaceBackTicks: true)} at the moment";
if (eventName?.Length > 10) if (eventName?.Length > 10)
noEventMsg = "No information about such event at the moment"; noEventMsg = "No information about such event at the moment";
else if (ctx.User.Id == 259997001880436737ul || ctx.User.Id == 377190919327318018ul) else if (ctx.User.Id is 259997001880436737ul or 377190919327318018ul)
{ {
noEventMsg = $"Haha, very funny, {ctx.User.Mention}. So original. Never saw this joke before."; noEventMsg = $"Haha, very funny, {ctx.User.Mention}. So original. Never saw this joke before.";
promo = null; promo = null;
@ -301,7 +301,10 @@ internal class EventsBaseCommand: BaseCommandModuleCustom
saveEdit.SetEnabled(evt.IsComplete()); saveEdit.SetEnabled(evt.IsComplete());
messageBuilder = new DiscordMessageBuilder() messageBuilder = new DiscordMessageBuilder()
.WithContent("Please specify a new **start date and time**") .WithContent("Please specify a new **start date and time**")
.WithEmbed(FormatEvent(evt, errorMsg, 1).WithDescription($"Example: `{DateTime.UtcNow:yyyy-MM-dd HH:mm}`\nBy default all times use UTC, only limited number of time zones supported")) .WithEmbed(FormatEvent(evt, errorMsg, 1).WithDescription($"""
Example: `{DateTime.UtcNow:yyyy-MM-dd HH:mm}`
By default all times use UTC, only limited number of time zones supported
"""))
.AddComponents(lastPage, nextPage) .AddComponents(lastPage, nextPage)
.AddComponents(saveEdit, abort); .AddComponents(saveEdit, abort);
errorMsg = null; errorMsg = null;

View File

@ -67,7 +67,7 @@ internal sealed class Explain: BaseCommandModuleCustom
var hasMention = false; var hasMention = false;
term = term.ToLowerInvariant(); term = term.ToLowerInvariant();
var result = await LookupTerm(term).ConfigureAwait(false); var result = await LookupTerm(term).ConfigureAwait(false);
if (result.explanation == null || !string.IsNullOrEmpty(result.fuzzyMatch)) if (result is {explanation: null} or {fuzzyMatch.Length: >0})
{ {
term = term.StripQuotes(); term = term.StripQuotes();
var idx = term.LastIndexOf(" to ", StringComparison.Ordinal); var idx = term.LastIndexOf(" to ", StringComparison.Ordinal);
@ -116,7 +116,7 @@ internal sealed class Explain: BaseCommandModuleCustom
term = term.ToLowerInvariant().StripQuotes(); term = term.ToLowerInvariant().StripQuotes();
byte[]? attachment = null; byte[]? attachment = null;
string? attachmentFilename = null; string? attachmentFilename = null;
if (ctx.Message.Attachments.FirstOrDefault() is DiscordAttachment att) if (ctx.Message.Attachments is [DiscordAttachment att, ..])
{ {
attachmentFilename = att.FileName; attachmentFilename = att.FileName;
try try
@ -165,7 +165,7 @@ internal sealed class Explain: BaseCommandModuleCustom
term = term.ToLowerInvariant().StripQuotes(); term = term.ToLowerInvariant().StripQuotes();
byte[]? attachment = null; byte[]? attachment = null;
string? attachmentFilename = null; string? attachmentFilename = null;
if (ctx.Message.Attachments.FirstOrDefault() is DiscordAttachment att) if (ctx.Message.Attachments is [DiscordAttachment att, ..])
{ {
attachmentFilename = att.FileName; attachmentFilename = att.FileName;
try try
@ -346,12 +346,12 @@ internal sealed class Explain: BaseCommandModuleCustom
} }
else else
{ {
if (!string.IsNullOrEmpty(item.Text)) if (item is { Text.Length: > 0 })
{ {
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(item.Text)); await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(item.Text));
await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddFile($"{termOrLink}.txt", stream)).ConfigureAwait(false); await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddFile($"{termOrLink}.txt", stream)).ConfigureAwait(false);
} }
if (!string.IsNullOrEmpty(item.AttachmentFilename) && item.Attachment?.Length > 0) if (item is { AttachmentFilename.Length: > 0, Attachment.Length: > 0 })
{ {
await using var stream = new MemoryStream(item.Attachment); await using var stream = new MemoryStream(item.Attachment);
await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddFile(item.AttachmentFilename, stream)).ConfigureAwait(false); await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddFile(item.AttachmentFilename, stream)).ConfigureAwait(false);

View File

@ -71,9 +71,8 @@ internal sealed class ForcedNicknames : BaseCommandModuleCustom
else else
{ {
if (enforceRules.Nickname == expectedNickname) if (enforceRules.Nickname == expectedNickname)
{
continue; continue;
}
enforceRules.Nickname = expectedNickname; enforceRules.Nickname = expectedNickname;
} }
} }
@ -187,7 +186,7 @@ internal sealed class ForcedNicknames : BaseCommandModuleCustom
var hex = BitConverter.ToString(nameBytes).Replace('-', ' '); var hex = BitConverter.ToString(nameBytes).Replace('-', ' ');
var result = $"User ID: {discordUser.Id}\nUsername: {hex}"; var result = $"User ID: {discordUser.Id}\nUsername: {hex}";
var member = ctx.Client.GetMember(ctx.Guild, discordUser); var member = ctx.Client.GetMember(ctx.Guild, discordUser);
if (member?.Nickname is string {Length: >0} nickname) if (member is { Nickname: { Length: > 0 } nickname })
{ {
nameBytes = StringUtils.Utf8.GetBytes(nickname); nameBytes = StringUtils.Utf8.GetBytes(nickname);
hex = BitConverter.ToString(nameBytes).Replace('-', ' '); hex = BitConverter.ToString(nameBytes).Replace('-', ' ');

View File

@ -46,20 +46,20 @@ internal sealed class Fortune : BaseCommandModuleCustom
fortune = await db.Fortune.AsNoTracking().Skip(selectedId).FirstOrDefaultAsync().ConfigureAwait(false); fortune = await db.Fortune.AsNoTracking().Skip(selectedId).FirstOrDefaultAsync().ConfigureAwait(false);
} while (fortune is null); } while (fortune is null);
var msg = fortune.Content.FixTypography();
var msgParts = msg.Split('\n');
var tmp = new StringBuilder(); var tmp = new StringBuilder();
var quote = true; var quote = true;
foreach (var l in msgParts) foreach (var l in fortune.Content.FixTypography().Split('\n'))
{ {
quote &= !l.StartsWith(" "); quote &= !l.StartsWith(" ");
if (quote) if (quote)
tmp.Append("> "); tmp.Append("> ");
tmp.Append(l).Append('\n'); tmp.Append(l).Append('\n');
} }
msg = tmp.ToString().TrimEnd().FixSpaces();
var msgBuilder = new DiscordMessageBuilder() var msgBuilder = new DiscordMessageBuilder()
.WithContent($"{user.Mention}, your fortune for today:\n{msg}") .WithContent($"""
{user.Mention}, your fortune for today:
{tmp.ToString().TrimEnd().FixSpaces()}
""")
.WithReply(message.Id); .WithReply(message.Id);
await message.Channel.SendMessageAsync(msgBuilder).ConfigureAwait(false); await message.Channel.SendMessageAsync(msgBuilder).ConfigureAwait(false);
} }
@ -136,7 +136,7 @@ internal sealed class Fortune : BaseCommandModuleCustom
|| buf.Length > 0) || buf.Length > 0)
&& !Config.Cts.IsCancellationRequested) && !Config.Cts.IsCancellationRequested)
{ {
if (line == "%" || line is null) if (line is "%" or null)
{ {
var content = buf.ToString().Replace("\r\n", "\n").Trim(); var content = buf.ToString().Replace("\r\n", "\n").Trim();
if (content.Length > 1900) if (content.Length > 1900)
@ -187,7 +187,7 @@ internal sealed class Fortune : BaseCommandModuleCustom
var progressMsg = $"Imported {count} fortune{(count == 1 ? "" : "s")}"; var progressMsg = $"Imported {count} fortune{(count == 1 ? "" : "s")}";
if (skipped > 0) if (skipped > 0)
progressMsg += $", skipped {skipped}"; progressMsg += $", skipped {skipped}";
if (response.Content.Headers.ContentLength is long len && len > 0) if (response.Content.Headers.ContentLength is long len and > 0)
progressMsg += $" ({stream.Position * 100.0 / len:0.##}%)"; progressMsg += $" ({stream.Position * 100.0 / len:0.##}%)";
await msg.UpdateOrCreateMessageAsync(ctx.Channel, progressMsg).ConfigureAwait(false); await msg.UpdateOrCreateMessageAsync(ctx.Channel, progressMsg).ConfigureAwait(false);
stopwatch.Restart(); stopwatch.Restart();
@ -246,7 +246,7 @@ internal sealed class Fortune : BaseCommandModuleCustom
[Description("Clears fortune database. Use with caution")] [Description("Clears fortune database. Use with caution")]
public async Task Clear(CommandContext ctx, [RemainingText, Description("Must be `with my blessing, I swear I exported the backup`")] string confirmation) public async Task Clear(CommandContext ctx, [RemainingText, Description("Must be `with my blessing, I swear I exported the backup`")] string confirmation)
{ {
if (confirmation != "with my blessing, I swear I exported the backup") if (confirmation is not "with my blessing, I swear I exported the backup")
{ {
await ctx.ReactWithAsync(Config.Reactions.Failure).ConfigureAwait(false); await ctx.ReactWithAsync(Config.Reactions.Failure).ConfigureAwait(false);
return; return;

View File

@ -130,13 +130,17 @@ internal sealed class Hardware: BaseCommandModuleCustom
var lowRam = mem.Where(i => i.Mem < 4 * 1024 - margin).Sum(i => i.Count); var lowRam = mem.Where(i => i.Mem < 4 * 1024 - margin).Sum(i => i.Count);
var ram4to6 = mem.Where(i => i.Mem is >= 4 * 1024 - margin and < 6 * 1024 - margin).Sum(i => i.Count); var ram4to6 = mem.Where(i => i.Mem is >= 4 * 1024 - margin and < 6 * 1024 - margin).Sum(i => i.Count);
var ram6to8 = mem.Where(i => i.Mem is >= 6 * 1024 - margin and < 8 * 1024 - margin).Sum(i => i.Count); var ram6to8 = mem.Where(i => i.Mem is >= 6 * 1024 - margin and < 8 * 1024 - margin).Sum(i => i.Count);
var highRam = mem.Where(i => i.Mem >= 8 * 1024 - margin).Sum(i => i.Count); var ram8to16 = mem.Where(i => i.Mem is >= 8 * 1024 - margin and < 16 * 1024 - margin).Sum(i => i.Count);
var ram16to32 = mem.Where(i => i.Mem is >= 16 * 1024 - margin and < 32 * 1024 - margin).Sum(i => i.Count);
var highRam = mem.Where(i => i.Mem >= 32 * 1024 - margin).Sum(i => i.Count);
var ramStats = new (int Count, string Mem)[] var ramStats = new (int Count, string Mem)[]
{ {
(lowRam, "less than 4 GB"), (lowRam, "less than 4 GB"),
(ram4to6, "4 to 6 GB"), (ram4to6, "4 to 6 GB"),
(ram6to8, "6 to 8 GB"), (ram6to8, "6 to 8 GB"),
(highRam, "8 GB or more"), (ram8to16, "8 to 16 GB"),
(ram16to32, "16 to 32 GB"),
(highRam, "32 GB or more"),
} }
.Where(i => i.Count > 0) .Where(i => i.Count > 0)
.Take(top) .Take(top)

View File

@ -164,8 +164,8 @@ internal sealed class Misc: BaseCommandModuleCustom
if (!int.TryParse(m.Groups["num"].Value, out var num)) if (!int.TryParse(m.Groups["num"].Value, out var num))
num = 1; num = 1;
if (int.TryParse(m.Groups["face"].Value, out var face) if (int.TryParse(m.Groups["face"].Value, out var face)
&& 0 < num && num < 101 && num is > 0 and < 101
&& 1 < face && face < 1001) && face is > 1 and < 1001)
{ {
List<int> rolls; List<int> rolls;
lock (rng) rolls = Enumerable.Range(0, num).Select(_ => rng.Next(face) + 1).ToList(); lock (rng) rolls = Enumerable.Range(0, num).Select(_ => rng.Next(face) + 1).ToList();
@ -312,7 +312,7 @@ internal sealed class Misc: BaseCommandModuleCustom
{ {
try try
{ {
var funMult = DateTime.UtcNow.Month == 4 && DateTime.UtcNow.Day == 1 ? 100 : Config.FunMultiplier; var funMult = DateTime.UtcNow is {Month: 4, Day: 1} ? 100 : Config.FunMultiplier;
var choices = RateAnswers; var choices = RateAnswers;
var choiceFlags = new HashSet<char>(); var choiceFlags = new HashSet<char>();
whatever = whatever.ToLowerInvariant().StripInvisibleAndDiacritics(); whatever = whatever.ToLowerInvariant().StripInvisibleAndDiacritics();

View File

@ -51,7 +51,7 @@ internal sealed partial class Moderation
await using var memoryStream = Config.MemoryStreamManager.GetStream(); await using var memoryStream = Config.MemoryStreamManager.GetStream();
await using var writer = new StreamWriter(memoryStream, new UTF8Encoding(false), 4096, true); await using var writer = new StreamWriter(memoryStream, new UTF8Encoding(false), 4096, true);
foreach (var member in members) foreach (var member in members)
await writer.WriteLineAsync($"{member.Username}\t{member.Nickname}\t{member.JoinedAt:O}\t{(string.Join(',', member.Roles.Select(r => r.Name)))}").ConfigureAwait(false); await writer.WriteLineAsync($"{member.Username}\t{member.Nickname}\t{member.JoinedAt:O}\t{string.Join(',', member.Roles.Select(r => r.Name))}").ConfigureAwait(false);
await writer.FlushAsync().ConfigureAwait(false); await writer.FlushAsync().ConfigureAwait(false);
memoryStream.Seek(0, SeekOrigin.Begin); memoryStream.Seek(0, SeekOrigin.Begin);
if (memoryStream.Length <= ctx.GetAttachmentSizeLimit()) if (memoryStream.Length <= ctx.GetAttachmentSizeLimit())
@ -192,11 +192,13 @@ internal sealed partial class Moderation
} }
} }
/* #if DEBUG
[Command("locales"), Aliases("locale", "languages", "language", "lang", "loc")] [Command("locales"), Aliases("locale", "languages", "language", "lang", "loc")]
public async Task UserLocales(CommandContext ctx) public async Task UserLocales(CommandContext ctx)
{ {
#pragma warning disable VSTHRD103
if (!CheckLock.Wait(0)) if (!CheckLock.Wait(0))
#pragma warning restore VSTHRD103
{ {
await ctx.Channel.SendMessageAsync("Another check is already in progress").ConfigureAwait(false); await ctx.Channel.SendMessageAsync("Another check is already in progress").ConfigureAwait(false);
return; return;
@ -236,7 +238,7 @@ internal sealed partial class Moderation
await ctx.RemoveReactionAsync(Config.Reactions.PleaseWait).ConfigureAwait(false); await ctx.RemoveReactionAsync(Config.Reactions.PleaseWait).ConfigureAwait(false);
} }
} }
*/ #endif
private static List<DiscordMember> GetMembers(DiscordClient client) private static List<DiscordMember> GetMembers(DiscordClient client)
{ {

View File

@ -284,7 +284,10 @@ internal sealed class Pr: BaseCommandModuleCustom
var avgBuildTime = (await azureClient.GetPipelineDurationAsync(Config.Cts.Token).ConfigureAwait(false)).Mean; var avgBuildTime = (await azureClient.GetPipelineDurationAsync(Config.Cts.Token).ConfigureAwait(false)).Mean;
if (now < mergeTime + avgBuildTime) if (now < mergeTime + avgBuildTime)
waitTime = mergeTime + avgBuildTime - now; waitTime = mergeTime + avgBuildTime - now;
embed.AddField("Latest master build", $"This pull request has been merged, and will be part of `master` very soon.\nPlease check again in {waitTime.AsTimeDeltaDescription()}."); embed.AddField("Latest master build", $"""
This pull request has been merged, and will be part of `master` very soon.
Please check again in {waitTime.AsTimeDeltaDescription()}.
""");
} }
} }
} }

View File

@ -18,23 +18,19 @@ internal sealed class SlashMisc: BaseApplicationCommandModuleCustom
Title = "RPCS3 Compatibility Bot", Title = "RPCS3 Compatibility Bot",
Url = "https://github.com/RPCS3/discord-bot", Url = "https://github.com/RPCS3/discord-bot",
Color = DiscordColor.Purple, Color = DiscordColor.Purple,
}.AddField("Made by", }.AddField("Made by", $"""
$"""
💮 13xforever 💮 13xforever
🇭🇷 Roberto Anić Banić aka nicba1010 🇭🇷 Roberto Anić Banić aka nicba1010
{clienthax} clienthax {clienthax} clienthax
""" """
) ).AddField("People who ~~broke~~ helped test the bot", $"""
.AddField("People who ~~broke~~ helped test the bot",
$"""
🐱 Juhn 🐱 Juhn
{hcorion} hcorion {hcorion} hcorion
🙃 TGE 🙃 TGE
🍒 Maru 🍒 Maru
Tourghool Tourghool
""" """
) ).WithFooter($"Running {Config.GitRevision}");
.WithFooter($"Running {Config.GitRevision}");
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(embed.Build()).AsEphemeral()); await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(embed.Build()).AsEphemeral());
} }
} }

View File

@ -31,9 +31,9 @@ internal partial class Sudo
foreach (var v in setVars) foreach (var v in setVars)
{ {
#if DEBUG #if DEBUG
result.Append(v.Key![SqlConfiguration.ConfigVarPrefix.Length ..]).Append(" = ").AppendLine(v.Value); result.Append(v.Key[SqlConfiguration.ConfigVarPrefix.Length ..]).Append(" = ").AppendLine(v.Value);
#else #else
result.AppendLine(v.Key![(SqlConfiguration.ConfigVarPrefix.Length)..]); result.AppendLine(v.Key[(SqlConfiguration.ConfigVarPrefix.Length)..]);
#endif #endif
} }
await ctx.Channel.SendMessageAsync(result.ToString()).ConfigureAwait(false); await ctx.Channel.SendMessageAsync(result.ToString()).ConfigureAwait(false);
@ -53,7 +53,7 @@ internal partial class Sudo
var stateValue = await db.BotState.FirstOrDefaultAsync(v => v.Key == key).ConfigureAwait(false); var stateValue = await db.BotState.FirstOrDefaultAsync(v => v.Key == key).ConfigureAwait(false);
if (stateValue == null) if (stateValue == null)
{ {
stateValue = new BotState {Key = key, Value = value}; stateValue = new() {Key = key, Value = value};
await db.BotState.AddAsync(stateValue).ConfigureAwait(false); await db.BotState.AddAsync(stateValue).ConfigureAwait(false);
} }
else else

View File

@ -30,7 +30,7 @@ internal partial class Sudo
msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Checking for dotnet updates...").ConfigureAwait(false); msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Checking for dotnet updates...").ConfigureAwait(false);
var (updated, stdout) = await UpdateAsync(version).ConfigureAwait(false); var (updated, stdout) = await UpdateAsync(version).ConfigureAwait(false);
if (!string.IsNullOrEmpty(stdout)) if (!string.IsNullOrEmpty(stdout))
await ctx.SendAutosplitMessageAsync("```" + stdout + "```").ConfigureAwait(false); await ctx.SendAutosplitMessageAsync($"```{stdout}```").ConfigureAwait(false);
if (!updated) if (!updated)
return; return;

View File

@ -21,8 +21,10 @@ internal partial class Sudo
if (await ModProvider.AddAsync(user.Id).ConfigureAwait(false)) if (await ModProvider.AddAsync(user.Id).ConfigureAwait(false))
{ {
await ctx.ReactWithAsync(Config.Reactions.Success, await ctx.ReactWithAsync(Config.Reactions.Success,
$"{user.Mention} was successfully added as moderator!\n" + $"""
$"Try using `{ctx.Prefix}help` to see new commands available to you" {user.Mention} was successfully added as moderator!
Try using `{ctx.Prefix}help` to see new commands available to you
"""
).ConfigureAwait(false); ).ConfigureAwait(false);
} }
else else
@ -36,7 +38,7 @@ internal partial class Sudo
if (ctx.Client.CurrentApplication.Owners.Any(u => u.Id == user.Id)) if (ctx.Client.CurrentApplication.Owners.Any(u => u.Id == user.Id))
{ {
var dm = await user.CreateDmChannelAsync().ConfigureAwait(false); var dm = await user.CreateDmChannelAsync().ConfigureAwait(false);
await dm.SendMessageAsync($@"Just letting you know that {ctx.Message.Author.Mention} just tried to strip you off of your mod role ¯\\_(ツ)_/¯").ConfigureAwait(false); await dm.SendMessageAsync($@"Just letting you know that {ctx.Message.Author.Mention} just tried to strip you off of your mod role ¯\\\_(ツ)\_/¯").ConfigureAwait(false);
await ctx.ReactWithAsync(Config.Reactions.Denied, $"{ctx.Message.Author.Mention} why would you even try this?! Alerting {user.Mention}", true).ConfigureAwait(false); await ctx.ReactWithAsync(Config.Reactions.Denied, $"{ctx.Message.Author.Mention} why would you even try this?! Alerting {user.Mention}", true).ConfigureAwait(false);
} }
else if (await ModProvider.RemoveAsync(user.Id).ConfigureAwait(false)) else if (await ModProvider.RemoveAsync(user.Id).ConfigureAwait(false))

View File

@ -33,7 +33,6 @@ internal sealed partial class Sudo : BaseCommandModuleCustom
public async Task Say(CommandContext ctx, [RemainingText, Description("Message text to send")] string message) public async Task Say(CommandContext ctx, [RemainingText, Description("Message text to send")] string message)
{ {
var msgParts = message.Split(' ', 2, StringSplitOptions.TrimEntries); var msgParts = message.Split(' ', 2, StringSplitOptions.TrimEntries);
var channel = ctx.Channel; var channel = ctx.Channel;
DiscordMessage? ogMsg = null; DiscordMessage? ogMsg = null;
if (msgParts.Length > 1) if (msgParts.Length > 1)

View File

@ -117,7 +117,7 @@ internal sealed class Vision: BaseCommandModuleCustom
//resize and shrink file size to get under azure limits //resize and shrink file size to get under azure limits
var quality = 90; var quality = 90;
var resized = false; var resized = false;
if (img.Width > 4000 || img.Height > 4000) if (img is {Width: >4000} or {Height: >4000})
{ {
img.Mutate(i => i.Resize(new ResizeOptions {Size = new(3840, 2160), Mode = ResizeMode.Min,})); img.Mutate(i => i.Resize(new ResizeOptions {Size = new(3840, 2160), Mode = ResizeMode.Min,}));
resized = true; resized = true;

View File

@ -52,7 +52,6 @@ internal sealed partial class Warnings: BaseCommandModuleCustom
var interact = ctx.Client.GetInteractivity(); var interact = ctx.Client.GetInteractivity();
await using var db = new BotDb(); await using var db = new BotDb();
var warnings = await db.Warning.Where(w => id.Equals(w.Id)).ToListAsync().ConfigureAwait(false); var warnings = await db.Warning.Where(w => id.Equals(w.Id)).ToListAsync().ConfigureAwait(false);
if (warnings.Count == 0) if (warnings.Count == 0)
{ {
await ctx.ReactWithAsync(Config.Reactions.Denied, $"{ctx.Message.Author.Mention} Warn not found", true); await ctx.ReactWithAsync(Config.Reactions.Denied, $"{ctx.Message.Author.Mention} Warn not found", true);
@ -60,7 +59,6 @@ internal sealed partial class Warnings: BaseCommandModuleCustom
} }
var warningToEdit = warnings.First(); var warningToEdit = warnings.First();
if (warningToEdit.IssuerId != ctx.User.Id) if (warningToEdit.IssuerId != ctx.User.Id)
{ {
await ctx.ReactWithAsync(Config.Reactions.Denied, $"{ctx.Message.Author.Mention} This warn wasn't issued by you :(", true); await ctx.ReactWithAsync(Config.Reactions.Denied, $"{ctx.Message.Author.Mention} This warn wasn't issued by you :(", true);
@ -83,7 +81,6 @@ internal sealed partial class Warnings: BaseCommandModuleCustom
} }
warningToEdit.Reason = response.Result.Content; warningToEdit.Reason = response.Result.Content;
await db.SaveChangesAsync().ConfigureAwait(false); await db.SaveChangesAsync().ConfigureAwait(false);
await ctx.Channel.SendMessageAsync($"Warning successfully edited!").ConfigureAwait(false); await ctx.Channel.SendMessageAsync($"Warning successfully edited!").ConfigureAwait(false);
} }
@ -166,7 +163,7 @@ internal sealed partial class Warnings: BaseCommandModuleCustom
{ {
await using var db = new BotDb(); await using var db = new BotDb();
var warn = await db.Warning.FirstOrDefaultAsync(w => w.Id == id).ConfigureAwait(false); var warn = await db.Warning.FirstOrDefaultAsync(w => w.Id == id).ConfigureAwait(false);
if (warn?.Retracted is true) if (warn is { Retracted: true })
{ {
warn.Retracted = false; warn.Retracted = false;
warn.RetractedBy = null; warn.RetractedBy = null;

View File

@ -27,7 +27,7 @@ internal class BotDb: DbContext
#if DEBUG #if DEBUG
optionsBuilder.UseLoggerFactory(Config.LoggerFactory); optionsBuilder.UseLoggerFactory(Config.LoggerFactory);
#endif #endif
optionsBuilder.UseSqlite($"Data Source=\"{dbPath}\""); optionsBuilder.UseSqlite($""" Data Source="{dbPath}" """);
} }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)

View File

@ -18,20 +18,20 @@ public static class DbImporter
public static async Task<bool> UpgradeAsync(CancellationToken cancellationToken) public static async Task<bool> UpgradeAsync(CancellationToken cancellationToken)
{ {
await using (var db = new BotDb()) await using (var db = new BotDb())
if (!await UpgradeAsync(db, Config.Cts.Token)) if (!await UpgradeAsync(db, cancellationToken).ConfigureAwait(false))
return false; return false;
await using (var db = new ThumbnailDb()) await using (var db = new ThumbnailDb())
{ {
if (!await UpgradeAsync(db, Config.Cts.Token)) if (!await UpgradeAsync(db,cancellationToken).ConfigureAwait(false))
return false; return false;
if (!await ImportNamesPool(db, Config.Cts.Token)) if (!await ImportNamesPool(db, cancellationToken).ConfigureAwait(false))
return false; return false;
} }
await using (var db = new HardwareDb()) await using (var db = new HardwareDb())
if (!await UpgradeAsync(db, Config.Cts.Token)) if (!await UpgradeAsync(db, cancellationToken).ConfigureAwait(false))
return false; return false;
return true; return true;

View File

@ -15,7 +15,7 @@ internal class HardwareDb : DbContext
#if DEBUG #if DEBUG
optionsBuilder.UseLoggerFactory(Config.LoggerFactory); optionsBuilder.UseLoggerFactory(Config.LoggerFactory);
#endif #endif
optionsBuilder.UseSqlite($"Data Source=\"{dbPath}\""); optionsBuilder.UseSqlite($""" Data Source="{dbPath}" """);
} }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)

View File

@ -276,8 +276,7 @@ internal static class ContentFilter
public static string? GetMatchedScope(Piracystring trigger, string? context) public static string? GetMatchedScope(Piracystring trigger, string? context)
=> context is { Length: >0 } => context is { Length: >0 }
&& trigger.ValidatingRegex is { Length: >0 } pattern && trigger.ValidatingRegex is { Length: >0 } pattern
&& Regex.Match(context, pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline) is { Success: true } m && Regex.Match(context, pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline) is { Success: true, Groups.Count: > 0 } m
&& m.Groups.Count > 0
? m.Groups[0].Value.Trim(256) ? m.Groups[0].Value.Trim(256)
: null; : null;
} }

View File

@ -26,7 +26,7 @@ internal static class DisabledCommandsProvider
if (DisabledCommands.Add(command)) if (DisabledCommands.Add(command))
{ {
using var db = new BotDb(); using var db = new BotDb();
db.DisabledCommands.Add(new DisabledCommand {Command = command}); db.DisabledCommands.Add(new() {Command = command});
db.SaveChanges(); db.SaveChanges();
} }
} }

View File

@ -19,7 +19,6 @@ internal static class ModProvider
} }
public static bool IsMod(ulong userId) => Moderators.ContainsKey(userId); public static bool IsMod(ulong userId) => Moderators.ContainsKey(userId);
public static bool IsSudoer(ulong userId) => Moderators.TryGetValue(userId, out var mod) && mod.Sudoer; public static bool IsSudoer(ulong userId) => Moderators.TryGetValue(userId, out var mod) && mod.Sudoer;
public static async Task<bool> AddAsync(ulong userId) public static async Task<bool> AddAsync(ulong userId)
@ -34,6 +33,7 @@ internal static class ModProvider
{ {
if (IsMod(userId)) if (IsMod(userId))
return false; return false;
Moderators[userId] = newMod; Moderators[userId] = newMod;
} }
return true; return true;

View File

@ -20,7 +20,7 @@ internal static class ScrapeStateProvider
var id = GetId(locale, containerId); var id = GetId(locale, containerId);
using var db = new ThumbnailDb(); using var db = new ThumbnailDb();
var timestamp = string.IsNullOrEmpty(id) ? db.State.OrderBy(s => s.Timestamp).FirstOrDefault() : db.State.FirstOrDefault(s => s.Locale == id); var timestamp = string.IsNullOrEmpty(id) ? db.State.OrderBy(s => s.Timestamp).FirstOrDefault() : db.State.FirstOrDefault(s => s.Locale == id);
if (timestamp?.Timestamp is long checkDate && checkDate > 0) if (timestamp is { Timestamp: long checkDate and > 0 })
return IsFresh(new DateTime(checkDate, DateTimeKind.Utc)); return IsFresh(new DateTime(checkDate, DateTimeKind.Utc));
return false; return false;
} }
@ -29,7 +29,7 @@ internal static class ScrapeStateProvider
{ {
using var db = new ThumbnailDb(); using var db = new ThumbnailDb();
var timestamp = string.IsNullOrEmpty(locale) ? db.State.OrderBy(s => s.Timestamp).FirstOrDefault() : db.State.FirstOrDefault(s => s.Locale == locale); var timestamp = string.IsNullOrEmpty(locale) ? db.State.OrderBy(s => s.Timestamp).FirstOrDefault() : db.State.FirstOrDefault(s => s.Locale == locale);
if (timestamp?.Timestamp is long checkDate && checkDate > 0) if (timestamp is { Timestamp: long checkDate and > 0 })
return new DateTime(checkDate, DateTimeKind.Utc) > dataTimestamp; return new DateTime(checkDate, DateTimeKind.Utc) > dataTimestamp;
return false; return false;
} }

View File

@ -25,13 +25,13 @@ internal static class ThumbnailProvider
productCode = productCode.ToUpperInvariant(); productCode = productCode.ToUpperInvariant();
var tmdbInfo = await PsnClient.GetTitleMetaAsync(productCode, Config.Cts.Token).ConfigureAwait(false); var tmdbInfo = await PsnClient.GetTitleMetaAsync(productCode, Config.Cts.Token).ConfigureAwait(false);
if (tmdbInfo?.Icon.Url is string tmdbIconUrl) if (tmdbInfo is { Icon.Url: string tmdbIconUrl })
return tmdbIconUrl; return tmdbIconUrl;
await using var db = new ThumbnailDb(); await using var db = new ThumbnailDb();
var thumb = await db.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productCode).ConfigureAwait(false); var thumb = await db.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productCode).ConfigureAwait(false);
//todo: add search task if not found //todo: add search task if not found
if (thumb?.EmbeddableUrl is string embeddableUrl && !string.IsNullOrEmpty(embeddableUrl)) if (thumb?.EmbeddableUrl is {Length: >0} embeddableUrl)
return embeddableUrl; return embeddableUrl;
if (string.IsNullOrEmpty(thumb?.Url) || !ScrapeStateProvider.IsFresh(thumb.Timestamp)) if (string.IsNullOrEmpty(thumb?.Url) || !ScrapeStateProvider.IsFresh(thumb.Timestamp))
@ -40,7 +40,7 @@ internal static class ThumbnailProvider
if (!string.IsNullOrEmpty(gameTdbCoverUrl)) if (!string.IsNullOrEmpty(gameTdbCoverUrl))
{ {
if (thumb is null) if (thumb is null)
thumb = (await db.Thumbnail.AddAsync(new Thumbnail {ProductCode = productCode, Url = gameTdbCoverUrl}).ConfigureAwait(false)).Entity; thumb = (await db.Thumbnail.AddAsync(new() {ProductCode = productCode, Url = gameTdbCoverUrl}).ConfigureAwait(false)).Entity;
else else
thumb.Url = gameTdbCoverUrl; thumb.Url = gameTdbCoverUrl;
thumb.Timestamp = DateTime.UtcNow.Ticks; thumb.Timestamp = DateTime.UtcNow.Ticks;
@ -81,7 +81,7 @@ internal static class ThumbnailProvider
if (!string.IsNullOrEmpty(title)) if (!string.IsNullOrEmpty(title))
{ {
if (thumb == null) if (thumb == null)
await db.Thumbnail.AddAsync(new Thumbnail await db.Thumbnail.AddAsync(new()
{ {
ProductCode = productCode, ProductCode = productCode,
Name = title, Name = title,
@ -107,7 +107,7 @@ internal static class ThumbnailProvider
contentId = contentId.ToUpperInvariant(); contentId = contentId.ToUpperInvariant();
await 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); var info = await db.Thumbnail.FirstOrDefaultAsync(ti => ti.ContentId == contentId, Config.Cts.Token).ConfigureAwait(false);
info ??= new Thumbnail{Url = url}; info ??= new() {Url = url};
if (info.Url is null) if (info.Url is null)
return (null, defaultColor); return (null, defaultColor);
@ -139,7 +139,7 @@ internal static class ThumbnailProvider
await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
} }
} }
var color = info.EmbedColor.HasValue ? new DiscordColor(info.EmbedColor.Value) : defaultColor; var color = info.EmbedColor.HasValue ? new(info.EmbedColor.Value) : defaultColor;
return (info.EmbeddableUrl, color); return (info.EmbeddableUrl, color);
} }

View File

@ -21,7 +21,7 @@ public static class TitleUpdateInfoProvider
productId = productId.ToUpper(); productId = productId.ToUpper();
var (update, xml) = await Client.GetTitleUpdatesAsync(productId, cancellationToken).ConfigureAwait(false); var (update, xml) = await Client.GetTitleUpdatesAsync(productId, cancellationToken).ConfigureAwait(false);
if (xml is string {Length: > 10}) if (xml is {Length: > 10})
{ {
var xmlChecksum = xml.GetStableHash(); var xmlChecksum = xml.GetStableHash();
await using var db = new ThumbnailDb(); await using var db = new ThumbnailDb();
@ -38,7 +38,7 @@ public static class TitleUpdateInfoProvider
updateInfo.Timestamp = DateTime.UtcNow.Ticks; updateInfo.Timestamp = DateTime.UtcNow.Ticks;
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
} }
if ((update?.Tag?.Packages?.Length ?? 0) == 0) if (update?.Tag?.Packages?.Length is null or 0)
{ {
await using var db = new ThumbnailDb(); await using var db = new ThumbnailDb();
var updateInfo = db.GameUpdateInfo.FirstOrDefault(ui => ui.ProductCode == productId); var updateInfo = db.GameUpdateInfo.FirstOrDefault(ui => ui.ProductCode == productId);
@ -50,8 +50,9 @@ public static class TitleUpdateInfoProvider
update = (TitlePatch?)xmlSerializer.Deserialize(memStream); update = (TitlePatch?)xmlSerializer.Deserialize(memStream);
if (update is not null) if (update is not null)
update.OfflineCacheTimestamp = updateInfo.Timestamp.AsUtc(); update.OfflineCacheTimestamp = updateInfo.Timestamp.AsUtc();
return update;
} }
return update; return update;
} }
@ -65,7 +66,7 @@ public static class TitleUpdateInfoProvider
{ {
var updateInfo = db.GameUpdateInfo.AsNoTracking().FirstOrDefault(ui => ui.ProductCode == titleId); var updateInfo = db.GameUpdateInfo.AsNoTracking().FirstOrDefault(ui => ui.ProductCode == titleId);
if (!cancellationToken.IsCancellationRequested if (!cancellationToken.IsCancellationRequested
&& ((updateInfo?.Timestamp ?? 0) == 0 || updateInfo!.Timestamp.AsUtc() < DateTime.UtcNow.AddMonths(-1))) && (updateInfo?.Timestamp is null or 0L || updateInfo.Timestamp.AsUtc() < DateTime.UtcNow.AddMonths(-1)))
{ {
await GetAsync(titleId, cancellationToken).ConfigureAwait(false); await GetAsync(titleId, cancellationToken).ConfigureAwait(false);
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);

View File

@ -23,7 +23,7 @@ internal class ThumbnailDb : DbContext
#if DEBUG #if DEBUG
optionsBuilder.UseLoggerFactory(Config.LoggerFactory); optionsBuilder.UseLoggerFactory(Config.LoggerFactory);
#endif #endif
optionsBuilder.UseSqlite($"Data Source=\"{dbPath}\""); optionsBuilder.UseSqlite($""" Data Source="{dbPath}" """);
} }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)

View File

@ -132,7 +132,7 @@ namespace CompatBot.EventHandlers
await using var db = new BotDb(); await using var db = new BotDb();
var matchedGroups = (from m in mc var matchedGroups = (from m in mc
from Group g in m.Groups from Group g in m.Groups
where g.Success && !string.IsNullOrEmpty(g.Value) where g is { Success: true, Value.Length: > 0 }
select g.Name select g.Name
).Distinct() ).Distinct()
.ToArray(); .ToArray();
@ -193,7 +193,7 @@ namespace CompatBot.EventHandlers
await msg.DeleteAsync("asked to shut up").ConfigureAwait(false); await msg.DeleteAsync("asked to shut up").ConfigureAwait(false);
} }
else else
await args.Message.ReactWithAsync(DiscordEmoji.FromUnicode("🙅"), @"No can do, boss ¯\\_(ツ)\_/¯").ConfigureAwait(false); await args.Message.ReactWithAsync(DiscordEmoji.FromUnicode("🙅"), @"No can do, boss ¯\\\_(ツ)\_/¯").ConfigureAwait(false);
} }
} }
} }

View File

@ -74,9 +74,7 @@ internal static class DeletedMessagesMonitor
{ {
if (attachmentContent?.Count > 0) if (attachmentContent?.Count > 0)
foreach (var f in attachmentContent.Values) foreach (var f in attachmentContent.Values)
#pragma warning disable VSTHRD103 await f.DisposeAsync();
f.Dispose();
#pragma warning restore VSTHRD103
} }
} }
} }

View File

@ -187,7 +187,7 @@ internal static class DiscordInviteFilter
public static async Task<(bool hasInvalidInvite, bool attemptToWorkaround, List<DiscordInvite> invites)> GetInvitesAsync(this DiscordClient client, string message, DiscordUser? author = null, bool tryMessageAsACode = false) public static async Task<(bool hasInvalidInvite, bool attemptToWorkaround, List<DiscordInvite> invites)> GetInvitesAsync(this DiscordClient client, string message, DiscordUser? author = null, bool tryMessageAsACode = false)
{ {
if (string.IsNullOrEmpty(message)) if (string.IsNullOrEmpty(message))
return (false, false, new List<DiscordInvite>(0)); return (false, false, new(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 discordMeLinks = InviteLink.Matches(message).Select(m => m.Groups["me_id"].Value).Distinct().Where(s => !string.IsNullOrEmpty(s)).ToList();
@ -202,7 +202,7 @@ internal static class DiscordInviteFilter
} }
} }
if (inviteCodes.Count == 0 && discordMeLinks.Count == 0 && !tryMessageAsACode) if (inviteCodes.Count == 0 && discordMeLinks.Count == 0 && !tryMessageAsACode)
return (false, attemptedWorkaround, new List<DiscordInvite>(0)); return (false, attemptedWorkaround, new(0));
var hasInvalidInvites = false; var hasInvalidInvites = false;
foreach (var meLink in discordMeLinks) foreach (var meLink in discordMeLinks)
@ -221,9 +221,9 @@ internal static class DiscordInviteFilter
using var handler = new HttpClientHandler {AllowAutoRedirect = false}; // needed to store cloudflare session cookies using var handler = new HttpClientHandler {AllowAutoRedirect = false}; // needed to store cloudflare session cookies
using var httpClient = HttpClientFactory.Create(handler, new CompressionMessageHandler()); using var httpClient = HttpClientFactory.Create(handler, new CompressionMessageHandler());
using var request = new HttpRequestMessage(HttpMethod.Get, "https://discord.me/" + meLink); using var request = new HttpRequestMessage(HttpMethod.Get, "https://discord.me/" + meLink);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html")); request.Headers.Accept.Add(new("text/html"));
request.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache"); request.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RPCS3CompatibilityBot", "2.0")); request.Headers.UserAgent.Add(new("RPCS3CompatibilityBot", "2.0"));
using var response = await httpClient.SendAsync(request).ConfigureAwait(false); using var response = await httpClient.SendAsync(request).ConfigureAwait(false);
var html = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var html = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
@ -244,8 +244,8 @@ internal static class DiscordInviteFilter
["serverEid"] = serverEidMatch.Groups["server_eid"].Value, ["serverEid"] = serverEidMatch.Groups["server_eid"].Value,
}!), }!),
}; };
postRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html")); postRequest.Headers.Accept.Add(new("text/html"));
postRequest.Headers.UserAgent.Add(new ProductInfoHeaderValue("RPCS3CompatibilityBot", "2.0")); postRequest.Headers.UserAgent.Add(new("RPCS3CompatibilityBot", "2.0"));
using var postResponse = await httpClient.SendAsync(postRequest).ConfigureAwait(false); using var postResponse = await httpClient.SendAsync(postRequest).ConfigureAwait(false);
if (postResponse.StatusCode == HttpStatusCode.Redirect) if (postResponse.StatusCode == HttpStatusCode.Redirect)
{ {
@ -254,7 +254,7 @@ internal static class DiscordInviteFilter
{ {
using var getDiscordRequest = new HttpRequestMessage(HttpMethod.Get, "https://discord.me/server/join/redirect/" + redirectId); using var getDiscordRequest = new HttpRequestMessage(HttpMethod.Get, "https://discord.me/server/join/redirect/" + redirectId);
getDiscordRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache"); getDiscordRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
getDiscordRequest.Headers.UserAgent.Add(new ProductInfoHeaderValue("RPCS3CompatibilityBot", "2.0")); getDiscordRequest.Headers.UserAgent.Add(new("RPCS3CompatibilityBot", "2.0"));
using var discordRedirect = await httpClient.SendAsync(getDiscordRequest).ConfigureAwait(false); using var discordRedirect = await httpClient.SendAsync(getDiscordRequest).ConfigureAwait(false);
if (discordRedirect.StatusCode == HttpStatusCode.Redirect) if (discordRedirect.StatusCode == HttpStatusCode.Redirect)
{ {

View File

@ -58,7 +58,7 @@ internal static class IsTheGamePlayableHandler
.Concat(matches.Select(m => m.Groups["game_title_2"].Value)) .Concat(matches.Select(m => m.Groups["game_title_2"].Value))
.Concat(matches.Select(m => m.Groups["game_title_3"].Value)) .Concat(matches.Select(m => m.Groups["game_title_3"].Value))
.FirstOrDefault(t => !string.IsNullOrEmpty(t)); .FirstOrDefault(t => !string.IsNullOrEmpty(t));
if (string.IsNullOrEmpty(gameTitle) || gameTitle.Length < 2) if (gameTitle is null or { Length: < 2 })
return; return;
gameTitle = CompatList.FixGameTitleSearch(gameTitle); gameTitle = CompatList.FixGameTitleSearch(gameTitle);
@ -89,7 +89,7 @@ internal static class IsTheGamePlayableHandler
if (!string.IsNullOrEmpty(info.Date)) if (!string.IsNullOrEmpty(info.Date))
msg += $" since {info.ToUpdated()}"; msg += $" since {info.ToUpdated()}";
} }
msg += $"\nfor more results please use compatibility list (<https://rpcs3.net/compatibility>) or `{Config.CommandPrefix}c` command in {botSpamChannel.Mention} (`!c {gameTitle.Sanitize()}`)"; msg += $"\nfor more results please use [compatibility list](<https://rpcs3.net/compatibility>) or `{Config.CommandPrefix}c` command in {botSpamChannel.Mention} (`!c {gameTitle.Sanitize()}`)";
await args.Channel.SendMessageAsync(msg).ConfigureAwait(false); await args.Channel.SendMessageAsync(msg).ConfigureAwait(false);
CooldownBuckets[args.Channel.Id] = DateTime.UtcNow; CooldownBuckets[args.Channel.Id] = DateTime.UtcNow;
} }

View File

@ -19,7 +19,7 @@ internal sealed class GzipHandler: IArchiveHandler
{ {
if (header.Length >= Header.Length) if (header.Length >= Header.Length)
{ {
if (header.Slice(0, Header.Length).SequenceEqual(Header)) if (header[..Header.Length].SequenceEqual(Header))
return (true, null); return (true, null);
} }
else if (fileName.EndsWith(".log.gz", StringComparison.InvariantCultureIgnoreCase) else if (fileName.EndsWith(".log.gz", StringComparison.InvariantCultureIgnoreCase)

View File

@ -18,7 +18,7 @@ internal sealed class PlainTextHandler: IArchiveHandler
if (fileName.Contains("tty.log", StringComparison.InvariantCultureIgnoreCase)) if (fileName.Contains("tty.log", StringComparison.InvariantCultureIgnoreCase))
return (false, null); return (false, null);
if (header.Length > 10 && Encoding.UTF8.GetString(header.Slice(0, 30)).Contains("RPCS3 v")) if (header.Length > 10 && Encoding.UTF8.GetString(header[..30]).Contains("RPCS3 v"))
return (true, null); return (true, null);
return (false, null); return (false, null);

View File

@ -18,7 +18,7 @@ internal sealed class RarHandler: IArchiveHandler
public (bool result, string? reason) CanHandle(string fileName, int fileSize, ReadOnlySpan<byte> header) public (bool result, string? reason) CanHandle(string fileName, int fileSize, ReadOnlySpan<byte> header)
{ {
if (header.Length >= Header.Length && header.Slice(0, Header.Length).SequenceEqual(Header) if (header.Length >= Header.Length && header[..Header.Length].SequenceEqual(Header)
|| header.Length == 0 && fileName.EndsWith(".rar", StringComparison.InvariantCultureIgnoreCase)) || header.Length == 0 && fileName.EndsWith(".rar", StringComparison.InvariantCultureIgnoreCase))
{ {
var firstEntry = Encoding.ASCII.GetString(header); var firstEntry = Encoding.ASCII.GetString(header);

View File

@ -17,7 +17,7 @@ internal sealed class SevenZipHandler: IArchiveHandler
public (bool result, string? reason) CanHandle(string fileName, int fileSize, ReadOnlySpan<byte> header) public (bool result, string? reason) CanHandle(string fileName, int fileSize, ReadOnlySpan<byte> header)
{ {
if (header.Length >= Header.Length && header.Slice(0, Header.Length).SequenceEqual(Header) if (header.Length >= Header.Length && header[..Header.Length].SequenceEqual(Header)
|| header.Length == 0 && fileName.EndsWith(".7z", StringComparison.InvariantCultureIgnoreCase)) || header.Length == 0 && fileName.EndsWith(".7z", StringComparison.InvariantCultureIgnoreCase))
{ {
if (fileSize > Config.AttachmentSizeLimit) if (fileSize > Config.AttachmentSizeLimit)

View File

@ -19,7 +19,7 @@ internal sealed class ZipHandler: IArchiveHandler
public (bool result, string? reason) CanHandle(string fileName, int fileSize, ReadOnlySpan<byte> header) public (bool result, string? reason) CanHandle(string fileName, int fileSize, ReadOnlySpan<byte> header)
{ {
if (header.Length >= Header.Length && header.Slice(0, Header.Length).SequenceEqual(Header) if (header.Length >= Header.Length && header[..Header.Length].SequenceEqual(Header)
|| header.Length == 0 && fileName.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase)) || header.Length == 0 && fileName.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase))
{ {
var firstEntry = Encoding.ASCII.GetString(header); var firstEntry = Encoding.ASCII.GetString(header);

View File

@ -13,7 +13,6 @@ namespace CompatBot.EventHandlers.LogParsing;
internal static partial class LogParser internal static partial class LogParser
{ {
private static readonly byte[] Bom = {0xEF, 0xBB, 0xBF}; private static readonly byte[] Bom = {0xEF, 0xBB, 0xBF};
private static readonly PoorMansTaskScheduler<LogParseState> TaskScheduler = new(); private static readonly PoorMansTaskScheduler<LogParseState> TaskScheduler = new();
public static async Task<LogParseState> ReadPipeAsync(PipeReader reader, CancellationToken cancellationToken) public static async Task<LogParseState> ReadPipeAsync(PipeReader reader, CancellationToken cancellationToken)

View File

@ -60,8 +60,7 @@ internal partial class LogParser
if (trigger == "{PPU[" || trigger == "⁂") if (trigger == "{PPU[" || trigger == "⁂")
{ {
if (state.WipCollection["serial"] is string serial if (state.WipCollection["serial"] is string serial
&& extractor.Match(buffer) is Match match && extractor.Match(buffer) is { Success: true } match
&& match.Success
&& match.Groups["syscall_name"].Value is string syscallName) && match.Groups["syscall_name"].Value is string syscallName)
{ {
lock (state) lock (state)
@ -81,8 +80,7 @@ internal partial class LogParser
foreach (Match match in matches) foreach (Match match in matches)
foreach (Group group in match.Groups) foreach (Group group in match.Groups)
{ {
if (string.IsNullOrEmpty(group.Name) if (group.Name is null or "" or "0"
|| group.Name == "0"
|| string.IsNullOrWhiteSpace(group.Value)) || string.IsNullOrWhiteSpace(group.Value))
continue; continue;

View File

@ -30,51 +30,50 @@ internal sealed class DropboxHandler : BaseSourceHandler
using var client = HttpClientFactory.Create(); using var client = HttpClientFactory.Create();
foreach (Match m in matches) foreach (Match m in matches)
{ {
if (m.Groups["dropbox_link"].Value is string lnk if (m.Groups["dropbox_link"].Value is not { Length: > 0 } lnk
&& !string.IsNullOrEmpty(lnk) || !Uri.TryCreate(lnk, UriKind.Absolute, out var uri))
&& Uri.TryCreate(lnk, UriKind.Absolute, out var uri)) continue;
try
{ {
uri = uri.SetQueryParameter("dl", "1");
var filename = Path.GetFileName(lnk);
var filesize = -1;
using (var request = new HttpRequestMessage(HttpMethod.Head, uri))
{
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Config.Cts.Token);
if (response.Content.Headers.ContentLength > 0)
filesize = (int)response.Content.Headers.ContentLength.Value;
if (response.Content.Headers.ContentDisposition?.FileNameStar is {Length: >0} fname)
filename = fname;
uri = response.RequestMessage?.RequestUri;
}
await using var stream = await client.GetStreamAsync(uri).ConfigureAwait(false);
var buf = BufferPool.Rent(SnoopBufferSize);
try try
{ {
uri = uri.SetQueryParameter("dl", "1"); var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
var filename = Path.GetFileName(lnk); foreach (var handler in handlers)
var filesize = -1;
using (var request = new HttpRequestMessage(HttpMethod.Head, uri))
{ {
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Config.Cts.Token); var (canHandle, reason) = handler.CanHandle(filename, filesize, buf.AsSpan(0, read));
if (response.Content.Headers.ContentLength > 0) if (canHandle)
filesize = (int)response.Content.Headers.ContentLength.Value; return (new DropboxSource(uri, handler, filename, filesize), null);
if (response.Content.Headers.ContentDisposition?.FileNameStar is string fname && !string.IsNullOrEmpty(fname)) else if (!string.IsNullOrEmpty(reason))
filename = fname; return (null, reason);
uri = response.RequestMessage?.RequestUri;
}
await using var stream = await client.GetStreamAsync(uri).ConfigureAwait(false);
var buf = BufferPool.Rent(SnoopBufferSize);
try
{
var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
foreach (var handler in handlers)
{
var (canHandle, reason) = handler.CanHandle(filename, filesize, buf.AsSpan(0, read));
if (canHandle)
return (new DropboxSource(uri, handler, filename, filesize), null);
else if (!string.IsNullOrEmpty(reason))
return (null, reason);
}
}
finally
{
BufferPool.Return(buf);
} }
} }
finally
catch (Exception e)
{ {
Config.Log.Warn(e, $"Error sniffing {m.Groups["dropbox_link"].Value}"); BufferPool.Return(buf);
} }
} }
catch (Exception e)
{
Config.Log.Warn(e, $"Error sniffing {m.Groups["dropbox_link"].Value}");
}
} }
return (null, null); return (null, null);
} }

View File

@ -28,51 +28,50 @@ internal sealed class GenericLinkHandler : BaseSourceHandler
using var client = HttpClientFactory.Create(); using var client = HttpClientFactory.Create();
foreach (Match m in matches) foreach (Match m in matches)
{ {
if (m.Groups["link"].Value is string lnk if (m.Groups["link"].Value is not { Length: > 0 } lnk
&& !string.IsNullOrEmpty(lnk) || !Uri.TryCreate(lnk, UriKind.Absolute, out var uri)
&& Uri.TryCreate(lnk, UriKind.Absolute, out var uri) || "tty.log".Equals(m.Groups["filename"].Value, StringComparison.InvariantCultureIgnoreCase))
&& !"tty.log".Equals(m.Groups["filename"].Value, StringComparison.InvariantCultureIgnoreCase)) continue;
try
{ {
var host = uri.Host;
var filename = Path.GetFileName(lnk);
var filesize = -1;
using (var request = new HttpRequestMessage(HttpMethod.Head, uri))
{
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Config.Cts.Token);
if (response.Content.Headers.ContentLength > 0)
filesize = (int)response.Content.Headers.ContentLength.Value;
if (response.Content.Headers.ContentDisposition?.FileNameStar is {Length: >0} fname)
filename = fname;
uri = response.RequestMessage?.RequestUri;
}
await using var stream = await client.GetStreamAsync(uri).ConfigureAwait(false);
var buf = BufferPool.Rent(SnoopBufferSize);
try try
{ {
var host = uri.Host; var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
var filename = Path.GetFileName(lnk); foreach (var handler in handlers)
var filesize = -1;
using (var request = new HttpRequestMessage(HttpMethod.Head, uri))
{ {
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Config.Cts.Token); var (canHandle, reason) = handler.CanHandle(filename, filesize, buf.AsSpan(0, read));
if (response.Content.Headers.ContentLength > 0) if (canHandle)
filesize = (int)response.Content.Headers.ContentLength.Value; return (new GenericSource(uri, handler, host, filename, filesize), null);
if (response.Content.Headers.ContentDisposition?.FileNameStar is string fname && !string.IsNullOrEmpty(fname)) else if (!string.IsNullOrEmpty(reason))
filename = fname; return (null, reason);
uri = response.RequestMessage?.RequestUri;
}
await using var stream = await client.GetStreamAsync(uri).ConfigureAwait(false);
var buf = BufferPool.Rent(SnoopBufferSize);
try
{
var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
foreach (var handler in handlers)
{
var (canHandle, reason) = handler.CanHandle(filename, filesize, buf.AsSpan(0, read));
if (canHandle)
return (new GenericSource(uri, handler, host, filename, filesize), null);
else if (!string.IsNullOrEmpty(reason))
return (null, reason);
}
}
finally
{
BufferPool.Return(buf);
} }
} }
catch (Exception e) finally
{ {
Config.Log.Warn(e, $"Error sniffing {m.Groups["link"].Value}"); BufferPool.Return(buf);
} }
} }
catch (Exception e)
{
Config.Log.Warn(e, $"Error sniffing {m.Groups["link"].Value}");
}
} }
return (null, null); return (null, null);
} }

View File

@ -39,42 +39,38 @@ internal sealed class GoogleDriveHandler: BaseSourceHandler
{ {
try try
{ {
if (m.Groups["gdrive_id"].Value is string fid if (m.Groups["gdrive_id"].Value is not { Length: > 0 } fid)
&& !string.IsNullOrEmpty(fid)) continue;
var fileInfoRequest = client.Files.Get(fid);
fileInfoRequest.Fields = "name, size, kind";
var fileMeta = await fileInfoRequest.ExecuteAsync(Config.Cts.Token).ConfigureAwait(false);
if (fileMeta is not { Kind: "drive#file", Size: > 0 })
continue;
var buf = BufferPool.Rent(SnoopBufferSize);
try
{ {
var fileInfoRequest = client.Files.Get(fid); await using var stream = new MemoryStream(buf, true);
fileInfoRequest.Fields = "name, size, kind"; var limit = Math.Min(SnoopBufferSize, fileMeta.Size.Value) - 1;
var fileMeta = await fileInfoRequest.ExecuteAsync(Config.Cts.Token).ConfigureAwait(false); var progress = await fileInfoRequest.DownloadRangeAsync(stream, new RangeHeaderValue(0, limit), Config.Cts.Token).ConfigureAwait(false);
if (fileMeta.Kind == "drive#file" && fileMeta.Size > 0) if (progress.Status != DownloadStatus.Completed)
{ continue;
var buf = BufferPool.Rent(SnoopBufferSize);
try
{
int read;
await using (var stream = new MemoryStream(buf, true))
{
var limit = Math.Min(SnoopBufferSize, fileMeta.Size.Value) - 1;
var progress = await fileInfoRequest.DownloadRangeAsync(stream, new RangeHeaderValue(0, limit), Config.Cts.Token).ConfigureAwait(false);
if (progress.Status != DownloadStatus.Completed)
continue;
read = (int)progress.BytesDownloaded; var read = (int)progress.BytesDownloaded;
} foreach (var handler in handlers)
foreach (var handler in handlers) {
{ var (canHandle, reason) = handler.CanHandle(fileMeta.Name, (int)fileMeta.Size, buf.AsSpan(0, read));
var (canHandle, reason) = handler.CanHandle(fileMeta.Name, (int)fileMeta.Size, buf.AsSpan(0, read)); if (canHandle)
if (canHandle) return (new GoogleDriveSource(client, fileInfoRequest, fileMeta, handler), null);
return (new GoogleDriveSource(client, fileInfoRequest, fileMeta, handler), null); else if (!string.IsNullOrEmpty(reason))
else if (!string.IsNullOrEmpty(reason)) return(null, reason);
return(null, reason);
}
}
finally
{
BufferPool.Return(buf);
}
} }
} }
finally
{
BufferPool.Return(buf);
}
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -31,56 +31,55 @@ internal sealed class MediafireHandler : BaseSourceHandler
using var client = HttpClientFactory.Create(); using var client = HttpClientFactory.Create();
foreach (Match m in matches) foreach (Match m in matches)
{ {
if (m.Groups["mediafire_link"].Value is string lnk if (m.Groups["mediafire_link"].Value is not { Length: > 0 } lnk
&& !string.IsNullOrEmpty(lnk) || !Uri.TryCreate(lnk, UriKind.Absolute, out var webLink))
&& Uri.TryCreate(lnk, UriKind.Absolute, out var webLink)) continue;
try
{ {
var filename = m.Groups["filename"].Value;
var filesize = -1;
Config.Log.Debug($"Trying to get download link for {webLink}...");
var directLink = await Client.GetDirectDownloadLinkAsync(webLink, Config.Cts.Token).ConfigureAwait(false);
if (directLink is null)
return (null, null);
Config.Log.Debug($"Trying to get content size for {directLink}...");
using (var request = new HttpRequestMessage(HttpMethod.Head, directLink))
{
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Config.Cts.Token);
if (response.Content.Headers.ContentLength > 0)
filesize = (int)response.Content.Headers.ContentLength.Value;
if (response.Content.Headers.ContentDisposition?.FileName is {Length: >0} fname)
filename = fname;
}
Config.Log.Debug($"Trying to get content stream for {directLink}...");
await using var stream = await client.GetStreamAsync(directLink).ConfigureAwait(false);
var buf = BufferPool.Rent(SnoopBufferSize);
try try
{ {
var filename = m.Groups["filename"].Value; var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
var filesize = -1; foreach (var handler in handlers)
Config.Log.Debug($"Trying to get download link for {webLink}...");
var directLink = await Client.GetDirectDownloadLinkAsync(webLink, Config.Cts.Token).ConfigureAwait(false);
if (directLink is null)
return (null, null);
Config.Log.Debug($"Trying to get content size for {directLink}...");
using (var request = new HttpRequestMessage(HttpMethod.Head, directLink))
{ {
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Config.Cts.Token); var (canHandle, reason) = handler.CanHandle(filename, filesize, buf.AsSpan(0, read));
if (response.Content.Headers.ContentLength > 0) if (canHandle)
filesize = (int)response.Content.Headers.ContentLength.Value; return (new MediafireSource(directLink, handler, filename, filesize), null);
if (response.Content.Headers.ContentDisposition?.FileName is string fname && !string.IsNullOrEmpty(fname)) else if (!string.IsNullOrEmpty(reason))
filename = fname; return (null, reason);
}
Config.Log.Debug($"Trying to get content stream for {directLink}...");
await using var stream = await client.GetStreamAsync(directLink).ConfigureAwait(false);
var buf = BufferPool.Rent(SnoopBufferSize);
try
{
var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
foreach (var handler in handlers)
{
var (canHandle, reason) = handler.CanHandle(filename, filesize, buf.AsSpan(0, read));
if (canHandle)
return (new MediafireSource(directLink, handler, filename, filesize), null);
else if (!string.IsNullOrEmpty(reason))
return (null, reason);
}
Config.Log.Debug("MediaFire Response:\n" + Encoding.UTF8.GetString(buf, 0, read));
}
finally
{
BufferPool.Return(buf);
} }
Config.Log.Debug("MediaFire Response:\n" + Encoding.UTF8.GetString(buf, 0, read));
} }
catch (Exception e) finally
{ {
Config.Log.Warn(e, $"Error sniffing {m.Groups["mediafire_link"].Value}"); BufferPool.Return(buf);
} }
} }
catch (Exception e)
{
Config.Log.Warn(e, $"Error sniffing {m.Groups["mediafire_link"].Value}");
}
} }
return (null, null); return (null, null);
} }

View File

@ -33,34 +33,32 @@ internal sealed class MegaHandler : BaseSourceHandler
{ {
try try
{ {
if (m.Groups["mega_link"].Value is string lnk if (m.Groups["mega_link"].Value is not { Length: > 0 } lnk
&& !string.IsNullOrEmpty(lnk) || !Uri.TryCreate(lnk, UriKind.Absolute, out var uri))
&& Uri.TryCreate(lnk, UriKind.Absolute, out var uri)) continue;
var node = await client.GetNodeFromLinkAsync(uri).ConfigureAwait(false);
if (node.Type is not NodeType.File)
continue;
var buf = BufferPool.Rent(SnoopBufferSize);
try
{ {
var node = await client.GetNodeFromLinkAsync(uri).ConfigureAwait(false); await using var stream = await client.DownloadAsync(uri, Doodad, Config.Cts.Token).ConfigureAwait(false);
if (node.Type == NodeType.File) var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
foreach (var handler in handlers)
{ {
var buf = BufferPool.Rent(SnoopBufferSize); var (canHandle, reason) = handler.CanHandle(node.Name, (int)node.Size, buf.AsSpan(0, read));
try if (canHandle)
{ return (new MegaSource(client, uri, node, handler), null);
int read; else if (!string.IsNullOrEmpty(reason))
await using (var stream = await client.DownloadAsync(uri, Doodad, Config.Cts.Token).ConfigureAwait(false)) return (null, reason);
read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
foreach (var handler in handlers)
{
var (canHandle, reason) = handler.CanHandle(node.Name, (int)node.Size, buf.AsSpan(0, read));
if (canHandle)
return (new MegaSource(client, uri, node, handler), null);
else if (!string.IsNullOrEmpty(reason))
return (null, reason);
}
}
finally
{
BufferPool.Return(buf);
}
} }
} }
finally
{
BufferPool.Return(buf);
}
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -32,42 +32,39 @@ internal sealed class OneDriveSourceHandler : BaseSourceHandler
{ {
try try
{ {
if (m.Groups["onedrive_link"].Value is string lnk if (m.Groups["onedrive_link"].Value is not { Length: > 0 } lnk
&& !string.IsNullOrEmpty(lnk) || !Uri.TryCreate(lnk, UriKind.Absolute, out var uri)
&& Uri.TryCreate(lnk, UriKind.Absolute, out var uri) || await Client.ResolveContentLinkAsync(uri, Config.Cts.Token).ConfigureAwait(false) is not { ContentDownloadUrl: { Length: > 0 } downloadUrl } itemMeta)
&& await Client.ResolveContentLinkAsync(uri, Config.Cts.Token).ConfigureAwait(false) is DriveItemMeta itemMeta continue;
&& itemMeta.ContentDownloadUrl is string downloadUrl) try
{ {
var filename = itemMeta.Name ?? "";
var filesize = itemMeta.Size;
uri = new(downloadUrl);
await using var stream = await httpClient.GetStreamAsync(uri).ConfigureAwait(false);
var buf = BufferPool.Rent(SnoopBufferSize);
try try
{ {
var filename = itemMeta.Name ?? ""; var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
var filesize = itemMeta.Size; foreach (var handler in handlers)
uri = new Uri(downloadUrl);
await using var stream = await httpClient.GetStreamAsync(uri).ConfigureAwait(false);
var buf = BufferPool.Rent(SnoopBufferSize);
try
{ {
var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false); var (canHandle, reason) = handler.CanHandle(filename, filesize, buf.AsSpan(0, read));
foreach (var handler in handlers) if (canHandle)
{ return (new OneDriveSource(uri, handler, filename, filesize), null);
var (canHandle, reason) = handler.CanHandle(filename, filesize, buf.AsSpan(0, read)); else if (!string.IsNullOrEmpty(reason))
if (canHandle) return (null, reason);
return (new OneDriveSource(uri, handler, filename, filesize), null);
else if (!string.IsNullOrEmpty(reason))
return (null, reason);
}
}
finally
{
BufferPool.Return(buf);
} }
} }
catch (Exception e) finally
{ {
Config.Log.Warn(e, $"Error sniffing {m.Groups["link"].Value}"); BufferPool.Return(buf);
} }
} }
catch (Exception e)
{
Config.Log.Warn(e, $"Error sniffing {m.Groups["link"].Value}");
}
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -29,31 +29,30 @@ internal sealed class PastebinHandler : BaseSourceHandler
{ {
try try
{ {
if (m.Groups["pastebin_id"].Value is string pid if (m.Groups["pastebin_id"].Value is not { Length: > 0 } pid)
&& !string.IsNullOrEmpty(pid)) continue;
var uri = new Uri("https://pastebin.com/raw/" + pid);
await using var stream = await client.GetStreamAsync(uri).ConfigureAwait(false);
var buf = BufferPool.Rent(SnoopBufferSize);
try
{ {
var uri = new Uri("https://pastebin.com/raw/" + pid); var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
await using var stream = await client.GetStreamAsync(uri).ConfigureAwait(false); var filename = pid + ".log";
var buf = BufferPool.Rent(SnoopBufferSize); var filesize = stream.CanSeek ? (int)stream.Length : 0;
try foreach (var handler in handlers)
{ {
var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false); var (canHandle, reason) = handler.CanHandle(filename, filesize, buf.AsSpan(0, read));
var filename = pid + ".log"; if (canHandle)
var filesize = stream.CanSeek ? (int)stream.Length : 0; return (new PastebinSource(uri, filename, filesize, handler), null);
foreach (var handler in handlers) else if (!string.IsNullOrEmpty(reason))
{ return (null, reason);
var (canHandle, reason) = handler.CanHandle(filename, filesize, buf.AsSpan(0, read));
if (canHandle)
return (new PastebinSource(uri, filename, filesize, handler), null);
else if (!string.IsNullOrEmpty(reason))
return (null, reason);
}
}
finally
{
BufferPool.Return(buf);
} }
} }
finally
{
BufferPool.Return(buf);
}
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -29,49 +29,48 @@ internal sealed class YandexDiskHandler: BaseSourceHandler
using var client = HttpClientFactory.Create(); using var client = HttpClientFactory.Create();
foreach (Match m in matches) foreach (Match m in matches)
{ {
if (m.Groups["yadisk_link"].Value is string lnk if (m.Groups["yadisk_link"].Value is not { Length: > 0 } lnk
&& !string.IsNullOrEmpty(lnk) || !Uri.TryCreate(lnk, UriKind.Absolute, out var webLink))
&& Uri.TryCreate(lnk, UriKind.Absolute, out var webLink)) continue;
try
{ {
var filename = "";
var filesize = -1;
var resourceInfo = await Client.GetResourceInfoAsync(webLink, Config.Cts.Token).ConfigureAwait(false);
if (string.IsNullOrEmpty(resourceInfo?.File))
return (null, null);
if (resourceInfo.Size.HasValue)
filesize = resourceInfo.Size.Value;
if (!string.IsNullOrEmpty(resourceInfo.Name))
filename = resourceInfo.Name;
await using var stream = await client.GetStreamAsync(resourceInfo.File).ConfigureAwait(false);
var buf = BufferPool.Rent(SnoopBufferSize);
try try
{ {
var filename = ""; var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
var filesize = -1; foreach (var handler in handlers)
var resourceInfo = await Client.GetResourceInfoAsync(webLink, Config.Cts.Token).ConfigureAwait(false);
if (string.IsNullOrEmpty(resourceInfo?.File))
return (null, null);
if (resourceInfo.Size.HasValue)
filesize = resourceInfo.Size.Value;
if (!string.IsNullOrEmpty(resourceInfo.Name))
filename = resourceInfo.Name;
await using var stream = await client.GetStreamAsync(resourceInfo.File).ConfigureAwait(false);
var buf = BufferPool.Rent(SnoopBufferSize);
try
{ {
var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false); var (canHandle, reason) = handler.CanHandle(filename, filesize, buf.AsSpan(0, read));
foreach (var handler in handlers) if (canHandle)
{ return (new YaDiskSource(resourceInfo.File, handler, filename, filesize), null);
var (canHandle, reason) = handler.CanHandle(filename, filesize, buf.AsSpan(0, read)); else if (!string.IsNullOrEmpty(reason))
if (canHandle) return (null, reason);
return (new YaDiskSource(resourceInfo.File, handler, filename, filesize), null);
else if (!string.IsNullOrEmpty(reason))
return (null, reason);
}
}
finally
{
BufferPool.Return(buf);
} }
} }
finally
catch (Exception e)
{ {
Config.Log.Warn(e, $"Error sniffing {m.Groups["yadisk_link"].Value}"); BufferPool.Return(buf);
} }
} }
catch (Exception e)
{
Config.Log.Warn(e, $"Error sniffing {m.Groups["yadisk_link"].Value}");
}
} }
return (null, null); return (null, null);
} }

View File

@ -136,8 +136,10 @@ public static class LogParsingHandler
{ {
botMsg = await botMsg.UpdateOrCreateMessageAsync(channel, embed: new DiscordEmbedBuilder botMsg = await botMsg.UpdateOrCreateMessageAsync(channel, embed: new DiscordEmbedBuilder
{ {
Description = "Log analysis failed, most likely cause is a truncated/invalid log.\n" + Description = """
"Please run the game again and re-upload a new copy.", Log analysis failed, most likely cause is a truncated/invalid log.
Please run the game again and re-upload a new copy.
""",
Color = Config.Colors.LogResultFailed, Color = Config.Colors.LogResultFailed,
} }
.AddAuthor(client, message, source) .AddAuthor(client, message, source)
@ -154,8 +156,8 @@ public static class LogParsingHandler
{ {
if (result.SelectedFilter is null) if (result.SelectedFilter is null)
{ {
Config.Log.Error("Piracy was detectedin log, but no trigger provided"); Config.Log.Error("Piracy was detected in log, but no trigger provided");
result.SelectedFilter = new Piracystring result.SelectedFilter = new()
{ {
String = "Unknown trigger, plz kick 13xforever", String = "Unknown trigger, plz kick 13xforever",
Actions = FilterAction.IssueWarning | FilterAction.RemoveContent, Actions = FilterAction.IssueWarning | FilterAction.RemoveContent,
@ -188,13 +190,14 @@ public static class LogParsingHandler
try try
{ {
botMsg = await botMsg.UpdateOrCreateMessageAsync(channel, botMsg = await botMsg.UpdateOrCreateMessageAsync(channel,
$"{message.Author.Mention}, please read carefully:\n" + $"""
"🏴‍☠️ **Pirated content detected** 🏴‍☠️\n" + # 🏴 **Pirated content detected** 🏴
"__You are being denied further support until you legally dump the game__.\n" + {message.Author.Mention}, please read carefully:
"Please note that the RPCS3 community and its developers do not support piracy.\n" + __You are being denied further support until you legally dump the game__.
"Most of the issues with pirated dumps occur due to them being modified in some way " + Please note that the RPCS3 community and its developers do not support piracy.
"that prevent them from working on RPCS3.\n" + Most of the issues with pirated dumps occur due to them being modified in some way that prevent them from working on RPCS3.
"If you need help obtaining valid working dump of the game you own, please read the quickstart guide at <https://rpcs3.net/quickstart>" If you need help obtaining valid working dump of the game you own, please read [the quickstart guide](<https://rpcs3.net/quickstart>).
"""
).ConfigureAwait(false); ).ConfigureAwait(false);
} }
catch (Exception e) catch (Exception e)
@ -233,8 +236,7 @@ public static class LogParsingHandler
if (isHelpChannel) if (isHelpChannel)
await botMsg.UpdateOrCreateMessageAsync( await botMsg.UpdateOrCreateMessageAsync(
channel, channel,
$"{message.Author.Mention} please describe the issue if you require help, " + $"{message.Author.Mention} please describe the issue if you require help, or upload log in {botSpamChannel.Mention} if you only need to check your logs automatically"
$"or upload log in {botSpamChannel.Mention} if you only need to check your logs automatically"
).ConfigureAwait(false); ).ConfigureAwait(false);
else else
{ {
@ -278,7 +280,11 @@ public static class LogParsingHandler
{ {
case "TXT": case "TXT":
{ {
await channel.SendMessageAsync($"{message.Author.Mention} Please upload the full RPCS3.log.gz (or RPCS3.log with a zip/rar icon) file after closing the emulator instead of copying the logs from RPCS3's interface, as it doesn't contain all the required information.").ConfigureAwait(false); await channel.SendMessageAsync(
$"{message.Author.Mention}, please upload the full RPCS3.log.gz (or RPCS3.log with a zip/rar icon) file " +
"after closing the emulator instead of copying the logs from RPCS3's interface, " +
"as it doesn't contain all the required information."
).ConfigureAwait(false);
Config.TelemetryClient?.TrackRequest(nameof(LogParsingHandler), start, DateTimeOffset.UtcNow - start, HttpStatusCode.BadRequest.ToString(), true); Config.TelemetryClient?.TrackRequest(nameof(LogParsingHandler), start, DateTimeOffset.UtcNow - start, HttpStatusCode.BadRequest.ToString(), true);
return; return;
} }
@ -398,7 +404,7 @@ public static class LogParsingHandler
private static DiscordEmbedBuilder GetAnalyzingMsgEmbed(DiscordClient client) private static DiscordEmbedBuilder GetAnalyzingMsgEmbed(DiscordClient client)
{ {
var indicator = client.GetEmoji(":kannamag:", Config.Reactions.PleaseWait); var indicator = client.GetEmoji(":kannamag:", Config.Reactions.PleaseWait);
return new DiscordEmbedBuilder return new()
{ {
Description = $"{indicator} Looking at the log, please wait... {indicator}", Description = $"{indicator} Looking at the log, please wait... {indicator}",
Color = Config.Colors.LogUnknown, Color = Config.Colors.LogUnknown,

View File

@ -24,9 +24,8 @@ internal static class NewBuildsMonitor
if (args.Author.IsBotSafeCheck() if (args.Author.IsBotSafeCheck()
&& !args.Author.IsCurrent && !args.Author.IsCurrent
&& "github".Equals(args.Channel.Name, StringComparison.InvariantCultureIgnoreCase) && "github".Equals(args.Channel.Name, StringComparison.InvariantCultureIgnoreCase)
&& args.Message.Embeds.FirstOrDefault() is DiscordEmbed embed && args.Message?.Embeds is [{ Title: { Length: > 0 } title }, ..]
&& !string.IsNullOrEmpty(embed.Title) && BuildResult.IsMatch(title)
&& BuildResult.IsMatch(embed.Title)
) )
{ {
Config.Log.Info("Found new PR merge message"); Config.Log.Info("Found new PR merge message");

View File

@ -20,8 +20,8 @@ internal static class PostLogHelpHandler
private static readonly TimeSpan ThrottlingThreshold = TimeSpan.FromSeconds(5); private static readonly TimeSpan ThrottlingThreshold = TimeSpan.FromSeconds(5);
private static readonly Dictionary<string, Explanation> DefaultExplanation = new() private static readonly Dictionary<string, Explanation> DefaultExplanation = new()
{ {
["log"] = new Explanation { Text = "To upload log, run the game, then completely close RPCS3, then drag and drop rpcs3.log.gz from the RPCS3 folder into Discord. The file may have a zip or rar icon." }, ["log"] = new() { Text = "To upload log, run the game, then completely close RPCS3, then drag and drop rpcs3.log.gz from the RPCS3 folder into Discord. The file may have a zip or rar icon." },
["vulkan-1"] = new Explanation { Text = "Please remove all the traces of video drivers using DDU, and then reinstall the latest driver version for your GPU." }, ["vulkan-1"] = new() { Text = "Please remove all the traces of video drivers using DDU, and then reinstall the latest driver version for your GPU." },
}; };
private static DateTime lastMention = DateTime.UtcNow.AddHours(-1); private static DateTime lastMention = DateTime.UtcNow.AddHours(-1);

View File

@ -21,7 +21,7 @@ internal static class ThumbnailCacheMonitor
await using var db = new ThumbnailDb(); await using var db = new ThumbnailDb();
var thumb = db.Thumbnail.FirstOrDefault(i => i.ContentId == args.Message.Content); var thumb = db.Thumbnail.FirstOrDefault(i => i.ContentId == args.Message.Content);
if (thumb?.EmbeddableUrl is string url && !string.IsNullOrEmpty(url) && args.Message.Attachments.Any(a => a.Url == url)) if (thumb is { EmbeddableUrl: { Length: > 0 } url } && args.Message.Attachments.Any(a => a.Url == url))
{ {
thumb.EmbeddableUrl = null; thumb.EmbeddableUrl = null;
await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);

View File

@ -15,8 +15,10 @@ namespace CompatBot.EventHandlers;
internal static class UnknownCommandHandler internal static class UnknownCommandHandler
{ {
private static readonly Regex BinaryQuestion = new(@"^\s*(am I|(are|is|do(es)|did|can(?!\s+of\s+)|should|must|have)(n't)?|shall|shan't|may|will|won't)\b", private static readonly Regex BinaryQuestion = new(
RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase); @"^\s*(am I|(are|is|do(es)|did|can(?!\s+of\s+)|should|must|have)(n't)?|shall|shan't|may|will|won't)\b",
RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase
);
public static Task OnError(CommandsNextExtension cne, CommandErrorEventArgs e) public static Task OnError(CommandsNextExtension cne, CommandErrorEventArgs e)
{ {

View File

@ -29,7 +29,10 @@ public static class UsernameZalgoMonitor
{ {
var suggestedName = StripZalgo(m.DisplayName, m.Username, m.Id).Sanitize(); var suggestedName = StripZalgo(m.DisplayName, m.Username, m.Id).Sanitize();
await c.ReportAsync("🔣 Potential display name issue", 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}**", $"""
User {m.GetMentionWithNickname()} has changed their __username__ and is now shown as **{m.DisplayName.Sanitize()}**
Automatically renamed to: **{suggestedName}**
""",
null, null,
ReportSeverity.Low); ReportSeverity.Low);
await DmAndRenameUserAsync(c, m, suggestedName).ConfigureAwait(false); await DmAndRenameUserAsync(c, m, suggestedName).ConfigureAwait(false);
@ -60,7 +63,10 @@ public static class UsernameZalgoMonitor
{ {
var suggestedName = StripZalgo(name, fallback, args.Member.Id).Sanitize(); var suggestedName = StripZalgo(name, fallback, args.Member.Id).Sanitize();
await c.ReportAsync("🔣 Potential display name issue", await c.ReportAsync("🔣 Potential display name issue",
$"Member {member.GetMentionWithNickname()} has changed their __display name__ and is now shown as **{name.Sanitize()}**\nAutomatically renamed to: **{suggestedName}**", $"""
Member {member.GetMentionWithNickname()} has changed their __display name__ and is now shown as **{name.Sanitize()}**
Automatically renamed to: **{suggestedName}**
""",
null, null,
ReportSeverity.Low); ReportSeverity.Low);
await DmAndRenameUserAsync(c, member, suggestedName).ConfigureAwait(false); await DmAndRenameUserAsync(c, member, suggestedName).ConfigureAwait(false);
@ -81,7 +87,10 @@ public static class UsernameZalgoMonitor
{ {
var suggestedName = StripZalgo(name, args.Member.Username, args.Member.Id).Sanitize(); var suggestedName = StripZalgo(name, args.Member.Username, args.Member.Id).Sanitize();
await c.ReportAsync("🔣 Potential display name issue", await c.ReportAsync("🔣 Potential display name issue",
$"New member joined the server: {args.Member.GetMentionWithNickname()} and is shown as **{name.Sanitize()}**\nAutomatically renamed to: **{suggestedName}**", $"""
New member joined the server: {args.Member.GetMentionWithNickname()} and is shown as **{name.Sanitize()}**
Automatically renamed to: **{suggestedName}**
""",
null, null,
ReportSeverity.Low); ReportSeverity.Low);
await DmAndRenameUserAsync(c, args.Member, suggestedName).ConfigureAwait(false); await DmAndRenameUserAsync(c, args.Member, suggestedName).ConfigureAwait(false);
@ -106,9 +115,11 @@ public static class UsernameZalgoMonitor
var renameTask = member.ModifyAsync(m => m.Nickname = suggestedName); var renameTask = member.ModifyAsync(m => m.Nickname = suggestedName);
Config.Log.Info($"Renamed {member.Username}#{member.Discriminator} ({member.Id}) to {suggestedName}"); Config.Log.Info($"Renamed {member.Username}#{member.Discriminator} ({member.Id}) to {suggestedName}");
var rulesChannel = await client.GetChannelAsync(Config.BotRulesChannelId).ConfigureAwait(false); var rulesChannel = await client.GetChannelAsync(Config.BotRulesChannelId).ConfigureAwait(false);
var msg = $"Hello, your current _display name_ is breaking {rulesChannel.Mention} #7, so you have been renamed to `{suggestedName}`.\n" + var msg = $"""
"I'm not perfect and can't clean all the junk in names in some cases, so change your nickname at your discretion.\n" + Hello, your current _display name_ is breaking {rulesChannel.Mention} #7, so you have been renamed to `{suggestedName}`.
"You can change your _display name_ by clicking on the server name at the top left and selecting **Change Nickname**."; I'm not perfect and can't clean all the junk in names in some cases, so change your nickname at your discretion.
You can change your _display name_ by clicking on the server name at the top left and selecting **Change Nickname**.
""";
var dm = await member.CreateDmChannelAsync().ConfigureAwait(false); var dm = await member.CreateDmChannelAsync().ConfigureAwait(false);
await dm.SendMessageAsync(msg).ConfigureAwait(false); await dm.SendMessageAsync(msg).ConfigureAwait(false);
await renameTask.ConfigureAwait(false); await renameTask.ConfigureAwait(false);

View File

@ -20,7 +20,10 @@ internal static class GameTdbScraper
{ {
private static readonly HttpClient HttpClient = HttpClientFactory.Create(new CompressionMessageHandler()); private static readonly HttpClient HttpClient = HttpClientFactory.Create(new CompressionMessageHandler());
private static readonly Uri TitleDownloadLink = new("https://www.gametdb.com/ps3tdb.zip?LANG=EN"); private static readonly Uri TitleDownloadLink = new("https://www.gametdb.com/ps3tdb.zip?LANG=EN");
private static readonly Regex CoverArtLink = new(@"(?<cover_link>https?://art\.gametdb\.com/ps3/cover(?!full)[/\w\d]+\.jpg(\?\d+)?)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.ExplicitCapture); private static readonly Regex CoverArtLink = new(
@"(?<cover_link>https?://art\.gametdb\.com/ps3/cover(?!full)[/\w\d]+\.jpg(\?\d+)?)",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.ExplicitCapture
);
//private static readonly List<string> PreferredOrder = new List<string>{"coverHQ", "coverM", "cover"}; //private static readonly List<string> PreferredOrder = new List<string>{"coverHQ", "coverM", "cover"};
public static async Task RunAsync(CancellationToken cancellationToken) public static async Task RunAsync(CancellationToken cancellationToken)
@ -47,10 +50,14 @@ internal static class GameTdbScraper
try try
{ {
var html = await HttpClient.GetStringAsync("https://www.gametdb.com/PS3/" + productCode).ConfigureAwait(false); var html = await HttpClient.GetStringAsync("https://www.gametdb.com/PS3/" + productCode).ConfigureAwait(false);
var coverLinks = CoverArtLink.Matches(html).Select(m => m.Groups["cover_link"].Value).Distinct().Where(l => l.Contains(productCode, StringComparison.InvariantCultureIgnoreCase)).ToList(); var coverLinks = CoverArtLink.Matches(html)
return coverLinks.FirstOrDefault(l => l.Contains("coverHQ", StringComparison.InvariantCultureIgnoreCase)) ?? .Select(m => m.Groups["cover_link"].Value)
coverLinks.FirstOrDefault(l => l.Contains("coverM", StringComparison.InvariantCultureIgnoreCase)) ?? .Distinct()
coverLinks.FirstOrDefault(); .Where(l => l.Contains(productCode, StringComparison.InvariantCultureIgnoreCase))
.ToList();
return coverLinks.FirstOrDefault(l => l.Contains("coverHQ", StringComparison.InvariantCultureIgnoreCase))
?? coverLinks.FirstOrDefault(l => l.Contains("coverM", StringComparison.InvariantCultureIgnoreCase))
?? coverLinks.FirstOrDefault();
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -17,7 +17,10 @@ namespace CompatBot.ThumbScrapper;
internal sealed class PsnScraper internal sealed class PsnScraper
{ {
private static readonly PsnClient.Client Client = new(); private static readonly PsnClient.Client Client = new();
public static readonly Regex ContentIdMatcher = new(@"(?<content_id>(?<service_id>(?<service_letters>\w\w)(?<service_number>\d{4}))-(?<product_id>(?<product_letters>\w{4})(?<product_number>\d{5}))_(?<part>\d\d)-(?<label>\w{16}))", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.ExplicitCapture); public static readonly Regex ContentIdMatcher = new(
@"(?<content_id>(?<service_id>(?<service_letters>\w\w)(?<service_number>\d{4}))-(?<product_id>(?<product_letters>\w{4})(?<product_number>\d{5}))_(?<part>\d\d)-(?<label>\w{16}))",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.ExplicitCapture
);
private static readonly SemaphoreSlim LockObj = new(1, 1); private static readonly SemaphoreSlim LockObj = new(1, 1);
private static List<string> psnStores = new(); private static List<string> psnStores = new();
private static DateTime storeRefreshTimestamp = DateTime.MinValue; private static DateTime storeRefreshTimestamp = DateTime.MinValue;
@ -63,7 +66,7 @@ internal sealed class PsnScraper
await LockObj.WaitAsync(cancellationToken).ConfigureAwait(false); await LockObj.WaitAsync(cancellationToken).ConfigureAwait(false);
try try
{ {
storesToScrape = new List<string>(psnStores); storesToScrape = new(psnStores);
} }
finally finally
{ {
@ -126,7 +129,7 @@ internal sealed class PsnScraper
await LockObj.WaitAsync(cancellationToken).ConfigureAwait(false); await LockObj.WaitAsync(cancellationToken).ConfigureAwait(false);
try try
{ {
storesToScrape = new List<string>(psnStores); storesToScrape = new(psnStores);
} }
finally finally
{ {

View File

@ -5,18 +5,13 @@ namespace CompatBot.Utils;
internal static class BotDbExtensions internal static class BotDbExtensions
{ {
public static bool IsComplete(this EventSchedule evt) public static bool IsComplete(this EventSchedule evt)
{ => evt is { Start: > 0, Year: > 0, Name.Length: >0 }
return evt.Start > 0 && evt.End > evt.Start;
&& evt.End > evt.Start
&& evt.Year > 0
&& !string.IsNullOrEmpty(evt.Name);
}
public static bool IsComplete(this Piracystring filter) public static bool IsComplete(this Piracystring filter)
{ {
var result = !string.IsNullOrEmpty(filter.String) var result = filter.Actions != 0
&& filter.String.Length >= Config.MinimumPiracyTriggerLength && filter.String.Length >= Config.MinimumPiracyTriggerLength;
&& filter.Actions != 0;
if (result && filter.Actions.HasFlag(FilterAction.ShowExplain)) if (result && filter.Actions.HasFlag(FilterAction.ShowExplain))
result = !string.IsNullOrEmpty(filter.ExplainTerm); result = !string.IsNullOrEmpty(filter.ExplainTerm);
return result; return result;

View File

@ -4,19 +4,8 @@ namespace CompatBot.Utils;
public static class DateTimeEx public static class DateTimeEx
{ {
public static DateTime AsUtc(this DateTime dateTime) public static DateTime AsUtc(this DateTime dateTime) => dateTime.Kind == DateTimeKind.Utc ? dateTime : new(dateTime.Ticks, DateTimeKind.Utc);
{ public static DateTime AsUtc(this long ticks) => new(ticks, DateTimeKind.Utc);
if (dateTime.Kind == DateTimeKind.Utc)
return dateTime;
return new DateTime(dateTime.Ticks, DateTimeKind.Utc);
}
public static DateTime AsUtc(this long ticks)
{
return new(ticks, DateTimeKind.Utc);
}
public static string AsShortTimespan(this TimeSpan timeSpan) public static string AsShortTimespan(this TimeSpan timeSpan)
{ {

View File

@ -14,11 +14,7 @@ public static class DiceCoefficientOptimized
var bgCount1 = input.Length - 1; var bgCount1 = input.Length - 1;
var bgCount2 = comparedTo.Length - 1; var bgCount2 = comparedTo.Length - 1;
if (comparedTo.Length < input.Length) if (comparedTo.Length < input.Length)
{ (input, comparedTo) = (comparedTo, input);
var tmp = input;
input = comparedTo;
comparedTo = tmp;
}
var matches = 0; var matches = 0;
for (var i = 0; i < input.Length - 1; i++) for (var i = 0; i < input.Length - 1; i++)
for (var j = 0; j < comparedTo.Length - 1; j++) for (var j = 0; j < comparedTo.Length - 1; j++)

View File

@ -15,12 +15,7 @@ namespace CompatBot.Utils;
public static class DiscordClientExtensions 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)
{ => guild is null ? GetMember(client, userId) : GetMember(client, guild.Id, userId);
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) public static DiscordMember? GetMember(this DiscordClient client, ulong guildId, ulong userId)
=> (from g in client.Guilds => (from g in client.Guilds
@ -87,15 +82,10 @@ public static class DiscordClientExtensions
} }
} }
public static Task RemoveReactionAsync(this CommandContext ctx, DiscordEmoji emoji) public static Task RemoveReactionAsync(this CommandContext ctx, DiscordEmoji emoji) => RemoveReactionAsync(ctx.Message, emoji);
{
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)
{ => ReactWithAsync(ctx.Message, emoji, fallbackMessage, showBoth ?? (ctx.Prefix == Config.AutoRemoveCommandPrefix));
return ReactWithAsync(ctx.Message, emoji, fallbackMessage, showBoth ?? (ctx.Prefix == Config.AutoRemoveCommandPrefix));
}
public static async Task<IReadOnlyCollection<DiscordMessage>> GetMessagesBeforeAsync(this DiscordChannel channel, ulong beforeMessageId, int limit = 100, DateTime? timeLimit = null) public static async Task<IReadOnlyCollection<DiscordMessage>> GetMessagesBeforeAsync(this DiscordChannel channel, ulong beforeMessageId, int limit = 100, DateTime? timeLimit = null)
{ {
@ -161,21 +151,20 @@ public static class DiscordClientExtensions
} }
public static string GetMentionWithNickname(this DiscordMember member) 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()}`)"; => 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) public static string GetUsernameWithNickname(this DiscordUser user, DiscordClient client, DiscordGuild? guild = null)
{ => client.GetMember(guild, user).GetUsernameWithNickname()
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)
{ => member is null
if (member == null) ? null
return null; : string.IsNullOrEmpty(member.Nickname)
? $"`{member.Username.Sanitize()}#{member.Discriminator}`"
return string.IsNullOrEmpty(member.Nickname) ? $"`{member.Username.Sanitize()}#{member.Discriminator}`" : $"`{member.Username.Sanitize()}#{member.Discriminator}` (shown as `{member.Nickname.Sanitize()}`)"; : $"`{member.Username.Sanitize()}#{member.Discriminator}` (shown as `{member.Nickname.Sanitize()}`)";
}
public static DiscordEmoji? GetEmoji(this DiscordClient client, string? emojiName, string? fallbackEmoji = null) public static DiscordEmoji? GetEmoji(this DiscordClient client, string? emojiName, string? fallbackEmoji = null)
=> GetEmoji(client, emojiName, fallbackEmoji == null ? null : DiscordEmoji.FromUnicode(fallbackEmoji)); => GetEmoji(client, emojiName, fallbackEmoji == null ? null : DiscordEmoji.FromUnicode(fallbackEmoji));
@ -187,9 +176,11 @@ public static class DiscordClientExtensions
if (DiscordEmoji.TryFromName(client, emojiName, true, out var result)) if (DiscordEmoji.TryFromName(client, emojiName, true, out var result))
return result; return result;
else if (DiscordEmoji.TryFromName(client, $":{emojiName}:", true, out result))
if (DiscordEmoji.TryFromName(client, $":{emojiName}:", true, out result))
return result; return result;
else if (DiscordEmoji.TryFromUnicode(emojiName, out result))
if (DiscordEmoji.TryFromUnicode(emojiName, out result))
return result; return result;
return fallbackEmoji; return fallbackEmoji;
} }

View File

@ -38,11 +38,10 @@ public static class EnumerableExtensions
{ {
if (collection.Count > 0) if (collection.Count > 0)
{ {
var rng = seed.HasValue ? new Random(seed.Value) : new Random(); var rng = seed.HasValue ? new(seed.Value) : new Random();
return collection[rng.Next(collection.Count)]; return collection[rng.Next(collection.Count)];
} }
else return default;
return default;
} }
public static bool AnyPatchesApplied(this Dictionary<string, int> patches) public static bool AnyPatchesApplied(this Dictionary<string, int> patches)

View File

@ -24,7 +24,7 @@ internal static class FilterActionExtensions
.Cast<FilterAction>() .Cast<FilterAction>()
.Select(fa => flags.HasFlag(fa) ? ActionFlags[fa] : '-') .Select(fa => flags.HasFlag(fa) ? ActionFlags[fa] : '-')
.ToArray(); .ToArray();
return new string(result); return new(result);
} }
public static string GetLegend(string wrapChar = "`") public static string GetLegend(string wrapChar = "`")

View File

@ -212,7 +212,7 @@ public static class StringUtils
{ {
result.Append('('); result.Append('(');
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
result.Append(@"\x").Append(Utf8ToLatin1RegexPatternEncoderFallback.CustomMapperFallbackBuffer.byteToHex[tmp[i]]); result.Append(@"\x").Append(Utf8ToLatin1RegexPatternEncoderFallback.CustomMapperFallbackBuffer.ByteToHex[tmp[i]]);
result.Append(')'); result.Append(')');
} }
span = span.Slice(1); span = span.Slice(1);

View File

@ -54,10 +54,10 @@ internal class FixedLengthBuffer<TKey, TValue>: IList<TValue>
public void TrimExcess() public void TrimExcess()
{ {
if (Count <= FixedLengthBuffer<TKey, TValue>.MaxLength) if (Count <= MaxLength)
return; return;
TrimOldItems(Count - FixedLengthBuffer<TKey, TValue>.MaxLength); TrimOldItems(Count - MaxLength);
} }
public List<TValue> GetOldItems(int count) public List<TValue> GetOldItems(int count)
@ -65,9 +65,9 @@ internal class FixedLengthBuffer<TKey, TValue>: IList<TValue>
public List<TValue> GetExcess() public List<TValue> GetExcess()
{ {
if (Count <= FixedLengthBuffer<TKey, TValue>.MaxLength) if (Count <= MaxLength)
return new List<TValue>(0); return new(0);
return GetOldItems(Count - FixedLengthBuffer<TKey, TValue>.MaxLength); return GetOldItems(Count - MaxLength);
} }
public TValue? Evict(TKey key) public TValue? Evict(TKey key)
@ -116,7 +116,7 @@ internal class FixedLengthBuffer<TKey, TValue>: IList<TValue>
public int Count => lookup.Count; public int Count => lookup.Count;
public bool IsReadOnly => false; public bool IsReadOnly => false;
public bool NeedTrimming => Count > FixedLengthBuffer<TKey, TValue>.MaxLength + 20; public bool NeedTrimming => Count > MaxLength + 20;
public TValue this[TKey index] => lookup[index]; public TValue this[TKey index] => lookup[index];
private static int MaxLength => Config.ChannelMessageHistorySize; private static int MaxLength => Config.ChannelMessageHistorySize;

View File

@ -1,6 +1,7 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
namespace CompatBot.Utils; namespace CompatBot.Utils;
public class NameUniqueObjectCollection<TValue>: IDictionary<string, UniqueList<TValue>> public class NameUniqueObjectCollection<TValue>: IDictionary<string, UniqueList<TValue>>
@ -10,7 +11,7 @@ public class NameUniqueObjectCollection<TValue>: IDictionary<string, UniqueList<
public NameUniqueObjectCollection(IEqualityComparer<string>? keyComparer = null, IEqualityComparer<TValue>? valueComparer = null) public NameUniqueObjectCollection(IEqualityComparer<string>? keyComparer = null, IEqualityComparer<TValue>? valueComparer = null)
{ {
dict = new Dictionary<string, UniqueList<TValue>>(keyComparer); dict = new(keyComparer);
this.valueComparer = valueComparer; this.valueComparer = valueComparer;
} }
@ -34,18 +35,21 @@ public class NameUniqueObjectCollection<TValue>: IDictionary<string, UniqueList<
c.Add(value); c.Add(value);
else else
{ {
dict[key] = c = new UniqueList<TValue>(valueComparer); dict[key] = c = new(valueComparer);
c.Add(value); c.Add(value);
} }
} }
public void Clear() => dict.Clear(); public void Clear() => dict.Clear();
bool ICollection<KeyValuePair<string, UniqueList<TValue>>>.Contains(KeyValuePair<string, UniqueList<TValue>> item) => ((IDictionary<string, UniqueList<TValue>>)dict).Contains(item); bool ICollection<KeyValuePair<string, UniqueList<TValue>>>.Contains(KeyValuePair<string, UniqueList<TValue>> item)
=> ((IDictionary<string, UniqueList<TValue>>)dict).Contains(item);
void ICollection<KeyValuePair<string, UniqueList<TValue>>>.CopyTo(KeyValuePair<string, UniqueList<TValue>>[] array, int arrayIndex) => ((IDictionary<string, UniqueList<TValue>>)dict).CopyTo(array, arrayIndex);
void ICollection<KeyValuePair<string, UniqueList<TValue>>>.CopyTo(KeyValuePair<string, UniqueList<TValue>>[] array, int arrayIndex)
bool ICollection<KeyValuePair<string, UniqueList<TValue>>>.Remove(KeyValuePair<string, UniqueList<TValue>> item) => ((IDictionary<string, UniqueList<TValue>>)dict).Remove(item); => ((IDictionary<string, UniqueList<TValue>>)dict).CopyTo(array, arrayIndex);
bool ICollection<KeyValuePair<string, UniqueList<TValue>>>.Remove(KeyValuePair<string, UniqueList<TValue>> item)
=> ((IDictionary<string, UniqueList<TValue>>)dict).Remove(item);
public int Count => dict.Count; public int Count => dict.Count;
public bool IsReadOnly => false; public bool IsReadOnly => false;
@ -59,7 +63,7 @@ public class NameUniqueObjectCollection<TValue>: IDictionary<string, UniqueList<
if (dict.TryGetValue(key, out value!)) if (dict.TryGetValue(key, out value!))
return true; return true;
dict[key] = value = new UniqueList<TValue>(valueComparer); dict[key] = value = new(valueComparer);
return false; return false;
} }

View File

@ -61,14 +61,18 @@ public static class OpenSslConfigurator
} }
} }
await writer.WriteLineAsync("openssl_conf = default_conf").ConfigureAwait(false); await writer.WriteLineAsync("""
await writer.WriteLineAsync("[default_conf]").ConfigureAwait(false); openssl_conf = default_conf
await writer.WriteLineAsync("ssl_conf = ssl_sect").ConfigureAwait(false);
await writer.WriteLineAsync("[ssl_sect]").ConfigureAwait(false);
await writer.WriteLineAsync("system_default = system_default_sect").ConfigureAwait(false);
await writer.WriteLineAsync("[system_default_sect]").ConfigureAwait(false);
await writer.WriteLineAsync("CipherString = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384").ConfigureAwait(false);
[default_conf]
ssl_conf = ssl_sect
[ssl_sect]
system_default = system_default_sect
[system_default_sect]
CipherString = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384
""").ConfigureAwait(false);
if (content.Length > 0) if (content.Length > 0)
await writer.WriteAsync(content).ConfigureAwait(false); await writer.WriteAsync(content).ConfigureAwait(false);
await writer.FlushAsync().ConfigureAwait(false); await writer.FlushAsync().ConfigureAwait(false);

View File

@ -219,42 +219,42 @@ public static class ProductCodeDecoder
if (productCode.IsEmpty || productCode.Length < 2) if (productCode.IsEmpty || productCode.Length < 2)
return; return;
switch ((productCode[0], productCode[1])) switch (productCode)
{ {
case ('0', '0'): case ['0', '0', ..]:
case ('0', '1'): case ['0', '1', ..]:
case ('0', '2'): case ['0', '2', ..]:
{ {
result.Add("European code range"); result.Add("European code range");
break; break;
} }
case ('2', '0'): case ['2', '0', ..]:
{ {
result.Add("Korean code range"); result.Add("Korean code range");
break; break;
} }
case ('3', '0'): case ['3', '0', ..]:
case ('3', '1'): case ['3', '1', ..]:
case ('4', '1'): case ['4', '1', ..]:
case ('8', '1'): case ['8', '1', ..]:
case ('8', '2'): case ['8', '2', ..]:
case ('8', '3'): case ['8', '3', ..]:
case ('9', '0'): case ['9', '0', ..]:
case ('9', '1'): case ['9', '1', ..]:
case ('9', '4'): case ['9', '4', ..]:
case ('9', '8'): case ['9', '8', ..]:
case ('9', '9'): case ['9', '9', ..]:
{ {
result.Add("USA code range"); result.Add("USA code range");
break; break;
} }
case ('5', '0'): case ['5', '0', ..]:
{ {
result.Add("Asian code range"); result.Add("Asian code range");
break; break;
} }
case ('6', '0'): case ['6', '0', ..]:
case ('6', '1'): case ['6', '1', ..]:
{ {
result.Add("Japanese code range"); result.Add("Japanese code range");
break; break;
@ -561,9 +561,9 @@ public static class ProductCodeDecoder
private static void DecodeMediaC(in ReadOnlySpan<char> productCode, List<string> result) private static void DecodeMediaC(in ReadOnlySpan<char> productCode, List<string> result)
{ {
switch ((productCode[0], productCode[1], productCode[2])) switch (productCode)
{ {
case ('U', 'S', 'A'): case ['U', 'S', 'A', ..]:
{ {
result.Add("Playstation 4 software"); result.Add("Playstation 4 software");
return; return;

View File

@ -11,8 +11,10 @@ namespace CompatBot.Utils.ResultFormatters;
internal static class FwInfoFormatter internal static class FwInfoFormatter
{ {
//2019_0828_c975768e5d70e105a72656f498cc9be9/PS3UPDAT.PUP //2019_0828_c975768e5d70e105a72656f498cc9be9/PS3UPDAT.PUP
private static readonly Regex FwLinkInfo = new(@"(?<year>\d{4})_(?<month>\d\d)(?<day>\d\d)_(?<md5>[0-9a-f]+)", private static readonly Regex FwLinkInfo = new(
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline | RegexOptions.IgnoreCase); @"(?<year>\d{4})_(?<month>\d\d)(?<day>\d\d)_(?<md5>[0-9a-f]+)",
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline | RegexOptions.IgnoreCase
);
private static readonly Dictionary<string, string> RegionToFlagMap = new(StringComparer.InvariantCultureIgnoreCase) private static readonly Dictionary<string, string> RegionToFlagMap = new(StringComparer.InvariantCultureIgnoreCase)
{ {
["us"] = "🇺🇸", ["us"] = "🇺🇸",
@ -41,8 +43,10 @@ internal static class FwInfoFormatter
{ {
result.Description = $"Latest version is **{fwInfoList[0].Version}** released on {info.Groups["year"].Value}-{info.Groups["month"].Value}-{info.Groups["day"].Value}\n" + result.Description = $"Latest version is **{fwInfoList[0].Version}** released on {info.Groups["year"].Value}-{info.Groups["month"].Value}-{info.Groups["day"].Value}\n" +
$"It is available in {fwInfoList.Count} region{(fwInfoList.Count == 1 ? "" : "s")} out of {RegionToFlagMap.Count}"; $"It is available in {fwInfoList.Count} region{(fwInfoList.Count == 1 ? "" : "s")} out of {RegionToFlagMap.Count}";
result.AddField("Checksums", $"MD5: `{info.Groups["md5"].Value}`\n" + result.AddField("Checksums", $"""
"You can use [HashCheck](https://github.com/gurnec/HashCheck/releases/latest) to verify your download"); MD5: `{info.Groups["md5"].Value}`
You can use [HashCheck](https://github.com/gurnec/HashCheck/releases/latest) to verify your download
""");
var links = new StringBuilder(); var links = new StringBuilder();
foreach (var fwi in fwInfoList) foreach (var fwi in fwInfoList)
{ {

View File

@ -448,7 +448,7 @@ internal static partial class LogParserResult
msg += $" (update available: v{gameUpVer})"; msg += $" (update available: v{gameUpVer})";
notes.Add(msg); notes.Add(msg);
} }
if (multiItems["ppu_patch"].FirstOrDefault() is string firstPpuPatch if (multiItems["ppu_patch"] is [string firstPpuPatch, ..]
&& ProgramHashPatch.Match(firstPpuPatch) is { Success: true } m && ProgramHashPatch.Match(firstPpuPatch) is { Success: true } m
&& m.Groups["hash"].Value is string firstPpuHash) && m.Groups["hash"].Value is string firstPpuHash)
{ {

View File

@ -1,10 +1,11 @@
using DSharpPlus.Entities; using DSharpPlus.Entities;
using Octokit;
namespace CompatBot.Utils.ResultFormatters; namespace CompatBot.Utils.ResultFormatters;
internal static class PrInfoFormatter internal static class PrInfoFormatter
{ {
public static DiscordEmbedBuilder AsEmbed(this Octokit.PullRequest prInfo) public static DiscordEmbedBuilder AsEmbed(this PullRequest prInfo)
{ {
var state = prInfo.GetState(); var state = prInfo.GetState();
var stateLabel = state.state == null ? null : $"[{state.state}] "; var stateLabel = state.state == null ? null : $"[{state.state}] ";
@ -12,7 +13,7 @@ internal static class PrInfoFormatter
return new() {Title = title, Url = prInfo.HtmlUrl, Description = prInfo.Title, Color = state.color}; return new() {Title = title, Url = prInfo.HtmlUrl, Description = prInfo.Title, Color = state.color};
} }
public static DiscordEmbedBuilder AsEmbed(this Octokit.Issue issueInfo) public static DiscordEmbedBuilder AsEmbed(this Issue issueInfo)
{ {
var state = issueInfo.GetState(); var state = issueInfo.GetState();
var stateLabel = state.state == null ? null : $"[{state.state}] "; var stateLabel = state.state == null ? null : $"[{state.state}] ";
@ -20,12 +21,12 @@ internal static class PrInfoFormatter
return new() {Title = title, Url = issueInfo.HtmlUrl, Description = issueInfo.Title, Color = state.color}; return new() {Title = title, Url = issueInfo.HtmlUrl, Description = issueInfo.Title, Color = state.color};
} }
public static (string? state, DiscordColor color) GetState(this Octokit.PullRequest prInfo) public static (string? state, DiscordColor color) GetState(this PullRequest prInfo)
{ {
if (prInfo.State == Octokit.ItemState.Open) if (prInfo.State == ItemState.Open)
return ("Open", Config.Colors.PrOpen); return ("Open", Config.Colors.PrOpen);
if (prInfo.State == Octokit.ItemState.Closed) if (prInfo.State == ItemState.Closed)
{ {
if (prInfo.MergedAt.HasValue) if (prInfo.MergedAt.HasValue)
return ("Merged", Config.Colors.PrMerged); return ("Merged", Config.Colors.PrMerged);
@ -36,12 +37,12 @@ internal static class PrInfoFormatter
return (null, Config.Colors.DownloadLinks); return (null, Config.Colors.DownloadLinks);
} }
public static (string? state, DiscordColor color) GetState(this Octokit.Issue issueInfo) public static (string? state, DiscordColor color) GetState(this Issue issueInfo)
{ {
if (issueInfo.State == Octokit.ItemState.Open) if (issueInfo.State == ItemState.Open)
return ("Open", Config.Colors.PrOpen); return ("Open", Config.Colors.PrOpen);
if (issueInfo.State == Octokit.ItemState.Closed) if (issueInfo.State == ItemState.Closed)
return ("Closed", Config.Colors.PrClosed); return ("Closed", Config.Colors.PrClosed);
return (null, Config.Colors.DownloadLinks); return (null, Config.Colors.DownloadLinks);

View File

@ -29,16 +29,8 @@ internal static class TitleInfoFormatter
? date.ToString(ApiConfig.DateOutputFormat) ? date.ToString(ApiConfig.DateOutputFormat)
: null; : null;
private static string? ToPrString(this TitleInfo info, string? defaultString, bool link = false) private static string ToPrString(this TitleInfo info)
{ => $"[#{info.Pr}](<https://github.com/RPCS3/rpcs3/pull/{info.Pr}>)";
if ((info.Pr ?? 0) == 0)
return defaultString;
if (link)
return $"[#{info.Pr}](https://github.com/RPCS3/rpcs3/pull/{info.Pr})";
return $"#{info.Pr}";
}
public static string AsString(this TitleInfo info, string titleId) public static string AsString(this TitleInfo info, string titleId)
{ {
@ -56,9 +48,11 @@ internal static class TitleInfoFormatter
result += " "; result += " ";
else else
result += $" since {info.ToUpdated(),-10}"; result += $" since {info.ToUpdated(),-10}";
result += $" (PR {info.ToPrString("#????"),-5})`"; result += '`';
if (info.Pr > 0)
result += $" PR {info.ToPrString(),-5}";
if (info.Thread > 0) if (info.Thread > 0)
result += $" <https://forums.rpcs3.net/thread-{info.Thread}.html>"; result += $", [forum](<https://forums.rpcs3.net/thread-{info.Thread}.html>)";
return result; return result;
} }
@ -77,7 +71,7 @@ internal static class TitleInfoFormatter
var thumb = db.Thumbnail.FirstOrDefault(t => t.ProductCode == titleId); var thumb = db.Thumbnail.FirstOrDefault(t => t.ProductCode == titleId);
if (thumb?.CompatibilityStatus != null) if (thumb?.CompatibilityStatus != null)
{ {
info = new TitleInfo info = new()
{ {
Date = thumb.CompatibilityChangeDate?.AsUtc().ToString("yyyy-MM-dd"), Date = thumb.CompatibilityChangeDate?.AsUtc().ToString("yyyy-MM-dd"),
Status = thumb.CompatibilityStatus.ToString(), Status = thumb.CompatibilityStatus.ToString(),
@ -90,10 +84,9 @@ internal static class TitleInfoFormatter
{ {
// apparently there's no formatting in the footer, but you need to escape everything in description; ugh // apparently there's no formatting in the footer, but you need to escape everything in description; ugh
var onlineOnlyPart = info.Network == 1 ? " 🌐" : ""; var onlineOnlyPart = info.Network == 1 ? " 🌐" : "";
var pr = info.ToPrString(null, true);
var desc = $"{info.Status} since {info.ToUpdated() ?? "forever"}"; var desc = $"{info.Status} since {info.ToUpdated() ?? "forever"}";
if (pr != null) if (info.Pr > 0)
desc += $" (PR {pr})"; desc += $" (PR {info.ToPrString()})";
if (!forLog && !string.IsNullOrEmpty(info.AlternativeTitle)) if (!forLog && !string.IsNullOrEmpty(info.AlternativeTitle))
desc = info.AlternativeTitle + Environment.NewLine + desc; desc = info.AlternativeTitle + Environment.NewLine + desc;
if (!string.IsNullOrEmpty(info.WikiTitle)) if (!string.IsNullOrEmpty(info.WikiTitle))

View File

@ -234,35 +234,32 @@ internal static class UpdateInfoFormatter
var seconds = (int)delta.TotalSeconds; var seconds = (int)delta.TotalSeconds;
return $"{seconds} second{(seconds == 1 ? "" : "s")}"; return $"{seconds} second{(seconds == 1 ? "" : "s")}";
} }
else if (delta.TotalHours < 1) if (delta.TotalHours < 1)
{ {
var minutes = (int)delta.TotalMinutes; var minutes = (int)delta.TotalMinutes;
return $"{minutes} minute{(minutes == 1 ? "" : "s")}"; return $"{minutes} minute{(minutes == 1 ? "" : "s")}";
} }
else if (delta.TotalDays < 1) if (delta.TotalDays < 1)
{ {
var hours = (int) delta.TotalHours; var hours = (int) delta.TotalHours;
return $"{hours} hour{(hours == 1 ? "": "s")}"; return $"{hours} hour{(hours == 1 ? "": "s")}";
} }
else if (delta.TotalDays < 7) if (delta.TotalDays < 7)
{ {
var days = (int) delta.TotalDays; var days = (int) delta.TotalDays;
return $"{days} day{(days == 1 ? "": "s")}"; return $"{days} day{(days == 1 ? "": "s")}";
} }
else if (delta.TotalDays < 30) if (delta.TotalDays < 30)
{ {
var weeks = (int)(delta.TotalDays/7); var weeks = (int)(delta.TotalDays/7);
return $"{weeks} week{(weeks == 1 ? "" : "s")}"; return $"{weeks} week{(weeks == 1 ? "" : "s")}";
} }
else if (delta.TotalDays < 365) if (delta.TotalDays < 365)
{ {
var months = (int)(delta.TotalDays/30); var months = (int)(delta.TotalDays/30);
return $"{months} month{(months == 1 ? "" : "s")}"; return $"{months} month{(months == 1 ? "" : "s")}";
} }
else var years = (int)(delta.TotalDays/365);
{ return $"{years} year{(years == 1 ? "" : "s")}";
var years = (int)(delta.TotalDays/365);
return $"{years} year{(years == 1 ? "" : "s")}";
}
} }
} }

View File

@ -55,14 +55,12 @@ public static class TimeParser
if (TimeZoneAcronyms.ContainsKey(a)) if (TimeZoneAcronyms.ContainsKey(a))
continue; continue;
if (!standardNames.ContainsKey(a)) standardNames.TryAdd(a, tzi);
standardNames[a] = tzi;
a = tzi.DaylightName.GetAcronym(includeAllCaps: true, includeAllDigits: true); a = tzi.DaylightName.GetAcronym(includeAllCaps: true, includeAllDigits: true);
if (TimeZoneAcronyms.ContainsKey(a) || standardNames.ContainsKey(a)) if (TimeZoneAcronyms.ContainsKey(a) || standardNames.ContainsKey(a))
continue; continue;
if (!daylightNames.ContainsKey(a)) daylightNames.TryAdd(a, tzi);
daylightNames[a] = tzi;
} }
} }
Config.Log.Trace("[TZI] Total matched acronyms: " + result.Count); Config.Log.Trace("[TZI] Total matched acronyms: " + result.Count);

View File

@ -43,7 +43,6 @@ public class UniqueList<T>: IList<T>
} }
public IEnumerator<T> GetEnumerator() => list.GetEnumerator(); public IEnumerator<T> GetEnumerator() => list.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)list).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)list).GetEnumerator();
public void Add(T item) public void Add(T item)
@ -65,7 +64,6 @@ public class UniqueList<T>: IList<T>
} }
public bool Contains(T item) => set.Contains(item); public bool Contains(T item) => set.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => list.CopyTo(array, arrayIndex); public void CopyTo(T[] array, int arrayIndex) => list.CopyTo(array, arrayIndex);
public bool Remove(T item) public bool Remove(T item)
@ -75,9 +73,7 @@ public class UniqueList<T>: IList<T>
} }
public int Count => list.Count; public int Count => list.Count;
public bool IsReadOnly => false; public bool IsReadOnly => false;
public int IndexOf(T item) => list.IndexOf(item); public int IndexOf(T item) => list.IndexOf(item);
public void Insert(int index, T item) public void Insert(int index, T item)

View File

@ -19,12 +19,12 @@ internal class Utf8ToLatin1RegexPatternEncoderFallback : EncoderFallback
private readonly byte[] buffer = new byte[MaxBufferSize]; private readonly byte[] buffer = new byte[MaxBufferSize];
private int size, remaining; private int size, remaining;
private static readonly Encoding Utf8 = new UTF8Encoding(false); private static readonly Encoding Utf8 = new UTF8Encoding(false);
internal static readonly List<string> byteToHex = new(256); internal static readonly List<string> ByteToHex = new(256);
static CustomMapperFallbackBuffer() static CustomMapperFallbackBuffer()
{ {
for (var i = 0; i < 256; i++) for (var i = 0; i < 256; i++)
byteToHex.Add(i.ToString("X2")); ByteToHex.Add(i.ToString("X2"));
} }
public CustomMapperFallbackBuffer() public CustomMapperFallbackBuffer()
@ -59,7 +59,7 @@ internal class Utf8ToLatin1RegexPatternEncoderFallback : EncoderFallback
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
ref var b = ref tmp[i]; ref var b = ref tmp[i];
var s = byteToHex[b]; var s = ByteToHex[b];
var offset = i * 4 + 1; var offset = i * 4 + 1;
buffer[offset + 0] = (byte)'\\'; buffer[offset + 0] = (byte)'\\';
buffer[offset + 2] =(byte)s[0]; buffer[offset + 2] =(byte)s[0];
@ -81,7 +81,7 @@ internal class Utf8ToLatin1RegexPatternEncoderFallback : EncoderFallback
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
ref var b = ref tmp[i]; ref var b = ref tmp[i];
var s = byteToHex[b]; var s = ByteToHex[b];
var offset = i * 4 + 1; var offset = i * 4 + 1;
buffer[offset + 0] = (byte)'\\'; buffer[offset + 0] = (byte)'\\';
buffer[offset + 2] =(byte)s[0]; buffer[offset + 2] =(byte)s[0];

View File

@ -0,0 +1,8 @@
<assembly name="DSharpPlus.CommandsNext">
<member name="T:DSharpPlus.CommandsNext.Attributes.GroupCommandAttribute">
<attribute ctor="M:JetBrains.Annotations.MeansImplicitUseAttribute.#ctor"/>
</member>
<member name="T:DSharpPlus.CommandsNext.Attributes.CommandAttribute">
<attribute ctor="M:JetBrains.Annotations.MeansImplicitUseAttribute.#ctor"/>
</member>
</assembly>

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -26,9 +27,10 @@ public static class Normalizer
}; };
// as per https://www.unicode.org/reports/tr39/#Confusable_Detection // as per https://www.unicode.org/reports/tr39/#Confusable_Detection
private static string ToSkeletonString(this string input) [return: NotNullIfNotNull(nameof(input))]
private static string? ToSkeletonString(this string? input)
{ {
if (string.IsNullOrEmpty(input)) if (input is null or "")
return input; return input;
// step 1: Convert X to NFD format, as described in [UAX15]. // step 1: Convert X to NFD format, as described in [UAX15].
@ -39,9 +41,10 @@ public static class Normalizer
return input.Normalize(NormalizationForm.FormD); return input.Normalize(NormalizationForm.FormD);
} }
public static string ToCanonicalForm(this string input) [return: NotNullIfNotNull(nameof(input))]
public static string? ToCanonicalForm(this string? input)
{ {
if (string.IsNullOrEmpty(input)) if (input is null or "")
return input; return input;
input = ToSkeletonString(input); input = ToSkeletonString(input);

View File

@ -1,6 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=6D9CA448_002D60C1_002D4D66_002D91D6_002DEC6C586508E6_002Fd_003ADatabase_002Fd_003AMigrations/@EntryIndexedValue">ExplicitlyExcluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=6D9CA448_002D60C1_002D4D66_002D91D6_002DEC6C586508E6_002Fd_003ADatabase_002Fd_003AMigrations/@EntryIndexedValue">ExplicitlyExcluded</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ASMJIT/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=ASMJIT/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Autosplit/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Bandicam/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Bandicam/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=BCAS/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=BCAS/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=BCES/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=BCES/@EntryIndexedValue">True</s:Boolean>
@ -16,11 +17,13 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=BLKS/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=BLKS/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=BLUS/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=BLUS/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Celeron/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Celeron/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=CHACHA/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=CJS7/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=CJS7/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Confusables/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Confusables/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cubeb/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Cubeb/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=desync/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=desync/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=EBOOT/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=EBOOT/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ECDHE/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=edat/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=edat/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=framerate/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=framerate/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=getllar/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=getllar/@EntryIndexedValue">True</s:Boolean>