├── Tests
├── Test Files
│ └── Put IRD files here.txt
├── Properties
│ └── GlobalSuppressions.cs
├── readme.md
├── StarsFormatTest.cs
├── UriFormattingTests.cs
├── FlagTests.cs
├── TimeParserTests.cs
├── RegexTest.cs
├── IrdTests.cs
├── RandomTests.cs
├── Tests.csproj
├── MemoryCacheExtensionTests.cs
└── ReflectionHacksTests.cs
├── .dockerignore
├── Clients
├── CirrusCiClient
│ ├── schema.extensions.graphql
│ ├── .graphqlrc.json
│ ├── POCOs
│ │ ├── BuildInfo.cs
│ │ └── ProjectBuildStats.cs
│ ├── CirrusCiClient.csproj
│ └── Queries
│ │ └── GetBuilds.graphql
├── CompatApiClient
│ ├── Formatters
│ │ ├── DashedNamingPolicy.cs
│ │ ├── SnakeCaseNamingPolicy.cs
│ │ ├── SpecialJsonNamingPolicy.cs
│ │ ├── CompatApiCommitHashConverter.cs
│ │ └── NamingStyles.cs
│ ├── CompatApiStatus.cs
│ ├── Compression
│ │ ├── ICompressor.cs
│ │ ├── GZipCompressor.cs
│ │ ├── DeflateCompressor.cs
│ │ ├── CompressedContent.cs
│ │ ├── DecompressedContent.cs
│ │ └── Compressor.cs
│ ├── CompatApiClient.csproj
│ ├── Utils
│ │ ├── ConsoleLogger.cs
│ │ ├── Utils.cs
│ │ └── Statistics.cs
│ ├── RequestBuilder.cs
│ ├── POCOs
│ │ ├── UpdateCheckResult.cs
│ │ └── CompatResult.cs
│ └── readme.md
├── IrdLibraryClient
│ ├── POCOs
│ │ ├── IrdInfo.cs
│ │ └── SearchResult.cs
│ ├── readme.md
│ ├── IrdLibraryClient.csproj
│ └── IrdFormat
│ │ ├── IsoHeaderParser.cs
│ │ └── Ird.cs
├── OneDriveClient
│ ├── OneDriveClient.csproj
│ └── POCOs
│ │ └── DriveItemMeta.cs
├── MediafireClient
│ ├── MediafireClient.csproj
│ └── POCOs
│ │ └── LinksResult.cs
├── YandexDiskClient
│ ├── YandexDiskClient.csproj
│ ├── POCOs
│ │ └── ResourceInfo.cs
│ └── Client.cs
├── PsnClient
│ ├── POCOs
│ │ ├── Relationships.cs
│ │ ├── Stores.cs
│ │ ├── App.cs
│ │ ├── TitleMeta.cs
│ │ ├── StoreNavigation.cs
│ │ └── TitlePatch.cs
│ ├── Utils
│ │ ├── LocaleUtils.cs
│ │ └── TmdbHasher.cs
│ ├── PsnClient.csproj
│ └── certificates
│ │ ├── CA01.cer
│ │ ├── CA02.cer
│ │ ├── CA03.cer
│ │ ├── CA04.cer
│ │ └── CA05.cer
├── GithubClient
│ ├── readme.md
│ └── GithubClient.csproj
└── readme.md
├── CompatBot
├── .editorconfig
├── Utils
│ ├── SandboxType.cs
│ ├── ReportSeverity.cs
│ ├── Extensions
│ │ ├── DiscordEmbedBuilderExtensions.cs
│ │ ├── DiscordGuildExtensions.cs
│ │ ├── ThumbnailDbExtensions.cs
│ │ ├── CompatResultExtensions.cs
│ │ ├── StreamExtensions.cs
│ │ ├── DiscordUserExtensions.cs
│ │ ├── StackTraceExtensions.cs
│ │ ├── DiscordComponentsExtensions.cs
│ │ ├── DiscordChannelExtensions.cs
│ │ ├── DiceCoefficientOptimized.cs
│ │ ├── BotDbExtensions.cs
│ │ ├── PsnMetaExtensions.cs
│ │ ├── EnumerableExtensions.cs
│ │ ├── FilterActionExtensions.cs
│ │ ├── MemoryCacheExtensions.cs
│ │ ├── Converters.cs
│ │ └── DateTimeEx.cs
│ ├── AsciiColumn.cs
│ ├── Hashing.cs
│ ├── SandboxDetector.cs
│ ├── DefaultHandlerFilter.cs
│ ├── PathUtils.cs
│ ├── GitRunner.cs
│ ├── CompatApiResultUtils.cs
│ ├── ResultFormatters
│ │ ├── IrdSearchResultFormatter.cs
│ │ └── PrInfoFormatter.cs
│ └── PoorMansTaskScheduler.cs
├── Properties
│ ├── launchSettings.json
│ ├── GlobalUsings.cs
│ └── GlobalSuppressions.cs
├── Ocr
│ ├── Backend
│ │ ├── IOcrBackend.cs
│ │ ├── BackendBase.cs
│ │ ├── AzureVision.cs
│ │ └── Florence2.cs
│ └── OcrProvider.cs
├── Commands
│ ├── Converters
│ │ └── readme.md
│ ├── ChoiceProviders
│ │ ├── ScoreTypeChoiceProvider.cs
│ │ ├── FilterContextChoiceProvider.cs
│ │ ├── CompatListStatusChoiceProvider.cs
│ │ └── FilterActionChoiceProvider.cs
│ ├── Attributes
│ │ ├── LimitedToSpecificChannels.cs
│ │ ├── CheckAttributeWithReactions.cs
│ │ └── readme.md
│ ├── Ird.cs
│ ├── Bot.Import.cs
│ ├── AutoCompleteProviders
│ │ ├── FilterActionAutoCompleteProvider.cs
│ │ ├── InviteAutoCompleteProvider.cs
│ │ ├── ContentFilterAutoCompleteProvider.cs
│ │ ├── EventIdAutoCompleteProvider.cs
│ │ └── BotConfigurationAutoCompleteProvider.cs
│ └── BotMath.cs
├── EventHandlers
│ ├── LogParsing
│ │ ├── ArchiveHandlers
│ │ │ ├── IArchiveHandler.cs
│ │ │ ├── PlainText.cs
│ │ │ └── GzipHandler.cs
│ │ ├── SourceHandlers
│ │ │ ├── ISourceHandler.cs
│ │ │ ├── BaseSourceHandler.cs
│ │ │ └── FileSourceHandler.cs
│ │ └── POCOs
│ │ │ ├── LogSection.cs
│ │ │ └── LogParseState.cs
│ ├── GlobalButtonHandler.cs
│ ├── Greeter.cs
│ ├── ThumbnailCacheMonitor.cs
│ ├── BotStatusMonitor.cs
│ ├── ContentFilterMonitor.cs
│ ├── MultiEventHandlerWrapper.cs
│ ├── UsernameRaidMonitor.cs
│ └── LogAsTextMonitor.cs
└── Database
│ ├── Migrations
│ ├── BotDb
│ │ ├── 20180719122730_WarningTimestamp.cs
│ │ ├── 20201113092108_MakeStateKeyRequired.cs
│ │ ├── 20190131223017_AddExplainAttachments.cs
│ │ ├── 20190728111050_AllowDuplicateTriggers.cs
│ │ ├── 20191002172950_PawsOfCommunity.cs
│ │ ├── 20191002180817_PawsOfCommunityV2.cs
│ │ ├── 20180804225045_DisabledCommands.cs
│ │ ├── 20210427131110_SusStrings.cs
│ │ ├── 20180908150603_BotState.cs
│ │ ├── 20190129194930_AddE3Schedule.cs
│ │ ├── 20180818153741_InvitesWhitelist.cs
│ │ ├── 20191129183704_AddForcedNickname.cs
│ │ ├── 20190301155219_PersistentStats.cs
│ │ ├── 20220704163631_AddStatsBucketColumn.cs
│ │ ├── 20190307173026_PermanentWarnings.cs
│ │ ├── 20180709154128_Explanations.cs
│ │ └── 20190723151803_ContentFilter2.0.cs
│ ├── ThumbnailDb
│ │ ├── 20210414190638_AddGameUpdateInfoTimestamp.cs
│ │ ├── 20210302213749_FortuneTable.cs
│ │ ├── 20210309212939_AddUserNamePool.cs
│ │ ├── 20200402181755_CompatFields.cs
│ │ ├── 20250806100313_AddNoCaseCollationForGameTitle.cs
│ │ ├── 20210414183007_AddGameUpdateInfo.cs
│ │ ├── 20190306154836_ThumbsColorCache.cs
│ │ ├── 20200321134554_RemoveTitleInfoTable.cs
│ │ └── 20200321124445_RemoveSyscallModules.cs
│ └── HardwareDb
│ │ └── 20220629192852_InitialCreate.cs
│ ├── PrimaryKeyConvention.cs
│ ├── NamingConventionConverter.cs
│ └── Providers
│ └── SqlConfiguration.cs
├── SourceGenerators
├── Properties
│ └── launchSettings.json
└── SourceGenerators.csproj
├── nuget.config
├── ExternalAnnotations
├── DSharpPlus.Commands.xml
└── DSharpPlus.CommandsNext.xml
├── .config
└── dotnet-tools.json
├── docker-compose.example.yml
├── HomoglyphConverter
├── HomoglyphConverter.csproj
└── readme.md
├── SECURITY.md
├── CONTRIBUTING.md
└── Dockerfile
/Tests/Test Files/Put IRD files here.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .vs/
2 | .vscode/
3 | _ReSharper.*/
--------------------------------------------------------------------------------
/Clients/CirrusCiClient/schema.extensions.graphql:
--------------------------------------------------------------------------------
1 | extend schema @key(fields: "id")
--------------------------------------------------------------------------------
/CompatBot/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # IDE0063: Use simple 'using' statement
4 | csharp_prefer_simple_using_statement = false:suggestion
5 |
--------------------------------------------------------------------------------
/CompatBot/Utils/SandboxType.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Utils;
2 |
3 | public enum SandboxType
4 | {
5 | None,
6 | Snap,
7 | Flatpak,
8 | Docker,
9 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/ReportSeverity.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Utils;
2 |
3 | public enum ReportSeverity
4 | {
5 | None,
6 | Low,
7 | Medium,
8 | High,
9 | }
--------------------------------------------------------------------------------
/CompatBot/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "default": {
4 | "commandName": "Project"
5 | },
6 | "Docker": {
7 | "commandName": "Docker"
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/SourceGenerators/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "RoslynAnalyzers": {
4 | "commandName": "DebugRoslynComponent",
5 | "targetProject": "..\\CompatBot\\CompatBot.csproj"
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ExternalAnnotations/DSharpPlus.Commands.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Formatters/DashedNamingPolicy.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 |
3 | namespace CompatApiClient;
4 |
5 | public sealed class DashedNamingPolicy: JsonNamingPolicy
6 | {
7 | public override string ConvertName(string name) => NamingStyles.Dashed(name);
8 | }
--------------------------------------------------------------------------------
/Clients/CompatApiClient/CompatApiStatus.cs:
--------------------------------------------------------------------------------
1 | namespace CompatApiClient;
2 |
3 | public enum CompatApiStatus: short
4 | {
5 | IllegalQuery = -3,
6 | Maintenance = -2,
7 | InternalError = -1,
8 | Success = 0,
9 | NoResults = 1,
10 | NoExactMatch = 2,
11 | }
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Formatters/SnakeCaseNamingPolicy.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 |
3 | namespace CompatApiClient;
4 |
5 | public sealed class SnakeCaseNamingPolicy: JsonNamingPolicy
6 | {
7 | public override string ConvertName(string name) => NamingStyles.Underscore(name);
8 | }
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Formatters/SpecialJsonNamingPolicy.cs:
--------------------------------------------------------------------------------
1 | namespace CompatApiClient.Formatters;
2 |
3 | public static class SpecialJsonNamingPolicy
4 | {
5 | public static SnakeCaseNamingPolicy SnakeCase { get; } = new();
6 | public static DashedNamingPolicy Dashed { get; } = new();
7 | }
--------------------------------------------------------------------------------
/CompatBot/Ocr/Backend/IOcrBackend.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Ocr.Backend;
2 |
3 | public interface IOcrBackend
4 | {
5 | string Name { get; }
6 | Task InitializeAsync(CancellationToken cancellationToken);
7 | Task<(string result, double confidence)> GetTextAsync(string imgUrl, CancellationToken cancellationToken);
8 | }
--------------------------------------------------------------------------------
/Clients/IrdLibraryClient/POCOs/IrdInfo.cs:
--------------------------------------------------------------------------------
1 | namespace IrdLibraryClient.POCOs;
2 |
3 | public class IrdInfo
4 | {
5 | public string Title { get; set; } = null!;
6 | public string? FwVer { get; set; }
7 | public string? GameVer { get; set; }
8 | public string? AppVer { get; set; }
9 | public string Link { get; set; } = null!;
10 | }
--------------------------------------------------------------------------------
/Clients/IrdLibraryClient/readme.md:
--------------------------------------------------------------------------------
1 | IRD Library Client
2 | ==================
3 |
4 | There's no official API or any documentation, so everything is reverse-engineered from the web UI. It's all rather straight-forward.
5 |
6 | We use this to search and download IRD files for various purposes. One note though: we cache IRD files on-disk to limit the download requests.
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Compression/ICompressor.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Threading.Tasks;
3 |
4 | namespace CompatApiClient.Compression;
5 |
6 | public interface ICompressor
7 | {
8 | string EncodingType { get; }
9 | Task CompressAsync(Stream source, Stream destination);
10 | Task DecompressAsync(Stream source, Stream destination);
11 | }
--------------------------------------------------------------------------------
/Clients/OneDriveClient/OneDriveClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | enable
5 | latest
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Clients/MediafireClient/MediafireClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | enable
5 | latest
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Clients/YandexDiskClient/YandexDiskClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | enable
5 | latest
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Clients/CirrusCiClient/.graphqlrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "schema": "schema.graphql",
3 | "documents": "**/*.graphql",
4 | "extensions": {
5 | "strawberryShake": {
6 | "name": "Client",
7 | "namespace": "CirrusCiClient.Generated",
8 | "url": "https://api.cirrus-ci.com/graphql",
9 | "dependencyInjection": true,
10 | "emitGeneratedCode": true
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/CompatBot/Commands/Converters/readme.md:
--------------------------------------------------------------------------------
1 | Converters
2 | ==========
3 |
4 | Here we have custom converter classes that could be used with DSharpPlus library.
5 |
6 | `TextOnlyDiscordChannelConverter`
7 | ---------------------------------
8 |
9 | This converter is used to more easily parse channel names to their respective library object, that prevents most common issues (such as trying to send messages in a voice or a category channel).
--------------------------------------------------------------------------------
/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "strawberryshake.tools": {
6 | "version": "15.1.8",
7 | "commands": [
8 | "dotnet-graphql"
9 | ],
10 | "rollForward": false
11 | },
12 | "dotnet-ef": {
13 | "version": "9.0.8",
14 | "commands": [
15 | "dotnet-ef"
16 | ],
17 | "rollForward": false
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/Clients/OneDriveClient/POCOs/DriveItemMeta.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace OneDriveClient.POCOs;
4 |
5 | public sealed class DriveItemMeta
6 | {
7 | public string? Id;
8 | public string? Name;
9 | public int Size;
10 | [JsonPropertyName("@odata.context")]
11 | public string? OdataContext;
12 | [JsonPropertyName("@content.downloadUrl")]
13 | public string? ContentDownloadUrl;
14 | }
--------------------------------------------------------------------------------
/ExternalAnnotations/DSharpPlus.CommandsNext.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tests/Properties/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 |
3 | // This file is used by Code Analysis to maintain SuppressMessage
4 | // attributes that are applied to this project.
5 | // Project-level suppressions either have no target or are given
6 | // a specific target and scoped to a namespace, type, member, etc.
7 |
8 | [assembly: SuppressMessage("Style", "VSTHRD200:Use \"Async\" suffix for async methods", Justification = "Test names are special")]
9 |
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/DiscordEmbedBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Utils;
2 |
3 | public static class DiscordEmbedBuilderExtensions
4 | {
5 | public static DiscordEmbedBuilder AddFieldEx(this DiscordEmbedBuilder builder, string header, string content, bool underline = false, bool inline = false)
6 | {
7 | content = string.IsNullOrEmpty(content) ? "-" : content;
8 | return builder.AddField(underline ? $"__{header}__" : header, content, inline);
9 | }
10 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/AsciiColumn.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Utils;
2 |
3 | public sealed class AsciiColumn
4 | {
5 | public AsciiColumn(string? name = null, bool disabled = false, bool alignToRight = false, int maxWidth = 80)
6 | {
7 | Name = name;
8 | Disabled = disabled;
9 | AlignToRight = alignToRight;
10 | MaxWidth = maxWidth;
11 | }
12 |
13 | public string? Name;
14 | public bool Disabled;
15 | public bool AlignToRight;
16 | public int MaxWidth;
17 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/Hashing.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Utils;
2 |
3 | public static class Hashing
4 | {
5 | public static byte[] GetSaltedHash(this byte[] data)
6 | {
7 | using var sha256 = System.Security.Cryptography.SHA256.Create();
8 | if (data.Length > 0)
9 | sha256.TransformBlock(data, 0, data.Length, null, 0);
10 | sha256.TransformFinalBlock(Config.CryptoSalt, 0, Config.CryptoSalt.Length);
11 | return sha256.Hash ?? Guid.Empty.ToByteArray();
12 | }
13 | }
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/LogParsing/ArchiveHandlers/IArchiveHandler.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.IO.Pipelines;
3 | using ResultNet;
4 |
5 | namespace CompatBot.EventHandlers.LogParsing.ArchiveHandlers;
6 |
7 | public interface IArchiveHandler
8 | {
9 | Result CanHandle(string fileName, int fileSize, ReadOnlySpan header);
10 | Task FillPipeAsync(Stream sourceStream, PipeWriter writer, CancellationToken cancellationToken);
11 | long LogSize { get; }
12 | long SourcePosition { get; }
13 | }
--------------------------------------------------------------------------------
/docker-compose.example.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | bot:
4 | image: rpcs3/discord-bot:latest
5 | volumes:
6 | # host_path:container_path
7 | - /home/MY_USER_NAME/.local/share/compat-bot:/bot-db
8 | - /home/MY_USER_NAME/.microsoft/usersecrets/c2e6548b-b215-4a18-a010-958ef294b310:/bot-config
9 | - /var/logs/compat-bot:/var/logs/compat-bot
10 | - /var/ird:/var/ird
11 | environment:
12 | Token: MY_BOT_TOKEN
13 | # paths inside container
14 | LogPath: /var/logs/compat-bot
15 | IrdCachePath: /var/ird
16 |
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Compression/GZipCompressor.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.IO.Compression;
3 |
4 | namespace CompatApiClient.Compression;
5 |
6 | public class GZipCompressor : Compressor
7 | {
8 | public override string EncodingType => "gzip";
9 |
10 | protected override Stream CreateCompressionStream(Stream output)
11 | => new GZipStream(output, CompressionMode.Compress, true);
12 |
13 | protected override Stream CreateDecompressionStream(Stream input)
14 | => new GZipStream(input, CompressionMode.Decompress, true);
15 | }
--------------------------------------------------------------------------------
/Clients/MediafireClient/POCOs/LinksResult.cs:
--------------------------------------------------------------------------------
1 | namespace MediafireClient.POCOs;
2 | #nullable disable
3 |
4 | public sealed class LinksResult
5 | {
6 | public LinksResponse Response;
7 | }
8 |
9 | public sealed class LinksResponse
10 | {
11 | public string Action;
12 | public string Result;
13 | public string CurrentApiVersion;
14 | public Link[] Links;
15 | }
16 |
17 | public sealed class Link
18 | {
19 | public string Quickkey;
20 | public string NormalDownload;
21 | public string DirectDownload;
22 | }
23 |
24 | #nullable restore
--------------------------------------------------------------------------------
/Clients/YandexDiskClient/POCOs/ResourceInfo.cs:
--------------------------------------------------------------------------------
1 | namespace YandexDiskClient.POCOs;
2 | #nullable disable
3 |
4 | public sealed class ResourceInfo
5 | {
6 | public int? Size;
7 | public string Name; //RPCS3.log.gz
8 | public string PublicKey;
9 | public string Type; //file
10 | public string MimeType; //application/x-gzip
11 | public string File; //
12 | public string MediaType; //compressed
13 | public string Md5;
14 | public string Sha256;
15 | public long? Revision;
16 | }
17 |
18 | #nullable restore
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Compression/DeflateCompressor.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.IO.Compression;
3 |
4 | namespace CompatApiClient.Compression;
5 |
6 | public class DeflateCompressor : Compressor
7 | {
8 | public override string EncodingType => "deflate";
9 |
10 | protected override Stream CreateCompressionStream(Stream output)
11 | => new DeflateStream(output, CompressionMode.Compress, true);
12 |
13 | protected override Stream CreateDecompressionStream(Stream input)
14 | => new DeflateStream(input, CompressionMode.Decompress, true);
15 | }
--------------------------------------------------------------------------------
/Clients/PsnClient/POCOs/Relationships.cs:
--------------------------------------------------------------------------------
1 | namespace PsnClient.POCOs;
2 | #nullable disable
3 | public class Relationships
4 | {
5 | public RelationshipsChildren Children;
6 | public RelationshipsLegacySkus LegacySkus;
7 | }
8 |
9 | public class RelationshipsChildren
10 | {
11 | public RelationshipsChildrenItem[] Data;
12 | }
13 |
14 | public class RelationshipsChildrenItem
15 | {
16 | public string Id;
17 | public string Type;
18 | }
19 |
20 | public class RelationshipsLegacySkus
21 | {
22 | public RelationshipsChildrenItem[] Data;
23 | }
24 | #nullable restore
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/DiscordGuildExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Utils;
2 |
3 | public static class DiscordGuildExtensions
4 | {
5 | public static int GetAttachmentSizeLimit(this CommandContext ctx)
6 | => ctx.Guild.GetAttachmentSizeLimit();
7 |
8 | public static int GetAttachmentSizeLimit(this DiscordGuild? guild)
9 | => guild?.PremiumTier switch
10 | {
11 | DiscordPremiumTier.Tier_3 => 100 * 1024 * 1024,
12 | DiscordPremiumTier.Tier_2 => 50 * 1024 * 1024,
13 | _ => Config.AttachmentSizeLimit,
14 | };
15 | }
--------------------------------------------------------------------------------
/Clients/PsnClient/Utils/LocaleUtils.cs:
--------------------------------------------------------------------------------
1 | namespace PsnClient.Utils;
2 |
3 | public static class LocaleUtils
4 | {
5 | public static (string language, string country) AsLocaleData(this string locale)
6 | {
7 | /*
8 | "zh-Hans-CN" -> zh-CN
9 | "zh-Hans-HK" -> zh-HK
10 | "zh-Hant-HK" -> ch-HK
11 | "zh-Hant-TW" -> ch-TW
12 | */
13 | locale = locale.Replace("zh-Hans", "zh").Replace("zh-Hant", "ch");
14 | var localeParts = locale.Split('-');
15 | return (localeParts[0], localeParts[1]);
16 | }
17 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/ThumbnailDbExtensions.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database;
2 |
3 | namespace CompatBot.Utils.Extensions;
4 |
5 | internal static class ThumbnailDbExtensions
6 | {
7 | internal static IQueryable WithStatus(this IQueryable queryBase, CompatStatus? status, bool exact)
8 | => (status, exact) switch
9 | {
10 | (not null, true) => queryBase.Where(g => g.CompatibilityStatus == status),
11 | (not null, false) => queryBase.Where(g => g.CompatibilityStatus >= status),
12 | (null, _) => queryBase
13 | };
14 | }
--------------------------------------------------------------------------------
/Clients/IrdLibraryClient/POCOs/SearchResult.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace IrdLibraryClient.POCOs;
4 |
5 | public sealed class SearchResult
6 | {
7 | public List? Data;
8 | }
9 |
10 | public sealed class SearchResultItem
11 | {
12 | public string? Id; // product code
13 | public string? Title;
14 | public string? GameVersion;
15 | public string? UpdateVersion;
16 | public string? Size;
17 | public string? FileCount;
18 | public string? FolderCount;
19 | public string? MD5;
20 | public string? IrdName;
21 |
22 | public string? Filename;
23 | }
--------------------------------------------------------------------------------
/CompatBot/Commands/ChoiceProviders/ScoreTypeChoiceProvider.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Commands.ChoiceProviders;
2 |
3 | public class ScoreTypeChoiceProvider : IChoiceProvider
4 | {
5 | private static readonly IReadOnlyList scoreType =
6 | [
7 | new("combined", "both"),
8 | new("critic score", "critic"),
9 | new("user score", "user"),
10 | ];
11 |
12 | public ValueTask> ProvideAsync(CommandParameter parameter)
13 | => ValueTask.FromResult>(scoreType);
14 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/SandboxDetector.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Utils;
2 |
3 | public static class SandboxDetector
4 | {
5 | public static SandboxType Detect()
6 | {
7 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SNAP")))
8 | return SandboxType.Snap;
9 |
10 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FLATPAK_SYSTEM_DIR")))
11 | return SandboxType.Flatpak;
12 |
13 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("RUNNING_IN_DOCKER")))
14 | return SandboxType.Docker;
15 |
16 | return SandboxType.None;
17 | }
18 | }
--------------------------------------------------------------------------------
/Clients/CompatApiClient/CompatApiClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | latest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Clients/GithubClient/readme.md:
--------------------------------------------------------------------------------
1 | GitHub Client
2 | =============
3 |
4 | [GitHub API documentation](https://developer.github.com/v3/) for reference. Anonymous API calls require `User-Agent` header, everything else is optional. Anonymous access is limited to 60 requests per hour, matched by client IP.
5 |
6 | We only use GitHub API to get PR information, and optionally, links to CIs. CI status information is unreliable though, as it's often outdated and the history is often inconsistent, so we prefer to find matching builds manually instead.
7 |
8 | As anonymous access is very limited, we try to cache every response. In the same vein, we try to limit GitHub API usage in general.
--------------------------------------------------------------------------------
/CompatBot/Utils/DefaultHandlerFilter.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Utils.Extensions;
2 |
3 | namespace CompatBot.Utils;
4 |
5 | internal static class DefaultHandlerFilter
6 | {
7 | internal static bool IsFluff(DiscordMessage? message)
8 | {
9 | if (message is null)
10 | return true;
11 |
12 | if (message.Author.IsBotSafeCheck())
13 | return true;
14 |
15 | if (string.IsNullOrEmpty(message.Content)
16 | || message.Content.StartsWith(Config.CommandPrefix)
17 | || message.Content.StartsWith(Config.AutoRemoveCommandPrefix))
18 | return true;
19 |
20 | return false;
21 | }
22 | }
--------------------------------------------------------------------------------
/Clients/CirrusCiClient/POCOs/BuildInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CirrusCiClient.Generated;
3 |
4 | namespace CirrusCiClient.POCOs;
5 |
6 | public record BuildOSInfo
7 | {
8 | public string? Filename { get; init; }
9 | public string? DownloadLink { get; init; }
10 | public TaskStatus? Status { get; init; }
11 | }
12 | public record BuildInfo
13 | {
14 | public string? Commit { get; init; }
15 | public DateTime StartTime { get; init; }
16 | public DateTime? FinishTime { get; init; }
17 | public BuildOSInfo? WindowsBuild { get; init; }
18 | public BuildOSInfo? LinuxBuild { get; init; }
19 | public BuildOSInfo? MacBuild { get; init; }
20 | }
--------------------------------------------------------------------------------
/Clients/IrdLibraryClient/IrdLibraryClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | latest
5 | enable
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/SourceGenerators/SourceGenerators.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | latest
6 | 1701;1702;RS2008;RS1036
7 | enable
8 | true
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/CompatResultExtensions.cs:
--------------------------------------------------------------------------------
1 | using CompatApiClient.POCOs;
2 |
3 | namespace CompatBot.Utils;
4 |
5 | public static class CompatResultExtensions
6 | {
7 | public static CompatResult Append(this CompatResult remote, CompatResult local)
8 | {
9 | if (remote.Results?.Count > 0)
10 | {
11 | foreach (var localItem in local.Results)
12 | {
13 | if (remote.Results.ContainsKey(localItem.Key))
14 | continue;
15 |
16 | remote.Results[localItem.Key] = localItem.Value;
17 | }
18 | return remote;
19 | }
20 | return local;
21 | }
22 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/PathUtils.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace CompatBot.Utils;
4 |
5 | public static class PathUtils
6 | {
7 | public static string[] GetSegments(string? path)
8 | {
9 | if (string.IsNullOrEmpty(path))
10 | return [];
11 |
12 | var result = new List();
13 | string segment;
14 | do
15 | {
16 | segment = Path.GetFileName(path);
17 | result.Add(segment is {Length: >0} ? segment : path);
18 | path = Path.GetDirectoryName(path);
19 | } while (segment is {Length: >0} && path is {Length: >0});
20 | result.Reverse();
21 | return result.ToArray();
22 | }
23 | }
--------------------------------------------------------------------------------
/Clients/GithubClient/GithubClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | latest
5 | enable
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/StreamExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace CompatBot.Utils;
4 |
5 | internal static class StreamExtensions
6 | {
7 | public static async Task ReadBytesAsync(this Stream stream, byte[] buffer, int count = 0)
8 | {
9 | if (count < 1 || count > buffer.Length)
10 | count = buffer.Length;
11 | var result = 0;
12 | int read;
13 | do
14 | {
15 | var remaining = count - result;
16 | read = await stream.ReadAsync(buffer.AsMemory(result, remaining)).ConfigureAwait(false);
17 | result += read;
18 | } while (read > 0 && result < count);
19 | return result;
20 | }
21 | }
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/LogParsing/SourceHandlers/ISourceHandler.cs:
--------------------------------------------------------------------------------
1 | using System.IO.Pipelines;
2 | using CompatBot.EventHandlers.LogParsing.ArchiveHandlers;
3 | using ResultNet;
4 |
5 | namespace CompatBot.EventHandlers.LogParsing.SourceHandlers;
6 |
7 | public interface ISourceHandler
8 | {
9 | Task> FindHandlerAsync(DiscordMessage message, ICollection handlers);
10 | }
11 |
12 | public interface ISource: IDisposable
13 | {
14 | string SourceType { get; }
15 | string FileName { get; }
16 | long SourceFileSize { get; }
17 | long SourceFilePosition { get; }
18 | long LogFileSize { get; }
19 | Task FillPipeAsync(PipeWriter writer, CancellationToken cancellationToken);
20 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/DiscordUserExtensions.cs:
--------------------------------------------------------------------------------
1 | using PsnClient.Utils;
2 |
3 | namespace CompatBot.Utils.Extensions;
4 |
5 | public static class DiscordUserExtensions
6 | {
7 | public static bool IsBotSafeCheck(this DiscordUser? user)
8 | {
9 | try
10 | {
11 | return user?.IsBot ?? false;
12 | }
13 | catch (KeyNotFoundException)
14 | {
15 | return false;
16 | }
17 | catch (Exception e)
18 | {
19 | Config.Log.Warn(e);
20 | return false;
21 | }
22 | }
23 |
24 | public static string ToSaltedSha256(this DiscordUser user)
25 | => BitConverter.GetBytes(user.Id).GetSaltedHash().ToHexString();
26 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20180719122730_WarningTimestamp.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class WarningTimestamp : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.AddColumn(
10 | name: "timestamp",
11 | table: "warning",
12 | nullable: true);
13 | }
14 |
15 | protected override void Down(MigrationBuilder migrationBuilder)
16 | {
17 | migrationBuilder.DropColumn(
18 | name: "timestamp",
19 | table: "warning");
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/CompatBot/Commands/ChoiceProviders/FilterContextChoiceProvider.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database;
2 |
3 | namespace CompatBot.Commands.ChoiceProviders;
4 |
5 | public class FilterContextChoiceProvider : IChoiceProvider
6 | {
7 | private static readonly IReadOnlyList contextType =
8 | [
9 | new("Default", 0),
10 | new("Chat", (int)FilterContext.Chat),
11 | new("Logs", (int)FilterContext.Log),
12 | new("Both", (int)(FilterContext.Chat | FilterContext.Log)),
13 | ];
14 |
15 | public ValueTask> ProvideAsync(CommandParameter parameter)
16 | => ValueTask.FromResult>(contextType);
17 | }
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Utils/ConsoleLogger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 |
4 | namespace CompatApiClient.Utils;
5 |
6 | public static class ConsoleLogger
7 | {
8 | public static void PrintError(Exception e, HttpResponseMessage? response, bool isError = true)
9 | {
10 | if (isError)
11 | ApiConfig.Log.Error(e, "HTTP error");
12 | else
13 | ApiConfig.Log.Warn(e, "HTTP error");
14 | if (response == null)
15 | return;
16 |
17 | try
18 | {
19 | ApiConfig.Log.Info(response.RequestMessage?.RequestUri);
20 | var msg = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
21 | ApiConfig.Log.Warn(msg);
22 | }
23 | catch { }
24 | }
25 | }
--------------------------------------------------------------------------------
/Tests/readme.md:
--------------------------------------------------------------------------------
1 | Tests
2 | =====
3 |
4 | I am using [NUnit](https://github.com/nunit/docs/wiki/NUnit-Documentation), mostly because I'm most familiar with this test framework. There's not a lot of tests for the code itself, it is mostly used for testing things out before implementation.
5 |
6 | You can use the regular `$ dotnet test` command to run the tests without any additional tools.
7 |
8 | If you want to contribute new test code, I have a couple of preferences:
9 | * Do use `Assert.That(expr, Is/Does/etc)` format instead of deprecated `Assert.AreEqual()` and similar.
10 |
11 | * Try to write the code in the way that does not require the use of `InternalsVisibleTo` attribute.
12 |
13 | * Tests that require any external data that must be manually supplied, should be disabled by default.
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Formatters/CompatApiCommitHashConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace CompatApiClient;
6 |
7 | public sealed class CompatApiCommitHashConverter : JsonConverter
8 | {
9 | public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
10 | {
11 | if (reader is not { TokenType: JsonTokenType.Number, HasValueSequence: false, ValueSpan: [(byte)'0'] })
12 | return reader.GetString();
13 |
14 | _ = reader.GetInt32();
15 | return null;
16 | }
17 |
18 | public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
19 | => writer.WriteStringValue(value);
20 | }
--------------------------------------------------------------------------------
/Clients/PsnClient/POCOs/Stores.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace PsnClient.POCOs;
4 | #nullable disable
5 | // https://store.playstation.com/kamaji/api/valkyrie_storefront/00_09_000/user/stores
6 | // requires session
7 | public class Stores
8 | {
9 | public StoresHeader Header;
10 | public StoresData Data;
11 | }
12 |
13 | public class StoresHeader
14 | {
15 | public string Details;
16 | [JsonPropertyName("errorUUID")]
17 | public string ErrorUuid;
18 |
19 | public string MessageKey; // "success"
20 | public string StatusCode; // "0x0000"
21 | }
22 |
23 | public class StoresData
24 | {
25 | public string BaseUrl;
26 | public string RootUrl;
27 | public string SearchUrl;
28 | public string TumblerUrl;
29 | }
30 | #nullable restore
--------------------------------------------------------------------------------
/HomoglyphConverter/HomoglyphConverter.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | latest
6 | enable
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/LogParsing/SourceHandlers/BaseSourceHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.Text.RegularExpressions;
3 | using CompatBot.EventHandlers.LogParsing.ArchiveHandlers;
4 | using ResultNet;
5 |
6 | namespace CompatBot.EventHandlers.LogParsing.SourceHandlers;
7 |
8 | internal abstract class BaseSourceHandler: ISourceHandler
9 | {
10 | protected const RegexOptions DefaultOptions = RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture;
11 | protected const int SnoopBufferSize = 4096;
12 | internal static readonly ArrayPool BufferPool = ArrayPool.Create(SnoopBufferSize, 64);
13 |
14 | public abstract Task> FindHandlerAsync(DiscordMessage message, ICollection handlers);
15 | }
--------------------------------------------------------------------------------
/Tests/StarsFormatTest.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Utils;
2 | using NUnit.Framework;
3 |
4 | namespace Tests;
5 |
6 | [TestFixture]
7 | public class StarsFormatTest
8 | {
9 | [TestCase(0.0, "🌑🌑🌑🌑🌑")]
10 | [TestCase(0.1, "🌑🌑🌑🌑🌑")]
11 |
12 | [TestCase(0.2, "🌘🌑🌑🌑🌑")]
13 | [TestCase(0.3, "🌘🌑🌑🌑🌑")]
14 |
15 | [TestCase(0.4, "🌗🌑🌑🌑🌑")]
16 | [TestCase(0.5, "🌗🌑🌑🌑🌑")]
17 | [TestCase(0.6, "🌗🌑🌑🌑🌑")]
18 |
19 | [TestCase(0.7, "🌖🌑🌑🌑🌑")]
20 | [TestCase(0.8, "🌖🌑🌑🌑🌑")]
21 |
22 | [TestCase(0.9, "🌕🌑🌑🌑🌑")]
23 | [TestCase(1.0, "🌕🌑🌑🌑🌑")]
24 | public void FormatTest(decimal score, string expectedValue)
25 | {
26 | Assert.That(StringUtils.GetMoons(score, false), Is.EqualTo(expectedValue), "Failed for " + score);
27 | }
28 | }
--------------------------------------------------------------------------------
/Tests/UriFormattingTests.cs:
--------------------------------------------------------------------------------
1 | using IrdLibraryClient;
2 | using NUnit.Framework;
3 |
4 | namespace Tests;
5 |
6 | [TestFixture]
7 | public class UriFormattingTests
8 | {
9 | [TestCase("file with spaces.ird")]
10 | [TestCase("file (with parenthesis).ird")]
11 | [TestCase("file/with/segments.ird")]
12 | public void IrdLinkFormatTest(string filename)
13 | {
14 | var uri = IrdClient.GetEscapedDownloadLink(filename);
15 | Assert.Multiple(() =>
16 | {
17 | Assert.That(uri, Does.Not.Contains(" "));
18 | Assert.That(uri, Does.Not.Contains("("));
19 | Assert.That(uri, Does.Not.Contains(")"));
20 | Assert.That(uri, Does.Not.Contains("%2F"));
21 | Assert.That(uri, Does.EndWith(".ird"));
22 | });
23 | }
24 | }
--------------------------------------------------------------------------------
/Tests/FlagTests.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database;
2 | using NUnit.Framework;
3 |
4 | namespace Tests;
5 |
6 | [TestFixture]
7 | public class FlagTests
8 | {
9 | [Test]
10 | public void MultipleFlagTest()
11 | {
12 | var testVal = FilterAction.IssueWarning | FilterAction.MuteModQueue;
13 | Assert.Multiple(() =>
14 | {
15 | Assert.That(testVal.HasFlag(FilterAction.IssueWarning), Is.True);
16 | Assert.That(testVal.HasFlag(FilterAction.IssueWarning | FilterAction.MuteModQueue), Is.True);
17 | Assert.That(testVal.HasFlag(FilterAction.IssueWarning | FilterAction.MuteModQueue | FilterAction.RemoveContent), Is.False);
18 | Assert.That(testVal.HasFlag(FilterAction.IssueWarning | FilterAction.SendMessage), Is.False);
19 | });
20 | }
21 | }
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/GlobalButtonHandler.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Commands;
2 |
3 | namespace CompatBot.EventHandlers;
4 |
5 | internal static class GlobalButtonHandler
6 | {
7 | internal const string ReplaceWithUpdatesPrefix = "replace with game updates:";
8 |
9 | public static async Task OnComponentInteraction(DiscordClient sender, ComponentInteractionCreatedEventArgs e)
10 | {
11 | if (e.Interaction is not { Type: DiscordInteractionType.Component }
12 | or not { Data.ComponentType: DiscordComponentType.Button }
13 | or not { Data.CustomId.Length: > 0 })
14 | return;
15 |
16 | var btnId = e.Interaction.Data.CustomId;
17 | if (btnId.StartsWith(ReplaceWithUpdatesPrefix))
18 | await Psn.Check.OnCheckUpdatesButtonClick(sender, e);
19 | }
20 | }
--------------------------------------------------------------------------------
/Clients/CirrusCiClient/CirrusCiClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | enable
5 | false
6 | latest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/LogParsing/POCOs/LogSection.cs:
--------------------------------------------------------------------------------
1 | using System.Text.RegularExpressions;
2 |
3 | namespace CompatBot.EventHandlers.LogParsing.POCOs;
4 |
5 | internal class LogSection
6 | {
7 | public string[] EndTrigger = null!;
8 |
9 | public Dictionary Extractors
10 | {
11 | get => extractors;
12 | init
13 | {
14 | var result = new Dictionary(value.Count);
15 | foreach (var key in value.Keys)
16 | {
17 | var r = value[key];
18 | result[key] = new(r.ToLatin8BitRegexPattern(), r.Options);
19 | }
20 | extractors = result;
21 | }
22 | }
23 |
24 | public Action? OnSectionEnd;
25 | private readonly Dictionary extractors = null!;
26 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/GitRunner.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | namespace CompatBot.Utils;
4 |
5 | public class GitRunner
6 | {
7 | public static async Task Exec(string arguments, CancellationToken cancellationToken)
8 | {
9 | using var git = new Process
10 | {
11 | StartInfo = new("git", arguments)
12 | {
13 | CreateNoWindow = true,
14 | UseShellExecute = false,
15 | RedirectStandardOutput = true,
16 | StandardOutputEncoding = Encoding.UTF8,
17 | },
18 | };
19 | git.Start();
20 | var stdout = await git.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
21 | await git.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
22 | return stdout;
23 | }
24 | }
--------------------------------------------------------------------------------
/Clients/PsnClient/POCOs/App.cs:
--------------------------------------------------------------------------------
1 | namespace PsnClient.POCOs;
2 | // https://transact.playstation.com/assets/app.json
3 | // returns an array of different objects
4 | // api endpoints, oauth, oauth authorize, telemetry, localization options, billing template, locales, country names, topup settings, paypal sandbox settings, gct, apm, sofort, ...
5 |
6 | // this is item #6 in App array
7 | public sealed class AppLocales
8 | {
9 | public string[]? EnabledLocales; // "ar-AE",…
10 | public AppLocaleOverride[]? Overrides;
11 | }
12 |
13 | public sealed class AppLocaleOverride
14 | {
15 | public AppLocaleOverrideCriteria? Criteria;
16 | public string? GensenLocale; // "ar-AE"
17 | }
18 |
19 | public sealed class AppLocaleOverrideCriteria
20 | {
21 | public string? Language; // "ar"
22 | public string? Country; // "AE|BH|KW|LB|OM|QA|SA"
23 | }
--------------------------------------------------------------------------------
/Clients/PsnClient/PsnClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | enable
5 | latest
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Current .NET version of the bot is supported (`master` branch).
6 |
7 | Old python verion is only for historical purposes
8 |
9 | ## Reporting a Vulnerability
10 |
11 | Best way to report an issue with the code is to send DM to mods in [RPCS3 discord server](https://discord.me/rpcs3) or an email to the repot maintainer (see github profiles).
12 |
13 | We'll fix any issue that can threaten safety of user data, bot or discord server availability or normal operation as fast as humanly possible.
14 |
15 | You can expect at the very least the thread assessment and proposed actions within a day, bar special circumstances.
16 |
17 | You can also expect inclusion in the list of people who ~~broke~~ helped test the bot for posterity (we'll ask for emoji to represent you beforehand).
18 |
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/ThumbnailDb/20210414190638_AddGameUpdateInfoTimestamp.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Migrations
4 | {
5 | public partial class AddGameUpdateInfoTimestamp : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.AddColumn(
10 | name: "timestamp",
11 | table: "game_update_info",
12 | type: "INTEGER",
13 | nullable: false,
14 | defaultValue: 0L);
15 | }
16 |
17 | protected override void Down(MigrationBuilder migrationBuilder)
18 | {
19 | migrationBuilder.DropColumn(
20 | name: "timestamp",
21 | table: "game_update_info");
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/Greeter.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace CompatBot.EventHandlers;
5 |
6 | internal static class Greeter
7 | {
8 | public static async Task OnMemberAdded(DiscordClient _, GuildMemberAddedEventArgs args)
9 | {
10 | await using var db = await BotDb.OpenReadAsync().ConfigureAwait(false);
11 | if (await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == "motd").ConfigureAwait(false) is {Text.Length: >0} explanation)
12 | {
13 | var dm = await args.Member.CreateDmChannelAsync().ConfigureAwait(false);
14 | await dm.SendMessageAsync(explanation.Text, explanation.Attachment, explanation.AttachmentFilename).ConfigureAwait(false);
15 | Config.Log.Info($"Sent motd to {args.Member.GetMentionWithNickname()}");
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/CompatBot/Commands/ChoiceProviders/CompatListStatusChoiceProvider.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Commands.ChoiceProviders;
2 |
3 | public class CompatListStatusChoiceProvider : IChoiceProvider
4 | {
5 | private static readonly IReadOnlyList compatListStatus =
6 | [
7 | new("playable", "playable"),
8 | new("ingame or better", "ingame"),
9 | new("intro or better", "intro"),
10 | new("loadable or better", "loadable"),
11 | new("only ingame", "ingameOnly"),
12 | new("only intro", "introOnly"),
13 | new("only loadable", "loadableOnly"),
14 | ];
15 |
16 | public ValueTask> ProvideAsync(CommandParameter parameter)
17 | => ValueTask.FromResult>(compatListStatus);
18 | }
19 |
--------------------------------------------------------------------------------
/CompatBot/Properties/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | // Global using directives
2 |
3 | global using System;
4 | global using System.Collections.Generic;
5 | global using System.ComponentModel;
6 | global using System.Linq;
7 | global using System.Text;
8 | global using System.Threading;
9 | global using System.Threading.Tasks;
10 | global using CompatBot.Commands.Attributes;
11 | global using CompatBot.Utils;
12 | global using DSharpPlus;
13 | global using DSharpPlus.Commands;
14 | global using DSharpPlus.Commands.ArgumentModifiers;
15 | global using DSharpPlus.Commands.Processors.SlashCommands;
16 | global using DSharpPlus.Commands.Trees;
17 | global using DSharpPlus.Commands.Trees.Metadata;
18 | global using DSharpPlus.Entities;
19 | global using DSharpPlus.EventArgs;
20 | global using DSharpPlus.Interactivity.Extensions;
21 | global using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers;
22 |
--------------------------------------------------------------------------------
/Clients/CirrusCiClient/POCOs/ProjectBuildStats.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace CirrusCiClient.POCOs;
4 |
5 | public record ProjectBuildStats
6 | {
7 | public TimeSpan Percentile95 { get; init; }
8 | public TimeSpan Percentile90 { get; init; }
9 | public TimeSpan Percentile85 { get; init; }
10 | public TimeSpan Percentile80 { get; init; }
11 | public TimeSpan Mean { get; init; }
12 | public TimeSpan StdDev { get; init; }
13 | public int BuildCount { get; init; }
14 |
15 | public static readonly ProjectBuildStats Defaults = new()
16 | {
17 | Percentile95 = TimeSpan.FromSeconds(1120),
18 | Percentile90 = TimeSpan.FromSeconds(900),
19 | Percentile85 = TimeSpan.FromSeconds(870),
20 | Percentile80 = TimeSpan.FromSeconds(865),
21 | Mean = TimeSpan.FromSeconds(860),
22 | StdDev = TimeSpan.FromSeconds(420),
23 | };
24 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/StackTraceExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace CompatBot.Utils.Extensions;
5 |
6 | public static class StackTraceExtensions
7 | {
8 | public static string GetCaller(this StackTrace trace) where T: DbContext
9 | {
10 | var st = trace.ToString();
11 | var lines = st.Split(Environment.NewLine);
12 | var openMethodName = typeof(T).Namespace + "." + typeof(T).Name + ".Open";
13 | try
14 | {
15 | var (idx, openLine) = lines.Index().LastOrDefault(i => i.Item.Contains(openMethodName));
16 | if (openLine is not null)
17 | return lines[idx + 1].TrimStart()[3..];
18 | }
19 | catch (Exception e)
20 | {
21 | Config.Log.Error(e, "Failed to get the caller from stacktrace");
22 | }
23 | return st;
24 | }
25 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/DiscordComponentsExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 |
3 | namespace CompatBot.Utils.Extensions;
4 |
5 | public static class DiscordComponentsExtensions
6 | {
7 | public static DiscordButtonComponent SetEnabled(this DiscordButtonComponent button, bool isEnabled)
8 | => isEnabled ? button.Enable() : button.Disable();
9 |
10 | public static DiscordButtonComponent SetDisabled(this DiscordButtonComponent button, bool isDisabled)
11 | => isDisabled ? button.Disable() : button.Enable();
12 |
13 | public static DiscordButtonComponent SetEmoji(this DiscordButtonComponent button, DiscordComponentEmoji emoji)
14 | {
15 | var property = button.GetType().GetProperty(nameof(button.Emoji));
16 | property?.SetValue(button, emoji, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty, null, null, null);
17 | return button;
18 | }
19 | }
--------------------------------------------------------------------------------
/CompatBot/Commands/ChoiceProviders/FilterActionChoiceProvider.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database;
2 |
3 | namespace CompatBot.Commands.ChoiceProviders;
4 |
5 | public class FilterActionChoiceProvider : IChoiceProvider
6 | {
7 | private static readonly IReadOnlyList actionType =
8 | [
9 | new("Default", 0),
10 | new("Remove content", (int)FilterAction.RemoveContent),
11 | new("Warn", (int)FilterAction.IssueWarning),
12 | new("Show explanation", (int)FilterAction.ShowExplain),
13 | new("Send message", (int)FilterAction.SendMessage),
14 | new("No mod log", (int)FilterAction.MuteModQueue),
15 | new("Kick user", (int)FilterAction.Kick),
16 | ];
17 |
18 | public ValueTask> ProvideAsync(CommandParameter parameter)
19 | => ValueTask.FromResult>(actionType);
20 | }
--------------------------------------------------------------------------------
/CompatBot/Commands/Attributes/LimitedToSpecificChannels.cs:
--------------------------------------------------------------------------------
1 | using DSharpPlus.Commands.ContextChecks;
2 |
3 | namespace CompatBot.Commands.Attributes;
4 |
5 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
6 | internal class LimitedToHelpChannelAttribute: ContextCheckAttribute;
7 |
8 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
9 | internal class LimitedToOfftopicChannelAttribute: ContextCheckAttribute;
10 |
11 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
12 | internal class LimitedToSpamChannelAttribute: ContextCheckAttribute;
13 |
14 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
15 | internal class RequiresDmAttribute: ContextCheckAttribute;
16 |
17 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
18 | internal class RequiresNotMediaAttribute: ContextCheckAttribute;
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/DiscordChannelExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Utils;
2 |
3 | internal static class DiscordChannelExtensions
4 | {
5 | internal static bool IsHelpChannel(this DiscordChannel channel)
6 | => channel.IsPrivate
7 | || channel.Name.Contains("help", StringComparison.OrdinalIgnoreCase)
8 | || channel.Name.Equals("donors", StringComparison.OrdinalIgnoreCase);
9 |
10 | internal static bool IsOfftopicChannel(this DiscordChannel channel)
11 | => channel.Name.Contains("off-topic", StringComparison.InvariantCultureIgnoreCase)
12 | || channel.Name.Contains("offtopic", StringComparison.InvariantCultureIgnoreCase);
13 |
14 | internal static bool IsSpamChannel(this DiscordChannel channel)
15 | => channel.IsPrivate
16 | || channel.Name.Contains("spam", StringComparison.OrdinalIgnoreCase)
17 | || channel.Name.Equals("testers", StringComparison.OrdinalIgnoreCase);
18 | }
--------------------------------------------------------------------------------
/HomoglyphConverter/readme.md:
--------------------------------------------------------------------------------
1 | Homoglyph Converter
2 | ===================
3 |
4 | This is a straight up implementation of the recommended [confusable detection algorithm](https://www.unicode.org/reports/tr39/#Confusable_Detection). It is mainly used to check for mod impersonation.
5 |
6 | You can get the latest version of the mappings from the [Unicode.org](https://www.unicode.org/Public/security/latest/confusables.txt). You'll need to manually gzip it for embedding in the resources.
7 |
8 | Code is split in two parts:
9 | * Builder will load the mapping file from the resources and will build the mapping dictionary that can be used to quickly substitute the character sequences.
10 |
11 | > One gotcha is that a lot of the characters are from the extended planes and require use of [surrogate pairs](https://en.wikipedia.org/wiki/UTF-16#U+010000_to_U+10FFFF), so we convert them to UTF32 and store as `uint`.
12 |
13 | * Normalizer implements the mapping and reducing steps of the algorithm
--------------------------------------------------------------------------------
/CompatBot/Commands/Ird.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Commands.AutoCompleteProviders;
2 | using CompatBot.Database.Providers;
3 | using CompatBot.Utils.ResultFormatters;
4 | using IrdLibraryClient;
5 |
6 | namespace CompatBot.Commands;
7 |
8 | internal static class Ird
9 | {
10 | private static readonly IrdClient Client = new();
11 |
12 | [Command("ird")]
13 | [Description("Search IRD Library")]
14 | public static async ValueTask Search(
15 | SlashCommandContext ctx,
16 | [Description("Product code or game title"), MinMaxLength(3)]
17 | [SlashAutoCompleteProvider]
18 | string query
19 | )
20 | {
21 | var ephemeral = !ctx.Channel.IsSpamChannel() && !ModProvider.IsMod(ctx.User.Id);
22 | var result = await IrdClient.SearchAsync(query, Config.Cts.Token).ConfigureAwait(false);
23 | await ctx.RespondAsync(embed: result.AsEmbed(), ephemeral: ephemeral).ConfigureAwait(false);
24 | }
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/ThumbnailCacheMonitor.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database;
2 |
3 | namespace CompatBot.EventHandlers;
4 |
5 | internal static class ThumbnailCacheMonitor
6 | {
7 | public static async Task OnMessageDeleted(DiscordClient _, MessageDeletedEventArgs args)
8 | {
9 | if (args.Channel.Id != Config.ThumbnailSpamId)
10 | return;
11 |
12 | if (string.IsNullOrEmpty(args.Message.Content))
13 | return;
14 |
15 | if (!args.Message.Attachments.Any())
16 | return;
17 |
18 | await using var wdb = await ThumbnailDb.OpenWriteAsync().ConfigureAwait(false);
19 | var thumb = wdb.Thumbnail.FirstOrDefault(i => i.ContentId == args.Message.Content);
20 | if (thumb is { EmbeddableUrl: { Length: > 0 } url } && args.Message.Attachments.Any(a => a.Url == url))
21 | {
22 | thumb.EmbeddableUrl = null;
23 | await wdb.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/Clients/CompatApiClient/RequestBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using CompatApiClient.Utils;
4 |
5 | namespace CompatApiClient;
6 |
7 | public class RequestBuilder
8 | {
9 | public string? Search { get; private set; } = "";
10 | public int AmountRequested { get; } = ApiConfig.ResultAmount[0];
11 |
12 | private RequestBuilder() {}
13 |
14 | public static RequestBuilder Start() => new();
15 |
16 | public RequestBuilder SetSearch(string search)
17 | {
18 | Search = search;
19 | return this;
20 | }
21 |
22 | public Uri Build(bool apiCall = true)
23 | {
24 | var parameters = new Dictionary
25 | {
26 | ["g"] = Search,
27 | ["r"] = AmountRequested.ToString(),
28 | };
29 | if (apiCall)
30 | {
31 | parameters["type"] = "All";
32 | parameters["api"] = "v" + ApiConfig.Version;
33 | }
34 | return ApiConfig.BaseUrl.SetQueryParameters(parameters);
35 | }
36 | }
--------------------------------------------------------------------------------
/Tests/TimeParserTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CompatBot.Utils;
3 | using NUnit.Framework;
4 |
5 | namespace Tests;
6 |
7 | [TestFixture]
8 | public class TimeParserTests
9 | {
10 | [TestCase("2019-8-19 6:00 PT", "2019-08-19T13:00Z")]
11 | [TestCase("2019-8-19 17:00 cest", "2019-08-19T15:00Z")]
12 | [TestCase("2019-9-1 22:00 jst", "2019-09-01T13:00Z")]
13 | public void TimeZoneConverterTest(string input, string utcInput)
14 | {
15 | var utc = DateTime.Parse(utcInput).Normalize();
16 | Assert.Multiple(() =>
17 | {
18 | Assert.That(TimeParser.TryParse(input, out var result), Is.True, $"{input} failed to parse\nSupported time zones: {string.Join(", ", TimeParser.GetSupportedTimeZoneAbbreviations())}");
19 | Assert.That(result, Is.EqualTo(utc));
20 | Assert.That(result.Kind, Is.EqualTo(DateTimeKind.Utc));
21 | });
22 | }
23 |
24 | [Test]
25 | public void TimeZoneInfoTest()
26 | {
27 | Assert.That(TimeParser.TimeZoneMap, Is.Not.Empty);
28 | }
29 | }
--------------------------------------------------------------------------------
/CompatBot/Database/PrimaryKeyConvention.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | namespace CompatBot.Database;
4 |
5 | internal static class PrimaryKeyConvention
6 | {
7 | public static void ConfigureDefaultPkConvention(this ModelBuilder modelBuilder, string keyProperty = "Id")
8 | {
9 | if (string.IsNullOrEmpty(keyProperty))
10 | throw new ArgumentException("Key property name is mandatory", nameof(keyProperty));
11 |
12 | foreach (var entity in modelBuilder.Model.GetEntityTypes())
13 | {
14 | var pk = entity.GetKeys().FirstOrDefault(k => k.IsPrimaryKey());
15 | pk?.SetName(keyProperty);
16 | }
17 | }
18 |
19 | public static void ConfigureNoPkConvention(this ModelBuilder modelBuilder)
20 | {
21 | foreach (var entity in modelBuilder.Model.GetEntityTypes())
22 | {
23 | var pk = entity.GetKeys().FirstOrDefault(k => k.IsPrimaryKey());
24 | if (pk != null)
25 | entity.RemoveKey(pk.Properties);
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/ThumbnailDb/20210302213749_FortuneTable.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Migrations
4 | {
5 | public partial class FortuneTable : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "fortune",
11 | columns: table => new
12 | {
13 | id = table.Column(type: "INTEGER", nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | content = table.Column(type: "TEXT", nullable: false)
16 | },
17 | constraints: table =>
18 | {
19 | table.PrimaryKey("id", x => x.id);
20 | });
21 | }
22 |
23 | protected override void Down(MigrationBuilder migrationBuilder)
24 | {
25 | migrationBuilder.DropTable(
26 | name: "fortune");
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Clients/PsnClient/POCOs/TitleMeta.cs:
--------------------------------------------------------------------------------
1 | using System.Xml.Serialization;
2 |
3 | namespace PsnClient.POCOs;
4 | #nullable disable
5 |
6 | [XmlRoot("title-info")]
7 | public class TitleMeta
8 | {
9 | [XmlAttribute("rev")]
10 | public int Rev { get; set; }
11 | [XmlElement("id")]
12 | public string Id { get; set; }
13 | [XmlElement("console")]
14 | public string Console { get; set; }
15 | [XmlElement("media-type")]
16 | public string MediaType { get; set; }
17 | [XmlElement("name")]
18 | public string Name { get; set; }
19 | [XmlElement("parental-level")]
20 | public int ParentalLevel { get; set; }
21 | [XmlElement("icon")]
22 | public TitleIcon Icon { get; set; }
23 | [XmlElement("resolution")]
24 | public string Resolution { get; set; }
25 | [XmlElement("sound-format")]
26 | public string SoundFormat { get; set; }
27 | }
28 |
29 | public class TitleIcon
30 | {
31 | [XmlAttribute("type")]
32 | public string Type { get; set; }
33 | [XmlText]
34 | public string Url { get; set; }
35 | }
36 |
37 | #nullable restore
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/ThumbnailDb/20210309212939_AddUserNamePool.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Migrations
4 | {
5 | public partial class AddUserNamePool : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "name_pool",
11 | columns: table => new
12 | {
13 | id = table.Column(type: "INTEGER", nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | name = table.Column(type: "TEXT", nullable: false)
16 | },
17 | constraints: table =>
18 | {
19 | table.PrimaryKey("id", x => x.id);
20 | });
21 | }
22 |
23 | protected override void Down(MigrationBuilder migrationBuilder)
24 | {
25 | migrationBuilder.DropTable(
26 | name: "name_pool");
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/ThumbnailDb/20200402181755_CompatFields.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Migrations
4 | {
5 | public partial class CompatFields : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.AddColumn(
10 | name: "compatibility_change_date",
11 | table: "thumbnail",
12 | nullable: true);
13 |
14 | migrationBuilder.AddColumn(
15 | name: "compatibility_status",
16 | table: "thumbnail",
17 | nullable: true);
18 | }
19 |
20 | protected override void Down(MigrationBuilder migrationBuilder)
21 | {
22 | migrationBuilder.DropColumn(
23 | name: "compatibility_change_date",
24 | table: "thumbnail");
25 |
26 | migrationBuilder.DropColumn(
27 | name: "compatibility_status",
28 | table: "thumbnail");
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20201113092108_MakeStateKeyRequired.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class MakeStateKeyRequired : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.AlterColumn(
10 | name: "key",
11 | table: "bot_state",
12 | type: "TEXT",
13 | nullable: false,
14 | defaultValue: "",
15 | oldClrType: typeof(string),
16 | oldType: "TEXT",
17 | oldNullable: true);
18 | }
19 |
20 | protected override void Down(MigrationBuilder migrationBuilder)
21 | {
22 | migrationBuilder.AlterColumn(
23 | name: "key",
24 | table: "bot_state",
25 | type: "TEXT",
26 | nullable: true,
27 | oldClrType: typeof(string),
28 | oldType: "TEXT");
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/DiceCoefficientOptimized.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Utils.Extensions;
2 |
3 | public static class DiceCoefficientOptimized
4 | {
5 | ///
6 | /// Dice Coefficient based on bigrams.
7 | /// A good value would be 0.33 or above, a value under 0.2 is not a good match, from 0.2 to 0.33 is iffy.
8 | ///
9 | ///
10 | ///
11 | ///
12 | public static double DiceIshCoefficientIsh(this string input, string comparedTo)
13 | {
14 | var bgCount1 = input.Length - 1;
15 | var bgCount2 = comparedTo.Length - 1;
16 | if (comparedTo.Length < input.Length)
17 | (input, comparedTo) = (comparedTo, input);
18 | var matches = 0;
19 | for (var i = 0; i < input.Length - 1; i++)
20 | for (var j = 0; j < comparedTo.Length - 1; j++)
21 | {
22 | if (input[i] == comparedTo[j] && input[i + 1] == comparedTo[j + 1])
23 | {
24 | matches++;
25 | break;
26 | }
27 | }
28 | if (matches == 0)
29 | return 0.0d;
30 |
31 | return 2.0 * matches / (bgCount1 + bgCount2);
32 | }
33 | }
--------------------------------------------------------------------------------
/Clients/CompatApiClient/POCOs/UpdateCheckResult.cs:
--------------------------------------------------------------------------------
1 | namespace CompatApiClient.POCOs;
2 |
3 | public class UpdateCheckResult
4 | {
5 | public StatusCode ReturnCode;
6 | public BuildInfo LatestBuild = null!;
7 | public BuildInfo? CurrentBuild;
8 | public VersionInfo[]? Changelog;
9 | }
10 |
11 | public class BuildInfo
12 | {
13 | public int? Pr;
14 | public string Datetime = null!;
15 | public string Version = null!;
16 | public BuildLink? Windows;
17 | public BuildLink? Linux;
18 | public BuildLink? Mac;
19 | }
20 |
21 | public class BuildLink
22 | {
23 | public string Download = null!;
24 | public int? Size;
25 | public string? Checksum;
26 | }
27 |
28 | public class VersionInfo
29 | {
30 | public string Verison = null!;
31 | public string? Title;
32 | }
33 |
34 | public enum StatusCode
35 | {
36 | IllegalSearch = -3,
37 | Maintenance = -2,
38 | UnknownBuild = -1,
39 | NoUpdates = 0,
40 | UpdatesAvailable = 1,
41 | }
42 |
43 | public static class ArchType
44 | {
45 | public const string X64 = "x64";
46 | public const string Arm = "arm64";
47 | }
48 |
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20190131223017_AddExplainAttachments.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class AddExplainAttachments : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.AddColumn(
10 | name: "attachment",
11 | table: "explanation",
12 | maxLength: 7340032,
13 | nullable: true);
14 |
15 | migrationBuilder.AddColumn(
16 | name: "attachment_filename",
17 | table: "explanation",
18 | nullable: true);
19 | }
20 |
21 | protected override void Down(MigrationBuilder migrationBuilder)
22 | {
23 | migrationBuilder.DropColumn(
24 | name: "attachment",
25 | table: "explanation");
26 |
27 | migrationBuilder.DropColumn(
28 | name: "attachment_filename",
29 | table: "explanation");
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20190728111050_AllowDuplicateTriggers.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class AllowDuplicateTriggers : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.DropIndex(
10 | name: "piracystring_string",
11 | table: "piracystring");
12 |
13 | migrationBuilder.CreateIndex(
14 | name: "piracystring_string",
15 | table: "piracystring",
16 | column: "string");
17 | }
18 |
19 | protected override void Down(MigrationBuilder migrationBuilder)
20 | {
21 | migrationBuilder.DropIndex(
22 | name: "piracystring_string",
23 | table: "piracystring");
24 |
25 | migrationBuilder.CreateIndex(
26 | name: "piracystring_string",
27 | table: "piracystring",
28 | column: "string",
29 | unique: true);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Clients/IrdLibraryClient/IrdFormat/IsoHeaderParser.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using System.IO.Compression;
4 | using System.Linq;
5 | using CompatApiClient;
6 | using DiscUtils.Iso9660;
7 |
8 | namespace IrdLibraryClient.IrdFormat;
9 |
10 | public static class IsoHeaderParser
11 | {
12 | public static List GetFilenames(this Ird ird)
13 | {
14 | using var decompressedStream = ApiConfig.MemoryStreamManager.GetStream();
15 | using (var compressedStream = new MemoryStream(ird.Header, false))
16 | {
17 | using var gzip = new GZipStream(compressedStream, CompressionMode.Decompress);
18 | gzip.CopyTo(decompressedStream);
19 | }
20 |
21 | decompressedStream.Seek(0, SeekOrigin.Begin);
22 | var reader = new CDReader(decompressedStream, true, true);
23 | return reader.GetFiles(reader.Root.FullName, "*.*", SearchOption.AllDirectories)
24 | .Distinct()
25 | .Select(n => n
26 | .TrimStart('\\')
27 | .Replace('\\', '/')
28 | .TrimEnd('.')
29 | ).ToList();
30 | }
31 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Introduction
2 | ============
3 | Please read [general architecture overview](architecture.md) of the project and it's parts (each folder contains a README.md with its content description).
4 |
5 | Unfortunately not all parts are documented right now, and some things might be slightly outdated / missing information about newer pieces of code.
6 |
7 | You can always ask for help/clarification/explanation in our [Discord server](https://discord.me/RPCS3).
8 |
9 | Just to reiterate, the main concerns are:
10 | 1. Code quality. The bot is used on a highly populated server, so performance and resource management can be an issue.
11 | 2. In-depth defense for potential bot misuse. See #1. Many people means many pranksters, or worse. No everyone is nice.
12 | 3. Having fun. This is a project for recreational programming, so if you always wanted to do something fun and it seems to be useful for the bot, just do it.
13 |
14 | Recommended development setup
15 | =============================
16 | See [readme](README.md) for detailed requirement and recommended IDE setup.
17 |
18 | **Note** that Docker image is currently experimental and wasn't tested in any way.
19 |
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/BotStatusMonitor.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace CompatBot.EventHandlers;
5 |
6 | internal static class BotStatusMonitor
7 | {
8 | public static async Task RefreshAsync(DiscordClient client)
9 | {
10 | try
11 | {
12 | await using var db = await BotDb.OpenReadAsync().ConfigureAwait(false);
13 | var status = await db.BotState.FirstOrDefaultAsync(s => s.Key == "bot-status-activity").ConfigureAwait(false);
14 | var txt = await db.BotState.FirstOrDefaultAsync(s => s.Key == "bot-status-text").ConfigureAwait(false);
15 | var msg = txt?.Value;
16 | if (Enum.TryParse(status?.Value ?? "Watching", true, out var activity)
17 | && msg is {Length: >0})
18 | await client.UpdateStatusAsync(new(msg, activity), DiscordUserStatus.Online).ConfigureAwait(false);
19 | else
20 | await client.UpdateStatusAsync(userStatus: DiscordUserStatus.Online).ConfigureAwait(false);
21 | }
22 | catch (Exception e)
23 | {
24 | Config.Log.Error(e);
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/sdk:9.0-noble AS base
2 |
3 | # Native libgdiplus dependencies
4 | RUN apt-get update
5 | RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get install -y --allow-unauthenticated libc6-dev libgdiplus libx11-dev fonts-roboto tzdata libarchive13t64 liblept5
6 | RUN wget https://archive.ubuntu.com/ubuntu/pool/main/t/tiff/libtiff5_4.3.0-6_amd64.deb
7 | RUN dpkg -i ./libtiff5_4.3.0-6_amd64.deb
8 | RUN rm ./libtiff5_4.3.0-6_amd64.deb
9 |
10 | # Regular stuff
11 | #COPY packages /root/.nuget/packages/
12 | WORKDIR /src
13 | COPY . .
14 | #RUN rm -rf ./packages
15 | RUN git status
16 | # Asks for user/pw otherwise..
17 | RUN git remote set-url origin https://github.com/RPCS3/discord-bot.git
18 | RUN git config --remove-section http."https://github.com/"
19 | # Build and test everything
20 | RUN dotnet restore "CompatBot/CompatBot.csproj"
21 | RUN dotnet build "CompatBot/CompatBot.csproj" -c Release
22 | ENV RUNNING_IN_DOCKER true
23 | # Limit server GC to 512 MB heap max
24 | ENV COMPlus_gcServer 1
25 | # ENV COMPlus_GCHeapHardLimit 0x20000000
26 | WORKDIR /src/CompatBot
27 | RUN dotnet run -c Release --dry-run
28 | ENTRYPOINT ["dotnet", "run", "-c", "Release", "CompatBot.csproj"]
29 | #ENTRYPOINT ["/bin/bash"]
30 |
--------------------------------------------------------------------------------
/CompatBot/Ocr/Backend/BackendBase.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Net.Http;
3 | using CompatApiClient.Compression;
4 |
5 | namespace CompatBot.Ocr.Backend;
6 |
7 | public abstract class BackendBase: IOcrBackend, IDisposable
8 | {
9 | protected static readonly HttpClient HttpClient = HttpClientFactory.Create(new CompressionMessageHandler());
10 |
11 | public abstract string Name { get; }
12 |
13 | public virtual Task InitializeAsync(CancellationToken cancellationToken)
14 | {
15 | try
16 | {
17 | if (!Directory.Exists(ModelCachePath))
18 | Directory.CreateDirectory(ModelCachePath);
19 | }
20 | catch (Exception e)
21 | {
22 | Config.Log.Error(e, $"Failed to create model cache folder '{ModelCachePath}'");
23 | return Task.FromResult(false);
24 | }
25 | return Task.FromResult(true);
26 | }
27 |
28 | public abstract Task<(string result, double confidence)> GetTextAsync(string imgUrl, CancellationToken cancellationToken);
29 |
30 | public virtual void Dispose() => HttpClient.Dispose();
31 |
32 | protected string ModelCachePath => Path.Combine(Config.BotAppDataFolder, "ocr-models", Name.ToLowerInvariant());
33 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20191002172950_PawsOfCommunity.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class PawsOfCommunity : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "kot",
11 | columns: table => new
12 | {
13 | id = table.Column(nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | user_id = table.Column(nullable: false)
16 | },
17 | constraints: table =>
18 | {
19 | table.PrimaryKey("id", x => x.id);
20 | });
21 |
22 | migrationBuilder.CreateIndex(
23 | name: "kot_user_id",
24 | table: "kot",
25 | column: "user_id",
26 | unique: true);
27 | }
28 |
29 | protected override void Down(MigrationBuilder migrationBuilder)
30 | {
31 | migrationBuilder.DropTable(
32 | name: "kot");
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20191002180817_PawsOfCommunityV2.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class PawsOfCommunityV2 : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "doggo",
11 | columns: table => new
12 | {
13 | id = table.Column(nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | user_id = table.Column(nullable: false)
16 | },
17 | constraints: table =>
18 | {
19 | table.PrimaryKey("id", x => x.id);
20 | });
21 |
22 | migrationBuilder.CreateIndex(
23 | name: "doggo_user_id",
24 | table: "doggo",
25 | column: "user_id",
26 | unique: true);
27 | }
28 |
29 | protected override void Down(MigrationBuilder migrationBuilder)
30 | {
31 | migrationBuilder.DropTable(
32 | name: "doggo");
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Clients/PsnClient/POCOs/StoreNavigation.cs:
--------------------------------------------------------------------------------
1 | namespace PsnClient.POCOs;
2 | #nullable disable
3 | public class StoreNavigation
4 | {
5 | public StoreNavigationData Data;
6 | //public StoreNavigationIncluded Included;
7 | }
8 |
9 | public class StoreNavigationData
10 | {
11 | public string Id;
12 | public string Type;
13 | public StoreNavigationAttributes Attributes;
14 | public Relationships Relationships;
15 | }
16 |
17 | public class StoreNavigationAttributes
18 | {
19 | public string Name;
20 | public StoreNavigationNavigation[] Navigation;
21 | }
22 |
23 | public class StoreNavigationNavigation
24 | {
25 | public string Id;
26 | public string Name;
27 | public string TargetContainerId;
28 | public string RouteName;
29 | public StoreNavigationSubmenu[] Submenu;
30 | }
31 |
32 | public class StoreNavigationSubmenu
33 | {
34 | public string Name;
35 | public string TargetContainerId;
36 | public int? TemplateDefId;
37 | public StoreNavigationSubmenuItem[] Items;
38 | }
39 |
40 | public class StoreNavigationSubmenuItem
41 | {
42 | public string Name;
43 | public string TargetContainerId;
44 | public string TargetContainerType;
45 | public int? TemplateDefId;
46 | public bool IsSeparator;
47 | }
48 | #nullable restore
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/LogParsing/POCOs/LogParseState.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Specialized;
2 | using CompatBot.Database;
3 |
4 | namespace CompatBot.EventHandlers.LogParsing.POCOs;
5 |
6 | public class LogParseState
7 | {
8 | public NameValueCollection? CompletedCollection;
9 | public NameUniqueObjectCollection? CompleteMultiValueCollection;
10 | public NameValueCollection WipCollection = new();
11 | public NameUniqueObjectCollection WipMultiValueCollection = new();
12 | public readonly Dictionary ValueHitStats = new();
13 | public readonly Dictionary> Syscalls = new();
14 | public int Id = 0;
15 | public ErrorCode Error = ErrorCode.None;
16 | public readonly Dictionary FilterTriggers = new();
17 | public Piracystring? SelectedFilter;
18 | public string? SelectedFilterContext;
19 | public long ReadBytes;
20 | public long TotalBytes;
21 | public TimeSpan ParsingTime;
22 | #if DEBUG
23 | public readonly Dictionary ExtractorHitStats = new();
24 | #endif
25 |
26 | public enum ErrorCode
27 | {
28 | None = 0,
29 | PiracyDetected = 1,
30 | SizeLimit = 2,
31 | UnknownError = 3,
32 | }
33 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20180804225045_DisabledCommands.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class DisabledCommands : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "disabled_commands",
11 | columns: table => new
12 | {
13 | id = table.Column(nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | command = table.Column(nullable: false)
16 | },
17 | constraints: table =>
18 | {
19 | table.PrimaryKey("id", x => x.id);
20 | });
21 |
22 | migrationBuilder.CreateIndex(
23 | name: "disabled_command_command",
24 | table: "disabled_commands",
25 | column: "command",
26 | unique: true);
27 | }
28 |
29 | protected override void Down(MigrationBuilder migrationBuilder)
30 | {
31 | migrationBuilder.DropTable(
32 | name: "disabled_commands");
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20210427131110_SusStrings.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class SusStrings : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "suspicious_string",
11 | columns: table => new
12 | {
13 | id = table.Column(type: "INTEGER", nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | @string = table.Column(name: "string", type: "TEXT", nullable: false)
16 | },
17 | constraints: table =>
18 | {
19 | table.PrimaryKey("id", x => x.id);
20 | });
21 |
22 | migrationBuilder.CreateIndex(
23 | name: "suspicious_string_string",
24 | table: "suspicious_string",
25 | column: "string");
26 | }
27 |
28 | protected override void Down(MigrationBuilder migrationBuilder)
29 | {
30 | migrationBuilder.DropTable(
31 | name: "suspicious_string");
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20180908150603_BotState.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class BotState : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "bot_state",
11 | columns: table => new
12 | {
13 | id = table.Column(nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | key = table.Column(nullable: true),
16 | value = table.Column(nullable: true)
17 | },
18 | constraints: table =>
19 | {
20 | table.PrimaryKey("id", x => x.id);
21 | });
22 |
23 | migrationBuilder.CreateIndex(
24 | name: "bot_state_key",
25 | table: "bot_state",
26 | column: "key",
27 | unique: true);
28 | }
29 |
30 | protected override void Down(MigrationBuilder migrationBuilder)
31 | {
32 | migrationBuilder.DropTable(
33 | name: "bot_state");
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/ThumbnailDb/20250806100313_AddNoCaseCollationForGameTitle.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | #nullable disable
4 |
5 | namespace CompatBot.Migrations
6 | {
7 | ///
8 | public partial class AddNoCaseCollationForGameTitle : Migration
9 | {
10 | ///
11 | protected override void Up(MigrationBuilder migrationBuilder)
12 | {
13 | migrationBuilder.AlterColumn(
14 | name: "name",
15 | table: "thumbnail",
16 | type: "TEXT",
17 | nullable: true,
18 | collation: "NOCASE",
19 | oldClrType: typeof(string),
20 | oldType: "TEXT",
21 | oldNullable: true);
22 | }
23 |
24 | ///
25 | protected override void Down(MigrationBuilder migrationBuilder)
26 | {
27 | migrationBuilder.AlterColumn(
28 | name: "name",
29 | table: "thumbnail",
30 | type: "TEXT",
31 | nullable: true,
32 | oldClrType: typeof(string),
33 | oldType: "TEXT",
34 | oldNullable: true,
35 | oldCollation: "NOCASE");
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Tests/RegexTest.cs:
--------------------------------------------------------------------------------
1 | using System.Text.RegularExpressions;
2 | using CompatBot.Utils;
3 | using NUnit.Framework;
4 |
5 | namespace Tests;
6 |
7 | [TestFixture]
8 | public partial class RegexTest
9 | {
10 | [GeneratedRegex(@"Rap file not found: (\xE2\x80\x9C)?(?.*?)(\xE2\x80\x9D)?\r?$", RegexOptions.Multiline | RegexOptions.ExplicitCapture)]
11 | private static partial Regex RapFileMissingLogLine();
12 |
13 | [Test]
14 | public void Utf8AsAsciiRegexTest()
15 | {
16 | const string input = """
17 | ·W 0:09:45.540824 {PPU[0x1000016] Thread (addContSyncThread) [HLE:0x01245834, LR:0x0019b834]} sceNp: npDrmIsAvailable(): Rap file not found: “/dev_hdd0/home/00000001/exdata/EP4062-NPEB02436_00-ADDCONTENT000001.rap”
18 | ·W 0:09:45.540866 {PPU[0x1000016] Thread (addContSyncThread) [HLE:0x01245834, LR:0x0019b834]} sceNp: sceNpDrmIsAvailable2(k_licensee=*0xd521b0, drm_path=*0xd00ddac0)
19 | """;
20 | var latin = input.ToLatin8BitEncoding();
21 | var match = RapFileMissingLogLine().Match(latin);
22 | Assert.Multiple(() =>
23 | {
24 | Assert.That(match.Success, Is.True);
25 | Assert.That(match.Groups["rap_file"].Value, Is.EqualTo("/dev_hdd0/home/00000001/exdata/EP4062-NPEB02436_00-ADDCONTENT000001.rap"));
26 | });
27 | }
28 | }
--------------------------------------------------------------------------------
/CompatBot/Database/NamingConventionConverter.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | namespace CompatBot.Database;
4 |
5 | internal static class NamingConventionConverter
6 | {
7 | public static void ConfigureMapping(this ModelBuilder modelBuilder, Func nameResolver)
8 | {
9 | if (nameResolver == null)
10 | throw new ArgumentNullException(nameof(nameResolver));
11 |
12 | foreach (var entity in modelBuilder.Model.GetEntityTypes())
13 | {
14 | if (entity.GetTableName() is string tableName)
15 | entity.SetTableName(nameResolver(tableName));
16 | foreach (var property in entity.GetProperties())
17 | property.SetColumnName(nameResolver(property.Name));
18 | foreach (var key in entity.GetKeys())
19 | if (key.GetName() is string name)
20 | key.SetName(nameResolver(name));
21 | foreach (var key in entity.GetForeignKeys())
22 | if (key.GetConstraintName() is string constraint)
23 | key.SetConstraintName(nameResolver(constraint));
24 | foreach (var index in entity.GetIndexes())
25 | if (index.GetDatabaseName() is string dbName)
26 | index.SetDatabaseName(nameResolver(dbName));
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/BotDbExtensions.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database;
2 | using Microsoft.Data.Sqlite;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace CompatBot.Utils;
6 |
7 | internal static class BotDbExtensions
8 | {
9 | public static bool IsComplete(this EventSchedule evt)
10 | => evt is { Start: > 0, Year: > 0, Name.Length: >0 }
11 | && evt.End > evt.Start;
12 |
13 | public static bool IsComplete(this Piracystring filter)
14 | {
15 | var result = filter.Actions != 0
16 | && filter.String.Length >= Config.MinimumPiracyTriggerLength;
17 | if (result && filter.Actions.HasFlag(FilterAction.ShowExplain))
18 | result = !string.IsNullOrEmpty(filter.ExplainTerm);
19 | return result;
20 | }
21 |
22 | public static T WithNoCase(this T ctx) where T: DbContext
23 | {
24 | var connection = (SqliteConnection)ctx.Database.GetDbConnection();
25 | // support unicode in NOCASE collation
26 | connection.CreateCollation("NOCASE", (x, y) => string.Compare(x, y, StringComparison.OrdinalIgnoreCase));
27 | // ignore case for str.Contains() translation
28 | connection.CreateFunction("instr", (string x, string y) => x.Contains(y, StringComparison.OrdinalIgnoreCase), isDeterministic: true);
29 | return ctx;
30 | }
31 | }
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Compression/CompressedContent.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Net;
3 | using System.Net.Http;
4 | using System.Threading.Tasks;
5 |
6 | namespace CompatApiClient.Compression;
7 |
8 | public class CompressedContent : HttpContent
9 | {
10 | private readonly HttpContent content;
11 | private readonly ICompressor compressor;
12 |
13 | public CompressedContent(HttpContent content, ICompressor compressor)
14 | {
15 | this.content = content;
16 | this.compressor = compressor;
17 | AddHeaders();
18 | }
19 |
20 | protected override bool TryComputeLength(out long length)
21 | {
22 | length = -1;
23 | return false;
24 | }
25 |
26 | protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
27 | {
28 | var contentStream = await content.ReadAsStreamAsync().ConfigureAwait(false);
29 | var compressedLength = await compressor.CompressAsync(contentStream, stream).ConfigureAwait(false);
30 | Headers.ContentLength = compressedLength;
31 | }
32 |
33 | private void AddHeaders()
34 | {
35 | foreach (var (key, value) in content.Headers)
36 | Headers.TryAddWithoutValidation(key, value);
37 | Headers.ContentEncoding.Add(compressor.EncodingType);
38 | Headers.ContentLength = null;
39 | }
40 | }
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Compression/DecompressedContent.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Net;
3 | using System.Net.Http;
4 | using System.Threading.Tasks;
5 |
6 | namespace CompatApiClient.Compression;
7 |
8 | public class DecompressedContent : HttpContent
9 | {
10 | private readonly HttpContent content;
11 | private readonly ICompressor compressor;
12 |
13 | public DecompressedContent(HttpContent content, ICompressor compressor)
14 | {
15 | this.content = content;
16 | this.compressor = compressor;
17 | RemoveHeaders();
18 | }
19 |
20 | protected override bool TryComputeLength(out long length)
21 | {
22 | length = -1;
23 | return false;
24 | }
25 |
26 | protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
27 | {
28 | var contentStream = await content.ReadAsStreamAsync().ConfigureAwait(false);
29 | var decompressedLength = await compressor.DecompressAsync(contentStream, stream).ConfigureAwait(false);
30 | Headers.ContentLength = decompressedLength;
31 | }
32 |
33 | private void RemoveHeaders()
34 | {
35 | foreach (var (key, value) in content.Headers)
36 | Headers.TryAddWithoutValidation(key, value);
37 | Headers.ContentEncoding.Clear();
38 | Headers.ContentLength = null;
39 | }
40 | }
--------------------------------------------------------------------------------
/Clients/CompatApiClient/POCOs/CompatResult.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace CompatApiClient.POCOs;
6 | #nullable disable
7 |
8 | public class CompatResult
9 | {
10 | public CompatApiStatus ReturnCode;
11 | public string SearchTerm;
12 | public Dictionary Results;
13 |
14 | [JsonIgnore]
15 | public TimeSpan RequestDuration;
16 | [JsonIgnore]
17 | public RequestBuilder RequestBuilder;
18 | }
19 |
20 | public class TitleInfo
21 | {
22 | public static readonly TitleInfo Maintenance = new() { Status = "Maintenance" };
23 | public static readonly TitleInfo CommunicationError = new() { Status = "Error" };
24 | public static readonly TitleInfo Unknown = new() { Status = "Unknown" };
25 |
26 | public string Title;
27 | [JsonPropertyName("alternative-title")]
28 | public string AlternativeTitle;
29 | [JsonPropertyName("wiki-title")]
30 | public string WikiTitle;
31 | [JsonPropertyName("wiki-id")]
32 | public int? WikiId;
33 | public string Status;
34 | public string Date;
35 | public int Thread;
36 | public string Commit;
37 | public int? Pr;
38 | public int? Network;
39 | public string Update;
40 | public bool? UsingLocalCache;
41 | public IReadOnlyCollection Languages;
42 | }
43 |
44 | #nullable restore
--------------------------------------------------------------------------------
/Tests/IrdTests.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using IrdLibraryClient.IrdFormat;
3 | using NUnit.Framework;
4 |
5 | namespace Tests;
6 |
7 | [TestFixture, Explicit("Requires files to run")]
8 | public class IrdTests
9 | {
10 | [Test]
11 | public void ParsingTest()
12 | {
13 | var baseDir = TestContext.CurrentContext.TestDirectory;
14 | var testFiles = Directory.GetFiles(baseDir, "*.ird", SearchOption.AllDirectories);
15 | Assert.That(testFiles, Is.Not.Empty);
16 |
17 | foreach (var file in testFiles)
18 | {
19 | var bytes = File.ReadAllBytes(file);
20 | Assert.That(() => IrdParser.Parse(bytes), Throws.Nothing, "Failed to parse " + Path.GetFileName(file));
21 | }
22 | }
23 |
24 | [Test]
25 | public void HeaderParsingTest()
26 | {
27 | var baseDir = TestContext.CurrentContext.TestDirectory;
28 | var testFiles = Directory.GetFiles(baseDir, "*.ird", SearchOption.AllDirectories);
29 | Assert.That(testFiles, Is.Not.Empty);
30 |
31 | foreach (var file in testFiles)
32 | {
33 | var bytes = File.ReadAllBytes(file);
34 | var ird = IrdParser.Parse(bytes);
35 | Assert.That(ird.FileCount, Is.GreaterThan(0));
36 |
37 | var fileList = ird.GetFilenames();
38 | Assert.That(fileList, Has.Count.EqualTo(ird.FileCount));
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/Clients/IrdLibraryClient/IrdFormat/Ird.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace IrdLibraryClient.IrdFormat;
6 |
7 | public sealed class Ird
8 | {
9 | internal Ird(){}
10 |
11 | public static readonly int Magic = BitConverter.ToInt32(Encoding.ASCII.GetBytes("3IRD"), 0);
12 | public byte Version;
13 | public string ProductCode = null!; // 9
14 | public byte TitleLength;
15 | public string Title = null!;
16 | public string UpdateVersion = null!; // 4
17 | public string GameVersion = null!; // 5
18 | public string AppVersion = null!; // 5
19 | public int Id; // v7 only?
20 | public int HeaderLength;
21 | public byte[] Header = null!; // gz
22 | public int FooterLength;
23 | public byte[] Footer = null!; // gz
24 | public byte RegionCount;
25 | public List RegionMd5Checksums = null!; // 16 each
26 | public int FileCount;
27 | public List Files = null!;
28 | public int Unknown; // always 0?
29 | public byte[] Pic = null!; // 115, v9 only?
30 | public byte[] Data1 = null!; // 16
31 | public byte[] Data2 = null!; // 16
32 | // Pic for CompressAsync(Stream source, Stream destination)
13 | {
14 | await using var memStream = ApiConfig.MemoryStreamManager.GetStream();
15 | await using (var compressed = CreateCompressionStream(memStream))
16 | await source.CopyToAsync(compressed).ConfigureAwait(false);
17 | memStream.Seek(0, SeekOrigin.Begin);
18 | await memStream.CopyToAsync(destination).ConfigureAwait(false);
19 | return memStream.Length;
20 | }
21 |
22 | public virtual async Task DecompressAsync(Stream source, Stream destination)
23 | {
24 | await using var memStream = ApiConfig.MemoryStreamManager.GetStream();
25 | await using (var decompressed = CreateDecompressionStream(source))
26 | await decompressed.CopyToAsync(memStream).ConfigureAwait(false);
27 | memStream.Seek(0, SeekOrigin.Begin);
28 | await memStream.CopyToAsync(destination).ConfigureAwait(false);
29 | return memStream.Length;
30 | }
31 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20190129194930_AddE3Schedule.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class AddE3Schedule : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "e3_schedule",
11 | columns: table => new
12 | {
13 | id = table.Column(nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | year = table.Column(nullable: false),
16 | start = table.Column(nullable: false),
17 | end = table.Column(nullable: false),
18 | name = table.Column(nullable: true)
19 | },
20 | constraints: table =>
21 | {
22 | table.PrimaryKey("id", x => x.id);
23 | });
24 |
25 | migrationBuilder.CreateIndex(
26 | name: "e3schedule_year",
27 | table: "e3_schedule",
28 | column: "year");
29 | }
30 |
31 | protected override void Down(MigrationBuilder migrationBuilder)
32 | {
33 | migrationBuilder.DropTable(
34 | name: "e3_schedule");
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/ContentFilterMonitor.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database.Providers;
2 | using CompatBot.Utils.Extensions;
3 |
4 | namespace CompatBot.EventHandlers;
5 |
6 | internal static class ContentFilterMonitor
7 | {
8 | public static async Task OnMessageCreated(DiscordClient c, MessageCreatedEventArgs args) => await ContentFilter.IsClean(c, args.Message).ConfigureAwait(false);
9 | public static async Task OnMessageUpdated(DiscordClient c, MessageUpdatedEventArgs args) => await ContentFilter.IsClean(c, args.Message).ConfigureAwait(false);
10 |
11 | public static async Task OnReaction(DiscordClient c, MessageReactionAddedEventArgs e)
12 | {
13 | if (e.User.IsBotSafeCheck())
14 | return;
15 |
16 | var emoji = c.GetEmoji(":piratethink:", Config.Reactions.PiracyCheck);
17 | if (e.Emoji != emoji)
18 | return;
19 |
20 | var message = e.Message;
21 | if (message.Author is null)
22 | {
23 | message = await e.Channel.GetMessageCachedAsync(e.Message.Id).ConfigureAwait(false);
24 | if (message?.Author is null)
25 | message = await e.Channel.GetMessageAsync(e.Message.Id).ConfigureAwait(false);
26 | }
27 | if (message.Attachments.Any() || message.Embeds.Any())
28 | MediaScreenshotMonitor.EnqueueOcrTask(message);
29 | await ContentFilter.IsClean(c, message).ConfigureAwait(false);
30 | }
31 | }
--------------------------------------------------------------------------------
/Clients/PsnClient/POCOs/TitlePatch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Xml.Serialization;
3 |
4 | namespace PsnClient.POCOs;
5 | #nullable disable
6 |
7 | [XmlRoot("titlepatch")]
8 | public class TitlePatch
9 | {
10 | [XmlAttribute("titleid")]
11 | public string TitleId { get; set; }
12 | [XmlAttribute("status")]
13 | public string Status { get; set; }
14 | [XmlElement("tag")]
15 | public TitlePatchTag Tag { get; set; }
16 | [XmlIgnore]
17 | public DateTime? OfflineCacheTimestamp { get; set; }
18 | }
19 |
20 | public class TitlePatchTag
21 | {
22 | [XmlAttribute("name")]
23 | public string Name { get; set; }
24 | //no root element
25 | [XmlElement("package")]
26 | public TitlePatchPackage[] Packages { get; set; }
27 | }
28 |
29 | public class TitlePatchPackage
30 | {
31 | [XmlAttribute("version")]
32 | public string Version { get; set; }
33 | [XmlAttribute("size")]
34 | public long Size { get; set; }
35 | [XmlAttribute("sha1sum")]
36 | public string Sha1Sum { get; set; }
37 | [XmlAttribute("url")]
38 | public string Url { get; set; }
39 | [XmlAttribute("ps3_system_ver")]
40 | public string Ps3SystemVer { get; set; }
41 | [XmlElement("paramsfo")]
42 | public TitlePatchParamSfo ParamSfo { get; set; }
43 | }
44 |
45 | public class TitlePatchParamSfo
46 | {
47 | [XmlElement("TITLE")]
48 | public string Title { get; set; }
49 | }
50 |
51 | #nullable restore
--------------------------------------------------------------------------------
/CompatBot/Commands/Bot.Import.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database;
2 | using DSharpPlus.Commands.Processors.TextCommands;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace CompatBot.Commands;
6 |
7 | internal static partial class Bot
8 | {
9 | [Command("import")]
10 | internal static class Import
11 | {
12 | [Command("metacritic"), LimitedToSpamChannel]
13 | [Description("Import Metacritic database dump and link it to existing PSN items")]
14 | public static async ValueTask ImportMc(TextCommandContext ctx)
15 | {
16 | if (await ImportLockObj.WaitAsync(0).ConfigureAwait(false))
17 | try
18 | {
19 | await CompatList.ImportMetacriticScoresAsync().ConfigureAwait(false);
20 | await using var db = await ThumbnailDb.OpenReadAsync().ConfigureAwait(false);
21 | var linkedItems = await db.Thumbnail.CountAsync(i => i.MetacriticId != null).ConfigureAwait(false);
22 | await ctx.Channel.SendMessageAsync($"Importing Metacritic info was successful, linked {linkedItems} items").ConfigureAwait(false);
23 | }
24 | finally
25 | {
26 | ImportLockObj.Release();
27 | }
28 | else
29 | await ctx.Channel.SendMessageAsync("Another import operation is already in progress").ConfigureAwait(false);
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20180818153741_InvitesWhitelist.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class InvitesWhitelist : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "whitelisted_invites",
11 | columns: table => new
12 | {
13 | id = table.Column(nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | guild_id = table.Column(nullable: false),
16 | name = table.Column(nullable: true),
17 | invite_code = table.Column(nullable: true)
18 | },
19 | constraints: table =>
20 | {
21 | table.PrimaryKey("id", x => x.id);
22 | });
23 |
24 | migrationBuilder.CreateIndex(
25 | name: "whitelisted_invite_guild_id",
26 | table: "whitelisted_invites",
27 | column: "guild_id",
28 | unique: true);
29 | }
30 |
31 | protected override void Down(MigrationBuilder migrationBuilder)
32 | {
33 | migrationBuilder.DropTable(
34 | name: "whitelisted_invites");
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Clients/PsnClient/Utils/TmdbHasher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Security.Cryptography;
4 | using System.Text;
5 |
6 | namespace PsnClient.Utils;
7 |
8 | public static class TmdbHasher
9 | {
10 | private static readonly byte[] HmacKey = "F5DE66D2680E255B2DF79E74F890EBF349262F618BCAE2A9ACCDEE5156CE8DF2CDF2D48C71173CDC2594465B87405D197CF1AED3B7E9671EEB56CA6753C2E6B0".FromHexString();
11 |
12 | public static string GetTitleHash(string productId)
13 | => HMACSHA1.HashData(HmacKey, Encoding.UTF8.GetBytes(productId)).ToHexString();
14 |
15 | public static byte[] FromHexString(this string hexString)
16 | {
17 | if (hexString.Length == 0)
18 | return [];
19 |
20 | if (hexString.Length % 2 != 0)
21 | throw new ArgumentException("Invalid hex string format: odd number of octets", nameof(hexString));
22 |
23 | var result = new byte[hexString.Length/2];
24 | for (int i = 0, j = 0; i < hexString.Length; i += 2, j++)
25 | result[j] = byte.Parse(hexString.Substring(i, 2), NumberStyles.HexNumber);
26 | return result;
27 | }
28 |
29 | public static string ToHexString(this byte[] array)
30 | {
31 | if (array.Length == 0)
32 | return "";
33 |
34 | var result = new StringBuilder(array.Length*2);
35 | foreach (var b in array)
36 | result.Append(b.ToString("X2"));
37 | return result.ToString();
38 | }
39 | }
--------------------------------------------------------------------------------
/Clients/readme.md:
--------------------------------------------------------------------------------
1 | Clients
2 | =======
3 |
4 | Here we keep all the 3rd party service clients used by the bot. Most infrastructure is in the CompatApi client, and other clients reference it to use these classes, along with the configured `Log`.
5 |
6 | * CompatApi is the [custom API](https://github.com/AniLeo/rpcs3-compatibility) provided by the [RPCS3 website](https://rpcs3.net/). It provides information about game compatibility and RPCS3 updates.
7 |
8 | * [IRD Library](http://jonnysp.bplaced.net/) contains the largest public repository of [IRD files](http://www.psdevwiki.com/ps3/Bluray_disc#IRD_file). It has no official API, so everything is reverse-engineered from the website web UI.
9 |
10 | > Client implements automatic caching of the downloaded IRD files on the local filesystem for future uses.
11 |
12 | * PSN Client is a result of reverse-engineering the JSON API of the [Playstation Store](https://store.playstation.com/). Currently it implements resolving metadata content by its ID, as well as full-text search.
13 |
14 | * GitHub Client implements a barebone [set of requests](https://developer.github.com/v3/) to resolve pull-request information, along with some additional data about the CI states.
15 |
16 | > We do not use any form of authentication, and are limited by the regular rate of 60 API requests per hour.
17 |
18 | * AppVeyor Client implements most of the [read-only calls](https://www.appveyor.com/docs/api/) to read the build history, job status, and artifact information.
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20191129183704_AddForcedNickname.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class AddForcedNickname : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "forced_nicknames",
11 | columns: table => new
12 | {
13 | id = table.Column(nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | guild_id = table.Column(nullable: false),
16 | user_id = table.Column(nullable: false),
17 | nickname = table.Column(nullable: false)
18 | },
19 | constraints: table =>
20 | {
21 | table.PrimaryKey("id", x => x.id);
22 | });
23 |
24 | migrationBuilder.CreateIndex(
25 | name: "forced_nickname_guild_id_user_id",
26 | table: "forced_nicknames",
27 | columns: ["guild_id", "user_id"],
28 | unique: true);
29 | }
30 |
31 | protected override void Down(MigrationBuilder migrationBuilder)
32 | {
33 | migrationBuilder.DropTable(
34 | name: "forced_nicknames");
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Clients/PsnClient/certificates/CA01.cer:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIID0jCCArqgAwIBAgIBADANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJKUDEp
3 | MCcGA1UEChMgU29ueSBDb21wdXRlciBFbnRlcnRhaW5tZW50IEluYy4xGjAYBgNV
4 | BAMTEVNDRUkgRE5BUyBSb290IDAxMB4XDTA0MDcxMjA4NTk0NloXDTM3MTIwNjA4
5 | NTk0NlowVDELMAkGA1UEBhMCSlAxKTAnBgNVBAoTIFNvbnkgQ29tcHV0ZXIgRW50
6 | ZXJ0YWlubWVudCBJbmMuMRowGAYDVQQDExFTQ0VJIEROQVMgUm9vdCAwMTCCASIw
7 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMsJCKYJ+0Rk+wpSQRcrdkEX8kFv
8 | DuxjUM4I+H+/Baz/gfH8ug3onZo6HcB8qZKhNaOFMoKwgBFXZXOpwt2+pj6pSEnS
9 | jpCC5lRcRN+N5BC1Y3qvaS8pQknWFWX+XO2N1FvzA6p/3dUf8rS7QMCSCs9/VDh/
10 | CPr42C6OGkJsRIqJRHndxdbUhtD+Q+ZPol8n+7u5BlkvScH6RAwBzjpSoe6oZh+p
11 | htzFdalyDud+IpmZaQzW9jSAS8r+k7u+JAEcaTmvxHtpBns+KTunB+Qo4bQOLDBv
12 | O9jV/AOUa609zntED7/mni/f5vuExf2phGU6qx9N0OOV2Y/cU/Fn5g6NdWcCAwEA
13 | AaOBrjCBqzAdBgNVHQ4EFgQUsh1S8qTY+KWBdqbGQHvLKzNp9c4wfAYDVR0jBHUw
14 | c4AUsh1S8qTY+KWBdqbGQHvLKzNp9c6hWKRWMFQxCzAJBgNVBAYTAkpQMSkwJwYD
15 | VQQKEyBTb255IENvbXB1dGVyIEVudGVydGFpbm1lbnQgSW5jLjEaMBgGA1UEAxMR
16 | U0NFSSBETkFTIFJvb3QgMDGCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUF
17 | AAOCAQEAcQ8MVErCjGyhYb2XictQOs1IBK8IzG+zGir4G9RFb8YcIk/0F7uy3jht
18 | aiTzjYlnXAaS0WCdlX2KcawiLd7QqXN7IonI6l+MA0A+fKGJ02iUKHKCNqT6XcQa
19 | alOH9mBuHpYjFSFlLgz3atfXMwXO3UsMX+AIVHQWQZg+wVhGO9UlqjYc2JlUDtak
20 | /yuTbMbwUgeNnTboYCceyZWgPn2JqNX57MohDosgh/tP7ebarr9zjJJMZe8FxEiv
21 | dwawFfmDI7kcUJzoSfVET7XD4kllBd627qs+dsMxzEsNT9gWHWWLD+hD0EbVyEpB
22 | Nm1w+4cnLEFZizg3pF4oV6G7BRAnrQ==
23 | -----END CERTIFICATE-----
24 |
--------------------------------------------------------------------------------
/Clients/PsnClient/certificates/CA02.cer:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIID0jCCArqgAwIBAgIBADANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJKUDEp
3 | MCcGA1UEChMgU29ueSBDb21wdXRlciBFbnRlcnRhaW5tZW50IEluYy4xGjAYBgNV
4 | BAMTEVNDRUkgRE5BUyBSb290IDAyMB4XDTA0MDcxMjA5MDAxMFoXDTM3MTIwNjA5
5 | MDAxMFowVDELMAkGA1UEBhMCSlAxKTAnBgNVBAoTIFNvbnkgQ29tcHV0ZXIgRW50
6 | ZXJ0YWlubWVudCBJbmMuMRowGAYDVQQDExFTQ0VJIEROQVMgUm9vdCAwMjCCASIw
7 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL94IA35sUVDA9EBDcigiLvrLo1j
8 | 1DM/2mOUJoivoz0ubRXzYiIpnZMwGJh8kNzcG7F3KbIoST6+n6rSqpyl4R1MEA5m
9 | uTpucGchLBo6vdFRNAAOVJuzjOe7yykpSBzL1Z5PKR0uegeB5rwF3AK1dS83AUXi
10 | qf4ro89Nw1rAZ1ukf1/Q89xekpoTVsNt4jj4f+Sa2hHoRG2OWaaB9pb0cmXjEezK
11 | ZlFDrrEXK9Y0JxEvPOUV57i2SdJqp8u3qHziL2W33MLqhrpNqbHRetgVUbwjV8nl
12 | sBNqsd5nfwkx/8JBsk8MytVd/bWgH0MBgxnkHr+HXGq0a1QI7mih5fOCnkECAwEA
13 | AaOBrjCBqzAdBgNVHQ4EFgQUR3JXrRvyIT3HHDSh1oSeUPQFgvcwfAYDVR0jBHUw
14 | c4AUR3JXrRvyIT3HHDSh1oSeUPQFgvehWKRWMFQxCzAJBgNVBAYTAkpQMSkwJwYD
15 | VQQKEyBTb255IENvbXB1dGVyIEVudGVydGFpbm1lbnQgSW5jLjEaMBgGA1UEAxMR
16 | U0NFSSBETkFTIFJvb3QgMDKCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUF
17 | AAOCAQEAf+tXSKWftS5sT1nQyIBaUQI7JQrw7VDuj2BgALvCdkGCbGy2zoTVnVb6
18 | wlLFexG8/7sBc0dV0tngmYt9aMOemauibaT2X6DVcKeK9jKYwl7bJ2YX8plCFtN8
19 | 46esv2L3RX++CyyYQybEFqF8YQc1zxxhiRgI6hFj2xWGjsIx6VrQ7SoqwknMPnLb
20 | IGERIU5psmAchIh1LMTPcJkzgBd21G1dNJWRWttKBJhTk1itbEfhV15HSMMjPWZs
21 | CXUrB7N478vftFmu9cfss2BmjC0tmxfToE8EfZgXAifvUc80o0KuOtAoutkVsJ8k
22 | w59A5TBx5WAj4SxAnXPKjsXBHoAiuA==
23 | -----END CERTIFICATE-----
24 |
--------------------------------------------------------------------------------
/Clients/PsnClient/certificates/CA03.cer:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIID0jCCArqgAwIBAgIBADANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJKUDEp
3 | MCcGA1UEChMgU29ueSBDb21wdXRlciBFbnRlcnRhaW5tZW50IEluYy4xGjAYBgNV
4 | BAMTEVNDRUkgRE5BUyBSb290IDAzMB4XDTA0MDcxMjA5MDAzMFoXDTM3MTIwNjA5
5 | MDAzMFowVDELMAkGA1UEBhMCSlAxKTAnBgNVBAoTIFNvbnkgQ29tcHV0ZXIgRW50
6 | ZXJ0YWlubWVudCBJbmMuMRowGAYDVQQDExFTQ0VJIEROQVMgUm9vdCAwMzCCASIw
7 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALpaZe91i8J9DKStk4eohmG1evBP
8 | wTHCjvf+bTRAxqZvpc5hhtBln6lCArfuN8SOSS2ovGA1T9EqOUnZR8lVzbQjqSYE
9 | 145zz29UUqhjA3Q7p3Vy910cZpnhRBrI0OV4VCZW68ffpJqsPVIlXvT/l9gVBirv
10 | ZwPXI8ny9leXNr+HOgxcOyN9EnIPMICfPk14/2pqLJIYwDtJRgTwe5RIlo8bMKeX
11 | k6/J2VC6Cpyswb/nISqx/VYMkz1g4z2CtiEgFx56t3OGLtJxyMrSepTiF6PGetkd
12 | FsA7l/wBKR3WfGlzN55X7k+ST70cfi3e3pxLPoL01pKmmd9ECvTEMeG3X90CAwEA
13 | AaOBrjCBqzAdBgNVHQ4EFgQU6YPY5JKVvbVuvqx/DrHbJ3GX9TAwfAYDVR0jBHUw
14 | c4AU6YPY5JKVvbVuvqx/DrHbJ3GX9TChWKRWMFQxCzAJBgNVBAYTAkpQMSkwJwYD
15 | VQQKEyBTb255IENvbXB1dGVyIEVudGVydGFpbm1lbnQgSW5jLjEaMBgGA1UEAxMR
16 | U0NFSSBETkFTIFJvb3QgMDOCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUF
17 | AAOCAQEAp8XhnbfT24t8H5ckLI2aLY99rNwehzhFUpwF3BzhD/l5vvONj57zjDGQ
18 | AgakyDz2/EyGF5oPZfSjbubYxgWRunA6Nt9ZE9EV7pVhoIQzkw5EOmbBkGQhvtz/
19 | JYAaRCPA2uBwTcXQrbShVMyNsTaU5SIgl9tWsUz7QTSVBWtPS+HNxEsgncNPgmaU
20 | OGvh0zCcVpzmMPRaIdXzTpGm3YvySBTzY2cZYbkg0sC5pNHcnPw+yWtNJ900Ev/R
21 | omD8Mo87VziAd0ZXqmC2r00mSWMT1t+LRBd6hvNvJSLkyQW3Kd/ioLJx4B8eg1yZ
22 | VDNJ4o0H4mfedXNiXfRlpQmy0wNb9Q==
23 | -----END CERTIFICATE-----
24 |
--------------------------------------------------------------------------------
/Clients/PsnClient/certificates/CA04.cer:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIID0jCCArqgAwIBAgIBADANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJKUDEp
3 | MCcGA1UEChMgU29ueSBDb21wdXRlciBFbnRlcnRhaW5tZW50IEluYy4xGjAYBgNV
4 | BAMTEVNDRUkgRE5BUyBSb290IDA0MB4XDTA0MDcxMjA5MDA1OVoXDTM3MTIwNjA5
5 | MDA1OVowVDELMAkGA1UEBhMCSlAxKTAnBgNVBAoTIFNvbnkgQ29tcHV0ZXIgRW50
6 | ZXJ0YWlubWVudCBJbmMuMRowGAYDVQQDExFTQ0VJIEROQVMgUm9vdCAwNDCCASIw
7 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL76snKxgAx7m7M6g7MtlpKRy9RR
8 | OIGL9M05ENXJJd/tNgKBgc3nrH+zw3oIUZKDElZkFpaSW48vRqyH2jaXExlss/ry
9 | RYvfpsR6uSrKINb3Qs3Jc2chdzKPOatjbOCYHE0n+Um4/QckdyGPepMnm0GqYnYw
10 | CXwCcXa7yrXXT8WCZORDPGPVAjDyyDsHN5SpWHgGiIKbmwZRtWihz9Ms2NaMTNlI
11 | efuUJaBUjvxQXfUlUZCadEgC93jUrQVL80nPl3JnfmXqzyqJYUa8YIeBNNMpcRhI
12 | VQEBt3+GyDpBXj6NX9F4j6E19lN6X8ZjjAGG6zLpDNg8m9Dokf2G9EfTE/cCAwEA
13 | AaOBrjCBqzAdBgNVHQ4EFgQUvJvEzmr0UT3oha4j11YeZv1AEN8wfAYDVR0jBHUw
14 | c4AUvJvEzmr0UT3oha4j11YeZv1AEN+hWKRWMFQxCzAJBgNVBAYTAkpQMSkwJwYD
15 | VQQKEyBTb255IENvbXB1dGVyIEVudGVydGFpbm1lbnQgSW5jLjEaMBgGA1UEAxMR
16 | U0NFSSBETkFTIFJvb3QgMDSCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUF
17 | AAOCAQEAT8IGrp+PN0YfuLi33IExImSaePRa0AwCMTsbmsQwNbeR4UpHrRAuoGBe
18 | k8qRoQ636qN+lJs1ZJ+JLhy927C3s9zRY9SJFB15PSedsdff2AIbgez3SHswHc1p
19 | X9hAw+uO9s1wFHGMGX/bmnM6JsTiyanOa5/rZHlupR/2xl6JWxroCk56iCGH96YI
20 | t0EjBqcj0CxCcZBajCX65upAOmax92zxAdUL3vKuiLLyPNKAMTpML/ZpRNjDzXw8
21 | 99+L6NNtfngPg9ioXKIxMe5shoVad/tsWXDLCPqp2AOfTWYlqJjPaKVif/WlJni8
22 | hCau/uB1QOET2FLgC2RjAl1QkVhehg==
23 | -----END CERTIFICATE-----
24 |
--------------------------------------------------------------------------------
/Clients/PsnClient/certificates/CA05.cer:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIID0jCCArqgAwIBAgIBADANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJKUDEp
3 | MCcGA1UEChMgU29ueSBDb21wdXRlciBFbnRlcnRhaW5tZW50IEluYy4xGjAYBgNV
4 | BAMTEVNDRUkgRE5BUyBSb290IDA1MB4XDTA0MDcxMjA5MDExOVoXDTM3MTIwNjA5
5 | MDExOVowVDELMAkGA1UEBhMCSlAxKTAnBgNVBAoTIFNvbnkgQ29tcHV0ZXIgRW50
6 | ZXJ0YWlubWVudCBJbmMuMRowGAYDVQQDExFTQ0VJIEROQVMgUm9vdCAwNTCCASIw
7 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANmPeza8PwCqlI7esOGIkoSESnIN
8 | g72ZD3Ut63jy7SdothPIvGBqVZWYkIpqJYJd1I4Nh//IpXQCQL0PnJLrh9BBeowq
9 | Muf5NNq3Us80Ihiu9CvNEAEO18g3OFV1TYdSwQ5zUsk33OUeI7h4aBPDVcZXYeHt
10 | dbPLqe4K8igian5prrAD5S6h28t8aAm+qMWRo+bW25B/841XwDGBP7/IxZv8Yoio
11 | rCo80CVYe6lGoU08eeqQiaHI5zAF281DWZSoVfLjJUEWmEnxqr8aOhszRGePi+Ei
12 | 7UQjHDuZX9rLhDI1zAND+BA259tn/iwOqVXe20OccJllHJcG4Ecmd98f5qMCAwEA
13 | AaOBrjCBqzAdBgNVHQ4EFgQUxlahM1tPzoN3YgVEhm0gV7Wv2twwfAYDVR0jBHUw
14 | c4AUxlahM1tPzoN3YgVEhm0gV7Wv2tyhWKRWMFQxCzAJBgNVBAYTAkpQMSkwJwYD
15 | VQQKEyBTb255IENvbXB1dGVyIEVudGVydGFpbm1lbnQgSW5jLjEaMBgGA1UEAxMR
16 | U0NFSSBETkFTIFJvb3QgMDWCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUF
17 | AAOCAQEACZPihjwXA27wJ03tEKcHAeFLi8aBw2ysH4GwuH1dWb3UpuznWOB0iQT1
18 | wQocnEFYCJx5XFEnj4aLWpSHLEq/sSO+my+aPoTEsy20ajF+YLYZm0bZxH50CJYh
19 | rkET4C2aC0XvhGp9k1JQ1o0W6+cFT5LTlXapsq8Btt31t+XDPX7RqGV4WGekt3hM
20 | T7xRc7JWXdAQijIrbYi8mtbM07KEGnPU6IT8C47+0mSurpwLOoWL1tPgo6ePpLNi
21 | c4quUMgh9RXVjeTyXOMmyYdeUm2gt7qErvQONli+6Epmhm0A2khpIMHSpQjTE8gV
22 | rZp42a6+zg1iYy2vFBOmiQ17GRUl0A==
23 | -----END CERTIFICATE-----
24 |
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20190301155219_PersistentStats.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class PersistentStats : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "stats",
11 | columns: table => new
12 | {
13 | id = table.Column(nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | category = table.Column(nullable: false),
16 | key = table.Column(nullable: false),
17 | value = table.Column(nullable: false),
18 | expiration_timestamp = table.Column(nullable: false)
19 | },
20 | constraints: table =>
21 | {
22 | table.PrimaryKey("id", x => x.id);
23 | });
24 |
25 | migrationBuilder.CreateIndex(
26 | name: "stats_category_key",
27 | table: "stats",
28 | columns: ["category", "key"],
29 | unique: true);
30 | }
31 |
32 | protected override void Down(MigrationBuilder migrationBuilder)
33 | {
34 | migrationBuilder.DropTable(
35 | name: "stats");
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Tests/RandomTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using NUnit.Framework;
3 |
4 | namespace Tests;
5 |
6 | [TestFixture]
7 | public class RandomTests
8 | {
9 | [Explicit]
10 | [TestCase(6, 100_000_000, 0.1)]
11 | public void DistributionUniformityTest(int dice, int sampleCount, double expectedDeviation)
12 | {
13 | var counterNaive = new int[dice];
14 | var counterRange = new int[dice];
15 | var rng = new Random();
16 | for (var i = 0; i < sampleCount; i++)
17 | {
18 | counterNaive[rng.Next(dice)]++;
19 | counterRange[rng.Next(1, dice + 1) - 1]++;
20 | }
21 | var expectedMedian = sampleCount / (double)dice;
22 | Console.WriteLine("Naive\tRange");
23 | Assert.Multiple(() =>
24 | {
25 | for (var i = 0; i < dice; i++)
26 | {
27 | var devNaive = GetPercent(counterNaive[i], expectedMedian);
28 | var devRange = GetPercent(counterRange[i], expectedMedian);
29 | Console.WriteLine($"{counterNaive[i]} ({devNaive:0.000})\t{counterRange[i]} ({devRange:0.000})");
30 | Assert.That(devNaive, Is.LessThan(expectedDeviation), $"Naive dice face {i + 1}");
31 | Assert.That(devRange, Is.LessThan(expectedDeviation), $"Range dice face {i + 1}");
32 | }
33 | });
34 | }
35 |
36 | private static double GetPercent(int actual, double expected)
37 | => Math.Abs(actual - expected) / expected * 100.0;
38 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/ThumbnailDb/20210414183007_AddGameUpdateInfo.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Migrations
4 | {
5 | public partial class AddGameUpdateInfo : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "game_update_info",
11 | columns: table => new
12 | {
13 | id = table.Column(type: "INTEGER", nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | product_code = table.Column(type: "TEXT", nullable: false),
16 | meta_hash = table.Column(type: "INTEGER", nullable: false),
17 | meta_xml = table.Column(type: "TEXT", nullable: false)
18 | },
19 | constraints: table =>
20 | {
21 | table.PrimaryKey("id", x => x.id);
22 | });
23 |
24 | migrationBuilder.CreateIndex(
25 | name: "game_update_info_product_code",
26 | table: "game_update_info",
27 | column: "product_code",
28 | unique: true);
29 | }
30 |
31 | protected override void Down(MigrationBuilder migrationBuilder)
32 | {
33 | migrationBuilder.DropTable(
34 | name: "game_update_info");
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Formatters/NamingStyles.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 |
4 | namespace CompatApiClient;
5 |
6 | public static class NamingStyles
7 | {
8 | public static string CamelCase(string value)
9 | {
10 | if (value == null)
11 | throw new ArgumentNullException(nameof(value));
12 |
13 | if (value.Length > 0)
14 | {
15 | if (char.IsUpper(value[0]))
16 | value = char.ToLower(value[0]) + value[1..];
17 | }
18 | return value;
19 | }
20 |
21 | public static string Dashed(string value) => Delimitied(value, '-');
22 | public static string Underscore(string value) => Delimitied(value, '_');
23 |
24 | private static string Delimitied(string value, char separator)
25 | {
26 | if (value == null)
27 | throw new ArgumentNullException(nameof(value));
28 |
29 | if (value.Length == 0)
30 | return value;
31 |
32 | var hasPrefix = true;
33 | var builder = new StringBuilder(value.Length + 3);
34 | foreach (var c in value)
35 | {
36 | var ch = c;
37 | if (char.IsUpper(ch))
38 | {
39 | ch = char.ToLower(ch);
40 | if (!hasPrefix)
41 | builder.Append(separator);
42 | hasPrefix = true;
43 | }
44 | else
45 | hasPrefix = false;
46 | builder.Append(ch);
47 | }
48 | return builder.ToString();
49 | }
50 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20220704163631_AddStatsBucketColumn.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | #nullable disable
4 |
5 | namespace CompatBot.Database.Migrations
6 | {
7 | public partial class AddStatsBucketColumn : Migration
8 | {
9 | protected override void Up(MigrationBuilder migrationBuilder)
10 | {
11 | migrationBuilder.DropIndex(
12 | name: "stats_category_key",
13 | table: "stats");
14 |
15 | migrationBuilder.AddColumn(
16 | name: "bucket",
17 | table: "stats",
18 | type: "TEXT",
19 | nullable: true);
20 |
21 | migrationBuilder.CreateIndex(
22 | name: "stats_category_bucket_key",
23 | table: "stats",
24 | columns: ["category", "bucket", "key"],
25 | unique: true);
26 | }
27 |
28 | protected override void Down(MigrationBuilder migrationBuilder)
29 | {
30 | migrationBuilder.DropIndex(
31 | name: "stats_category_bucket_key",
32 | table: "stats");
33 |
34 | migrationBuilder.DropColumn(
35 | name: "bucket",
36 | table: "stats");
37 |
38 | migrationBuilder.CreateIndex(
39 | name: "stats_category_key",
40 | table: "stats",
41 | columns: ["category", "key"],
42 | unique: true);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Tests/Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | false
5 | latest
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | all
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 |
17 |
18 | all
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Always
30 |
31 |
32 |
--------------------------------------------------------------------------------
/CompatBot/Commands/Attributes/CheckAttributeWithReactions.cs:
--------------------------------------------------------------------------------
1 | using DSharpPlus.Commands.ContextChecks;
2 |
3 | namespace CompatBot.Commands.Attributes;
4 |
5 | internal abstract class CheckAttributeWithReactions(
6 | DiscordEmoji? reactOnSuccess = null,
7 | DiscordEmoji? reactOnFailure = null
8 | ) : ContextCheckAttribute
9 | {
10 | public DiscordEmoji? ReactOnSuccess { get; } = reactOnSuccess;
11 | public DiscordEmoji? ReactOnFailure { get; } = reactOnFailure;
12 | }
13 |
14 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
15 | internal class RequiresBotSudoerRoleAttribute(): CheckAttributeWithReactions(reactOnFailure: Config.Reactions.Denied);
16 |
17 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
18 | internal class RequiresBotModRoleAttribute(): CheckAttributeWithReactions(reactOnFailure: Config.Reactions.Denied);
19 |
20 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
21 | internal class RequiresWhitelistedRoleAttribute(): CheckAttributeWithReactions(reactOnFailure: Config.Reactions.Denied);
22 |
23 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
24 | internal class RequiresSmartlistedRoleAttribute(): CheckAttributeWithReactions(reactOnFailure: Config.Reactions.Denied);
25 |
26 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
27 | internal class RequiresSupporterRoleAttribute(): CheckAttributeWithReactions(reactOnFailure: Config.Reactions.Denied);
28 |
--------------------------------------------------------------------------------
/CompatBot/Utils/CompatApiResultUtils.cs:
--------------------------------------------------------------------------------
1 | using CompatApiClient.POCOs;
2 |
3 | namespace CompatBot.Utils;
4 |
5 | internal static class CompatApiResultUtils
6 | {
7 | public static List<(string code, TitleInfo info, double score)> GetSortedList(this CompatResult result)
8 | {
9 | var search = result.RequestBuilder.Search;
10 | if (string.IsNullOrEmpty(search) || !result.Results.Any())
11 | return result.Results
12 | .OrderBy(kvp => kvp.Value.Title)
13 | .ThenBy(kvp => kvp.Key)
14 | .Select(kvp => (kvp.Key, kvp.Value, 0.0))
15 | .ToList();
16 |
17 | var sortedList = result.Results
18 | .Select(kvp => (code: kvp.Key, info: kvp.Value, score: GetScore(search, kvp.Value)))
19 | .OrderByDescending(t => t.score)
20 | .ThenBy(t => t.info.Title)
21 | .ThenBy(t => t.code)
22 | .ToList();
23 | if (sortedList.First().score < 0.2)
24 | sortedList = sortedList
25 | .OrderBy(kvp => kvp.info.Title)
26 | .ThenBy(kvp => kvp.code)
27 | .ToList();
28 | return sortedList;
29 | }
30 |
31 | public static double GetScore(string? search, TitleInfo titleInfo)
32 | {
33 | var score = Math.Max(
34 | search.GetFuzzyCoefficientCached(titleInfo.Title),
35 | search.GetFuzzyCoefficientCached(titleInfo.AlternativeTitle)
36 | );
37 | if (score > 0.3)
38 | return score;
39 | return 0;
40 | }
41 | }
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/MultiEventHandlerWrapper.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.EventHandlers;
2 |
3 | public class MultiEventHandlerWrapper where T: DiscordEventArgs
4 | {
5 | private readonly ICollection>> orderedHandlers;
6 | private readonly ICollection> unorderedHandlers;
7 |
8 | public MultiEventHandlerWrapper(ICollection>> orderedHandlers, ICollection> unorderedHandlers)
9 | {
10 | this.orderedHandlers = orderedHandlers;
11 | this.unorderedHandlers = unorderedHandlers;
12 | }
13 |
14 | public async Task OnEvent(DiscordClient client, T eventArgs)
15 | {
16 | try
17 | {
18 | foreach (var h in orderedHandlers)
19 | if (!await h(client, eventArgs).ConfigureAwait(false))
20 | return;
21 |
22 | var unorderedTasks = unorderedHandlers.Select(async h => await h(client, eventArgs).ConfigureAwait(false));
23 | await Task.WhenAll(unorderedTasks).ConfigureAwait(false);
24 | }
25 | catch (Exception e)
26 | {
27 | Config.Log.Error(e);
28 | }
29 | }
30 |
31 | public static Func CreateOrdered(ICollection>> orderedHandlers)
32 | => new MultiEventHandlerWrapper(orderedHandlers, []).OnEvent;
33 |
34 | public static Func CreateUnordered(ICollection> unorderedHandlers)
35 | => new MultiEventHandlerWrapper([], unorderedHandlers).OnEvent;
36 | }
--------------------------------------------------------------------------------
/CompatBot/Properties/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Runtime.CompilerServices;
3 |
4 | // This file is used by Code Analysis to maintain SuppressMessage
5 | // attributes that are applied to this project.
6 | // Project-level suppressions either have no target or are given
7 | // a specific target and scoped to a namespace, type, member, etc.
8 |
9 | [assembly: SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "async background check by design", Scope = "member", Target = "~M:CompatBot.Commands.Moderation.Audit.SpoofingCheck(DSharpPlus.Commands.CommandContext)")]
10 | [assembly: SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "async background check by design", Scope = "member", Target = "~M:CompatBot.ThumbScrapper.PsnScraper.CheckContentIdAsync(DSharpPlus.Commands.CommandContext,System.String,System.Threading.CancellationToken)")]
11 | [assembly: SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "async background check by design", Scope = "member", Target = "~M:CompatBot.EventHandlers.LogParsingHandler.EnqueueLogProcessing(DSharpPlus.DiscordClient,DSharpPlus.Entities.DiscordChannel,DSharpPlus.Entities.DiscordMessage,DSharpPlus.Entities.DiscordMember,System.Boolean,System.Boolean)")]
12 | [assembly: SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "async background check by design", Scope = "member", Target = "~M:CompatBot.EventHandlers.UnknownCommandHandler.OnErrorInternal(DSharpPlus.Commands.CommandsNextExtension,DSharpPlus.Commands.CommandErrorEventArgs)")]
13 | [assembly: InternalsVisibleTo("Tests")]
--------------------------------------------------------------------------------
/Tests/MemoryCacheExtensionTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CompatBot.Utils;
3 | using Microsoft.Extensions.Caching.Memory;
4 | using NUnit.Framework;
5 |
6 | namespace Tests;
7 |
8 | [TestFixture]
9 | public class MemoryCacheExtensionTests
10 | {
11 | [Test]
12 | public void GetCacheKeysTest()
13 | {
14 | var cache = new MemoryCache(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromHours(1) });
15 | Assert.That(cache.GetCacheKeys(), Is.Empty);
16 |
17 | const string testVal = "vale13";
18 | cache.Set(13, testVal);
19 | cache.Set("bob", 69);
20 | using (Assert.EnterMultipleScope())
21 | {
22 | Assert.That(cache.TryGetValue(13, out string? expectedVal), Is.True);
23 | Assert.That(expectedVal, Is.EqualTo(testVal));
24 | Assert.That(cache.TryGetValue("bob", out int? expectedValInt), Is.True);
25 | Assert.That(expectedValInt, Is.EqualTo(69));
26 | Assert.That(cache.GetCacheKeys(), Has.Count.EqualTo(1));
27 | }
28 | }
29 |
30 | [Test]
31 | public void GetCacheEntriesTest()
32 | {
33 | var cache = new MemoryCache(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromHours(1) });
34 | Assert.That(cache.GetCacheEntries(), Is.Empty);
35 |
36 | cache.Set(13, "val13");
37 | cache.Set("bob", 69);
38 | using (Assert.EnterMultipleScope())
39 | {
40 | Assert.That(cache.GetCacheEntries(), Has.Count.EqualTo(1));
41 | Assert.That(cache.GetCacheEntries(), Has.Count.EqualTo(1));
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/ResultFormatters/IrdSearchResultFormatter.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using CompatApiClient;
3 | using CompatApiClient.Utils;
4 | using IrdLibraryClient;
5 | using IrdLibraryClient.POCOs;
6 |
7 | namespace CompatBot.Utils.ResultFormatters
8 | {
9 | public static class IrdSearchResultFormatter
10 | {
11 | public static DiscordEmbedBuilder AsEmbed(this List irdInfos)
12 | {
13 | var result = new DiscordEmbedBuilder
14 | {
15 | // Title = "IRD Library Search Result",
16 | Color = Config.Colors.DownloadLinks,
17 | };
18 | if (irdInfos is not {Count: >0})
19 | {
20 | result.Color = Config.Colors.LogResultFailed;
21 | result.Description = "No matches were found";
22 | return result;
23 | }
24 |
25 | foreach (var item in irdInfos.Where(i => i.Link is {Length: >5}).Take(EmbedPager.MaxFields))
26 | {
27 | try
28 | {
29 | result.AddField(
30 | $"{item.Title.Sanitize().Trim(EmbedPager.MaxFieldTitleLength - 18)} [v{item.GameVer} FW {item.FwVer}]",
31 | $"[⏬ {Path.GetFileName(item.Link).Replace("]", @"\]")}]({IrdClient.GetEscapedDownloadLink(item.Link)})"
32 | );
33 | }
34 | catch (Exception e)
35 | {
36 | ApiConfig.Log.Warn(e, "Failed to format embed field for IRD search result");
37 | }
38 | }
39 | return result;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/UsernameRaidMonitor.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.EventHandlers;
2 |
3 | public static class UsernameRaidMonitor
4 | {
5 | public static async Task OnMemberUpdated(DiscordClient c, GuildMemberUpdatedEventArgs args)
6 | {
7 | try
8 | {
9 | //member object most likely will not be updated in client cache at this moment
10 | string? fallback;
11 | if (args.NicknameAfter is string name)
12 | fallback = args.Member.Username;
13 | else
14 | {
15 | name = args.Member.Username;
16 | fallback = null;
17 | }
18 |
19 | var member = await args.Guild.GetMemberAsync(args.Member.Id).ConfigureAwait(false) ?? args.Member;
20 | if (NeedsKick(name))
21 | {
22 | await args.Member.RemoveAsync("Anti Raid").ConfigureAwait(false);
23 | }
24 | }
25 | catch (Exception e)
26 | {
27 | Config.Log.Error(e);
28 | }
29 | }
30 |
31 | public static async Task OnMemberAdded(DiscordClient c, GuildMemberAddedEventArgs args)
32 | {
33 | try
34 | {
35 | var name = args.Member.DisplayName;
36 | if (NeedsKick(name))
37 | {
38 | await args.Member.RemoveAsync("Anti Raid").ConfigureAwait(false);
39 | }
40 | }
41 | catch (Exception e)
42 | {
43 | Config.Log.Error(e);
44 | }
45 | }
46 |
47 | public static bool NeedsKick(string displayName)
48 | {
49 | displayName = displayName.Normalize().TrimEager();
50 | return displayName.Equals("D𝗂scord");
51 | }
52 |
53 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20190307173026_PermanentWarnings.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class PermanentWarnings : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.AddColumn(
10 | name: "retracted",
11 | table: "warning",
12 | nullable: false,
13 | defaultValue: false);
14 |
15 | migrationBuilder.AddColumn(
16 | name: "retracted_by",
17 | table: "warning",
18 | nullable: true);
19 |
20 | migrationBuilder.AddColumn(
21 | name: "retraction_reason",
22 | table: "warning",
23 | nullable: true);
24 |
25 | migrationBuilder.AddColumn(
26 | name: "retraction_timestamp",
27 | table: "warning",
28 | nullable: true);
29 | }
30 |
31 | protected override void Down(MigrationBuilder migrationBuilder)
32 | {
33 | migrationBuilder.DropColumn(
34 | name: "retracted",
35 | table: "warning");
36 |
37 | migrationBuilder.DropColumn(
38 | name: "retracted_by",
39 | table: "warning");
40 |
41 | migrationBuilder.DropColumn(
42 | name: "retraction_reason",
43 | table: "warning");
44 |
45 | migrationBuilder.DropColumn(
46 | name: "retraction_timestamp",
47 | table: "warning");
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/CompatBot/Commands/Attributes/readme.md:
--------------------------------------------------------------------------------
1 | Command Attributes
2 | ==================
3 |
4 | This folder contains various custom attributes that could be used with command groups (classes) and commands themselves (functions).
5 |
6 | They're intended for easier management of said commands, such as implementing access rights management and spam reduction policies.
7 |
8 | `CheckBaseAttributeWithReactions`
9 | ---------------------------------
10 |
11 | This is a base class that implements an ability to create Discord reactions in case the check has passed or failed, and also logs the check itself for audit purposes.
12 |
13 | `LimitedToXyzChannel`, `RequiresDm`, `RequiresNotMedia`
14 | -------------------------------------------------------
15 |
16 | These attributes are intended to limit potential impact of command usage outside of specific channels. There are some allowances for users with "trusted" roles sometimes, but in general it's an easy way to make sure bot won't reply when it is not needed.
17 |
18 | `RequiresXyzRole`
19 | -----------------
20 |
21 | Similarly implement command access rights management to prevent their use by regular users. As a bonus, every user will only get the list of commands that they can actually use when using `!help`.
22 |
23 | `TriggersTyping`
24 | ---------------
25 |
26 | This is a legacy attribute that will trigger `Typing...` message at the bottom of the chat window. It was used to indicate that the bot is working on the command, but it has several issues that prevents it from being very useful (you can't control how long it will be shown, and it eats up an API call).
27 |
28 | Generally speaking bot replies too fast to have any special indicator for most commands. Whenever it is needed, it is better to create a reaction, or post and then update an explicit message.
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/LogParsing/ArchiveHandlers/PlainText.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.IO.Pipelines;
3 | using ResultNet;
4 |
5 | namespace CompatBot.EventHandlers.LogParsing.ArchiveHandlers;
6 |
7 | internal sealed class PlainTextHandler: IArchiveHandler
8 | {
9 | public long LogSize { get; private set; }
10 | public long SourcePosition { get; private set; }
11 |
12 | public Result CanHandle(string fileName, int fileSize, ReadOnlySpan header)
13 | {
14 | LogSize = fileSize;
15 | if (fileName.Contains("tty.log", StringComparison.InvariantCultureIgnoreCase))
16 | return Result.Failure();
17 |
18 | if (header.Length > 10 && Encoding.UTF8.GetString(header[..30]).Contains("RPCS3 v"))
19 | return Result.Success();
20 |
21 | return Result.Failure();
22 | }
23 |
24 | public async Task FillPipeAsync(Stream sourceStream, PipeWriter writer, CancellationToken cancellationToken)
25 | {
26 | try
27 | {
28 | int read;
29 | FlushResult flushed;
30 | do
31 | {
32 | var memory = writer.GetMemory(Config.MinimumBufferSize);
33 | read = await sourceStream.ReadAsync(memory, cancellationToken);
34 | if (read > 0)
35 | writer.Advance(read);
36 | flushed = await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
37 | } while (read > 0 && !(flushed.IsCompleted || flushed.IsCanceled || cancellationToken.IsCancellationRequested));
38 | }
39 | catch (Exception e)
40 | {
41 | Config.Log.Error(e, "Error filling the log pipe");
42 | await writer.CompleteAsync(e);
43 | return;
44 | }
45 | await writer.CompleteAsync();
46 | }
47 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/PsnMetaExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using Microsoft.Extensions.Caching.Memory;
3 | using PsnClient.POCOs;
4 |
5 | namespace CompatBot.Utils;
6 |
7 | public static class PsnMetaExtensions
8 | {
9 | private static readonly MemoryCache ParsedData = new(new MemoryCacheOptions {ExpirationScanFrequency = TimeSpan.FromHours(1)});
10 | private static readonly TimeSpan CacheDuration = TimeSpan.FromDays(1);
11 |
12 | internal static List<(string resolution, string aspectRatio)> GetSupportedResolutions(this TitleMeta meta)
13 | => GetSupportedResolutions(meta.Resolution);
14 |
15 | internal static List<(string resolution, string aspectRatio)> GetSupportedResolutions(string resolutionList)
16 | {
17 | if (ParsedData.TryGetValue(resolutionList, out List<(string, string)>? result) && result is not null)
18 | return result;
19 |
20 | var resList = resolutionList
21 | .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
22 | .Select(Convert)
23 | .ToList();
24 | ParsedData.Set(resolutionList, resList, CacheDuration);
25 | return resList;
26 | }
27 |
28 | private static (string resolution, string aspectRatio) Convert(string verticalRes)
29 | => verticalRes.ToUpper() switch
30 | {
31 | "480SQ" => ("720x480", "4:3"),
32 | "576SQ" => ("720x576", "4:3"),
33 |
34 | "480" => ("720x480", "16:9"),
35 | "576" => ("720x576", "16:9"),
36 | "720" => ("1280x720", "16:9"),
37 | "1080" => ("1920x1080", "16:9"),
38 | #if DEBUG
39 | _ => throw new InvalidDataException($"Unknown resolution {verticalRes} in PSN meta data"),
40 | #else
41 | _ => (verticalRes, "16:9"),
42 | #endif
43 | };
44 | }
--------------------------------------------------------------------------------
/Clients/CirrusCiClient/Queries/GetBuilds.graphql:
--------------------------------------------------------------------------------
1 | query GetPrBuilds($branch: String, $after: String) {
2 | ownerRepository(platform: "github", owner: "RPCS3", name: "rpcs3") {
3 | builds(branch: $branch, after: $after, last: 20) {
4 | edges {
5 | node {
6 | id
7 | changeIdInRepo
8 | pullRequest
9 | ...BaseNodeInfo
10 | tasks {
11 | id
12 | name
13 | status
14 | artifacts {
15 | files {
16 | path
17 | size
18 | }
19 | }
20 | }
21 | }
22 | }
23 | }
24 | }
25 | }
26 |
27 | query GetBuildWithArtifacts($buildId: ID!) {
28 | build(id: $buildId) {
29 | pullRequest
30 | buildCreatedTimestamp
31 | clockDurationInSeconds
32 | tasks {
33 | name
34 | status
35 | artifacts {
36 | name
37 | files {
38 | path
39 | size
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
46 | query GetLastFewBuilds($count: Int!) {
47 | ownerRepository(platform: "github", owner: "RPCS3", name: "rpcs3") {
48 | builds(last: $count) {
49 | edges {
50 | node {
51 | ...BaseNodeInfo
52 | }
53 | }
54 | }
55 | }
56 | }
57 |
58 | fragment BaseNodeInfo on Build {
59 | status
60 | buildCreatedTimestamp
61 | clockDurationInSeconds
62 | latestGroupTasks {
63 | finalStatusTimestamp
64 | }
65 | }
--------------------------------------------------------------------------------
/Clients/CompatApiClient/readme.md:
--------------------------------------------------------------------------------
1 | Compatibility API Client
2 | ========================
3 |
4 | There is no documentation, but the [source code is available](https://github.com/AniLeo/rpcs3-compatibility).
5 |
6 | This project also contains all of the Web API infrastructure to facilitate the automatic serialization/deserialization of data.
7 |
8 | Some terminology:
9 | * `POCO` - plain old C# object, is a barebones class with fields/properties only, that is used for automatic [de]serialization.
10 |
11 | General advise on web client implementation and usage:
12 | * Do use `HttpClientFactory.Create()` instead of `new HttpClient()`, as every instance will reserve an outgoing port number, and factory keeps a pool.
13 | * Do reuse the same client instance whenever possible, it's thread-safe and there's no reason not to keep a single copy of it.
14 |
15 | [Compression](Compression/) contains handler implementation that provides support for transparent http request compression (`Content-Encoding` header), and implements standard gzip/deflate types.
16 |
17 | [Formatters](Formatters/) contain JSON contract resolver that handles popular naming conventions for [de]serialization (`dashed-style`, `underscore_style`, and `PascalStyle`).
18 |
19 | [Utils](Utils/) have some handy `Uri` extension methods for easy query parameters manipulation.
20 |
21 | Game Compatibility Status
22 | -------------------------
23 |
24 | Does game status lookup by `product code`, `game title` (English or using romaji for Japanese titles), or `game title abbreviation`. We use this for most embeds, including log parsing results, standalone game information embeds, compatibility lists, etc.
25 |
26 | RPCS3 Update Information
27 | ------------------------
28 |
29 | Accepts current build commit hash as an argument. Provides information about the build requested, as well as information about the latest build available.
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/EnumerableExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Utils;
2 |
3 | public static class EnumerableExtensions
4 | {
5 | public static IEnumerable Pairwise(this IEnumerable source, Func selector)
6 | {
7 | if (source == null)
8 | throw new ArgumentNullException(nameof(source));
9 |
10 | if (selector == null)
11 | throw new ArgumentNullException(nameof(selector));
12 |
13 | using var e = source.GetEnumerator();
14 | if (!e.MoveNext())
15 | yield break;
16 |
17 | T prev = e.Current;
18 | if (!e.MoveNext())
19 | yield break;
20 |
21 | do
22 | {
23 | yield return selector(prev, e.Current);
24 | prev = e.Current;
25 | } while (e.MoveNext());
26 | }
27 |
28 | public static IEnumerable Single(T item)
29 | {
30 | yield return item;
31 | }
32 |
33 | public static T? RandomElement(this IList collection, int? seed = null)
34 | {
35 | if (collection.Count > 0)
36 | {
37 | var rng = seed.HasValue ? new(seed.Value) : new Random();
38 | return collection[rng.Next(collection.Count)];
39 | }
40 | return default;
41 | }
42 |
43 | public static bool AnyPatchesApplied(this Dictionary patches)
44 | => patches.Values.Any(v => v > 0);
45 |
46 | public static IEnumerable> Batch(this IEnumerable items, int maxItems)
47 | {
48 | return items.Select((item, inx) => new { item, inx })
49 | .GroupBy(x => x.inx / maxItems)
50 | .Select(g => g.Select(x => x.item));
51 | }
52 |
53 | public static List ToList(this IAsyncEnumerable items)
54 | => items.ToBlockingEnumerable().ToList();
55 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/FilterActionExtensions.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database;
2 |
3 | namespace CompatBot.Utils.Extensions;
4 |
5 | internal static class FilterActionExtensions
6 | {
7 | internal static readonly FilterAction[] ActionFlagValues = Enum.GetValues();
8 | private static readonly Dictionary ActionFlagToChar = new()
9 | {
10 | [FilterAction.RemoveContent] = 'r',
11 | [FilterAction.IssueWarning] = 'w',
12 | [FilterAction.SendMessage] = 'm',
13 | [FilterAction.ShowExplain] = 'e',
14 | [FilterAction.MuteModQueue] = 'u',
15 | [FilterAction.Kick] = 'k',
16 | };
17 |
18 | private static readonly Dictionary CharToActionFlag = new()
19 | {
20 | ['r'] = FilterAction.RemoveContent,
21 | ['w'] = FilterAction.IssueWarning,
22 | ['m'] = FilterAction.SendMessage,
23 | ['e'] = FilterAction.ShowExplain,
24 | ['u'] = FilterAction.MuteModQueue,
25 | ['k'] = FilterAction.Kick,
26 | };
27 |
28 | public static string ToFlagsString(this FilterAction flags)
29 | => new(
30 | ActionFlagValues
31 | .Select(fa => flags.HasFlag(fa) ? ActionFlagToChar[fa] : '-')
32 | .ToArray()
33 | );
34 |
35 | public static FilterAction ToFilterAction(this string flags)
36 | => flags.ToCharArray()
37 | .Select(c => CharToActionFlag.TryGetValue(c, out var f)? f: 0)
38 | .Aggregate((a, b) => a | b);
39 |
40 | public static string GetLegend(string wrapChar = "`")
41 | {
42 | var result = new StringBuilder("Actions flag legend:");
43 | foreach (FilterAction fa in ActionFlagValues)
44 | result.Append($"\n{wrapChar}{ActionFlagToChar[fa]}{wrapChar} = {fa}");
45 | return result.ToString();
46 | }
47 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/PoorMansTaskScheduler.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 |
3 | namespace CompatBot.Utils;
4 |
5 | internal class PoorMansTaskScheduler where T: notnull
6 | {
7 | private readonly int queueLimit;
8 | private readonly ConcurrentDictionary taskQueue = new();
9 | public PoorMansTaskScheduler() : this(Math.Max(1, Environment.ProcessorCount / 2)) { }
10 |
11 | public PoorMansTaskScheduler(int simultaneousTaskCountLimit)
12 | {
13 | if (simultaneousTaskCountLimit < 1)
14 | throw new ArgumentException("Task count can't be lower than 1", nameof(simultaneousTaskCountLimit));
15 |
16 | queueLimit = simultaneousTaskCountLimit;
17 | }
18 |
19 | public async Task AddAsync(T tag, Task task)
20 | {
21 | if (taskQueue.Count < queueLimit)
22 | {
23 | taskQueue.TryAdd(task, tag);
24 | return;
25 | }
26 |
27 | var completedTasks = taskQueue.Keys.Where(t => t.IsCompleted).ToList();
28 | if (completedTasks.Count > 0)
29 | foreach (var t in completedTasks)
30 | taskQueue.TryRemove(t, out _);
31 |
32 | if (taskQueue.Count < queueLimit)
33 | {
34 | taskQueue.TryAdd(task, tag);
35 | return;
36 | }
37 |
38 | var result = await Task.WhenAny(taskQueue.Keys).ConfigureAwait(false);
39 | taskQueue.TryRemove(result, out _);
40 | taskQueue.TryAdd(task, tag);
41 | }
42 |
43 | public async Task WaitForClearTagAsync(T tag)
44 | {
45 | var tasksToWait = taskQueue.Where(kvp => tag.Equals(kvp.Value)).Select(kvp => kvp.Key).ToList();
46 | if (tasksToWait.Count == 0)
47 | return;
48 |
49 | await Task.WhenAll(tasksToWait).ConfigureAwait(false);
50 | foreach (var t in tasksToWait)
51 | taskQueue.TryRemove(t, out _);
52 | }
53 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/MemoryCacheExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 | using System.Reflection;
3 | using Microsoft.Extensions.Caching.Memory;
4 |
5 | namespace CompatBot.Utils;
6 |
7 | internal static class MemoryCacheExtensions
8 | {
9 | public static List GetCacheKeys(this MemoryCache memoryCache)
10 | => memoryCache.Keys.OfType().ToList();
11 |
12 | public static Dictionary GetCacheEntries(this MemoryCache memoryCache)
13 | where TKey: notnull
14 | {
15 | //memoryCache.TryGetValue();
16 | var stateField = memoryCache.GetType()
17 | .GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
18 | .FirstOrDefault(fi => fi.Name == "_coherentState");
19 | var coherentState = stateField?.GetValue(memoryCache);
20 | if (coherentState is null)
21 | {
22 | Config.Log.Error($"Looks like {nameof(MemoryCache)} internals have changed in {nameof(coherentState)}");
23 | return new();
24 | }
25 |
26 | var entriesName = "_nonStringEntries";
27 | if (typeof(TKey) == typeof(string))
28 | entriesName = "_stringEntries";
29 | var entriesField = coherentState.GetType() // MemoryCache.CoherentState
30 | .GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
31 | .FirstOrDefault(fi => fi.Name == entriesName);
32 | var cacheEntries = (IDictionary?)entriesField?.GetValue(coherentState);
33 | if (cacheEntries is null)
34 | {
35 | Config.Log.Error($"Looks like {nameof(MemoryCache)} internals have changed in {nameof(coherentState)} cache entries");
36 | return new(0);
37 | }
38 |
39 | var result = new Dictionary(cacheEntries.Count);
40 | foreach (DictionaryEntry e in cacheEntries)
41 | result.Add((TKey)e.Key, (ICacheEntry?)e.Value);
42 | return result;
43 | }
44 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/ResultFormatters/PrInfoFormatter.cs:
--------------------------------------------------------------------------------
1 | using Octokit;
2 |
3 | namespace CompatBot.Utils.ResultFormatters;
4 |
5 | internal static class PrInfoFormatter
6 | {
7 | public static DiscordEmbedBuilder AsEmbed(this PullRequest prInfo)
8 | {
9 | var state = prInfo.GetState();
10 | var stateLabel = state.state == null ? null : $"[{state.state}] ";
11 | var title = $"{stateLabel}PR #{prInfo.Number} by {prInfo.User?.Login ?? "???"}";
12 | return new() {Title = title, Url = prInfo.HtmlUrl, Description = prInfo.Title, Color = state.color};
13 | }
14 |
15 | public static DiscordEmbedBuilder AsEmbed(this Issue issueInfo)
16 | {
17 | var state = issueInfo.GetState();
18 | var stateLabel = state.state == null ? null : $"[{state.state}] ";
19 | var title = $"{stateLabel}Issue #{issueInfo.Number} from {issueInfo.User?.Login ?? "???"}";
20 | return new() {Title = title, Url = issueInfo.HtmlUrl, Description = issueInfo.Title, Color = state.color};
21 | }
22 |
23 | public static (string? state, DiscordColor color) GetState(this PullRequest prInfo)
24 | {
25 | if (prInfo.State == ItemState.Open)
26 | return ("Open", Config.Colors.PrOpen);
27 |
28 | if (prInfo.State == ItemState.Closed)
29 | {
30 | if (prInfo.MergedAt.HasValue)
31 | return ("Merged", Config.Colors.PrMerged);
32 |
33 | return ("Closed", Config.Colors.PrClosed);
34 | }
35 |
36 | return (null, Config.Colors.DownloadLinks);
37 | }
38 |
39 | public static (string? state, DiscordColor color) GetState(this Issue issueInfo)
40 | {
41 | if (issueInfo.State == ItemState.Open)
42 | return ("Open", Config.Colors.PrOpen);
43 |
44 | if (issueInfo.State == ItemState.Closed)
45 | return ("Closed", Config.Colors.PrClosed);
46 |
47 | return (null, Config.Colors.DownloadLinks);
48 | }
49 | }
--------------------------------------------------------------------------------
/CompatBot/Commands/AutoCompleteProviders/FilterActionAutoCompleteProvider.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database;
2 | using CompatBot.Utils.Extensions;
3 | using TResult = System.Collections.Generic.IEnumerable;
4 |
5 | namespace CompatBot.Commands.AutoCompleteProviders;
6 |
7 | public class FilterActionAutoCompleteProvider: IAutoCompleteProvider
8 | {
9 | static FilterActionAutoCompleteProvider()
10 | {
11 | var validValues = FilterActionExtensions.ActionFlagValues;
12 | minValue = (int)validValues[0];
13 | maxValue = (int)validValues.Aggregate((a, b) => a | b);
14 | choiceList = new DiscordAutoCompleteChoice[maxValue+1];
15 | choiceList[0] = new("Default", 0);
16 | choiceList[maxValue] = new("All", maxValue);
17 | for (var i = minValue; i < maxValue; i++)
18 | choiceList[i] = new($"{((FilterAction)i).ToFlagsString()}: {((FilterAction)i).ToString()}", i);
19 | }
20 |
21 | private static readonly int minValue;
22 | private static readonly int maxValue;
23 | private static readonly DiscordAutoCompleteChoice[] choiceList;
24 | private static readonly char[] Delimiters = { ',', ' ' };
25 |
26 | public ValueTask AutoCompleteAsync(AutoCompleteContext context)
27 | => ValueTask.FromResult(GetChoices(Parse(context.UserInput)));
28 |
29 | private static int Parse(string? input)
30 | {
31 | if (input is not {Length: >0 and <7})
32 | return 0;
33 | return (int)input.ToFilterAction();
34 | }
35 |
36 | private static TResult GetChoices(int start)
37 | {
38 | List result = [choiceList[start]];
39 | for(var i=minValue; i<=maxValue; i <<= 1)
40 | {
41 | var nextVal = start | i;
42 | if (nextVal != start)
43 | result.Add(choiceList[nextVal]);
44 | }
45 | return result.Take(25);
46 | }
47 | }
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/Converters.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Database;
2 | using SixLabors.ImageSharp;
3 | using SixLabors.ImageSharp.ColorSpaces;
4 | using SixLabors.ImageSharp.ColorSpaces.Conversion;
5 | using SixLabors.ImageSharp.PixelFormats;
6 |
7 | namespace CompatBot.Utils.Extensions;
8 |
9 | internal static class Converters
10 | {
11 | private static readonly ColorSpaceConverter ColorSpaceConverter = new();
12 |
13 | public static Color GetComplementary(this Color src, bool preserveOpacity = false)
14 | {
15 | var c = src.ToPixel();
16 | var a = c.A;
17 | //if RGB values are close to each other by a diff less than 10%, then if RGB values are lighter side,
18 | //decrease the blue by 50% (eventually it will increase in conversion below),
19 | //if RBB values are on darker side, decrease yellow by about 50% (it will increase in conversion)
20 | var avgColorValue = (c.R + c.G + c.B) / 3.0;
21 | var dR = Math.Abs(c.R - avgColorValue);
22 | var dG = Math.Abs(c.G - avgColorValue);
23 | var dB = Math.Abs(c.B - avgColorValue);
24 | if (dR < 20 && dG < 20 && dB < 20) //The color is a shade of gray
25 | {
26 | if (avgColorValue < 123) //color is dark
27 | c = new Rgba32(220, 230, 50, a); // #dce632
28 | else
29 | c = new Rgba32(255, 255, 50, a); // #ffff32
30 | }
31 | if (!preserveOpacity)
32 | a = Math.Max(a, (byte)127); //We don't want contrast color to be more than 50% transparent ever.
33 | var hsb = ColorSpaceConverter.ToHsv(new Rgb24(c.R, c.G, c.B));
34 | var h = hsb.H;
35 | h = h < 180 ? h + 180 : h - 180;
36 | var r = ColorSpaceConverter.ToRgb(new Hsv(h, hsb.S, hsb.V));
37 | return new Rgba32(r.R, r.G, r.B, a);
38 | }
39 |
40 | public static (bool exact, CompatStatus? status) ParseStatus(string status)
41 | {
42 | status = status.ToLowerInvariant();
43 | var exact = status.EndsWith("only");
44 | if (exact)
45 | status = status[..^4];
46 | if (Enum.TryParse(status, true, out var s))
47 | return (exact, s);
48 | return (false, null);
49 | }
50 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20180709154128_Explanations.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class Explanations : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.AddColumn(
10 | name: "issuer_id",
11 | table: "warning",
12 | nullable: false,
13 | defaultValue: 0ul);
14 |
15 | migrationBuilder.CreateTable(
16 | name: "explanation",
17 | columns: table => new
18 | {
19 | id = table.Column(nullable: false)
20 | .Annotation("Sqlite:Autoincrement", true)
21 | ,
22 | keyword = table.Column(nullable: false),
23 | text = table.Column(nullable: false)
24 | },
25 | constraints: table =>
26 | {
27 | table.PrimaryKey("id", x => x.id);
28 | });
29 |
30 | migrationBuilder.CreateIndex(
31 | name: "warning_discord_id",
32 | table: "warning",
33 | column: "discord_id");
34 |
35 | migrationBuilder.CreateIndex(
36 | name: "explanation_keyword",
37 | table: "explanation",
38 | column: "keyword",
39 | unique: true);
40 | }
41 |
42 | protected override void Down(MigrationBuilder migrationBuilder)
43 | {
44 | migrationBuilder.DropTable(
45 | name: "explanation");
46 |
47 | migrationBuilder.DropIndex(
48 | name: "warning_discord_id",
49 | table: "warning");
50 |
51 | migrationBuilder.DropColumn(
52 | name: "issuer_id",
53 | table: "warning");
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/CompatBot/Utils/Extensions/DateTimeEx.cs:
--------------------------------------------------------------------------------
1 | namespace CompatBot.Utils;
2 |
3 | public static class DateTimeEx
4 | {
5 | public static DateTime AsUtc(this DateTime dateTime) => dateTime.Kind == DateTimeKind.Utc ? dateTime : new(dateTime.Ticks, DateTimeKind.Utc);
6 | public static DateTime AsUtc(this long ticks) => new(ticks, DateTimeKind.Utc);
7 |
8 | public static string AsShortTimespan(this TimeSpan timeSpan)
9 | {
10 | var totalSeconds = timeSpan.TotalSeconds;
11 | var totalSecondsInt = (int)totalSeconds;
12 | var totalMinutes = totalSeconds / 60;
13 | var totalMinutesInt = (int)totalMinutes;
14 | var totalHours = totalMinutes / 60;
15 | var totalHoursInt = (int)totalHours;
16 | var totalDays = totalHours / 24;
17 | var totalDaysInt = (int)totalDays;
18 | var totalWeeks = totalDays / 7;
19 | var totalWeeksInt = (int)totalWeeks;
20 | var totalMonths = totalDays / 30;
21 | var totalMonthsInt = (int)totalMonths;
22 | var totalYears = totalDays / 365.25;
23 | var totalYearsInt = (int)totalYears;
24 |
25 | var years = totalYearsInt;
26 | var months = totalMonthsInt - years * 12;
27 | var weeks = totalWeeksInt - years * 52 - months * 4;
28 | var days = totalDaysInt - totalWeeksInt * 7;
29 | var hours = totalHoursInt - totalDaysInt * 24;
30 | var minutes = totalMinutesInt - totalHoursInt * 60;
31 | var seconds = totalSecondsInt - totalMinutesInt * 60;
32 |
33 | var result = "";
34 | if (years > 0)
35 | result += years + "y ";
36 | if (months > 0)
37 | result += months + "m ";
38 | if (weeks > 0)
39 | result += weeks + "w ";
40 | if (days > 0)
41 | result += days + "d ";
42 | if (hours > 0)
43 | result += hours + "h ";
44 | if (minutes > 0)
45 | result += minutes + "m ";
46 | if (string.IsNullOrEmpty(result))
47 | result = seconds + "s";
48 | return result.TrimEnd();
49 | }
50 |
51 | }
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Utils/Utils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace CompatApiClient.Utils;
4 |
5 | public static class Utils
6 | {
7 | private const long UnderKB = 1000;
8 | private const long UnderMB = 1000 * 1024;
9 | private const long UnderGB = 1000 * 1024 * 1024;
10 |
11 | public static string Trim(this string? str, int maxLength)
12 | {
13 | if (str is null)
14 | return "";
15 |
16 | if (str.Length > maxLength)
17 | return str[..(maxLength - 1)] + "…";
18 |
19 | return str;
20 | }
21 |
22 | public static string Truncate(this string str, int maxLength)
23 | {
24 | if (maxLength < 1)
25 | throw new ArgumentException("Argument must be positive, but was " + maxLength, nameof(maxLength));
26 |
27 | if (str.Length <= maxLength)
28 | return str;
29 |
30 | return str[..maxLength];
31 | }
32 |
33 | public static string Sanitize(this string str, bool breakLinks = true, bool replaceBackTicks = false)
34 | {
35 | var result = str.Replace("`", "`\u200d").Replace("@", "@\u200d");
36 | if (replaceBackTicks)
37 | result = result.Replace('`', '\'');
38 | if (breakLinks)
39 | result = result.Replace(".", ".\u200d").Replace(":", ":\u200d");
40 | return result;
41 | }
42 |
43 | public static int Clamp(this int amount, int low, int high)
44 | => Math.Min(high, Math.Max(amount, low));
45 |
46 | public static double Clamp(this double amount, double low, double high)
47 | => Math.Min(high, Math.Max(amount, low));
48 |
49 | public static string AsStorageUnit(this int bytes)
50 | => AsStorageUnit((long)bytes);
51 |
52 | public static string AsStorageUnit(this long bytes)
53 | => bytes switch
54 | {
55 | < UnderKB => $"{bytes} byte{(bytes == 1 ? "" : "s")}",
56 | < UnderMB => $"{bytes / 1024.0:0.##} KB",
57 | < UnderGB => $"{bytes / (1024.0 * 1024):0.##} MB",
58 | _ => $"{bytes / (1024.0 * 1024 * 1024):0.##} GB"
59 | };
60 | }
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/LogParsing/SourceHandlers/FileSourceHandler.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.IO.Pipelines;
3 | using CompatBot.EventHandlers.LogParsing.ArchiveHandlers;
4 | using ResultNet;
5 |
6 | namespace CompatBot.EventHandlers.LogParsing.SourceHandlers;
7 |
8 | internal class FileSource : ISource
9 | {
10 | private readonly string path;
11 | private readonly IArchiveHandler handler;
12 |
13 | public FileSource(string path, IArchiveHandler handler)
14 | {
15 | this.path = path;
16 | this.handler = handler;
17 | var fileInfo = new FileInfo(path);
18 | SourceFileSize = fileInfo.Length;
19 | FileName = fileInfo.Name;
20 | }
21 |
22 | public string SourceType => "File";
23 | public string FileName { get; }
24 | public long SourceFileSize { get; }
25 | public long SourceFilePosition { get; }
26 | public long LogFileSize { get; }
27 |
28 | public async Task FillPipeAsync(PipeWriter writer, CancellationToken cancellationToken)
29 | {
30 | await using var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
31 | await handler.FillPipeAsync(stream, writer, cancellationToken).ConfigureAwait(false);
32 | }
33 |
34 | public static async Task DetectArchiveHandlerAsync(string path, ICollection handlers)
35 | {
36 | var buf = new byte[4096];
37 | await using var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
38 | var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
39 | foreach (var handler in handlers)
40 | {
41 | var result = handler.CanHandle(Path.GetFileName(path), (int)stream.Length, buf.AsSpan(0, read));
42 | if (result.IsSuccess())
43 | return new FileSource(path, handler);
44 |
45 | if (result.Message is {Length: >0} reason)
46 | throw new InvalidOperationException(reason);
47 | }
48 | throw new InvalidOperationException("Unknown source type");
49 | }
50 |
51 | public void Dispose() { }
52 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/ThumbnailDb/20190306154836_ThumbsColorCache.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Migrations
4 | {
5 | public partial class ThumbsColorCache : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.CreateTable(
10 | name: "title_info",
11 | columns: table => new
12 | {
13 | id = table.Column(nullable: false)
14 | .Annotation("Sqlite:Autoincrement", true),
15 | content_id = table.Column(nullable: false),
16 | thumbnail_url = table.Column(nullable: true),
17 | thumbnail_embeddable_url = table.Column(nullable: true),
18 | embed_color = table.Column(nullable: true),
19 | timestamp = table.Column(nullable: false)
20 | },
21 | constraints: table =>
22 | {
23 | table.PrimaryKey("id", x => x.id);
24 | });
25 |
26 | migrationBuilder.CreateIndex(
27 | name: "thumbnail_content_id",
28 | table: "thumbnail",
29 | column: "content_id",
30 | unique: true);
31 |
32 | migrationBuilder.CreateIndex(
33 | name: "title_info_content_id",
34 | table: "title_info",
35 | column: "content_id",
36 | unique: true);
37 |
38 | migrationBuilder.CreateIndex(
39 | name: "title_info_timestamp",
40 | table: "title_info",
41 | column: "timestamp");
42 | }
43 |
44 | protected override void Down(MigrationBuilder migrationBuilder)
45 | {
46 | migrationBuilder.DropTable(
47 | name: "title_info");
48 |
49 | migrationBuilder.DropIndex(
50 | name: "thumbnail_content_id",
51 | table: "thumbnail");
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/CompatBot/Database/Providers/SqlConfiguration.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using CompatBot.EventHandlers.LogParsing.SourceHandlers;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace CompatBot.Database.Providers;
6 |
7 | internal static class SqlConfiguration
8 | {
9 | internal const string ConfigVarPrefix = "ENV-";
10 |
11 | public static async ValueTask RestoreAsync()
12 | {
13 | await using var db = await BotDb.OpenReadAsync().ConfigureAwait(false);
14 | var setVars = await db.BotState.AsNoTracking().Where(v => v.Key.StartsWith(ConfigVarPrefix)).ToListAsync().ConfigureAwait(false);
15 | if (setVars.Count is 0)
16 | return;
17 |
18 | foreach (var stateVar in setVars)
19 | if (stateVar.Value is string value)
20 | Config.InMemorySettings[stateVar.Key[ConfigVarPrefix.Length ..]] = value;
21 | if (!Config.InMemorySettings.TryGetValue(nameof(Config.GoogleApiCredentials), out var googleCreds) ||
22 | string.IsNullOrEmpty(googleCreds))
23 | {
24 | if (Path.Exists(Config.GoogleApiConfigPath))
25 | {
26 | Config.Log.Info("Migrating Google API credentials storage from file to db…");
27 | try
28 | {
29 | googleCreds = await File.ReadAllTextAsync(Config.GoogleApiConfigPath).ConfigureAwait(false);
30 | if (GoogleDriveHandler.ValidateCredentials(googleCreds))
31 | {
32 | Config.InMemorySettings[nameof(Config.GoogleApiCredentials)] = googleCreds;
33 | Config.Log.Info("Successfully migrated Google API credentials");
34 | }
35 | else
36 | {
37 | Config.Log.Error("Failed to migrate Google API credentials");
38 | }
39 | }
40 | catch (Exception e)
41 | {
42 | Config.Log.Error(e, "Failed to migrate Google API credentials");
43 | }
44 | }
45 | }
46 | Config.RebuildConfiguration();
47 | }
48 | }
--------------------------------------------------------------------------------
/CompatBot/Commands/AutoCompleteProviders/InviteAutoCompleteProvider.cs:
--------------------------------------------------------------------------------
1 | using CompatApiClient.Utils;
2 | using CompatBot.Database;
3 | using CompatBot.Database.Providers;
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace CompatBot.Commands.AutoCompleteProviders;
7 |
8 | public class InviteAutoCompleteProvider: IAutoCompleteProvider
9 | {
10 | public async ValueTask> AutoCompleteAsync(AutoCompleteContext context)
11 | {
12 | if (!ModProvider.IsMod(context.User.Id))
13 | return [new($"{Config.Reactions.Denied} You are not authorized to use this command.", -1)];
14 |
15 | await using var db = await BotDb.OpenReadAsync().ConfigureAwait(false);
16 | db.WithNoCase();
17 | IQueryable result;
18 | if (context.UserInput is not { Length: > 0 } input)
19 | result = db.WhitelistedInvites
20 | .OrderByDescending(e => e.Id);
21 | else
22 | {
23 | input = input.ToLowerInvariant();
24 | var prefixMatches = db.WhitelistedInvites
25 | .OrderBy(i => i.Id)
26 | .Where(
27 | i => i.Id.ToString().StartsWith(input)
28 | || i.GuildId.ToString().StartsWith(input)
29 | || (i.Name != null && i.Name.StartsWith(input))
30 | ).Take(25);
31 | var substringMatches= db.WhitelistedInvites
32 | .OrderBy(i => i.Id)
33 | .Where(
34 | i => i.Id.ToString().Contains(input)
35 | || i.GuildId.ToString().Contains(input)
36 | || (i.Name != null && i.Name.Contains(input))
37 | ).Take(50);
38 | result = prefixMatches
39 | .Concat(substringMatches)
40 | .Distinct();
41 | }
42 | return result
43 | .Take(25)
44 | .AsNoTracking()
45 | .AsEnumerable()
46 | .Select(i => new DiscordAutoCompleteChoice($"{i.Id}: {i.Name} ({i.GuildId})".Trim(100), i.Id))
47 | .ToList();
48 | }
49 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/HardwareDb/20220629192852_InitialCreate.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | #nullable disable
4 |
5 | namespace CompatBot.Database.Migrations.HardwareDbMigrations
6 | {
7 | public partial class InitialCreate : Migration
8 | {
9 | protected override void Up(MigrationBuilder migrationBuilder)
10 | {
11 | migrationBuilder.CreateTable(
12 | name: "hw_info",
13 | columns: table => new
14 | {
15 | install_id = table.Column(type: "BLOB", maxLength: 64, nullable: false),
16 | timestamp = table.Column(type: "INTEGER", nullable: false),
17 | cpu_maker = table.Column(type: "TEXT", nullable: false),
18 | cpu_model = table.Column(type: "TEXT", nullable: false),
19 | thread_count = table.Column(type: "INTEGER", nullable: false),
20 | cpu_features = table.Column(type: "INTEGER", nullable: false),
21 | ram_in_mb = table.Column(type: "INTEGER", nullable: false),
22 | gpu_maker = table.Column(type: "TEXT", nullable: false),
23 | gpu_model = table.Column(type: "TEXT", nullable: false),
24 | os_type = table.Column(type: "INTEGER", nullable: false),
25 | os_name = table.Column(type: "TEXT", nullable: true),
26 | os_version = table.Column(type: "TEXT", nullable: true)
27 | },
28 | constraints: table =>
29 | {
30 | table.PrimaryKey("pk_hw_info", x => x.install_id);
31 | });
32 |
33 | migrationBuilder.CreateIndex(
34 | name: "hardware_timestamp",
35 | table: "hw_info",
36 | column: "timestamp");
37 | }
38 |
39 | protected override void Down(MigrationBuilder migrationBuilder)
40 | {
41 | migrationBuilder.DropTable(
42 | name: "hw_info");
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/CompatBot/Commands/AutoCompleteProviders/ContentFilterAutoCompleteProvider.cs:
--------------------------------------------------------------------------------
1 | using CompatApiClient.Utils;
2 | using CompatBot.Database;
3 | using CompatBot.Database.Providers;
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace CompatBot.Commands.AutoCompleteProviders;
7 |
8 | public class ContentFilterAutoCompleteProvider: IAutoCompleteProvider
9 | {
10 | public async ValueTask> AutoCompleteAsync(AutoCompleteContext context)
11 | {
12 | if (!ModProvider.IsMod(context.User.Id))
13 | return [new($"{Config.Reactions.Denied} You are not authorized to use this command.", -1)];
14 |
15 | await using var db = await BotDb.OpenReadAsync().ConfigureAwait(false);
16 | db.WithNoCase();
17 | IEnumerable<(int id, string trigger)> result;
18 | if (context.UserInput is not {Length: >0} prefix)
19 | result = db.Piracystring
20 | .Where(f => !f.Disabled)
21 | .OrderByDescending(e=>e.Id)
22 | .Take(25)
23 | .AsNoTracking()
24 | .AsEnumerable()
25 | .Select(i => (id: i.Id, trigger:i.String));
26 | else
27 | {
28 | prefix = prefix.ToLowerInvariant();
29 | var prefixMatches = db.Piracystring
30 | .Where(f => !f.Disabled)
31 | .Where(i => i.Id.ToString().StartsWith(prefix) || i.String.StartsWith(prefix))
32 | .Take(25);
33 | var substringMatches= db.Piracystring
34 | .Where(f => !f.Disabled)
35 | .Where(i => i.Id.ToString().Contains(prefix) || i.String.Contains(prefix))
36 | .Take(50);
37 | result = prefixMatches
38 | .Concat(substringMatches)
39 | .Distinct()
40 | .OrderBy(i => i.Id)
41 | .Take(25)
42 | .AsNoTracking()
43 | .AsEnumerable()
44 | .Select(i => (id: i.Id, trigger: i.String));
45 | }
46 | return result.Select(i => new DiscordAutoCompleteChoice($"{i.id}: {i.trigger}".Trim(100), i.id)).ToList();
47 | }
48 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/ThumbnailDb/20200321134554_RemoveTitleInfoTable.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Migrations
4 | {
5 | public partial class RemoveTitleInfoTable : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.DropTable(
10 | name: "title_info");
11 |
12 | migrationBuilder.AddColumn(
13 | name: "embed_color",
14 | table: "thumbnail",
15 | nullable: true);
16 | }
17 |
18 | protected override void Down(MigrationBuilder migrationBuilder)
19 | {
20 | migrationBuilder.DropColumn(
21 | name: "embed_color",
22 | table: "thumbnail");
23 |
24 | migrationBuilder.CreateTable(
25 | name: "title_info",
26 | columns: table => new
27 | {
28 | id = table.Column(type: "INTEGER", nullable: false)
29 | .Annotation("Sqlite:Autoincrement", true),
30 | content_id = table.Column(type: "TEXT", nullable: false),
31 | embed_color = table.Column(type: "INTEGER", nullable: true),
32 | thumbnail_embeddable_url = table.Column(type: "TEXT", nullable: true),
33 | thumbnail_url = table.Column(type: "TEXT", nullable: true),
34 | timestamp = table.Column(type: "INTEGER", nullable: false)
35 | },
36 | constraints: table =>
37 | {
38 | table.PrimaryKey("id", x => x.id);
39 | });
40 |
41 | migrationBuilder.CreateIndex(
42 | name: "title_info_content_id",
43 | table: "title_info",
44 | column: "content_id",
45 | unique: true);
46 |
47 | migrationBuilder.CreateIndex(
48 | name: "title_info_timestamp",
49 | table: "title_info",
50 | column: "timestamp");
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Clients/CompatApiClient/Utils/Statistics.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Numerics;
4 |
5 | namespace CompatApiClient.Utils;
6 |
7 | public static class Statistics
8 | {
9 | public static long Mean(this IEnumerable data)
10 | {
11 | BigInteger sum = 0;
12 | var itemCount = 0;
13 | foreach (var value in data)
14 | {
15 | sum += value;
16 | itemCount ++;
17 | }
18 | if (itemCount == 0)
19 | throw new ArgumentException("Sequence must contain elements", nameof(data));
20 |
21 | return (long)(sum / itemCount);
22 | }
23 |
24 | public static double Mean(this IEnumerable data)
25 | {
26 | double sum = 0;
27 | var itemCount = 0;
28 | foreach (var value in data)
29 | {
30 | sum += value;
31 | itemCount ++;
32 | }
33 | if (itemCount == 0)
34 | throw new ArgumentException("Sequence must contain elements", nameof(data));
35 |
36 | return (long)(sum / itemCount);
37 | }
38 |
39 | public static double StdDev(this IEnumerable data)
40 | {
41 | BigInteger σx = 0, σx2 = 0;
42 | var n = 0;
43 | foreach (var value in data)
44 | {
45 | σx += value;
46 | σx2 += (BigInteger)value * value;
47 | n++;
48 | }
49 | if (n < 2)
50 | throw new ArgumentException("Sequence must contain at least two elements", nameof(data));
51 |
52 | var σ2 = σx * σx;
53 | return Math.Sqrt((double)((n * σx2) - σ2) / ((n - 1) * n));
54 | }
55 |
56 | public static double StdDev(this IEnumerable data)
57 | {
58 | double σx = 0, σx2 = 0;
59 | var n = 0;
60 | foreach (var value in data)
61 | {
62 | σx += value;
63 | σx2 += value * value;
64 | n++;
65 | }
66 | if (n < 2)
67 | throw new ArgumentException("Sequence must contain at least two elements", nameof(data));
68 |
69 | var σ2 = σx * σx;
70 | return Math.Sqrt((double)((n * σx2) - σ2) / ((n - 1) * n));
71 | }
72 | }
--------------------------------------------------------------------------------
/CompatBot/Commands/BotMath.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using org.mariuszgromada.math.mxparser;
3 | using License = org.mariuszgromada.math.mxparser.License;
4 |
5 | namespace CompatBot.Commands;
6 |
7 | internal static class BotMath
8 | {
9 | static BotMath()
10 | {
11 | License.iConfirmNonCommercialUse("RPCS3");
12 | }
13 |
14 | [Command("calculate")]
15 | [Description("Math; there you go, Juhn")]
16 | public static async ValueTask Calc(SlashCommandContext ctx, [Description("Math expression or `help` for syntax link")] string expression)
17 | {
18 | var ephemeral = !ctx.Channel.IsSpamChannel();
19 | if (expression.Equals("help", StringComparison.OrdinalIgnoreCase))
20 | {
21 | await ctx.RespondAsync("Help for all the features and built-in constants and functions could be found at [mXparser website]()", ephemeral);
22 | return;
23 | }
24 |
25 | var result = """
26 | Something went wrong ¯\\\_(ツ)\_/¯
27 | Math is hard, yo
28 | """;
29 | try
30 | {
31 | mXparser.resetCancelCurrentCalculationFlag();
32 | var expr = new Expression(expression);
33 | const int timeout = 1_000;
34 | var cts = new CancellationTokenSource(timeout);
35 | // ReSharper disable once MethodSupportsCancellation
36 | var delayTask = Task.Delay(timeout);
37 | var calcTask = Task.Run(() => expr.calculate().ToString(CultureInfo.InvariantCulture), cts.Token);
38 | await Task.WhenAny(calcTask, delayTask).ConfigureAwait(false);
39 | if (calcTask.IsCompletedSuccessfully)
40 | {
41 | result = await calcTask;
42 | }
43 | else
44 | {
45 | mXparser.cancelCurrentCalculation();
46 | result = "Calculation took too much time and all operations were cancelled";
47 | }
48 | }
49 | catch (Exception e)
50 | {
51 | Config.Log.Warn(e, "Math failed");
52 | }
53 | await ctx.RespondAsync(result, ephemeral).ConfigureAwait(false);
54 | }
55 | }
--------------------------------------------------------------------------------
/CompatBot/Ocr/Backend/AzureVision.cs:
--------------------------------------------------------------------------------
1 | using CompatApiClient.Utils;
2 | using Microsoft.Azure.CognitiveServices.Vision.ComputerVision;
3 | using Microsoft.Azure.CognitiveServices.Vision.ComputerVision.Models;
4 |
5 | namespace CompatBot.Ocr.Backend;
6 |
7 | public class AzureVision: IOcrBackend
8 | {
9 | private ComputerVisionClient cvClient;
10 |
11 | public string Name => "azure";
12 |
13 | public Task InitializeAsync(CancellationToken cancellationToken)
14 | {
15 | if (Config.AzureComputerVisionKey is not { Length: > 0 })
16 | return Task.FromResult(false);
17 |
18 | cvClient = new(new ApiKeyServiceClientCredentials(Config.AzureComputerVisionKey))
19 | {
20 | Endpoint = Config.AzureComputerVisionEndpoint
21 | };
22 | return Task.FromResult(true);
23 | }
24 |
25 | public async Task<(string result, double confidence)> GetTextAsync(string imgUrl, CancellationToken cancellationToken)
26 | {
27 | var headers = await cvClient.ReadAsync(imgUrl, cancellationToken: cancellationToken).ConfigureAwait(false);
28 | var operationId = new Guid(new Uri(headers.OperationLocation).Segments.Last());
29 | ReadOperationResult? result;
30 | bool waiting;
31 | do
32 | {
33 | result = await cvClient.GetReadResultAsync(operationId, Config.Cts.Token).ConfigureAwait(false);
34 | waiting = result.Status is OperationStatusCodes.NotStarted or OperationStatusCodes.Running;
35 | if (waiting)
36 | await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
37 | } while (waiting);
38 | if (result.Status is OperationStatusCodes.Succeeded)
39 | {
40 | if (result.AnalyzeResult?.ReadResults?.SelectMany(r => r.Lines).Any() ?? false)
41 | {
42 | var ocrTextBuf = new StringBuilder();
43 | foreach (var r in result.AnalyzeResult.ReadResults)
44 | foreach (var l in r.Lines)
45 | ocrTextBuf.AppendLine(l.Text);
46 | return (ocrTextBuf.ToString(), 1);
47 | }
48 | }
49 | Config.Log.Warn($"Failed to OCR image {imgUrl}: {result.Status}");
50 | return ("", 0);
51 | }
52 | }
--------------------------------------------------------------------------------
/CompatBot/Ocr/OcrProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 | using CompatBot.Ocr.Backend;
3 |
4 | namespace CompatBot.Ocr;
5 |
6 | public static class OcrProvider
7 | {
8 | private static IOcrBackend? backend;
9 |
10 | public static bool IsAvailable => backend is not null;
11 | public static string BackendName => backend?.Name ?? "not configured";
12 |
13 | public static async Task InitializeAsync(CancellationToken cancellationToken)
14 | {
15 | var backendName = Config.OcrBackend;
16 | if (GetBackend(backendName) is not {} result)
17 | {
18 | if (Config.AzureComputerVisionKey is { Length: > 0 })
19 | backendName = "azure";
20 | else if (GC.GetGCMemoryInfo().TotalAvailableMemoryBytes > 4L * 1024 * 1024 * 1024
21 | || RuntimeInformation.OSArchitecture is not (Architecture.X64 or Architecture.X86))
22 | {
23 | backendName = "florence2";
24 | }
25 | else
26 | backendName = "tesseract";
27 | result = GetBackend(backendName)!;
28 | }
29 | Config.Log.Info($"Initializing OCR backend {BackendName}…");
30 | if (await result.InitializeAsync(cancellationToken).ConfigureAwait(false))
31 | {
32 | backend = result;
33 | Config.Log.Info($"Initialized OCR backend {BackendName}");
34 | }
35 | }
36 |
37 | public static async Task<(string result, double confidence)> GetTextAsync(string imageUrl, CancellationToken cancellationToken)
38 | {
39 | if (backend is null)
40 | return ("", -1);
41 |
42 | try
43 | {
44 | return await backend.GetTextAsync(imageUrl, cancellationToken).ConfigureAwait(false);
45 | }
46 | catch (Exception e)
47 | {
48 | Config.Log.Warn(e, $"Failed to OCR image {imageUrl}");
49 | return ("", 0);
50 | }
51 | }
52 |
53 | private static IOcrBackend? GetBackend(string name)
54 | => name.ToLowerInvariant() switch
55 | {
56 | "tesseract" => new Backend.Tesseract(),
57 | "florence2" => new Backend.Florence2(),
58 | "azure" => new Backend.AzureVision(),
59 | _ => null,
60 | };
61 | }
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/LogAsTextMonitor.cs:
--------------------------------------------------------------------------------
1 | using System.Text.RegularExpressions;
2 |
3 | namespace CompatBot.EventHandlers;
4 |
5 | internal static partial class LogAsTextMonitor
6 | {
7 | [GeneratedRegex(@"^[`""]?(·|(\w|!)) ({(rsx|PPU|SPU)|LDR:)|E LDR:", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
8 | private static partial Regex LogLine();
9 |
10 | public static async Task OnMessageCreated(DiscordClient _, MessageCreatedEventArgs args)
11 | {
12 | if (DefaultHandlerFilter.IsFluff(args.Message))
13 | return;
14 |
15 | if (!args.Channel.IsHelpChannel())
16 | return;
17 |
18 | if ((args.Message.Author as DiscordMember)?.Roles.Any() ?? false)
19 | return;
20 |
21 | if (LogLine().IsMatch(args.Message.Content))
22 | {
23 | var brokenDump = false;
24 | string msg = "";
25 | if (args.Message.Content.Contains("LDR:"))
26 | {
27 | brokenDump = true;
28 | if (args.Message.Content.Contains("fs::file is null"))
29 | msg = $"{args.Message.Author.Mention} this error usually indicates a missing `.rap` license file.\n";
30 | else if (args.Message.Content.Contains("Invalid or unsupported file format"))
31 | msg = $"{args.Message.Author.Mention} this error usually indicates an encrypted or corrupted game dump.\n";
32 | else
33 | brokenDump = false;
34 | }
35 | var logUploadExplain = await PostLogHelpHandler.GetExplanationAsync("log").ConfigureAwait(false);
36 | if (brokenDump)
37 | msg += "Please follow the quickstart guide to get a proper dump of a digital title.\n" +
38 | "Also please upload the full RPCS3 log instead of pasting only a section which may be completely irrelevant.\n" +
39 | logUploadExplain.Text;
40 | else
41 | msg = $"{args.Message.Author.Mention} please upload the full RPCS3 log instead of pasting only a section which may be completely irrelevant." +
42 | logUploadExplain.Text;
43 | await args.Channel.SendMessageAsync(msg, logUploadExplain.Attachment, logUploadExplain.AttachmentFilename).ConfigureAwait(false);
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/Tests/ReflectionHacksTests.cs:
--------------------------------------------------------------------------------
1 | using CompatBot.Utils;
2 | using CompatBot.Utils.Extensions;
3 | using DSharpPlus.Entities;
4 | using NUnit.Framework;
5 |
6 | namespace Tests;
7 |
8 | [TestFixture]
9 | public class ReflectionHacksTests
10 | {
11 | [Test]
12 | public void DiscordButtonComponentEmojiSetterTest()
13 | {
14 | var button = new DiscordButtonComponent(DiscordButtonStyle.Primary, "test", "Test");
15 | var property = button.GetType().GetProperty(nameof(button.Emoji));
16 | Assert.That(property, Is.Not.Null);
17 | Assert.That(property.GetMethod?.IsPublic, Is.True);
18 |
19 | var setter = property.SetMethod;
20 | Assert.That(setter, Is.Not.Null);
21 | Assert.That(setter.IsPublic, Is.False, $"{nameof(DiscordButtonComponent)}.{nameof(DiscordButtonComponent.Emoji)} setter is now public, please remove hack in {nameof(DiscordComponentsExtensions)}.{nameof(DiscordComponentsExtensions.SetEmoji)}");
22 | }
23 |
24 | [Test]
25 | public void DiscordMessageBuilderReplyIdSetterTest()
26 | {
27 | var messageBuilder = new DiscordMessageBuilder();
28 | var property = messageBuilder.GetType().GetProperty(nameof(messageBuilder.ReplyId));
29 | Assert.That(property, Is.Not.Null);
30 | Assert.That(property.GetMethod?.IsPublic, Is.True);
31 |
32 | var setter = property.SetMethod;
33 | Assert.That(setter, Is.Not.Null);
34 | Assert.That(setter.IsPublic, Is.False, $"{nameof(DiscordMessageBuilder)}.{nameof(DiscordMessageBuilder.ReplyId)} setter is now public, please remove hack in {nameof(DiscordMessageExtensions)}.{nameof(DiscordMessageExtensions.UpdateOrCreateMessageAsync)}");
35 | }
36 |
37 | [Test]
38 | public void DiscordMessageChannelSetterTest()
39 | {
40 | var property = typeof(DiscordMessage).GetProperty(nameof(DiscordMessage.Channel));
41 | Assert.That(property, Is.Not.Null);
42 | Assert.That(property.GetMethod?.IsPublic, Is.True);
43 |
44 | var setter = property.SetMethod;
45 | Assert.That(setter, Is.Not.Null);
46 | Assert.That(setter.IsPublic, Is.False, $"{nameof(DiscordMessage)}.{nameof(DiscordMessage.Channel)} setter is now public, please remove hack in {nameof(DiscordMessageExtensions)}.{nameof(DiscordMessageExtensions.UpdateOrCreateMessageAsync)}");
47 | }
48 | }
--------------------------------------------------------------------------------
/CompatBot/Commands/AutoCompleteProviders/EventIdAutoCompleteProvider.cs:
--------------------------------------------------------------------------------
1 | using CompatApiClient.Utils;
2 | using CompatBot.Database;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace CompatBot.Commands.AutoCompleteProviders;
6 |
7 | public class EventIdAutoCompleteProvider: IAutoCompleteProvider
8 | {
9 | public async ValueTask> AutoCompleteAsync(AutoCompleteContext context)
10 | {
11 | await using var db = await BotDb.OpenReadAsync().ConfigureAwait(false);
12 | IEnumerable query;
13 | if (context.UserInput is not { Length: > 0 } input)
14 | {
15 | query = db.EventSchedule
16 | .OrderByDescending(e => e.Id)
17 | .Take(25)
18 | .AsNoTracking()
19 | .AsEnumerable();
20 | }
21 | else
22 | {
23 | var prefix = db.EventSchedule
24 | .Where(e => e.Id.ToString().StartsWith(input) || e.Name != null && e.Name.StartsWith(input))
25 | .OrderBy(e => e.Start)
26 | .Take(25);
27 | var sub = db.EventSchedule
28 | .Where(e => e.Id.ToString().Contains(input) || e.Name != null && e.Name.Contains(input))
29 | .OrderBy(e => e.Start)
30 | .Take(50);
31 | var currentTicks = DateTime.UtcNow.Ticks;
32 | var fuzzy = db.EventSchedule
33 | .Where(e => e.End >= currentTicks && e.Name != null)
34 | .OrderBy(e => e.Start)
35 | .AsNoTracking()
36 | .AsEnumerable()
37 | .Select(e => new { coef = e.Name.GetFuzzyCoefficientCached(input), evt = e })
38 | .Where(i => i.coef > 0.5)
39 | .OrderByDescending(i => i.coef)
40 | .Take(25)
41 | .Select(i => i.evt);
42 | query = prefix
43 | .Concat(sub)
44 | .Distinct()
45 | .Take(25)
46 | .AsNoTracking()
47 | .AsEnumerable()
48 | .Concat(fuzzy)
49 | .Distinct();
50 | }
51 | return query
52 | .Distinct()
53 | .Take(25)
54 | .Select(n => new DiscordAutoCompleteChoice($"{n.Id}: {n.Name}".Trim(100), n.Id))
55 | .ToList();
56 | }
57 | }
--------------------------------------------------------------------------------
/Clients/YandexDiskClient/Client.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Net.Http.Json;
4 | using System.Text.Json;
5 | using System.Text.Json.Serialization;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using CompatApiClient;
9 | using CompatApiClient.Compression;
10 | using CompatApiClient.Formatters;
11 | using CompatApiClient.Utils;
12 | using YandexDiskClient.POCOs;
13 |
14 | namespace YandexDiskClient;
15 |
16 | public sealed class Client
17 | {
18 | private readonly HttpClient client;
19 | private readonly JsonSerializerOptions jsonOptions;
20 |
21 | public Client()
22 | {
23 | client = HttpClientFactory.Create(new CompressionMessageHandler());
24 | jsonOptions = new()
25 | {
26 | PropertyNamingPolicy = SpecialJsonNamingPolicy.SnakeCase,
27 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
28 | IncludeFields = true,
29 | };
30 | }
31 |
32 | public Task GetResourceInfoAsync(string shareKey, CancellationToken cancellationToken)
33 | => GetResourceInfoAsync(new Uri($"https://yadi.sk/d/{shareKey}"), cancellationToken);
34 |
35 | public async Task GetResourceInfoAsync(Uri publicUri, CancellationToken cancellationToken)
36 | {
37 | try
38 | {
39 | var uri = new Uri("https://cloud-api.yandex.net/v1/disk/public/resources").SetQueryParameters(
40 | ("public_key", publicUri.ToString()),
41 | ("fields", "size,name,file")
42 | );
43 | using var message = new HttpRequestMessage(HttpMethod.Get, uri);
44 | message.Headers.UserAgent.Add(ApiConfig.ProductInfoHeader);
45 | using var response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false);
46 | try
47 | {
48 | await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
49 | return await response.Content.ReadFromJsonAsync(jsonOptions, cancellationToken).ConfigureAwait(false);
50 | }
51 | catch (Exception e)
52 | {
53 | ConsoleLogger.PrintError(e, response);
54 | }
55 | }
56 | catch (Exception e)
57 | {
58 | ApiConfig.Log.Error(e);
59 | }
60 | return null;
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/BotDb/20190723151803_ContentFilter2.0.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Database.Migrations
4 | {
5 | public partial class ContentFilter20 : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | migrationBuilder.AddColumn(
10 | name: "actions",
11 | table: "piracystring",
12 | nullable: false,
13 | defaultValue: 11);
14 |
15 | migrationBuilder.AddColumn(
16 | name: "context",
17 | table: "piracystring",
18 | nullable: false,
19 | defaultValue: (byte)3);
20 |
21 | migrationBuilder.AddColumn(
22 | name: "custom_message",
23 | table: "piracystring",
24 | nullable: true);
25 |
26 | migrationBuilder.AddColumn(
27 | name: "disabled",
28 | table: "piracystring",
29 | nullable: false,
30 | defaultValue: false);
31 |
32 | migrationBuilder.AddColumn(
33 | name: "explain_term",
34 | table: "piracystring",
35 | nullable: true);
36 |
37 | migrationBuilder.AddColumn(
38 | name: "validating_regex",
39 | table: "piracystring",
40 | nullable: true);
41 | }
42 |
43 | protected override void Down(MigrationBuilder migrationBuilder)
44 | {
45 | migrationBuilder.DropColumn(
46 | name: "actions",
47 | table: "piracystring");
48 |
49 | migrationBuilder.DropColumn(
50 | name: "context",
51 | table: "piracystring");
52 |
53 | migrationBuilder.DropColumn(
54 | name: "custom_message",
55 | table: "piracystring");
56 |
57 | migrationBuilder.DropColumn(
58 | name: "disabled",
59 | table: "piracystring");
60 |
61 | migrationBuilder.DropColumn(
62 | name: "explain_term",
63 | table: "piracystring");
64 |
65 | migrationBuilder.DropColumn(
66 | name: "validating_regex",
67 | table: "piracystring");
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/CompatBot/Ocr/Backend/Florence2.cs:
--------------------------------------------------------------------------------
1 | using Florence2;
2 |
3 | namespace CompatBot.Ocr.Backend;
4 |
5 | public class Florence2: BackendBase
6 | {
7 | private Florence2Model model;
8 |
9 | public override string Name => "florence2";
10 |
11 | public override async Task InitializeAsync(CancellationToken cancellationToken)
12 | {
13 | if (!await base.InitializeAsync(cancellationToken).ConfigureAwait(false))
14 | return false;
15 |
16 | var modelSource = new FlorenceModelDownloader(ModelCachePath);
17 | try
18 | {
19 | var errors = false;
20 | await modelSource.DownloadModelsAsync(s =>
21 | {
22 | if (s.Error is { Length: > 0 } errorMsg)
23 | {
24 | Config.Log.Error($"Failed to download Florence2 model files: {errorMsg}");
25 | errors = true;
26 | }
27 | else if (s.Message is { Length: > 0 } msg)
28 | {
29 | Config.Log.Info($"Florence2 model download message: {msg}");
30 | }
31 | },
32 | Config.LoggerFactory.CreateLogger("florence2"),
33 | cancellationToken
34 | ).ConfigureAwait(false);
35 | if (errors)
36 | return false;
37 | }
38 | catch (Exception e)
39 | {
40 | Config.Log.Error(e, "Failed to download Florence2 model files");
41 | return false;
42 | }
43 |
44 | try
45 | {
46 | model = new(modelSource);
47 | }
48 | catch (Exception e)
49 | {
50 | Config.Log.Error(e, "Failed to initialize Florence2 model");
51 | return false;
52 | }
53 | return true;
54 | }
55 |
56 | public override async Task<(string result, double confidence)> GetTextAsync(string imgUrl, CancellationToken cancellationToken)
57 | {
58 | await using var imgStream = await HttpClient.GetStreamAsync(imgUrl, cancellationToken).ConfigureAwait(false);
59 | var results = model.Run(TaskTypes.OCR_WITH_REGION, [imgStream], "", CancellationToken.None);
60 | var result = new StringBuilder();
61 | foreach (var box in results[0].OCRBBox)
62 | result.AppendLine(box.Text);
63 | return (result.ToString().TrimEnd(), 1);
64 | }
65 | }
--------------------------------------------------------------------------------
/CompatBot/EventHandlers/LogParsing/ArchiveHandlers/GzipHandler.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.IO.Compression;
3 | using System.IO.Pipelines;
4 | using ResultNet;
5 |
6 | namespace CompatBot.EventHandlers.LogParsing.ArchiveHandlers;
7 |
8 | internal sealed class GzipHandler: IArchiveHandler
9 | {
10 | private static readonly byte[] Header = [0x1F, 0x8B, 0x08];
11 |
12 | public long LogSize { get; private set; }
13 | public long SourcePosition { get; private set; }
14 |
15 | public Result CanHandle(string fileName, int fileSize, ReadOnlySpan header)
16 | {
17 | if (header.Length >= Header.Length)
18 | {
19 | if (header[..Header.Length].SequenceEqual(Header))
20 | return Result.Success();
21 | }
22 | else if (fileName.EndsWith(".log.gz", StringComparison.InvariantCultureIgnoreCase)
23 | && !fileName.Contains("tty.log", StringComparison.InvariantCultureIgnoreCase))
24 | return Result.Success();
25 | return Result.Failure();
26 | }
27 |
28 | public async Task FillPipeAsync(Stream sourceStream, PipeWriter writer, CancellationToken cancellationToken)
29 | {
30 | await using var statsStream = new BufferCopyStream(sourceStream);
31 | await using var gzipStream = new GZipStream(statsStream, CompressionMode.Decompress);
32 | try
33 | {
34 | int read;
35 | FlushResult flushed;
36 | do
37 | {
38 | var memory = writer.GetMemory(Config.MinimumBufferSize);
39 | read = await gzipStream.ReadAsync(memory, cancellationToken);
40 | if (read > 0)
41 | writer.Advance(read);
42 | SourcePosition = statsStream.Position;
43 | flushed = await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
44 | } while (read > 0 && !(flushed.IsCompleted || flushed.IsCanceled || cancellationToken.IsCancellationRequested));
45 |
46 | var buf = statsStream.GetBufferedBytes();
47 | if (buf.Length > 3)
48 | LogSize = BitConverter.ToInt32(buf.AsSpan(buf.Length - 4, 4));
49 | }
50 | catch (Exception e)
51 | {
52 | Config.Log.Error(e, "Error filling the log pipe");
53 | await writer.CompleteAsync(e);
54 | return;
55 | }
56 | await writer.CompleteAsync();
57 | }
58 | }
--------------------------------------------------------------------------------
/CompatBot/Commands/AutoCompleteProviders/BotConfigurationAutoCompleteProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using CompatApiClient.Utils;
3 | using CompatBot.Database;
4 | using CompatBot.Database.Providers;
5 | using Microsoft.EntityFrameworkCore;
6 |
7 | namespace CompatBot.Commands.AutoCompleteProviders;
8 |
9 | public class BotConfigurationAutoCompleteProvider: IAutoCompleteProvider
10 | {
11 | private static readonly List KnownConfigVariables = typeof(Config)
12 | .GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.ExactBinding)
13 | .Select(pi => pi.Name)
14 | .OrderBy(n => n)
15 | .ToList();
16 |
17 | public async ValueTask> AutoCompleteAsync(AutoCompleteContext context)
18 | {
19 | if (!ModProvider.IsSudoer(context.User.Id))
20 | return [new($"{Config.Reactions.Denied} You are not authorized to use this command.", -1)];
21 |
22 | await using var db = await BotDb.OpenReadAsync().ConfigureAwait(false);
23 | IEnumerable result;
24 | var input = context.UserInput;
25 | if (input is not { Length: > 0 })
26 | {
27 | var set = db.BotState
28 | .AsNoTracking()
29 | .Where(v => v.Key.StartsWith(SqlConfiguration.ConfigVarPrefix))
30 | .OrderBy(v => v.Key)
31 | .Take(25)
32 | .Select(v => v.Key)
33 | .AsEnumerable()
34 | .Select(k => k[SqlConfiguration.ConfigVarPrefix.Length ..]);
35 | result = set.Concat(KnownConfigVariables);
36 | }
37 | else
38 | {
39 | var prefix = KnownConfigVariables
40 | .Where(n => n.StartsWith(input, StringComparison.OrdinalIgnoreCase))
41 | .Take(25);
42 | var sub = KnownConfigVariables
43 | .Where(n => n.Contains(input, StringComparison.OrdinalIgnoreCase))
44 | .Take(50);
45 | var fuzzy = KnownConfigVariables
46 | .Select(n => new { coef = n.GetFuzzyCoefficientCached(input), val = n })
47 | .Where(i => i.coef > 0.5)
48 | .OrderByDescending(i => i.coef)
49 | .Take(25)
50 | .Select(i => i.val);
51 | result = prefix
52 | .Concat(sub)
53 | .Concat(fuzzy);
54 | }
55 | return result
56 | .Distinct()
57 | .Take(25)
58 | .Select(n => new DiscordAutoCompleteChoice(n.Trim(100), n)).ToList();
59 | }
60 | }
--------------------------------------------------------------------------------
/CompatBot/Database/Migrations/ThumbnailDb/20200321124445_RemoveSyscallModules.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | namespace CompatBot.Migrations
4 | {
5 | public partial class RemoveSyscallModules : Migration
6 | {
7 | protected override void Up(MigrationBuilder migrationBuilder)
8 | {
9 | //disable constraints
10 | migrationBuilder.Sql("PRAGMA foreign_keys=off;", true);
11 |
12 | //drop old indices
13 | migrationBuilder.DropIndex(
14 | name: "syscall_info_module",
15 | table: "syscall_info");
16 |
17 | migrationBuilder.DropIndex(
18 | name: "syscall_info_function",
19 | table: "syscall_info");
20 |
21 | //create new table
22 | migrationBuilder.CreateTable(
23 | name: "syscall_info_tmp",
24 | columns: table => new
25 | {
26 | id = table.Column(nullable: false)
27 | .Annotation("Sqlite:Autoincrement", true),
28 | function = table.Column(nullable: false)
29 | },
30 | constraints: table =>
31 | {
32 | table.PrimaryKey("id", x => x.id);
33 | });
34 |
35 | //copy data to the new table
36 | migrationBuilder.Sql("INSERT INTO syscall_info_tmp(id, function) SELECT id, function FROM syscall_info;");
37 |
38 | //drop old table
39 | migrationBuilder.DropTable("syscall_info");
40 |
41 | //rename new table to the old table
42 | migrationBuilder.RenameTable(
43 | name: "syscall_info_tmp",
44 | newName: "syscall_info");
45 |
46 | //re-create index
47 | migrationBuilder.CreateIndex(
48 | name: "syscall_info_function",
49 | table: "syscall_info",
50 | column: "function");
51 |
52 | //re-enable constraints
53 | migrationBuilder.Sql("PRAGMA foreign_keys=on;", true);
54 | }
55 |
56 | protected override void Down(MigrationBuilder migrationBuilder)
57 | {
58 | migrationBuilder.AddColumn(
59 | name: "module",
60 | table: "syscall_info",
61 | nullable: false,
62 | defaultValue: "");
63 |
64 | migrationBuilder.CreateIndex(
65 | name: "syscall_info_module",
66 | table: "syscall_info",
67 | column: "module");
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------