├── 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 | --------------------------------------------------------------------------------