mirror of
https://github.com/RPCS3/discord-bot.git
synced 2024-11-23 02:09:38 +00:00
RPCS3 Compatibility Bot reimplemented in C# for .NET Core
RPCS3 Compatibility Bot reimplemented in C# for .NET Core Current status of this PR: * tested and targeted for .NET Core 2.1 * all functionality is either on par or improved compared to the python version * compatibility with current bot.db should be preserved in all upgrade scenarios * some bot management commands were changed (now under !sudo bot) * standard help generator for the new discord client is ... different; compatibility with old format could be restored through custom formatter if needed * everything has been split in more loosely tied components for easier extensibility and maintenance * log parsing has been rewritten and should work ~2x as fast
This commit is contained in:
parent
1709f445b4
commit
7fd7d09973
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
329
.gitignore
vendored
329
.gitignore
vendored
@ -1,111 +1,264 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Python template
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Django stuff:
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
.static_storage/
|
||||
.media/
|
||||
local_settings.py
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
.idea
|
||||
discord.log
|
||||
/.vscode/settings.json
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
#*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
launchSettings.json
|
||||
bot.db
|
||||
.vscode/
|
||||
|
22
.vscode/launch.json
vendored
22
.vscode/launch.json
vendored
@ -1,22 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"stopOnEntry": false,
|
||||
"pythonPath": "${config:python.pythonPath}",
|
||||
"program": "${workspaceFolder}/bot.py",
|
||||
"args": [
|
||||
"put_bot_user_token_here"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {},
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"debugOptions": [
|
||||
"RedirectOutput"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
102
CompatApiClient/ApiConfig.cs
Normal file
102
CompatApiClient/ApiConfig.cs
Normal file
@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CompatApiClient
|
||||
{
|
||||
using ReturnCodeType = Dictionary<int, (bool displayResults, bool overrideAll, bool displayFooter, string info)>;
|
||||
|
||||
public static class ApiConfig
|
||||
{
|
||||
public static int Version { get; } = 1;
|
||||
public static Uri BaseUrl { get; } = new Uri("https://rpcs3.net/compatibility");
|
||||
public static string DateInputFormat { get; } = "yyyy-M-d";
|
||||
public static string DateOutputFormat { get; } = "yyy-MM-dd";
|
||||
public static string DateQueryFormat { get; } = "yyyyMMdd";
|
||||
|
||||
public static readonly ReturnCodeType ReturnCodes = new ReturnCodeType
|
||||
{
|
||||
{0, (true, false, true, "Results successfully retrieved.")},
|
||||
{1, (false, false, true, "No results.") },
|
||||
{2, (true, false, true, "No match was found, displaying results for: ***{0}***.") },
|
||||
{-1, (false, true, false, "{0}: Internal error occurred, please contact Ani and Nicba1010") },
|
||||
{-2, (false, true, false, "{0}: API is undergoing maintenance, please try again later.") },
|
||||
{-3, (false, false, false, "Illegal characters found, please try again with a different search term.") },
|
||||
};
|
||||
|
||||
public static readonly List<int> ResultAmount = new List<int> {25, 50, 100, 200};
|
||||
|
||||
public static readonly Dictionary<char, string[]> Directions = new Dictionary<char, string[]>
|
||||
{
|
||||
{'a', new []{"a", "asc", "ascending"}},
|
||||
{'d', new []{"d", "desc", "descending"} },
|
||||
};
|
||||
|
||||
public static readonly Dictionary<char, string[]> Regions = new Dictionary<char, string[]>
|
||||
{
|
||||
{'j', new[] {"j", "jp", "ja", "japan", "japanese", "JPN"}},
|
||||
{'u', new[] {"u", "us", "america", "american", "USA"}},
|
||||
{'e', new[] {"e", "eu", "europe", "european", "EUR"}},
|
||||
{'a', new[] {"a", "asia", "asian", "ch", "china", "chinese", "CHN"}},
|
||||
{'k', new[] {"k", "kr", "korea", "korean", "KOR"}},
|
||||
{'h', new[] {"h", "hong", "kong", "hongkong", "hong-kong", "HK"}},
|
||||
};
|
||||
|
||||
public static readonly Dictionary<string, int> Statuses = new Dictionary<string, int>
|
||||
{
|
||||
{"all", 0 },
|
||||
{"playable", 1 },
|
||||
{"ingame", 2 },
|
||||
{"intro", 3 },
|
||||
{"loadable", 4 },
|
||||
{"nothing", 5 },
|
||||
};
|
||||
|
||||
public static readonly Dictionary<string, int> SortTypes = new Dictionary<string, int>
|
||||
{
|
||||
{"id", 1 },
|
||||
{"title", 2 },
|
||||
{"status", 3 },
|
||||
{"date", 4 },
|
||||
};
|
||||
|
||||
public static readonly Dictionary<char, string[]> ReleaseTypes = new Dictionary<char, string[]>
|
||||
{
|
||||
{'b', new[] {"b", "d", "disc", "disk", "bluray", "blu-ray"}},
|
||||
{'n', new[] {"n", "p", "PSN"}},
|
||||
};
|
||||
|
||||
public static readonly Dictionary<string, char> ReverseDirections;
|
||||
public static readonly Dictionary<string, char> ReverseRegions;
|
||||
public static readonly Dictionary<string, char> ReverseReleaseTypes;
|
||||
public static readonly Dictionary<int, string> ReverseStatuses;
|
||||
public static readonly Dictionary<int, string> ReverseSortTypes;
|
||||
|
||||
private static Dictionary<TV, TK> Reverse<TK, TV>(this Dictionary<TK, TV[]> dic, IEqualityComparer<TV> comparer)
|
||||
{
|
||||
return (
|
||||
from kvp in dic
|
||||
from val in kvp.Value
|
||||
select (val, kvp.Key)
|
||||
).ToDictionary(rkvp => rkvp.val, rkvp => rkvp.Key, comparer);
|
||||
}
|
||||
|
||||
static ApiConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
ReverseDirections = Directions.Reverse(StringComparer.InvariantCultureIgnoreCase);
|
||||
ReverseRegions = Regions.Reverse(StringComparer.InvariantCultureIgnoreCase);
|
||||
ReverseReleaseTypes = ReleaseTypes.Reverse(StringComparer.InvariantCultureIgnoreCase);
|
||||
ReverseStatuses = Statuses.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
|
||||
ReverseSortTypes = SortTypes.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine(e);
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
CompatApiClient/Client.cs
Normal file
111
CompatApiClient/Client.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Formatting;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient.Compression;
|
||||
using CompatApiClient.POCOs;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CompatApiClient
|
||||
{
|
||||
public class Client
|
||||
{
|
||||
private readonly HttpClient client;
|
||||
private readonly MediaTypeFormatterCollection formatters;
|
||||
|
||||
private static readonly Dictionary<string, PrInfo> prInfoCache = new Dictionary<string, PrInfo>();
|
||||
|
||||
public Client()
|
||||
{
|
||||
client = HttpClientFactory.Create(new CompressionMessageHandler());
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new JsonContractResolver(NamingStyles.Underscore),
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
formatters = new MediaTypeFormatterCollection(new[] {new JsonMediaTypeFormatter {SerializerSettings = settings}});
|
||||
}
|
||||
|
||||
//todo: cache results
|
||||
public async Task<CompatResult> GetCompatResultAsync(RequestBuilder requestBuilder, CancellationToken cancellationToken)
|
||||
{
|
||||
var message = new HttpRequestMessage(HttpMethod.Get, requestBuilder.Build());
|
||||
var startTime = DateTime.UtcNow;
|
||||
CompatResult result;
|
||||
try
|
||||
{
|
||||
var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
|
||||
result = await response.Content.ReadAsAsync<CompatResult>(formatters, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PrintError(e);
|
||||
result = new CompatResult{ReturnCode = -2};
|
||||
}
|
||||
result.RequestBuilder = requestBuilder;
|
||||
result.RequestDuration = DateTime.UtcNow - startTime;
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<UpdateInfo> GetUpdateAsync(CancellationToken cancellationToken, string commit = "somecommit")
|
||||
{
|
||||
var message = new HttpRequestMessage(HttpMethod.Get, "https://update.rpcs3.net/?c=" + commit);
|
||||
UpdateInfo result;
|
||||
try
|
||||
{
|
||||
var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
|
||||
result = await response.Content.ReadAsAsync<UpdateInfo>(formatters, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PrintError(e);
|
||||
result = new UpdateInfo { ReturnCode = -2 };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<PrInfo> GetPrInfoAsync(string pr, CancellationToken cancellationToken)
|
||||
{
|
||||
if (prInfoCache.TryGetValue(pr, out var result))
|
||||
return result;
|
||||
|
||||
var message = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/RPCS3/rpcs3/pulls/" + pr);
|
||||
HttpContent content = null;
|
||||
try
|
||||
{
|
||||
message.Headers.UserAgent.Add(new ProductInfoHeaderValue("RPCS3CompatibilityBot", "2.0"));
|
||||
var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
|
||||
content = response.Content;
|
||||
await content.LoadIntoBufferAsync().ConfigureAwait(false);
|
||||
result = await content.ReadAsAsync<PrInfo>(formatters, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PrintError(e);
|
||||
if (content != null)
|
||||
try { Console.WriteLine(await content.ReadAsStringAsync().ConfigureAwait(false)); } catch {}
|
||||
int.TryParse(pr, out var prnum);
|
||||
return new PrInfo{Number = prnum};
|
||||
}
|
||||
|
||||
lock (prInfoCache)
|
||||
{
|
||||
if (prInfoCache.TryGetValue(pr, out var cachedResult))
|
||||
return cachedResult;
|
||||
|
||||
prInfoCache[pr] = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintError(Exception e)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine("Error communicating with api: " + e.Message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
}
|
20
CompatApiClient/CompatApiClient.csproj
Normal file
20
CompatApiClient/CompatApiClient.csproj
Normal file
@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
54
CompatApiClient/Compression/CompressedContent.cs
Normal file
54
CompatApiClient/Compression/CompressedContent.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CompatApiClient.Compression
|
||||
{
|
||||
public class CompressedContent : HttpContent
|
||||
{
|
||||
private readonly HttpContent content;
|
||||
private readonly ICompressor compressor;
|
||||
|
||||
public CompressedContent(HttpContent content, ICompressor compressor)
|
||||
{
|
||||
if (content == null)
|
||||
throw new ArgumentNullException(nameof(content));
|
||||
|
||||
if (compressor == null)
|
||||
throw new ArgumentNullException(nameof(compressor));
|
||||
|
||||
this.content = content;
|
||||
this.compressor = compressor;
|
||||
AddHeaders();
|
||||
}
|
||||
|
||||
protected override bool TryComputeLength(out long length)
|
||||
{
|
||||
length = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
|
||||
{
|
||||
if (stream == null)
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
|
||||
using (content)
|
||||
{
|
||||
var contentStream = await content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
var compressedLength = await compressor.CompressAsync(contentStream, stream).ConfigureAwait(false);
|
||||
Headers.ContentLength = compressedLength;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddHeaders()
|
||||
{
|
||||
foreach (var header in content.Headers)
|
||||
Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
Headers.ContentEncoding.Add(compressor.EncodingType);
|
||||
Headers.ContentLength = null;
|
||||
}
|
||||
}
|
||||
}
|
79
CompatApiClient/Compression/CompressionMessageHandler.cs
Normal file
79
CompatApiClient/Compression/CompressionMessageHandler.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CompatApiClient.Compression
|
||||
{
|
||||
public class CompressionMessageHandler : DelegatingHandler
|
||||
{
|
||||
public ICollection<ICompressor> Compressors { get; }
|
||||
public static readonly string PostCompressionFlag = "X-Set-Content-Encoding";
|
||||
public static readonly string[] DefaultContentEncodings = { "gzip", "deflate" };
|
||||
public static readonly string DefaultAcceptEncodings = "gzip, deflate";
|
||||
|
||||
private bool isServer;
|
||||
private bool isClient => !isServer;
|
||||
|
||||
public CompressionMessageHandler(bool isServer = false)
|
||||
{
|
||||
this.isServer = isServer;
|
||||
Compressors = new ICompressor[]
|
||||
{
|
||||
new GZipCompressor(),
|
||||
new DeflateCompressor(),
|
||||
};
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (isServer && request.Content?.Headers?.ContentEncoding != null)
|
||||
{
|
||||
var encoding = request.Content.Headers.ContentEncoding.FirstOrDefault();
|
||||
if (encoding != null)
|
||||
{
|
||||
var compressor = Compressors.FirstOrDefault(c => c.EncodingType.Equals(encoding, StringComparison.InvariantCultureIgnoreCase));
|
||||
if (compressor != null)
|
||||
request.Content = new DecompressedContent(request.Content, compressor);
|
||||
}
|
||||
}
|
||||
if (isClient && (request.Method == HttpMethod.Post || request.Method == HttpMethod.Put) && request.Content != null && request.Headers != null && request.Headers.Contains(PostCompressionFlag))
|
||||
{
|
||||
var encoding = request.Headers.GetValues(PostCompressionFlag).FirstOrDefault();
|
||||
if (encoding != null)
|
||||
{
|
||||
var compressor = Compressors.FirstOrDefault(c => c.EncodingType.Equals(encoding, StringComparison.InvariantCultureIgnoreCase));
|
||||
if (compressor != null)
|
||||
request.Content = new CompressedContent(request.Content, compressor);
|
||||
}
|
||||
}
|
||||
request.Headers?.Remove(PostCompressionFlag);
|
||||
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (isClient && response.Content?.Headers?.ContentEncoding != null)
|
||||
{
|
||||
var encoding = response.Content.Headers.ContentEncoding.FirstOrDefault();
|
||||
if (encoding != null)
|
||||
{
|
||||
var compressor = Compressors.FirstOrDefault(c => c.EncodingType.Equals(encoding, StringComparison.InvariantCultureIgnoreCase));
|
||||
if (compressor != null)
|
||||
response.Content = new DecompressedContent(response.Content, compressor);
|
||||
}
|
||||
}
|
||||
if (isServer && response.Content != null && request.Headers?.AcceptEncoding != null)
|
||||
{
|
||||
var encoding = request.Headers.AcceptEncoding.FirstOrDefault();
|
||||
if (encoding == null)
|
||||
return response;
|
||||
|
||||
var compressor = Compressors.FirstOrDefault(c => c.EncodingType.Equals(encoding.Value, StringComparison.InvariantCultureIgnoreCase));
|
||||
if (compressor == null)
|
||||
return response;
|
||||
|
||||
response.Content = new CompressedContent(response.Content, compressor);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
36
CompatApiClient/Compression/Compressor.cs
Normal file
36
CompatApiClient/Compression/Compressor.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CompatApiClient.Compression
|
||||
{
|
||||
public abstract class Compressor : ICompressor
|
||||
{
|
||||
public abstract string EncodingType { get; }
|
||||
public abstract Stream CreateCompressionStream(Stream output);
|
||||
public abstract Stream CreateDecompressionStream(Stream input);
|
||||
|
||||
public virtual async Task<long> CompressAsync(Stream source, Stream destination)
|
||||
{
|
||||
using (var memStream = new MemoryStream())
|
||||
{
|
||||
using (var compressed = CreateCompressionStream(memStream))
|
||||
await source.CopyToAsync(compressed).ConfigureAwait(false);
|
||||
memStream.Seek(0, SeekOrigin.Begin);
|
||||
await memStream.CopyToAsync(destination).ConfigureAwait(false);
|
||||
return memStream.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<long> DecompressAsync(Stream source, Stream destination)
|
||||
{
|
||||
using (var memStream = new MemoryStream())
|
||||
{
|
||||
using (var decompressed = CreateDecompressionStream(source))
|
||||
await decompressed.CopyToAsync(memStream).ConfigureAwait(false);
|
||||
memStream.Seek(0, SeekOrigin.Begin);
|
||||
await memStream.CopyToAsync(destination).ConfigureAwait(false);
|
||||
return memStream.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
CompatApiClient/Compression/DecompressedContent.cs
Normal file
54
CompatApiClient/Compression/DecompressedContent.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CompatApiClient.Compression
|
||||
{
|
||||
public class DecompressedContent : HttpContent
|
||||
{
|
||||
private readonly HttpContent content;
|
||||
private readonly ICompressor compressor;
|
||||
|
||||
public DecompressedContent(HttpContent content, ICompressor compressor)
|
||||
{
|
||||
if (content == null)
|
||||
throw new ArgumentNullException(nameof(content));
|
||||
|
||||
if (compressor == null)
|
||||
throw new ArgumentNullException(nameof(compressor));
|
||||
|
||||
this.content = content;
|
||||
this.compressor = compressor;
|
||||
RemoveHeaders();
|
||||
}
|
||||
|
||||
protected override bool TryComputeLength(out long length)
|
||||
{
|
||||
length = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
|
||||
{
|
||||
if (stream == null)
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
|
||||
using (content)
|
||||
{
|
||||
var contentStream = await content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
var decompressedLength = await compressor.DecompressAsync(contentStream, stream).ConfigureAwait(false);
|
||||
Headers.ContentLength = decompressedLength;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveHeaders()
|
||||
{
|
||||
foreach (var header in content.Headers)
|
||||
Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
Headers.ContentEncoding.Clear();
|
||||
Headers.ContentLength = null;
|
||||
}
|
||||
}
|
||||
}
|
20
CompatApiClient/Compression/DeflateCompressor.cs
Normal file
20
CompatApiClient/Compression/DeflateCompressor.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace CompatApiClient.Compression
|
||||
{
|
||||
public class DeflateCompressor : Compressor
|
||||
{
|
||||
public override string EncodingType => "deflate";
|
||||
|
||||
public override Stream CreateCompressionStream(Stream output)
|
||||
{
|
||||
return new DeflateStream(output, CompressionMode.Compress, true);
|
||||
}
|
||||
|
||||
public override Stream CreateDecompressionStream(Stream input)
|
||||
{
|
||||
return new DeflateStream(input, CompressionMode.Decompress, true);
|
||||
}
|
||||
}
|
||||
}
|
20
CompatApiClient/Compression/GZipCompressor.cs
Normal file
20
CompatApiClient/Compression/GZipCompressor.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace CompatApiClient.Compression
|
||||
{
|
||||
public class GZipCompressor : Compressor
|
||||
{
|
||||
public override string EncodingType => "gzip";
|
||||
|
||||
public override Stream CreateCompressionStream(Stream output)
|
||||
{
|
||||
return new GZipStream(output, CompressionMode.Compress, true);
|
||||
}
|
||||
|
||||
public override Stream CreateDecompressionStream(Stream input)
|
||||
{
|
||||
return new GZipStream(input, CompressionMode.Decompress, true);
|
||||
}
|
||||
}
|
||||
}
|
12
CompatApiClient/Compression/ICompressor.cs
Normal file
12
CompatApiClient/Compression/ICompressor.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CompatApiClient.Compression
|
||||
{
|
||||
public interface ICompressor
|
||||
{
|
||||
string EncodingType { get; }
|
||||
Task<long> CompressAsync(Stream source, Stream destination);
|
||||
Task<long> DecompressAsync(Stream source, Stream destination);
|
||||
}
|
||||
}
|
32
CompatApiClient/Formatters/JsonContractResolver.cs
Normal file
32
CompatApiClient/Formatters/JsonContractResolver.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace CompatApiClient
|
||||
{
|
||||
public class JsonContractResolver : DefaultContractResolver
|
||||
{
|
||||
private readonly Func<string, string> propertyNameResolver;
|
||||
|
||||
public JsonContractResolver()
|
||||
: this(NamingStyles.Dashed)
|
||||
{
|
||||
}
|
||||
|
||||
public JsonContractResolver(Func<string, string> propertyNameResolver)
|
||||
{
|
||||
if (propertyNameResolver == null)
|
||||
throw new ArgumentNullException(nameof(propertyNameResolver));
|
||||
|
||||
this.propertyNameResolver = propertyNameResolver;
|
||||
}
|
||||
|
||||
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
|
||||
{
|
||||
var property = base.CreateProperty(member, memberSerialization);
|
||||
property.PropertyName = propertyNameResolver(property.PropertyName);
|
||||
return property;
|
||||
}
|
||||
}
|
||||
}
|
58
CompatApiClient/Formatters/NamingStyles.cs
Normal file
58
CompatApiClient/Formatters/NamingStyles.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace CompatApiClient
|
||||
{
|
||||
public static class NamingStyles
|
||||
{
|
||||
public static string CamelCase(string value)
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
if (value.Length > 0)
|
||||
{
|
||||
if (char.IsUpper(value[0]))
|
||||
value = char.ToLower(value[0]) + value.Substring(1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static string Dashed(string value)
|
||||
{
|
||||
return Delimitied(value, '-');
|
||||
}
|
||||
|
||||
public static string Underscore(string value)
|
||||
{
|
||||
return Delimitied(value, '_');
|
||||
}
|
||||
|
||||
private static string Delimitied(string value, char separator)
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
if (value.Length == 0)
|
||||
return value;
|
||||
|
||||
var hasPrefix = true;
|
||||
var builder = new StringBuilder(value.Length + 3);
|
||||
foreach (var c in value)
|
||||
{
|
||||
var ch = c;
|
||||
if (char.IsUpper(ch))
|
||||
{
|
||||
ch = char.ToLower(ch);
|
||||
if (!hasPrefix)
|
||||
builder.Append(separator);
|
||||
hasPrefix = true;
|
||||
}
|
||||
else
|
||||
hasPrefix = false;
|
||||
builder.Append(ch);
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
32
CompatApiClient/POCOs/CompatResult.cs
Normal file
32
CompatApiClient/POCOs/CompatResult.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CompatApiClient.POCOs
|
||||
{
|
||||
public class CompatResult
|
||||
{
|
||||
public int ReturnCode;
|
||||
public string SearchTerm;
|
||||
public Dictionary<string, TitleInfo> Results;
|
||||
|
||||
[JsonIgnore]
|
||||
public TimeSpan RequestDuration;
|
||||
[JsonIgnore]
|
||||
public RequestBuilder RequestBuilder;
|
||||
}
|
||||
|
||||
public class TitleInfo
|
||||
{
|
||||
public static readonly TitleInfo Maintenance = new TitleInfo { Status = "Maintenance" };
|
||||
public static readonly TitleInfo CommunicationError = new TitleInfo { Status = "Error" };
|
||||
public static readonly TitleInfo Unknown = new TitleInfo { Status = "Unknown" };
|
||||
|
||||
public string Title;
|
||||
public string Status;
|
||||
public string Date;
|
||||
public int Thread;
|
||||
public string Commit;
|
||||
public int? Pr;
|
||||
}
|
||||
}
|
21
CompatApiClient/POCOs/PrInfo.cs
Normal file
21
CompatApiClient/POCOs/PrInfo.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CompatApiClient.POCOs
|
||||
{
|
||||
public class PrInfo
|
||||
{
|
||||
public int Number;
|
||||
public string Title;
|
||||
public GithubUser User;
|
||||
public int Additions;
|
||||
public int Deletions;
|
||||
public int ChangedFiles;
|
||||
}
|
||||
|
||||
public class GithubUser
|
||||
{
|
||||
public string login;
|
||||
}
|
||||
}
|
23
CompatApiClient/POCOs/UpdateInfo.cs
Normal file
23
CompatApiClient/POCOs/UpdateInfo.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
|
||||
namespace CompatApiClient.POCOs
|
||||
{
|
||||
public class UpdateInfo
|
||||
{
|
||||
public int ReturnCode;
|
||||
public BuildInfo LatestBuild;
|
||||
}
|
||||
|
||||
public class BuildInfo
|
||||
{
|
||||
public string Pr;
|
||||
public BuildLink Windows;
|
||||
public BuildLink Linux;
|
||||
}
|
||||
|
||||
public class BuildLink
|
||||
{
|
||||
public string Datetime;
|
||||
public string Download;
|
||||
}
|
||||
}
|
122
CompatApiClient/RequestBuilder.cs
Normal file
122
CompatApiClient/RequestBuilder.cs
Normal file
@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CompatApiClient
|
||||
{
|
||||
public class RequestBuilder
|
||||
{
|
||||
public string customHeader { get; private set; }
|
||||
public string search { get; private set; }
|
||||
private int? status;
|
||||
private string start;
|
||||
private string sort;
|
||||
private string date;
|
||||
public char? releaseType { get; private set; }
|
||||
public char? region { get; private set; }
|
||||
private int amount = ApiConfig.ResultAmount[0];
|
||||
public int amountRequested { get; private set; } = ApiConfig.ResultAmount[0];
|
||||
|
||||
private RequestBuilder()
|
||||
{
|
||||
}
|
||||
|
||||
public static RequestBuilder Start()
|
||||
{
|
||||
return new RequestBuilder();
|
||||
}
|
||||
|
||||
public RequestBuilder SetSearch(string search)
|
||||
{
|
||||
this.search = search;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder SetHeader(string header)
|
||||
{
|
||||
this.customHeader = header;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder SetStatus(string status)
|
||||
{
|
||||
if (ApiConfig.Statuses.TryGetValue(status, out var statusCode))
|
||||
this.status = statusCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder SetStartsWith(string prefix)
|
||||
{
|
||||
if (prefix == "num" || prefix == "09")
|
||||
start = "09";
|
||||
else if (prefix == "sym" || prefix == "#")
|
||||
start = "sym";
|
||||
else if (prefix?.Length == 1)
|
||||
start = prefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder SetSort(string type, string direction)
|
||||
{
|
||||
if (ApiConfig.SortTypes.TryGetValue(type, out var sortType) && ApiConfig.ReverseDirections.TryGetValue(direction, out var dir))
|
||||
sort = sortType.ToString() + dir;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder SetDate(string date)
|
||||
{
|
||||
if (DateTime.TryParseExact(date, ApiConfig.DateInputFormat, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out var parsedDate))
|
||||
this.date = parsedDate.ToString(ApiConfig.DateQueryFormat);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder SetReleaseType(string type)
|
||||
{
|
||||
if (ApiConfig.ReverseReleaseTypes.TryGetValue(type, out var releaseType))
|
||||
this.releaseType = releaseType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder SetRegion(string region)
|
||||
{
|
||||
if (ApiConfig.ReverseRegions.TryGetValue(region, out var regionCode))
|
||||
this.region = regionCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder SetAmount(int amount)
|
||||
{
|
||||
if (amount < 1)
|
||||
return this;
|
||||
|
||||
foreach (var bracket in ApiConfig.ResultAmount)
|
||||
{
|
||||
if (amount <= bracket)
|
||||
{
|
||||
this.amount = bracket;
|
||||
this.amountRequested = amount;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Uri Build(bool apiCall = true)
|
||||
{
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{"g", search},
|
||||
{"s", status?.ToString()},
|
||||
{"c", start},
|
||||
{"o", sort},
|
||||
{"d", date},
|
||||
{"t", releaseType?.ToString()},
|
||||
{"f", region?.ToString()},
|
||||
{"r", amount.ToString()},
|
||||
};
|
||||
if (apiCall)
|
||||
parameters["api"] = "v" + ApiConfig.Version;
|
||||
return ApiConfig.BaseUrl.SetQueryParameters(parameters);
|
||||
}
|
||||
}
|
||||
}
|
110
CompatApiClient/Utils/UriExtensions.cs
Normal file
110
CompatApiClient/Utils/UriExtensions.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
namespace CompatApiClient
|
||||
{
|
||||
public static class UriExtensions
|
||||
{
|
||||
private static readonly Uri FakeHost = new Uri("sc://q"); // s:// will be parsed as file:///s:// for some reason
|
||||
|
||||
public static NameValueCollection ParseQueryString(Uri uri)
|
||||
{
|
||||
if (!uri.IsAbsoluteUri)
|
||||
uri = new Uri(FakeHost, uri);
|
||||
return uri.ParseQueryString();
|
||||
}
|
||||
|
||||
public static string GetQueryParameter(this Uri uri, string name)
|
||||
{
|
||||
var parameters = ParseQueryString(uri);
|
||||
return parameters[name];
|
||||
}
|
||||
|
||||
public static Uri AddQueryParameter(this Uri uri, string name, string value)
|
||||
{
|
||||
var queryValue = WebUtility.UrlEncode(name) + "=" + WebUtility.UrlEncode(value);
|
||||
return AddQueryValue(uri, queryValue);
|
||||
}
|
||||
|
||||
public static Uri AddQueryParameters(Uri uri, IEnumerable<KeyValuePair<string, string>> parameters)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
if (builder.Length > 0)
|
||||
builder.Append('&');
|
||||
builder.Append(Uri.EscapeDataString(param.Key));
|
||||
builder.Append('=');
|
||||
builder.Append(Uri.EscapeDataString(param.Value));
|
||||
}
|
||||
return AddQueryValue(uri, builder.ToString());
|
||||
}
|
||||
|
||||
public static Uri SetQueryParameter(this Uri uri, string name, string value)
|
||||
{
|
||||
var parameters = ParseQueryString(uri);
|
||||
parameters[name] = value;
|
||||
return SetQueryValue(uri, FormatUriParams(parameters));
|
||||
}
|
||||
|
||||
public static Uri SetQueryParameters(this Uri uri, IEnumerable<KeyValuePair<string, string>> items)
|
||||
{
|
||||
var parameters = ParseQueryString(uri);
|
||||
foreach (var item in items)
|
||||
parameters[item.Key] = item.Value;
|
||||
return SetQueryValue(uri, FormatUriParams(parameters));
|
||||
}
|
||||
|
||||
public static string FormatUriParams(NameValueCollection parameters)
|
||||
{
|
||||
if (parameters == null || parameters.Count == 0)
|
||||
return "";
|
||||
|
||||
var result = new StringBuilder();
|
||||
foreach (var key in parameters.AllKeys)
|
||||
{
|
||||
var value = parameters[key];
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
result.AppendFormat("&{0}={1}", Uri.EscapeDataString(key), Uri.EscapeDataString(value));
|
||||
}
|
||||
if (result.Length == 0)
|
||||
return "";
|
||||
return result.ToString(1, result.Length - 1);
|
||||
}
|
||||
|
||||
private static Uri AddQueryValue(Uri uri, string queryToAppend)
|
||||
{
|
||||
var query = uri.IsAbsoluteUri ? uri.Query : new Uri(FakeHost, uri).Query;
|
||||
if (!string.IsNullOrEmpty(query) && query.Length > 1)
|
||||
query = query.Substring(1) + "&" + queryToAppend;
|
||||
else
|
||||
query = queryToAppend;
|
||||
return SetQueryValue(uri, query);
|
||||
}
|
||||
|
||||
private static Uri SetQueryValue(Uri uri, string value)
|
||||
{
|
||||
var isAbsolute = uri.IsAbsoluteUri;
|
||||
if (isAbsolute)
|
||||
{
|
||||
var builder = new UriBuilder(uri) { Query = value };
|
||||
return new Uri(builder.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
var startWithSlash = uri.OriginalString.StartsWith("/");
|
||||
uri = new Uri(FakeHost, uri);
|
||||
var builder = new UriBuilder(uri) { Query = value };
|
||||
var additionalStrip = startWithSlash ? 0 : 1;
|
||||
var newUri = builder.ToString().Substring(FakeHost.OriginalString.Length + additionalStrip);
|
||||
return new Uri(newUri, UriKind.Relative);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
44
CompatApiClient/Utils/Utils.cs
Normal file
44
CompatApiClient/Utils/Utils.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
|
||||
namespace CompatApiClient
|
||||
{
|
||||
public static class Utils
|
||||
{
|
||||
public static string Trim(this string str, int maxLength)
|
||||
{
|
||||
const int minSaneLimit = 4;
|
||||
|
||||
if (maxLength < minSaneLimit)
|
||||
throw new ArgumentException("Argument cannot be less than " + minSaneLimit, nameof(maxLength));
|
||||
|
||||
if (string.IsNullOrEmpty(str))
|
||||
return str;
|
||||
|
||||
if (str.Length > maxLength)
|
||||
return str.Substring(0, maxLength - 3) + "...";
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
public static string Truncate(this string str, int maxLength)
|
||||
{
|
||||
if (maxLength < 1)
|
||||
throw new ArgumentException("Argument must be positive, but was " + maxLength, nameof(maxLength));
|
||||
|
||||
if (string.IsNullOrEmpty(str) || str.Length <= maxLength)
|
||||
return str;
|
||||
|
||||
return str.Substring(0, maxLength);
|
||||
}
|
||||
|
||||
public static string Sanitize(this string str)
|
||||
{
|
||||
return str?.Replace("`", "`\u200d").Replace("@", "@\u200d");
|
||||
}
|
||||
|
||||
public static int Clamp(this int amount, int low, int high)
|
||||
{
|
||||
return Math.Min(high, Math.Max(amount, low));
|
||||
}
|
||||
}
|
||||
}
|
38
CompatBot/Attributes/CheckBaseAttributeWithReactions.cs
Normal file
38
CompatBot/Attributes/CheckBaseAttributeWithReactions.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System.Threading.Tasks;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.CommandsNext.Attributes;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.Attributes
|
||||
{
|
||||
internal abstract class CheckBaseAttributeWithReactions: CheckBaseAttribute
|
||||
{
|
||||
protected abstract Task<bool> IsAllowed(CommandContext ctx, bool help);
|
||||
|
||||
public DiscordEmoji ReactOnSuccess { get; }
|
||||
public DiscordEmoji ReactOnFailure { get; }
|
||||
|
||||
public CheckBaseAttributeWithReactions(DiscordEmoji reactOnSuccess = null, DiscordEmoji reactOnFailure = null)
|
||||
{
|
||||
ReactOnSuccess = reactOnSuccess;
|
||||
ReactOnFailure = reactOnFailure;
|
||||
}
|
||||
|
||||
public override async Task<bool> ExecuteCheckAsync(CommandContext ctx, bool help)
|
||||
{
|
||||
var result = await IsAllowed(ctx, help);
|
||||
//await ctx.RespondAsync($"Check for {GetType().Name} resulted in {result}").ConfigureAwait(false);
|
||||
if (result)
|
||||
{
|
||||
if (ReactOnSuccess != null && !help)
|
||||
await ctx.Message.CreateReactionAsync(ReactOnSuccess).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ReactOnFailure != null && !help)
|
||||
await ctx.Message.CreateReactionAsync(ReactOnFailure).ConfigureAwait(false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
18
CompatBot/Attributes/RequiresBotModRole.cs
Normal file
18
CompatBot/Attributes/RequiresBotModRole.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Providers;
|
||||
using DSharpPlus.CommandsNext;
|
||||
|
||||
namespace CompatBot.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
|
||||
internal class RequiresBotModRole: CheckBaseAttributeWithReactions
|
||||
{
|
||||
public RequiresBotModRole() : base(reactOnFailure: Config.Reactions.Denied) { }
|
||||
|
||||
protected override Task<bool> IsAllowed(CommandContext ctx, bool help)
|
||||
{
|
||||
return Task.FromResult(ModProvider.IsMod(ctx.User.Id));
|
||||
}
|
||||
}
|
||||
}
|
18
CompatBot/Attributes/RequiresBotSudoerRole.cs
Normal file
18
CompatBot/Attributes/RequiresBotSudoerRole.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Providers;
|
||||
using DSharpPlus.CommandsNext;
|
||||
|
||||
namespace CompatBot.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
|
||||
internal class RequiresBotSudoerRole: CheckBaseAttributeWithReactions
|
||||
{
|
||||
public RequiresBotSudoerRole(): base(reactOnFailure: Config.Reactions.Denied) { }
|
||||
|
||||
protected override Task<bool> IsAllowed(CommandContext ctx, bool help)
|
||||
{
|
||||
return Task.FromResult(ModProvider.IsSudoer(ctx.User.Id));
|
||||
}
|
||||
}
|
||||
}
|
20
CompatBot/Attributes/RequiresDm.cs
Normal file
20
CompatBot/Attributes/RequiresDm.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.CommandsNext.Attributes;
|
||||
|
||||
namespace CompatBot.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
|
||||
internal class RequiresDm: CheckBaseAttribute
|
||||
{
|
||||
public override async Task<bool> ExecuteCheckAsync(CommandContext ctx, bool help)
|
||||
{
|
||||
if (ctx.Channel.IsPrivate || help)
|
||||
return true;
|
||||
|
||||
await ctx.RespondAsync($"{ctx.Message.Author.Mention} https://i.imgflip.com/24qx11.jpg").ConfigureAwait(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
71
CompatBot/Commands/Antipiracy.cs
Normal file
71
CompatBot/Commands/Antipiracy.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Attributes;
|
||||
using CompatBot.Database;
|
||||
using CompatBot.Providers;
|
||||
using CompatBot.Utils;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.CommandsNext.Attributes;
|
||||
using DSharpPlus.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CompatBot.Commands
|
||||
{
|
||||
[Group("piracy"), RequiresBotModRole, RequiresDm]
|
||||
[Description("Used to manage piracy filters **in DM**")]
|
||||
internal sealed class Antipiracy: BaseCommandModule
|
||||
{
|
||||
[Command("list"), Aliases("show")]
|
||||
[Description("Lists all filters")]
|
||||
public async Task List(CommandContext ctx)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
var result = new StringBuilder("```")
|
||||
.AppendLine("ID | Trigger")
|
||||
.AppendLine("-----------------------------");
|
||||
foreach (var item in await BotDb.Instance.Piracystring.ToListAsync().ConfigureAwait(false))
|
||||
result.AppendLine($"{item.Id:0000} | {item.String}");
|
||||
await ctx.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false);
|
||||
await typingTask;
|
||||
}
|
||||
|
||||
[Command("add")]
|
||||
[Description("Adds a new piracy filter trigger")]
|
||||
public async Task Add(CommandContext ctx, [RemainingText, Description("A plain string to match")] string trigger)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
var wasSuccessful = await PiracyStringProvider.AddAsync(trigger).ConfigureAwait(false);
|
||||
(DiscordEmoji reaction, string msg) result = wasSuccessful
|
||||
? (Config.Reactions.Success, "New trigger successfully saved!")
|
||||
: (Config.Reactions.Failure, "Trigger already defined.");
|
||||
await Task.WhenAll(
|
||||
ctx.RespondAsync(result.msg),
|
||||
ctx.Message.CreateReactionAsync(result.reaction),
|
||||
typingTask
|
||||
).ConfigureAwait(false);
|
||||
if (wasSuccessful)
|
||||
await List(ctx).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("remove"), Aliases("delete", "del")]
|
||||
[Description("Removes a piracy filter trigger")]
|
||||
public async Task Remove(CommandContext ctx, [Description("Filter ids to remove separated with spaces")] params int[] ids)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
(DiscordEmoji reaction, string msg) result = (Config.Reactions.Success, $"Trigger{(ids.Length == 1 ? "" : "s")} successfully removed!");
|
||||
var failedIds = new List<int>();
|
||||
foreach (var id in ids)
|
||||
if (!await PiracyStringProvider.RemoveAsync(id).ConfigureAwait(false))
|
||||
failedIds.Add(id);
|
||||
if (failedIds.Count > 0)
|
||||
result = (Config.Reactions.Failure, "Some ids couldn't be removed: " + string.Join(", ", failedIds));
|
||||
await Task.WhenAll(
|
||||
ctx.RespondAsync(result.msg),
|
||||
ctx.Message.CreateReactionAsync(result.reaction),
|
||||
typingTask
|
||||
).ConfigureAwait(false);
|
||||
await List(ctx).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
180
CompatBot/Commands/CompatList.cs
Normal file
180
CompatBot/Commands/CompatList.cs
Normal file
@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.POCOs;
|
||||
using CompatBot.ResultFormatters;
|
||||
using CompatBot.Utils;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.CommandsNext.Attributes;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.Commands
|
||||
{
|
||||
internal sealed class CompatList : BaseCommandModule
|
||||
{
|
||||
private static readonly Client client = new Client();
|
||||
|
||||
[Command("compat"), Aliases("c")]
|
||||
[Description("Searches the compatibility database, USE: !compat searchterm")]
|
||||
public async Task Compat(CommandContext ctx, [RemainingText, Description("Game title to look up")] string title)
|
||||
{
|
||||
try
|
||||
{
|
||||
var requestBuilder = RequestBuilder.Start().SetSearch(title?.Truncate(40));
|
||||
await DoRequestAndRespond(ctx, requestBuilder).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ctx.Client.DebugLogger.LogMessage(LogLevel.Error, "asdf", e.Message, DateTime.Now);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("top")]
|
||||
[Description(@"
|
||||
Gets the x (default is 10 new) top games by specified criteria; order is flexible
|
||||
Example usage:
|
||||
!top 10 new
|
||||
!top 10 new jpn
|
||||
!top 10 playable
|
||||
!top 10 new ingame eu
|
||||
!top 10 old psn intro
|
||||
!top 10 old loadable us bluray")]
|
||||
public async Task Top(CommandContext ctx, [Description("To see all filters do !filters")] params string[] filters)
|
||||
{
|
||||
var requestBuilder = RequestBuilder.Start();
|
||||
var age = "new";
|
||||
var amount = 10;
|
||||
foreach (var term in filters.Select(s => s.ToLowerInvariant()))
|
||||
{
|
||||
switch (term)
|
||||
{
|
||||
case "old": case "new":
|
||||
age = term;
|
||||
break;
|
||||
case string status when ApiConfig.Statuses.ContainsKey(status):
|
||||
requestBuilder.SetStatus(status);
|
||||
break;
|
||||
case string rel when ApiConfig.ReverseReleaseTypes.ContainsKey(rel):
|
||||
requestBuilder.SetReleaseType(rel);
|
||||
break;
|
||||
case string reg when ApiConfig.ReverseRegions.ContainsKey(reg):
|
||||
requestBuilder.SetRegion(reg);
|
||||
break;
|
||||
case string num when int.TryParse(num, out var newAmount):
|
||||
amount = newAmount.Clamp(1, Config.TopLimit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
requestBuilder.SetAmount(amount);
|
||||
if (age == "old")
|
||||
{
|
||||
requestBuilder.SetSort("date", "asc");
|
||||
requestBuilder.SetHeader("{0} requested top {1} oldest {2} {3} updated games");
|
||||
}
|
||||
else
|
||||
{
|
||||
requestBuilder.SetSort("date", "desc");
|
||||
requestBuilder.SetHeader("{0} requested top {1} newest {2} {3} updated games");
|
||||
}
|
||||
await DoRequestAndRespond(ctx, requestBuilder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("filters")]
|
||||
[Description("Provides information about available filters for the !top command")]
|
||||
public async Task Filters(CommandContext ctx)
|
||||
{
|
||||
var getDmTask = ctx.CreateDmAsync();
|
||||
if (ctx.Channel.IsPrivate)
|
||||
await ctx.TriggerTypingAsync().ConfigureAwait(false);
|
||||
var embed = new DiscordEmbedBuilder {Description = "List of recognized tokens in each filter category", Color = Config.Colors.Help}
|
||||
.AddField("Regions", DicToDesc(ApiConfig.Regions))
|
||||
.AddField("Statuses", DicToDesc(ApiConfig.Statuses))
|
||||
.AddField("Release types", DicToDesc(ApiConfig.ReleaseTypes))
|
||||
.Build();
|
||||
var dm = await getDmTask.ConfigureAwait(false);
|
||||
await dm.SendMessageAsync(embed: embed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("latest"), Aliases("download")]
|
||||
[Description("Provides links to the latest RPCS3 build")]
|
||||
public async Task Latest(CommandContext ctx)
|
||||
{
|
||||
var getDmTask = ctx.CreateDmAsync();
|
||||
if (ctx.Channel.IsPrivate)
|
||||
await ctx.TriggerTypingAsync().ConfigureAwait(false);
|
||||
var info = await client.GetUpdateAsync(Config.Cts.Token).ConfigureAwait(false);
|
||||
var embed = await info.AsEmbedAsync().ConfigureAwait(false);
|
||||
var dm = await getDmTask.ConfigureAwait(false);
|
||||
await dm.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string DicToDesc(Dictionary<char, string[]> dictionary)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
foreach (var lst in dictionary.Values)
|
||||
result.AppendLine(string.Join(", ", lst.Reverse()));
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private static string DicToDesc(Dictionary<string, int> dictionary)
|
||||
{
|
||||
return string.Join(", ", dictionary.Keys);
|
||||
}
|
||||
|
||||
private async Task DoRequestAndRespond(CommandContext ctx, RequestBuilder requestBuilder)
|
||||
{
|
||||
var botChannelTask = ctx.Client.GetChannelAsync(Config.BotChannelId);
|
||||
Console.WriteLine(requestBuilder.Build());
|
||||
var result = await client.GetCompatResultAsync(requestBuilder, Config.Cts.Token).ConfigureAwait(false);
|
||||
var botChannel = await botChannelTask.ConfigureAwait(false);
|
||||
foreach (var msg in FormatSearchResults(ctx, result))
|
||||
await botChannel.SendAutosplitMessageAsync(msg).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private IEnumerable<string> FormatSearchResults(CommandContext ctx, CompatResult compatResult)
|
||||
{
|
||||
var returnCode = ApiConfig.ReturnCodes[compatResult.ReturnCode];
|
||||
var request = compatResult.RequestBuilder;
|
||||
|
||||
if (returnCode.overrideAll)
|
||||
yield return string.Format(returnCode.info, ctx.Message.Author.Mention);
|
||||
else
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
if (string.IsNullOrEmpty(request.customHeader))
|
||||
result.AppendLine($"{ctx.Message.Author.Mention} searched for: ***{request.search.Sanitize()}***");
|
||||
else
|
||||
{
|
||||
string[] region = null, media = null;
|
||||
if (request.region.HasValue) ApiConfig.Regions.TryGetValue(request.region.Value, out region);
|
||||
if (request.releaseType.HasValue) ApiConfig.Regions.TryGetValue(request.releaseType.Value, out media);
|
||||
var formattedHeader = string.Format(request.customHeader, ctx.Message.Author.Mention, request.amountRequested, region?.Last(), media?.Last());
|
||||
result.AppendLine(formattedHeader.Replace(" ", " ").Replace(" ", " "));
|
||||
}
|
||||
result.AppendFormat(returnCode.info, compatResult.SearchTerm);
|
||||
yield return result.ToString();
|
||||
result.Clear();
|
||||
var footer = $"Retrieved from: *<{request.Build(false).ToString().Replace(' ', '+')}>* in {compatResult.RequestDuration.TotalMilliseconds:0} milliseconds!";
|
||||
|
||||
if (returnCode.displayResults)
|
||||
{
|
||||
result.Append("```");
|
||||
foreach (var resultInfo in compatResult.Results.Take(request.amountRequested))
|
||||
{
|
||||
var info = resultInfo.AsString();
|
||||
result.AppendLine(info);
|
||||
}
|
||||
result.Append("```");
|
||||
yield return result.ToString();
|
||||
yield return footer;
|
||||
}
|
||||
else if (returnCode.displayFooter)
|
||||
yield return footer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
194
CompatBot/Commands/Explain.cs
Normal file
194
CompatBot/Commands/Explain.cs
Normal file
@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Attributes;
|
||||
using CompatBot.Database;
|
||||
using CompatBot.Utils;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.CommandsNext.Attributes;
|
||||
using DSharpPlus.CommandsNext.Converters;
|
||||
using DSharpPlus.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CompatBot.Commands
|
||||
{
|
||||
[Group("explain"), Aliases("botsplain", "define")]
|
||||
//[Cooldown(1, 1, CooldownBucketType.Channel)]
|
||||
[Description("Used to manage and show explanations")]
|
||||
internal sealed class Explain: BaseCommandModule
|
||||
{
|
||||
[GroupCommand]
|
||||
public async Task ShowExplanation(CommandContext ctx, [RemainingText, Description("Term to explain")] string term)
|
||||
{
|
||||
await ctx.TriggerTypingAsync().ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(term))
|
||||
{
|
||||
await ctx.RespondAsync($"You may want to look at available terms by using `{Config.CommandPrefix}explain list`").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var explanation = await BotDb.Instance.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
|
||||
if (explanation != null)
|
||||
{
|
||||
await ctx.RespondAsync(explanation.Text).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
term = term.StripQuotes();
|
||||
var idx = term.LastIndexOf(" to ", StringComparison.InvariantCultureIgnoreCase);
|
||||
if (idx > 0)
|
||||
{
|
||||
var potentialUserId = term.Substring(idx + 4).Trim();
|
||||
bool hasMention = false;
|
||||
try
|
||||
{
|
||||
var lookup = await new DiscordUserConverter().ConvertAsync(potentialUserId, ctx).ConfigureAwait(false);
|
||||
hasMention = lookup.HasValue;
|
||||
}
|
||||
catch { }
|
||||
if (hasMention)
|
||||
{
|
||||
term = term.Substring(0, idx).TrimEnd();
|
||||
explanation = await BotDb.Instance.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
|
||||
if (explanation != null)
|
||||
{
|
||||
await ctx.RespondAsync(explanation.Text).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.Message.CreateReactionAsync(Config.Reactions.Failure).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("add")]
|
||||
[Description("Adds a new explanation to the list")]
|
||||
public async Task Add(CommandContext ctx,
|
||||
[Description("A term to explain. Quote it if it contains spaces")] string term,
|
||||
[RemainingText, Description("Explanation text")] string explanation)
|
||||
{
|
||||
term = term.StripQuotes();
|
||||
if (string.IsNullOrEmpty(explanation))
|
||||
{
|
||||
await Task.WhenAll(
|
||||
ctx.Message.CreateReactionAsync(Config.Reactions.Failure),
|
||||
ctx.RespondAsync("An explanation for the term must be provided")
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
else if (await BotDb.Instance.Explanation.AnyAsync(e => e.Keyword == term).ConfigureAwait(false))
|
||||
await Task.WhenAll(
|
||||
ctx.Message.CreateReactionAsync(Config.Reactions.Failure),
|
||||
ctx.RespondAsync($"'{term}' is already defined. Use `update` to update an existing term.")
|
||||
).ConfigureAwait(false);
|
||||
else
|
||||
{
|
||||
await BotDb.Instance.Explanation.AddAsync(new Explanation {Keyword = term, Text = explanation}).ConfigureAwait(false);
|
||||
await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false);
|
||||
await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("update"), Aliases("replace"), RequiresBotModRole]
|
||||
[Description("Update explanation for a given term")]
|
||||
public async Task Update(CommandContext ctx,
|
||||
[Description("A term to update. Quote it if it contains spaces")] string term,
|
||||
[RemainingText, Description("New explanation text")] string explanation)
|
||||
{
|
||||
term = term.StripQuotes();
|
||||
var item = await BotDb.Instance.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
|
||||
if (item == null)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
ctx.Message.CreateReactionAsync(Config.Reactions.Failure),
|
||||
ctx.RespondAsync($"Term '{term}' is not defined")
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Text = explanation;
|
||||
await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false);
|
||||
await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("rename"), Priority(10), RequiresBotModRole]
|
||||
public async Task Rename(CommandContext ctx,
|
||||
[Description("A term to rename. Remember quotes if it contains spaces")] string oldTerm,
|
||||
[Description("New term. Again, quotes")] string newTerm)
|
||||
{
|
||||
oldTerm = oldTerm.StripQuotes();
|
||||
newTerm = newTerm.StripQuotes();
|
||||
var item = await BotDb.Instance.Explanation.FirstOrDefaultAsync(e => e.Keyword == oldTerm).ConfigureAwait(false);
|
||||
if (item == null)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
ctx.Message.CreateReactionAsync(Config.Reactions.Failure),
|
||||
ctx.RespondAsync($"Term '{oldTerm}' is not defined")
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Keyword = newTerm;
|
||||
await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false);
|
||||
await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("rename"), Priority(1), RequiresBotModRole]
|
||||
[Description("Renames a term in case you misspelled it or something")]
|
||||
public async Task Rename(CommandContext ctx,
|
||||
[Description("A term to rename. Remember quotes if it contains spaces")] string oldTerm,
|
||||
[Description("Constant \"to'")] string to,
|
||||
[Description("New term. Again, quotes")] string newTerm)
|
||||
{
|
||||
if ("to".Equals(to, StringComparison.InvariantCultureIgnoreCase))
|
||||
await Rename(ctx, oldTerm, newTerm).ConfigureAwait(false);
|
||||
else
|
||||
await ctx.Message.CreateReactionAsync(Config.Reactions.Failure).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("list")]
|
||||
[Description("List all known terms that could be used for !explain command")]
|
||||
public async Task List(CommandContext ctx)
|
||||
{
|
||||
await ctx.TriggerTypingAsync().ConfigureAwait(false);
|
||||
var keywords = await BotDb.Instance.Explanation.Select(e => e.Keyword).OrderBy(t => t).ToListAsync().ConfigureAwait(false);
|
||||
if (keywords.Count == 0)
|
||||
await ctx.RespondAsync("Nothing has been defined yet").ConfigureAwait(false);
|
||||
else
|
||||
try
|
||||
{
|
||||
foreach (var embed in new EmbedPager().BreakInEmbeds(new DiscordEmbedBuilder {Title = "Defined terms", Color = Config.Colors.Help}, keywords))
|
||||
await ctx.RespondAsync(embed: embed).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ctx.Client.DebugLogger.LogMessage(LogLevel.Error, "", e.ToString(), DateTime.Now);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Command("remove"), Aliases("delete", "del"), RequiresBotModRole]
|
||||
[Description("Removes an explanation from the definition list")]
|
||||
public async Task Remove(CommandContext ctx, [RemainingText, Description("Term to remove")] string term)
|
||||
{
|
||||
term = term.StripQuotes();
|
||||
var item = await BotDb.Instance.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
|
||||
if (item == null)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
ctx.Message.CreateReactionAsync(Config.Reactions.Failure),
|
||||
ctx.RespondAsync($"Term '{term}' is not defined")
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
BotDb.Instance.Explanation.Remove(item);
|
||||
await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false);
|
||||
await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
128
CompatBot/Commands/Misc.cs
Normal file
128
CompatBot/Commands/Misc.cs
Normal file
@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.CommandsNext.Attributes;
|
||||
using DSharpPlus.Entities;
|
||||
using org.mariuszgromada.math.mxparser;
|
||||
|
||||
namespace CompatBot.Commands
|
||||
{
|
||||
internal sealed class Misc: BaseCommandModule
|
||||
{
|
||||
private readonly Random rng = new Random();
|
||||
|
||||
private static readonly List<string> EightBallAnswers = new List<string> {
|
||||
"Nah mate", "Ya fo sho", "Fo shizzle mah nizzle", "Yuuuup", "Nope", "Njet", "Da", "Maybe", "I don't know",
|
||||
"I don't care", "Affirmative", "Sure", "Yeah, why not", "Most likely", "Sim", "Oui", "Heck yeah!", "Roger that",
|
||||
"Aye!", "Yes without a doubt m8!", "Who cares", "Maybe yes, maybe not", "Maybe not, maybe yes", "Ugh",
|
||||
"Probably", "Ask again later", "Error 404: answer not found", "Don't ask me that again",
|
||||
"You should think twice before asking", "You what now?", "Bloody hell, answering that ain't so easy",
|
||||
"Of course not", "Seriously no", "Noooooooooo", "Most likely not", "Não", "Non", "Hell no", "Absolutely not",
|
||||
"Ask Neko", "Ask Ani", "I'm pretty sure that's illegal!", "<:cell_ok_hand:324618647857397760>",
|
||||
"Don't be an idiot. YES.", "What do *you* think?", "Only on Wednesdays", "Look in the mirror, you know the answer already"
|
||||
};
|
||||
|
||||
private static readonly List<string> RateAnswers = new List<string>
|
||||
{
|
||||
"Bad", "Very bad", "Pretty bad", "Horrible", "Ugly", "Disgusting", "Literally the worst",
|
||||
"Not interesting", "Simply ugh", "I don't like it! You shouldn't either!", "Just like you, 💩",
|
||||
"Not approved", "Big Mistake", "Ask MsLow", "The opposite of good",
|
||||
"Could be better", "Could be worse", "Not so bad",
|
||||
"I likesss!", "Pretty good", "Guchi gud", "Amazing!", "Glorious!", "Very good", "Excellent...",
|
||||
"Magnificent", "Rate bot says he likes, so you like too",
|
||||
"If you reorganize the words it says \"pretty cool\"", "I approve",
|
||||
"I need more time to think about it", "It's ok, nothing and no one is perfect",
|
||||
"<:morgana_sparkle:315899996274688001> やるじゃねーか!", "Not half bad 👍", "🆗", "😐", "🤮", "Belissimo!",
|
||||
"So-so"
|
||||
};
|
||||
|
||||
[Command("credits"), Aliases("about")]
|
||||
[Description("Author Credit")]
|
||||
public async Task Credits(CommandContext ctx)
|
||||
{
|
||||
var embed = new DiscordEmbedBuilder
|
||||
{
|
||||
Title = "RPCS3 Compatibility Bot",
|
||||
Url = "https://github.com/RPCS3/discord-bot",
|
||||
Description = "Made by:\n" +
|
||||
"\tRoberto Anić Banić aka nicba1010\n" +
|
||||
"\t13xforever",
|
||||
Color = DiscordColor.Purple,
|
||||
};
|
||||
await ctx.RespondAsync(embed: embed.Build());
|
||||
}
|
||||
|
||||
[Command("math")]
|
||||
[Description("Math, here you go Juhn")]
|
||||
public async Task Math(CommandContext ctx, [RemainingText, Description("Math expression")] string expression)
|
||||
{
|
||||
var typing = ctx.TriggerTypingAsync();
|
||||
var result = @"Something went wrong ¯\\_(ツ)\_/¯" + "\nMath is hard, yo";
|
||||
try
|
||||
{
|
||||
var expr = new Expression(expression);
|
||||
result = expr.calculate().ToString();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ctx.Client.DebugLogger.LogMessage(LogLevel.Warning, "", "Math failed: " + e.Message, DateTime.Now);
|
||||
}
|
||||
await ctx.RespondAsync(result).ConfigureAwait(false);
|
||||
await typing.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("roll")]
|
||||
[Description("Generates a random number between 1 and N (default 10). Can also roll dices like `2d6`")]
|
||||
public async Task Roll(CommandContext ctx, [Description("Some positive number or a dice")] string something)
|
||||
{
|
||||
var result = "";
|
||||
switch (something)
|
||||
{
|
||||
case string val when int.TryParse(val, out var maxValue) && maxValue > 1:
|
||||
lock (rng) result = (rng.Next(maxValue) + 1).ToString();
|
||||
break;
|
||||
case string dice when Regex.IsMatch(dice, @"\d+d\d+"):
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
var diceParts = dice.Split('d', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (int.TryParse(diceParts[0], out var num) && int.TryParse(diceParts[1], out var face) &&
|
||||
0 < num && num < 101 &&
|
||||
1 < face && face < 1001)
|
||||
{
|
||||
List<int> rolls;
|
||||
lock (rng) rolls = Enumerable.Range(0, num).Select(_ => rng.Next(face) + 1).ToList();
|
||||
result = "Total: " + rolls.Sum();
|
||||
if (rolls.Count > 1)
|
||||
result += "\nRolls: " + string.Join(' ', rolls);
|
||||
}
|
||||
await typingTask.ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
if (string.IsNullOrEmpty(result))
|
||||
await ctx.Message.CreateReactionAsync(DiscordEmoji.FromUnicode("💩")).ConfigureAwait(false);
|
||||
else
|
||||
await ctx.RespondAsync(result).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("8ball")]
|
||||
[Description("Generates a ~~random~~ perfect answer to your question")]
|
||||
public async Task EightBall(CommandContext ctx, [RemainingText, Description("A yes/no question")] string question)
|
||||
{
|
||||
string answer;
|
||||
lock (rng) answer = EightBallAnswers[rng.Next(EightBallAnswers.Count)];
|
||||
await ctx.RespondAsync(answer).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("rate")]
|
||||
[Description("Gives an ~~unrelated~~ expert judgement on the matter at hand")]
|
||||
public async Task Rate(CommandContext ctx, [RemainingText, Description("Something to rate")] string whatever)
|
||||
{
|
||||
string answer;
|
||||
lock (rng) answer = RateAnswers[rng.Next(RateAnswers.Count)];
|
||||
await ctx.RespondAsync(answer).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
111
CompatBot/Commands/Sudo.Bot.cs
Normal file
111
CompatBot/Commands/Sudo.Bot.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.CommandsNext.Attributes;
|
||||
|
||||
namespace CompatBot.Commands
|
||||
{
|
||||
internal partial class Sudo
|
||||
{
|
||||
[Group("bot")]
|
||||
[Description("Commands to manage the bot instance")]
|
||||
public class Bot: BaseCommandModule
|
||||
{
|
||||
|
||||
[Command("version")]
|
||||
[Description("Returns currently checked out bot commit")]
|
||||
public async Task Version(CommandContext ctx)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
using (var git = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo("git", "log -1 --oneline")
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
},
|
||||
})
|
||||
{
|
||||
git.Start();
|
||||
var stdout = await git.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
|
||||
git.WaitForExit();
|
||||
if (!string.IsNullOrEmpty(stdout))
|
||||
await ctx.RespondAsync("```" + stdout + "```").ConfigureAwait(false);
|
||||
}
|
||||
await typingTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("restart"), Aliases("update")]
|
||||
[Description("Restarts bot and pulls newest commit")]
|
||||
public async Task Restart(CommandContext ctx)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
if (Monitor.TryEnter(updateObj))
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var git = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo("git", "pull")
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
},
|
||||
})
|
||||
{
|
||||
git.Start();
|
||||
var stdout = await git.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
|
||||
git.WaitForExit();
|
||||
if (!string.IsNullOrEmpty(stdout))
|
||||
await ctx.RespondAsync("```" + stdout + "```").ConfigureAwait(false);
|
||||
}
|
||||
await ctx.RespondAsync("Restarting...").ConfigureAwait(false);
|
||||
using (var self = new Process
|
||||
{
|
||||
#if DEBUG
|
||||
StartInfo = new ProcessStartInfo("dotnet", $"run -- {Config.Token} {ctx.Channel.Id}")
|
||||
#else
|
||||
StartInfo = new ProcessStartInfo("dotnet", $"run -c Release -- {Config.Token} {ctx.Channel.Id}")
|
||||
#endif
|
||||
})
|
||||
{
|
||||
self.Start();
|
||||
Config.Cts.Cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await ctx.RespondAsync("Updating failed: " + e.Message).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Monitor.Exit(updateObj);
|
||||
}
|
||||
}
|
||||
else
|
||||
await ctx.RespondAsync("Update is already in progress").ConfigureAwait(false);
|
||||
await typingTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("stop"), Aliases("exit", "shutdown", "terminate")]
|
||||
[Description("Stops the bot. Useful if you can't find where you left one running")]
|
||||
public async Task Stop(CommandContext ctx)
|
||||
{
|
||||
await ctx.RespondAsync(ctx.Channel.IsPrivate
|
||||
? $"Shutting down bot instance on {Environment.MachineName}..."
|
||||
: "Shutting down the bot..."
|
||||
).ConfigureAwait(false);
|
||||
Config.Cts.Cancel();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
118
CompatBot/Commands/Sudo.Mod.cs
Normal file
118
CompatBot/Commands/Sudo.Mod.cs
Normal file
@ -0,0 +1,118 @@
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Providers;
|
||||
using CompatBot.Utils;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.CommandsNext.Attributes;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.Commands
|
||||
{
|
||||
internal partial class Sudo
|
||||
{
|
||||
[Group("mod")]
|
||||
[Description("Used to manage bot moderators")]
|
||||
public class Mod : BaseCommandModule
|
||||
{
|
||||
[Command("add")]
|
||||
[Description("Adds a new moderator")]
|
||||
public async Task Add(CommandContext ctx, [Description("Discord user to add to the bot mod list")] DiscordMember user)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
(DiscordEmoji reaction, string msg) result =
|
||||
await ModProvider.AddAsync(user.Id).ConfigureAwait(false)
|
||||
? (Config.Reactions.Success, $"{user.Mention} was successfully added as moderator, you now have access to editing the piracy trigger list and other useful things! " +
|
||||
"I will send you the available commands to your message box!")
|
||||
: (Config.Reactions.Failure, $"{user.Mention} is already a moderator");
|
||||
await Task.WhenAll(
|
||||
ctx.Message.CreateReactionAsync(result.reaction),
|
||||
ctx.RespondAsync(result.msg),
|
||||
typingTask
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("remove"), Aliases("delete", "del")]
|
||||
[Description("Removes a moderator")]
|
||||
public async Task Remove(CommandContext ctx, [Description("Discord user to remove from the bot mod list")] DiscordMember user)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
(DiscordEmoji reaction, string msg) result;
|
||||
if (user.Id == Config.BotAdminId)
|
||||
{
|
||||
result = (Config.Reactions.Denied, $"{ctx.Message.Author.Mention} why would you even try this?! Alerting {user.Mention}");
|
||||
var dm = await user.CreateDmChannelAsync().ConfigureAwait(false);
|
||||
await dm.SendMessageAsync($@"Just letting you know that {ctx.Message.Author.Mention} just tried to strip you off of your mod role ¯\_(ツ)_/¯").ConfigureAwait(false);
|
||||
}
|
||||
else if (await ModProvider.RemoveAsync(user.Id).ConfigureAwait(false))
|
||||
result = (Config.Reactions.Success, $"{user.Mention} removed as moderator!");
|
||||
else
|
||||
result = (Config.Reactions.Failure, $"{user.Mention} is not a moderator");
|
||||
await Task.WhenAll(
|
||||
ctx.Message.CreateReactionAsync(result.reaction),
|
||||
ctx.RespondAsync(result.msg),
|
||||
typingTask
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("list"), Aliases("show")]
|
||||
[Description("Lists all moderators")]
|
||||
public async Task List(CommandContext ctx)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
var list = new StringBuilder("```");
|
||||
foreach (var mod in ModProvider.Mods.Values)
|
||||
list.AppendLine($"{await ctx.GetUserNameAsync(mod.DiscordId),-32} | {(mod.Sudoer ? "sudo" : "not sudo")}");
|
||||
await ctx.SendAutosplitMessageAsync(list.Append("```")).ConfigureAwait(false);
|
||||
await typingTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("sudo")]
|
||||
[Description("Makes a moderator a sudoer")]
|
||||
public async Task Sudo(CommandContext ctx, [Description("Discord user on the moderator list to grant the sudoer rights to")] DiscordMember moderator)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
(DiscordEmoji reaction, string msg) result;
|
||||
if (ModProvider.IsMod(moderator.Id))
|
||||
{
|
||||
result = await ModProvider.MakeSudoerAsync(moderator.Id).ConfigureAwait(false)
|
||||
? (Config.Reactions.Success, $"{moderator.Mention} is now a sudoer")
|
||||
: (Config.Reactions.Failure, $"{moderator.Mention} is already a sudoer");
|
||||
}
|
||||
else
|
||||
result = (Config.Reactions.Failure, $"{moderator.Mention} is not a moderator (yet)");
|
||||
await Task.WhenAll(
|
||||
ctx.Message.CreateReactionAsync(result.reaction),
|
||||
ctx.RespondAsync(result.msg),
|
||||
typingTask
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("unsudo")]
|
||||
[Description("Makes a sudoer a regular moderator")]
|
||||
public async Task Unsudo(CommandContext ctx, [Description("Discord user on the moderator list to strip the sudoer rights from")] DiscordMember sudoer)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
(DiscordEmoji reaction, string msg) result;
|
||||
if (sudoer.Id == Config.BotAdminId)
|
||||
{
|
||||
result = (Config.Reactions.Denied, $"{ctx.Message.Author.Mention} why would you even try this?! Alerting {sudoer.Mention}");
|
||||
var dm = await sudoer.CreateDmChannelAsync().ConfigureAwait(false);
|
||||
await dm.SendMessageAsync($@"Just letting you know that {ctx.Message.Author.Mention} just tried to strip you off of your sudoer permissions ¯\_(ツ)_/¯").ConfigureAwait(false);
|
||||
}
|
||||
else if (ModProvider.IsMod(sudoer.Id))
|
||||
{
|
||||
result = await ModProvider.UnmakeSudoerAsync(sudoer.Id).ConfigureAwait(false)
|
||||
? (Config.Reactions.Success, $"{sudoer.Mention} is no longer a sudoer")
|
||||
: (Config.Reactions.Failure, $"{sudoer.Mention} is not a sudoer");
|
||||
}
|
||||
else
|
||||
result = (Config.Reactions.Failure, $"{sudoer.Mention} is not even a moderator!");
|
||||
await Task.WhenAll(
|
||||
ctx.Message.CreateReactionAsync(result.reaction),
|
||||
ctx.RespondAsync(result.msg),
|
||||
typingTask
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
CompatBot/Commands/Sudo.cs
Normal file
33
CompatBot/Commands/Sudo.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Attributes;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.CommandsNext.Attributes;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.Commands
|
||||
{
|
||||
[Group("sudo"), RequiresBotSudoerRole]
|
||||
[Description("Used to manage bot moderators and sudoers")]
|
||||
internal sealed partial class Sudo : BaseCommandModule
|
||||
{
|
||||
private static readonly object updateObj = new object();
|
||||
|
||||
[Command("say"), Priority(10)]
|
||||
[Description("Make bot say things, optionally in a specific channel")]
|
||||
public async Task Say(CommandContext ctx, [Description("Discord channel (can use just #name in DM)")] DiscordChannel channel, [RemainingText, Description("Message text to send")] string message)
|
||||
{
|
||||
var typingTask = channel.TriggerTypingAsync();
|
||||
// simulate bot typing the message at 300 cps
|
||||
await Task.Delay(message.Length * 10 / 3).ConfigureAwait(false);
|
||||
await channel.SendMessageAsync(message).ConfigureAwait(false);
|
||||
await typingTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("say"), Priority(10)]
|
||||
[Description("Make bot say things, optionally in a specific channel")]
|
||||
public Task Say(CommandContext ctx, [RemainingText, Description("Message text to send")] string message)
|
||||
{
|
||||
return Say(ctx, ctx.Channel, message);
|
||||
}
|
||||
}
|
||||
}
|
118
CompatBot/Commands/Warnings.ListGroup.cs
Normal file
118
CompatBot/Commands/Warnings.ListGroup.cs
Normal file
@ -0,0 +1,118 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using CompatBot.Attributes;
|
||||
using CompatBot.Database;
|
||||
using CompatBot.Providers;
|
||||
using CompatBot.Utils;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.CommandsNext.Attributes;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.Commands
|
||||
{
|
||||
internal sealed partial class Warnings
|
||||
{
|
||||
[Group("list"), Aliases("show")]
|
||||
[Description("Allows to list warnings in various ways. Users can only see their own warnings.")]
|
||||
public class ListGroup : BaseCommandModule
|
||||
{
|
||||
[GroupCommand, Priority(10)]
|
||||
[Description("Show warning list for a user. Default is to show warning list for yourself")]
|
||||
public async Task List(CommandContext ctx, [Description("Discord user to warn")] DiscordUser user)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
if (await CheckListPermissionAsync(ctx, user.Id).ConfigureAwait(false))
|
||||
await ListUserWarningsAsync(ctx.Client, ctx.Message, user.Id, user.Username.Sanitize(), false);
|
||||
await typingTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[GroupCommand]
|
||||
public async Task List(CommandContext ctx, [Description("Id of the user to warn")] ulong userId)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
if (await CheckListPermissionAsync(ctx, userId).ConfigureAwait(false))
|
||||
await ListUserWarningsAsync(ctx.Client, ctx.Message, userId, $"<@{userId}>", false);
|
||||
await typingTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[GroupCommand]
|
||||
[Description("Show your own warning list")]
|
||||
public async Task List(CommandContext ctx)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
await List(ctx, ctx.Message.Author).ConfigureAwait(false);
|
||||
await typingTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("users"), RequiresBotModRole]
|
||||
[Description("List users with warnings, sorted from most warned to least")]
|
||||
public async Task Users(CommandContext ctx)
|
||||
{
|
||||
await ctx.TriggerTypingAsync().ConfigureAwait(false);
|
||||
var userIdColumn = ctx.Channel.IsPrivate ? $"{"User ID",-18} | " : "";
|
||||
var header = $"{"User",-25} | {userIdColumn}Count";
|
||||
var result = new StringBuilder("Warning count per user:").AppendLine("```")
|
||||
.AppendLine(header)
|
||||
.AppendLine("".PadLeft(header.Length, '-'));
|
||||
var query = from warn in BotDb.Instance.Warning
|
||||
group warn by warn.DiscordId into userGroup
|
||||
let row = new { discordId = userGroup.Key, count = userGroup.Count() }
|
||||
orderby row.count descending
|
||||
select row;
|
||||
foreach (var row in query)
|
||||
{
|
||||
var username = await ctx.GetUserNameAsync(row.discordId).ConfigureAwait(false);
|
||||
result.Append($"{username,-25} | ");
|
||||
if (ctx.Channel.IsPrivate)
|
||||
result.Append($"{row.discordId,-18} | ");
|
||||
result.AppendLine($"{row.count,2}");
|
||||
}
|
||||
await ctx.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("recent"), Aliases("last", "all"), RequiresBotModRole]
|
||||
[Description("Shows last issued warnings in chronological order")]
|
||||
public async Task Last(CommandContext ctx, [Description("Optional number of items to show. Default is 10")] int number = 10)
|
||||
{
|
||||
await ctx.TriggerTypingAsync().ConfigureAwait(false);
|
||||
if (number < 1)
|
||||
number = 10;
|
||||
var userIdColumn = ctx.Channel.IsPrivate ? $"{"User ID",-18} | " : "";
|
||||
var header = $"ID | {"User",-25} | {userIdColumn}{"Issued by",-25} | Reason ";
|
||||
var result = new StringBuilder("Last issued warnings:").AppendLine("```")
|
||||
.AppendLine(header)
|
||||
.AppendLine("".PadLeft(header.Length, '-'));
|
||||
var query = from warn in BotDb.Instance.Warning
|
||||
orderby warn.Id descending
|
||||
select warn;
|
||||
foreach (var row in query.Take(number))
|
||||
{
|
||||
var username = await ctx.GetUserNameAsync(row.DiscordId).ConfigureAwait(false);
|
||||
var modname = await ctx.GetUserNameAsync(row.IssuerId, defaultName: "Unknown mod").ConfigureAwait(false);
|
||||
result.Append($"{row.Id:00000} | {username,-25} | ");
|
||||
if (ctx.Channel.IsPrivate)
|
||||
result.Append($"{row.DiscordId,-18} | ");
|
||||
result.Append($"{modname,-25} | {row.Reason}");
|
||||
if (ctx.Channel.IsPrivate)
|
||||
result.Append(" | ").Append(row.FullReason);
|
||||
result.AppendLine();
|
||||
}
|
||||
await ctx.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<bool> CheckListPermissionAsync(CommandContext ctx, ulong userId)
|
||||
{
|
||||
if (userId == ctx.Message.Author.Id || ModProvider.IsMod(ctx.Message.Author.Id))
|
||||
return true;
|
||||
|
||||
await Task.WhenAll(
|
||||
ctx.Message.CreateReactionAsync(Config.Reactions.Denied),
|
||||
ctx.RespondAsync("Regular users can only view their own warnings")
|
||||
).ConfigureAwait(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
157
CompatBot/Commands/Warnings.cs
Normal file
157
CompatBot/Commands/Warnings.cs
Normal file
@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using CompatBot.Attributes;
|
||||
using CompatBot.Database;
|
||||
using CompatBot.Utils;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.CommandsNext.Attributes;
|
||||
using DSharpPlus.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CompatBot.Commands
|
||||
{
|
||||
[Group("warn")]
|
||||
[Description("Command used to manage warnings")]
|
||||
internal sealed partial class Warnings: BaseCommandModule
|
||||
{
|
||||
[GroupCommand] //attributes on overloads do not work, so no easy permission checks
|
||||
[Description("Command used to issue a new warning")]
|
||||
public async Task Warn(CommandContext ctx, [Description("User to warn. Can also use @id")] DiscordUser user, [RemainingText, Description("Warning explanation")] string reason)
|
||||
{
|
||||
//need to do manual check of the attribute in all GroupCommand overloads :(
|
||||
if (!await new RequiresBotModRole().ExecuteCheckAsync(ctx, false).ConfigureAwait(false))
|
||||
return;
|
||||
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
if (await AddAsync(ctx.Client, ctx.Message, user.Id, user.Username.Sanitize(), ctx.Message.Author, reason).ConfigureAwait(false))
|
||||
await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false);
|
||||
else
|
||||
await ctx.Message.CreateReactionAsync(Config.Reactions.Failure).ConfigureAwait(false);
|
||||
await typingTask;
|
||||
}
|
||||
|
||||
[GroupCommand, RequiresBotModRole]
|
||||
public async Task Warn(CommandContext ctx, [Description("ID of a user to warn")] ulong userId, [RemainingText, Description("Warning explanation")] string reason)
|
||||
{
|
||||
if (!await new RequiresBotModRole().ExecuteCheckAsync(ctx, false).ConfigureAwait(false))
|
||||
return;
|
||||
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
if (await AddAsync(ctx.Client, ctx.Message, userId, $"<@{userId}>", ctx.Message.Author, reason).ConfigureAwait(false))
|
||||
await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false);
|
||||
else
|
||||
await ctx.Message.CreateReactionAsync(Config.Reactions.Failure).ConfigureAwait(false);
|
||||
await typingTask;
|
||||
}
|
||||
|
||||
[Command("remove"), Aliases("delete", "del"), RequiresBotModRole]
|
||||
[Description("Removes specified warnings")]
|
||||
public async Task Remove(CommandContext ctx, [Description("Warning IDs to remove separated with space")] params int[] ids)
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
var warningsToRemove = await BotDb.Instance.Warning.Where(w => ids.Contains(w.Id)).ToListAsync().ConfigureAwait(false);
|
||||
BotDb.Instance.Warning.RemoveRange(warningsToRemove);
|
||||
var removedCount = await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false);
|
||||
(DiscordEmoji reaction, string msg) result = removedCount == ids.Length
|
||||
? (Config.Reactions.Success, $"Warning{(ids.Length == 1 ? "" : "s")} successfully removed!")
|
||||
: (Config.Reactions.Failure, $"Removed {removedCount} items, but was asked to remove {ids.Length}");
|
||||
await Task.WhenAll(
|
||||
ctx.RespondAsync(result.msg),
|
||||
ctx.Message.CreateReactionAsync(result.reaction),
|
||||
typingTask
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("clear"), RequiresBotModRole]
|
||||
[Description("Removes **all** warings for a user")]
|
||||
public Task Clear(CommandContext ctx, [Description("User to clear warnings for")] DiscordUser user)
|
||||
{
|
||||
return Clear(ctx, user.Id);
|
||||
}
|
||||
|
||||
[Command("clear"), RequiresBotModRole]
|
||||
public async Task Clear(CommandContext ctx, [Description("User ID to clear warnings for")] ulong userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var typingTask = ctx.TriggerTypingAsync();
|
||||
//var removed = await BotDb.Instance.Database.ExecuteSqlCommandAsync($"DELETE FROM `warning` WHERE `discord_id`={userId}").ConfigureAwait(false);
|
||||
var warningsToRemove = await BotDb.Instance.Warning.Where(w => w.DiscordId == userId).ToListAsync().ConfigureAwait(false);
|
||||
BotDb.Instance.Warning.RemoveRange(warningsToRemove);
|
||||
var removed = await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false);
|
||||
await Task.WhenAll(
|
||||
ctx.RespondAsync($"{removed} warning{(removed == 1 ? "" : "s")} successfully removed!"),
|
||||
ctx.Message.CreateReactionAsync(Config.Reactions.Success),
|
||||
typingTask
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ctx.Client.DebugLogger.LogMessage(LogLevel.Error, "", e.ToString(), DateTime.Now);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal static async Task<bool> AddAsync(DiscordClient client, DiscordMessage message, ulong userId, string userName, DiscordUser issuer, string reason, string fullReason = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(reason))
|
||||
{
|
||||
await message.RespondAsync("A reason needs to be provided").ConfigureAwait(false);
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
await BotDb.Instance.Warning.AddAsync(new Warning {DiscordId = userId, IssuerId = issuer.Id, Reason = reason, FullReason = fullReason ?? "", Timestamp = DateTime.UtcNow.Ticks}).ConfigureAwait(false);
|
||||
await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false);
|
||||
var count = await BotDb.Instance.Warning.CountAsync(w => w.DiscordId == userId).ConfigureAwait(false);
|
||||
await message.RespondAsync($"User warning saved! User currently has {count} warning{(count % 10 == 1 && count % 100 != 11 ? "" : "s")}!").ConfigureAwait(false);
|
||||
if (count > 1)
|
||||
await ListUserWarningsAsync(client, message, userId, userName).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
client.DebugLogger.LogMessage(LogLevel.Error, "", "Couldn't save the warning: " + e, DateTime.Now);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//note: be sure to pass a sanitized userName
|
||||
private static async Task ListUserWarningsAsync(DiscordClient client, DiscordMessage message, ulong userId, string userName, bool skipIfOne = true)
|
||||
{
|
||||
await message.Channel.TriggerTypingAsync().ConfigureAwait(false);
|
||||
var count = await BotDb.Instance.Warning.CountAsync(w => w.DiscordId == userId).ConfigureAwait(false);
|
||||
if (count == 0)
|
||||
{
|
||||
await message.RespondAsync(userName + " has no warnings, is a standup citizen, and a pillar of this community").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (count == 1 && skipIfOne)
|
||||
return;
|
||||
|
||||
var isPrivate = message.Channel.IsPrivate;
|
||||
var result = new StringBuilder("Warning list for ").Append(userName).AppendLine(":")
|
||||
.AppendLine("```");
|
||||
var header = $"{"ID",-5} | {"Issued by",-25} | {"On date",-20} | Reason";
|
||||
if (isPrivate)
|
||||
header += " | Full reason";
|
||||
result.AppendLine(header)
|
||||
.AppendLine("".PadLeft(header.Length, '-'));
|
||||
foreach (var warning in BotDb.Instance.Warning.Where(w => w.DiscordId == userId))
|
||||
{
|
||||
var issuerName = warning.IssuerId == 0 ? "" : await client.GetUserNameAsync(message.Channel, warning.IssuerId, isPrivate, "unknown mod").ConfigureAwait(false);
|
||||
var timestamp = warning.Timestamp.HasValue ? new DateTime(warning.Timestamp.Value, DateTimeKind.Utc).ToString("u") : null;
|
||||
result.Append($"{warning.Id:00000} | {issuerName,-25} | {timestamp,-20} | {warning.Reason}");
|
||||
if (isPrivate)
|
||||
result.Append(" | ").Append(warning.FullReason);
|
||||
result.AppendLine();
|
||||
}
|
||||
await message.Channel.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
33
CompatBot/CompatBot.csproj
Normal file
33
CompatBot/CompatBot.csproj
Normal file
@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<RootNamespace>CompatBot</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DSharpPlus" Version="4.0.0-beta-00470" />
|
||||
<PackageReference Include="DSharpPlus.CommandsNext" Version="4.0.0-beta-00470" />
|
||||
<PackageReference Include="MathParser.org-mXparser" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.1" />
|
||||
<PackageReference Include="NReco.Text.AhoCorasickDoubleArrayTrie" Version="1.0.1" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CompatApiClient\CompatApiClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
110
CompatBot/Config.cs
Normal file
110
CompatBot/Config.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot
|
||||
{
|
||||
internal static class Config
|
||||
{
|
||||
public static readonly string CommandPrefix = "!";
|
||||
public static readonly ulong BotChannelId = 291679908067803136;
|
||||
public static readonly ulong BotSpamId = 319224795785068545;
|
||||
public static readonly ulong BotLogId = 436972161572536329;
|
||||
public static readonly ulong BotRulesChannelId = 311894275015049216;
|
||||
public static readonly ulong BotAdminId = 267367850706993152;
|
||||
|
||||
public static readonly int ProductCodeLookupHistoryThrottle = 7;
|
||||
|
||||
public static readonly int TopLimit = 15;
|
||||
public static readonly int AttachmentSizeLimit = 8 * 1024 * 1024;
|
||||
public static readonly int LogSizeLimit = 64 * 1024 * 1024;
|
||||
public static readonly int MinimumBufferSize = 512;
|
||||
|
||||
public static readonly string Token;
|
||||
|
||||
public static readonly CancellationTokenSource Cts = new CancellationTokenSource();
|
||||
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
public static class Colors
|
||||
{
|
||||
public static readonly DiscordColor Help = DiscordColor.Azure;
|
||||
public static readonly DiscordColor DownloadLinks = new DiscordColor(0x3b88c3);
|
||||
public static readonly DiscordColor Maintenance = new DiscordColor(0xffff00);
|
||||
|
||||
public static readonly DiscordColor CompatStatusNothing = new DiscordColor(0x455556);
|
||||
public static readonly DiscordColor CompatStatusLoadable = new DiscordColor(0xe74c3c);
|
||||
public static readonly DiscordColor CompatStatusIntro = new DiscordColor(0xe08a1e);
|
||||
public static readonly DiscordColor CompatStatusIngame = new DiscordColor(0xf9b32f);
|
||||
public static readonly DiscordColor CompatStatusPlayable = new DiscordColor(0x1ebc61);
|
||||
public static readonly DiscordColor CompatStatusUnknown = new DiscordColor(0x3198ff);
|
||||
|
||||
public static readonly DiscordColor LogResultFailed = DiscordColor.Gray;
|
||||
|
||||
public static readonly DiscordColor LogAlert = new DiscordColor(0xe74c3c);
|
||||
public static readonly DiscordColor LogNotice = new DiscordColor(0xf9b32f);
|
||||
}
|
||||
|
||||
public static class Reactions
|
||||
{
|
||||
public static readonly DiscordEmoji Success = DiscordEmoji.FromUnicode("👌");
|
||||
public static readonly DiscordEmoji Failure = DiscordEmoji.FromUnicode("⛔");
|
||||
public static readonly DiscordEmoji Denied = DiscordEmoji.FromUnicode("👮");
|
||||
public static readonly DiscordEmoji Starbucks = DiscordEmoji.FromUnicode("☕");
|
||||
}
|
||||
|
||||
public static class Moderation
|
||||
{
|
||||
public static readonly int StarbucksThreshold = 5;
|
||||
|
||||
public static readonly IReadOnlyList<ulong> Channels = new List<ulong>
|
||||
{
|
||||
272875751773306881,
|
||||
319224795785068545,
|
||||
}.AsReadOnly();
|
||||
|
||||
public static readonly IReadOnlyCollection<string> RoleWhiteList = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)
|
||||
{
|
||||
"Administrator",
|
||||
"Community Manager",
|
||||
"Web Developer",
|
||||
"Moderator",
|
||||
"Lead Graphics Developer",
|
||||
"Lead Core Developer",
|
||||
"Developers",
|
||||
"Affiliated",
|
||||
"Contributors",
|
||||
};
|
||||
}
|
||||
|
||||
static Config()
|
||||
{
|
||||
try
|
||||
{
|
||||
var envVars = Environment.GetEnvironmentVariables();
|
||||
foreach (var member in typeof(Config).GetFields(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
if (envVars.Contains(member.Name))
|
||||
{
|
||||
if (member.FieldType == typeof(ulong) && ulong.TryParse(envVars[member.Name] as string, out var ulongValue))
|
||||
member.SetValue(null, ulongValue);
|
||||
if (member.FieldType == typeof(int) && int.TryParse(envVars[member.Name] as string, out var intValue))
|
||||
member.SetValue(null, intValue);
|
||||
if (member.FieldType == typeof(string))
|
||||
member.SetValue(null, envVars[member.Name] as string);
|
||||
}
|
||||
}
|
||||
var args = Environment.CommandLine.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (args.Length > 1)
|
||||
Token = args[1];
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine("Error initializing settings: " + e.Message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
CompatBot/Converters/CustomDiscordChannelConverter.cs
Normal file
57
CompatBot/Converters/CustomDiscordChannelConverter.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.CommandsNext.Converters;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.Converters
|
||||
{
|
||||
internal sealed class CustomDiscordChannelConverter : IArgumentConverter<DiscordChannel>
|
||||
{
|
||||
private static Regex ChannelRegex { get; } = new Regex(@"^<#(\d+)>$", RegexOptions.ECMAScript | RegexOptions.Compiled);
|
||||
|
||||
public async Task<Optional<DiscordChannel>> ConvertAsync(string value, CommandContext ctx)
|
||||
{
|
||||
var guildList = new List<DiscordGuild>(ctx.Client.Guilds.Count);
|
||||
if (ctx.Guild == null)
|
||||
foreach (var g in ctx.Client.Guilds.Keys)
|
||||
guildList.Add(await ctx.Client.GetGuildAsync(g).ConfigureAwait(false));
|
||||
else
|
||||
guildList.Add(ctx.Guild);
|
||||
|
||||
if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var cid))
|
||||
{
|
||||
var result = (
|
||||
from g in guildList
|
||||
from ch in g.Channels
|
||||
select ch
|
||||
).FirstOrDefault(xc => xc.Id == cid);
|
||||
var ret = result == null ? Optional<DiscordChannel>.FromNoValue() : Optional<DiscordChannel>.FromValue(result);
|
||||
return ret;
|
||||
}
|
||||
|
||||
var m = ChannelRegex.Match(value);
|
||||
if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out cid))
|
||||
{
|
||||
var result = (
|
||||
from g in guildList
|
||||
from ch in g.Channels
|
||||
select ch
|
||||
).FirstOrDefault(xc => xc.Id == cid);
|
||||
var ret = result != null ? Optional<DiscordChannel>.FromValue(result) : Optional<DiscordChannel>.FromNoValue();
|
||||
return ret;
|
||||
}
|
||||
|
||||
value = value.ToLowerInvariant();
|
||||
var chn = (
|
||||
from g in guildList
|
||||
from ch in g.Channels
|
||||
select ch
|
||||
).FirstOrDefault(xc => xc.Name.ToLowerInvariant() == value);
|
||||
return chn != null ? Optional<DiscordChannel>.FromValue(chn) : Optional<DiscordChannel>.FromNoValue();
|
||||
}
|
||||
}
|
||||
}
|
77
CompatBot/Database/BotDb.cs
Normal file
77
CompatBot/Database/BotDb.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using CompatApiClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CompatBot.Database
|
||||
{
|
||||
internal class BotDb: DbContext
|
||||
{
|
||||
private static readonly Lazy<BotDb> instance = new Lazy<BotDb>(() => new BotDb());
|
||||
public static BotDb Instance => instance.Value;
|
||||
|
||||
public DbSet<Moderator> Moderator { get; set; }
|
||||
public DbSet<Piracystring> Piracystring { get; set; }
|
||||
public DbSet<Warning> Warning { get; set; }
|
||||
public DbSet<Explanation> Explanation { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder.UseSqlite("Data Source=bot.db");
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
//configure indices
|
||||
modelBuilder.Entity<Moderator>().HasIndex(m => m.DiscordId).IsUnique().HasName("moderator_discord_id");
|
||||
|
||||
modelBuilder.Entity<Piracystring>().HasIndex(ps => ps.String).IsUnique().HasName("piracystring_string");
|
||||
|
||||
modelBuilder.Entity<Warning>().HasIndex(w => w.DiscordId).HasName("warning_discord_id");
|
||||
|
||||
modelBuilder.Entity<Explanation>().HasIndex(e => e.Keyword).IsUnique().HasName("explanation_keyword");
|
||||
|
||||
//configure default policy of Id being the primary key
|
||||
modelBuilder.ConfigureDefaultPkConvention();
|
||||
|
||||
//configure name conversion for all configured entities from CamelCase to snake_case
|
||||
modelBuilder.ConfigureMapping(NamingStyles.Underscore);
|
||||
}
|
||||
}
|
||||
|
||||
internal class Moderator
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public ulong DiscordId { get; set; }
|
||||
public bool Sudoer { get; set; }
|
||||
}
|
||||
|
||||
internal class Piracystring
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[Required, Column(TypeName = "varchar(255)")]
|
||||
public string String { get; set; }
|
||||
}
|
||||
|
||||
internal class Warning
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public ulong DiscordId { get; set; }
|
||||
public ulong IssuerId { get; set; }
|
||||
[Required]
|
||||
public string Reason { get; set; }
|
||||
[Required]
|
||||
public string FullReason { get; set; }
|
||||
public long? Timestamp { get; set; }
|
||||
}
|
||||
|
||||
internal class Explanation
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[Required]
|
||||
public string Keyword { get; set; }
|
||||
[Required]
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
116
CompatBot/Database/DbImporter.cs
Normal file
116
CompatBot/Database/DbImporter.cs
Normal file
@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Database.Migrations;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Migrations.Internal;
|
||||
|
||||
namespace CompatBot.Database
|
||||
{
|
||||
internal static class DbImporter
|
||||
{
|
||||
public static async Task<bool> UpgradeAsync(BotDb dbContext, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Upgrading database if needed...");
|
||||
await dbContext.Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (SqliteException e)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine(e.Message);
|
||||
Console.WriteLine("Database upgrade failed, probably importing an unversioned one.");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine("Trying to apply a manual fixup...");
|
||||
try
|
||||
{
|
||||
await ImportAsync(dbContext, cancellationToken).ConfigureAwait(false);
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine("Manual fixup worked great. Let's try migrations again...");
|
||||
Console.ResetColor();
|
||||
await dbContext.Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine(ex.Message);
|
||||
Console.WriteLine("Well shit, I hope you had backups, son. You'll have to figure this one out on your own.");
|
||||
Console.ResetColor();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!await dbContext.Moderator.AnyAsync(m => m.DiscordId == Config.BotAdminId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await dbContext.Moderator.AddAsync(new Moderator {DiscordId = Config.BotAdminId, Sudoer = true}, cancellationToken).ConfigureAwait(false);
|
||||
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
Console.WriteLine("Database is ready.");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task ImportAsync(BotDb dbContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var db = dbContext.Database;
|
||||
using (var tx = await db.BeginTransactionAsync(cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
// __EFMigrationsHistory table will be already created by the failed migration attempt
|
||||
await db.ExecuteSqlCommandAsync($"INSERT INTO `__EFMigrationsHistory`(`MigrationId`,`ProductVersion`) VALUES ({new InitialCreate().GetId()},'manual')", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync($"INSERT INTO `__EFMigrationsHistory`(`MigrationId`,`ProductVersion`) VALUES ({new Explanations().GetId()},'manual')", cancellationToken);
|
||||
// create constraints on moderator
|
||||
await db.ExecuteSqlCommandAsync(@"CREATE TABLE `temp_new_moderator` (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`discord_id` INTEGER NOT NULL,
|
||||
`sudoer` INTEGER NOT NULL
|
||||
)", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("INSERT INTO temp_new_moderator SELECT `id`,`discord_id`,`sudoer` FROM `moderator`", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("DROP TABLE `moderator`", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("ALTER TABLE `temp_new_moderator` RENAME TO `moderator`", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("CREATE UNIQUE INDEX `moderator_discord_id` ON `moderator` (`discord_id`)", cancellationToken);
|
||||
// create constraints on piracystring
|
||||
await db.ExecuteSqlCommandAsync(@"CREATE TABLE `temp_new_piracystring` (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`string` varchar ( 255 ) NOT NULL
|
||||
)", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("INSERT INTO temp_new_piracystring SELECT `id`,`string` FROM `piracystring`", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("DROP TABLE `piracystring`", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("ALTER TABLE `temp_new_piracystring` RENAME TO `piracystring`", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("CREATE UNIQUE INDEX `piracystring_string` ON `piracystring` (`string`)", cancellationToken);
|
||||
// create constraints on warning
|
||||
await db.ExecuteSqlCommandAsync(@"CREATE TABLE `temp_new_warning` (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`discord_id` INTEGER NOT NULL,
|
||||
`reason` TEXT NOT NULL,
|
||||
`full_reason` TEXT NOT NULL,
|
||||
`issuer_id` INTEGER NOT NULL DEFAULT 0
|
||||
)", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("INSERT INTO temp_new_warning SELECT `id`,`discord_id`,`reason`,`full_reason`,`issuer_id` FROM `warning`", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("DROP TABLE `warning`", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("ALTER TABLE `temp_new_warning` RENAME TO `warning`", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("CREATE INDEX `warning_discord_id` ON `warning` (`discord_id`)", cancellationToken);
|
||||
// create constraints on explanation
|
||||
await db.ExecuteSqlCommandAsync(@"CREATE TABLE `temp_new_explanation` (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`keyword` TEXT NOT NULL,
|
||||
`text` TEXT NOT NULL
|
||||
)", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("INSERT INTO temp_new_explanation SELECT `id`,`keyword`,`text` FROM `explanation`", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("DROP TABLE `explanation`", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("ALTER TABLE `temp_new_explanation` RENAME TO `explanation`", cancellationToken);
|
||||
await db.ExecuteSqlCommandAsync("CREATE UNIQUE INDEX `explanation_keyword` ON `explanation` (`keyword`)", cancellationToken);
|
||||
tx.Commit();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//tx.Rollback();
|
||||
tx.Commit();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
88
CompatBot/Database/Migrations/20180709153348_InitialCreate.Designer.cs
generated
Normal file
88
CompatBot/Database/Migrations/20180709153348_InitialCreate.Designer.cs
generated
Normal file
@ -0,0 +1,88 @@
|
||||
// <auto-generated />
|
||||
using CompatBot.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace CompatBot.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(BotDb))]
|
||||
[Migration("20180709153348_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846");
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Moderator", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<ulong>("DiscordId")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<bool>("Sudoer")
|
||||
.HasColumnName("sudoer");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("DiscordId")
|
||||
.IsUnique()
|
||||
.HasName("moderator_discord_id");
|
||||
|
||||
b.ToTable("moderator");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Piracystring", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("String")
|
||||
.IsRequired()
|
||||
.HasColumnName("string")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("String")
|
||||
.IsUnique()
|
||||
.HasName("piracystring_string");
|
||||
|
||||
b.ToTable("piracystring");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Warning", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<ulong>("DiscordId")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("FullReason")
|
||||
.IsRequired()
|
||||
.HasColumnName("full_reason");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.ToTable("warning");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace CompatBot.Database.Migrations
|
||||
{
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "moderator",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
,
|
||||
discord_id = table.Column<ulong>(nullable: false),
|
||||
sudoer = table.Column<bool>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("id", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "piracystring",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
,
|
||||
@string = table.Column<string>(name: "string", type: "varchar(255)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("id", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "warning",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
,
|
||||
discord_id = table.Column<ulong>(nullable: false),
|
||||
reason = table.Column<string>(nullable: false),
|
||||
full_reason = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("id", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "moderator_discord_id",
|
||||
table: "moderator",
|
||||
column: "discord_id",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "piracystring_string",
|
||||
table: "piracystring",
|
||||
column: "string",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "moderator");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "piracystring");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "warning");
|
||||
}
|
||||
}
|
||||
}
|
118
CompatBot/Database/Migrations/20180709154128_Explanations.Designer.cs
generated
Normal file
118
CompatBot/Database/Migrations/20180709154128_Explanations.Designer.cs
generated
Normal file
@ -0,0 +1,118 @@
|
||||
// <auto-generated />
|
||||
using CompatBot.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace CompatBot.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(BotDb))]
|
||||
[Migration("20180709154128_Explanations")]
|
||||
partial class Explanations
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846");
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Explanation", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Keyword")
|
||||
.IsRequired()
|
||||
.HasColumnName("keyword");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasColumnName("text");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("Keyword")
|
||||
.IsUnique()
|
||||
.HasName("explanation_keyword");
|
||||
|
||||
b.ToTable("explanation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Moderator", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<ulong>("DiscordId")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<bool>("Sudoer")
|
||||
.HasColumnName("sudoer");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("DiscordId")
|
||||
.IsUnique()
|
||||
.HasName("moderator_discord_id");
|
||||
|
||||
b.ToTable("moderator");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Piracystring", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("String")
|
||||
.IsRequired()
|
||||
.HasColumnName("string")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("String")
|
||||
.IsUnique()
|
||||
.HasName("piracystring_string");
|
||||
|
||||
b.ToTable("piracystring");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Warning", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<ulong>("DiscordId")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("FullReason")
|
||||
.IsRequired()
|
||||
.HasColumnName("full_reason");
|
||||
|
||||
b.Property<ulong>("IssuerId")
|
||||
.HasColumnName("issuer_id");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("DiscordId")
|
||||
.HasName("warning_discord_id");
|
||||
|
||||
b.ToTable("warning");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
56
CompatBot/Database/Migrations/20180709154128_Explanations.cs
Normal file
56
CompatBot/Database/Migrations/20180709154128_Explanations.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace CompatBot.Database.Migrations
|
||||
{
|
||||
public partial class Explanations : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<ulong>(
|
||||
name: "issuer_id",
|
||||
table: "warning",
|
||||
nullable: false,
|
||||
defaultValue: 0ul);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "explanation",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
,
|
||||
keyword = table.Column<string>(nullable: false),
|
||||
text = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("id", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "warning_discord_id",
|
||||
table: "warning",
|
||||
column: "discord_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "explanation_keyword",
|
||||
table: "explanation",
|
||||
column: "keyword",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "explanation");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "warning_discord_id",
|
||||
table: "warning");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "issuer_id",
|
||||
table: "warning");
|
||||
}
|
||||
}
|
||||
}
|
122
CompatBot/Database/Migrations/20180719122730_WarningTimestamp.Designer.cs
generated
Normal file
122
CompatBot/Database/Migrations/20180719122730_WarningTimestamp.Designer.cs
generated
Normal file
@ -0,0 +1,122 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using CompatBot.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace CompatBot.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(BotDb))]
|
||||
[Migration("20180719122730_WarningTimestamp")]
|
||||
partial class WarningTimestamp
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846");
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Explanation", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Keyword")
|
||||
.IsRequired()
|
||||
.HasColumnName("keyword");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasColumnName("text");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("Keyword")
|
||||
.IsUnique()
|
||||
.HasName("explanation_keyword");
|
||||
|
||||
b.ToTable("explanation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Moderator", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<ulong>("DiscordId")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<bool>("Sudoer")
|
||||
.HasColumnName("sudoer");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("DiscordId")
|
||||
.IsUnique()
|
||||
.HasName("moderator_discord_id");
|
||||
|
||||
b.ToTable("moderator");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Piracystring", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("String")
|
||||
.IsRequired()
|
||||
.HasColumnName("string")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("String")
|
||||
.IsUnique()
|
||||
.HasName("piracystring_string");
|
||||
|
||||
b.ToTable("piracystring");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Warning", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<ulong>("DiscordId")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("FullReason")
|
||||
.IsRequired()
|
||||
.HasColumnName("full_reason");
|
||||
|
||||
b.Property<ulong>("IssuerId")
|
||||
.HasColumnName("issuer_id");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<long?>("Timestamp")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("DiscordId")
|
||||
.HasName("warning_discord_id");
|
||||
|
||||
b.ToTable("warning");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace CompatBot.Database.Migrations
|
||||
{
|
||||
public partial class WarningTimestamp : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "timestamp",
|
||||
table: "warning",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "timestamp",
|
||||
table: "warning");
|
||||
}
|
||||
}
|
||||
}
|
120
CompatBot/Database/Migrations/BotDbModelSnapshot.cs
Normal file
120
CompatBot/Database/Migrations/BotDbModelSnapshot.cs
Normal file
@ -0,0 +1,120 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using CompatBot.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace CompatBot.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(BotDb))]
|
||||
partial class BotDbModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846");
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Explanation", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Keyword")
|
||||
.IsRequired()
|
||||
.HasColumnName("keyword");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasColumnName("text");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("Keyword")
|
||||
.IsUnique()
|
||||
.HasName("explanation_keyword");
|
||||
|
||||
b.ToTable("explanation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Moderator", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<ulong>("DiscordId")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<bool>("Sudoer")
|
||||
.HasColumnName("sudoer");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("DiscordId")
|
||||
.IsUnique()
|
||||
.HasName("moderator_discord_id");
|
||||
|
||||
b.ToTable("moderator");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Piracystring", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("String")
|
||||
.IsRequired()
|
||||
.HasColumnName("string")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("String")
|
||||
.IsUnique()
|
||||
.HasName("piracystring_string");
|
||||
|
||||
b.ToTable("piracystring");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CompatBot.Database.Warning", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<ulong>("DiscordId")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("FullReason")
|
||||
.IsRequired()
|
||||
.HasColumnName("full_reason");
|
||||
|
||||
b.Property<ulong>("IssuerId")
|
||||
.HasColumnName("issuer_id");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<long?>("Timestamp")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
b.HasIndex("DiscordId")
|
||||
.HasName("warning_discord_id");
|
||||
|
||||
b.ToTable("warning");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
27
CompatBot/Database/NamingConventionConverter.cs
Normal file
27
CompatBot/Database/NamingConventionConverter.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CompatBot.Database
|
||||
{
|
||||
internal static class NamingConventionConverter
|
||||
{
|
||||
public static void ConfigureMapping(this ModelBuilder modelBuilder, Func<string, string> nameResolver)
|
||||
{
|
||||
if (nameResolver == null)
|
||||
throw new ArgumentNullException(nameof(nameResolver));
|
||||
|
||||
foreach (var entity in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
entity.Relational().TableName = nameResolver(entity.Relational().TableName);
|
||||
foreach (var property in entity.GetProperties())
|
||||
property.Relational().ColumnName = nameResolver(property.Name);
|
||||
foreach (var key in entity.GetKeys())
|
||||
key.Relational().Name = nameResolver(key.Relational().Name);
|
||||
foreach (var key in entity.GetForeignKeys())
|
||||
key.Relational().Name = nameResolver(key.Relational().Name);
|
||||
foreach (var index in entity.GetIndexes())
|
||||
index.Relational().Name = nameResolver(index.Relational().Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
CompatBot/Database/PrimaryKeyConvention.cs
Normal file
32
CompatBot/Database/PrimaryKeyConvention.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
|
||||
namespace CompatBot.Database
|
||||
{
|
||||
internal static class PrimaryKeyConvention
|
||||
{
|
||||
public static void ConfigureDefaultPkConvention(this ModelBuilder modelBuilder, string keyProperty = "Id")
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyProperty))
|
||||
throw new ArgumentException(nameof(keyProperty));
|
||||
|
||||
foreach (var entity in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
var pk = entity.GetKeys().FirstOrDefault(k => k.IsPrimaryKey());
|
||||
if (pk != null)
|
||||
pk.Relational().Name = keyProperty;
|
||||
}
|
||||
}
|
||||
|
||||
public static void ConfigureNoPkConvention(this ModelBuilder modelBuilder)
|
||||
{
|
||||
foreach (var entity in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
var pk = entity.GetKeys().FirstOrDefault(k => k.IsPrimaryKey());
|
||||
entity.RemoveKey(pk.Properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
63
CompatBot/EventHandlers/AntipiracyMonitor.cs
Normal file
63
CompatBot/EventHandlers/AntipiracyMonitor.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Commands;
|
||||
using CompatBot.Providers;
|
||||
using CompatBot.Utils;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.Entities;
|
||||
using DSharpPlus.EventArgs;
|
||||
|
||||
namespace CompatBot.EventHandlers
|
||||
{
|
||||
internal static class AntipiracyMonitor
|
||||
{
|
||||
public static async Task OnMessageCreated(MessageCreateEventArgs args)
|
||||
{
|
||||
args.Handled = !await IsClean(args.Client, args.Message).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task OnMessageEdit(MessageUpdateEventArgs args)
|
||||
{
|
||||
args.Handled = !await IsClean(args.Client, args.Message).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<bool> IsClean(DiscordClient client, DiscordMessage message)
|
||||
{
|
||||
if (message.Author.IsBot)
|
||||
return true;
|
||||
|
||||
if (string.IsNullOrEmpty(message.Content) || message.Content.StartsWith(Config.CommandPrefix))
|
||||
return true;
|
||||
|
||||
string trigger = null;
|
||||
bool needsAttention = false;
|
||||
try
|
||||
{
|
||||
trigger = await PiracyStringProvider.FindTriggerAsync(message.Content);
|
||||
if (trigger == null)
|
||||
return true;
|
||||
|
||||
await message.Channel.DeleteMessageAsync(message, $"Mention of piracy trigger '{trigger}'").ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
client.DebugLogger.LogMessage(LogLevel.Warning, "", $"Couldn't delete message in {message.Channel.Name}: {e.Message}", DateTime.Now);
|
||||
needsAttention = true;
|
||||
}
|
||||
try
|
||||
{
|
||||
var rules = await client.GetChannelAsync(Config.BotRulesChannelId).ConfigureAwait(false);
|
||||
await Task.WhenAll(
|
||||
message.Channel.SendMessageAsync($"{message.Author.Mention} Please follow the {rules.Mention} and do not discuss piracy on this server. Repeated offence may result in a ban."),
|
||||
client.ReportAsync("Mention of piracy", message, trigger, message.Content, needsAttention),
|
||||
Warnings.AddAsync(client, message, message.Author.Id, message.Author.Username, client.CurrentUser, "Mention of piracy", message.Content)
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
client.DebugLogger.LogMessage(LogLevel.Warning, "", $"Couldn't finish piracy trigger actions for a message in {message.Channel.Name}: {e}", DateTime.Now);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
91
CompatBot/EventHandlers/LogInfoHandler.cs
Normal file
91
CompatBot/EventHandlers/LogInfoHandler.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Commands;
|
||||
using CompatBot.LogParsing;
|
||||
using CompatBot.LogParsing.SourceHandlers;
|
||||
using CompatBot.ResultFormatters;
|
||||
using CompatBot.Utils;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.EventArgs;
|
||||
|
||||
namespace CompatBot.EventHandlers
|
||||
{
|
||||
internal static class LogInfoHandler
|
||||
{
|
||||
private static readonly ISourceHandler[] handlers =
|
||||
{
|
||||
new GzipHandler(),
|
||||
new PlainTextHandler(),
|
||||
new ZipHandler(),
|
||||
};
|
||||
|
||||
private static readonly char[] linkSeparator = {' ', '>', '\r', '\n'};
|
||||
|
||||
public static async Task OnMessageCreated(MessageCreateEventArgs args)
|
||||
{
|
||||
var message = args.Message;
|
||||
if (message.Author.IsBot)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrEmpty(message.Content) && message.Content.StartsWith(Config.CommandPrefix))
|
||||
return;
|
||||
|
||||
foreach (var attachment in message.Attachments.Where(a => a.FileSize < Config.AttachmentSizeLimit))
|
||||
foreach (var handler in handlers)
|
||||
if (await handler.CanHandleAsync(attachment).ConfigureAwait(false))
|
||||
{
|
||||
await args.Channel.TriggerTypingAsync().ConfigureAwait(false);
|
||||
LogParseState result = null;
|
||||
try
|
||||
{
|
||||
var pipe = new Pipe();
|
||||
var fillPipeTask = handler.FillPipeAsync(attachment, pipe.Writer);
|
||||
result = await LogParser.ReadPipeAsync(pipe.Reader).ConfigureAwait(false);
|
||||
await fillPipeTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
args.Client.DebugLogger.LogMessage(LogLevel.Error, "", "Log parsing failed: " + e, DateTime.Now);
|
||||
}
|
||||
if (result == null)
|
||||
await args.Channel.SendMessageAsync("Log analysis failed, most likely cause is a truncated/invalid log.").ConfigureAwait(false);
|
||||
else
|
||||
{
|
||||
await args.Channel.SendMessageAsync(embed: await result.AsEmbedAsync(args.Client, args.Message).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
if (result.Error == LogParseState.ErrorCode.PiracyDetected)
|
||||
{
|
||||
bool needsAttention = false;
|
||||
try
|
||||
{
|
||||
await message.DeleteAsync("Piracy detected in log").ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
needsAttention = true;
|
||||
args.Client.DebugLogger.LogMessage(LogLevel.Warning, "", $"Unable to delete message in {args.Channel.Name}: {e.Message}", DateTime.Now);
|
||||
}
|
||||
await Task.WhenAll(
|
||||
args.Client.ReportAsync("Pirated Release", args.Message, result.PiracyTrigger, result.PiracyContext, needsAttention),
|
||||
Warnings.AddAsync(args.Client, args.Message, args.Message.Author.Id, args.Message.Author.Username, args.Client.CurrentUser,
|
||||
"Pirated Release", $"{message.CreationTimestamp:O} - {message.Content} - {result.PiracyTrigger}")
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(message.Content) || !"help".Equals(args.Channel.Name, StringComparison.InvariantCultureIgnoreCase))
|
||||
return;
|
||||
|
||||
var linkStart = message.Content.IndexOf("http");
|
||||
if (linkStart > -1)
|
||||
{
|
||||
var link = message.Content.Substring(linkStart).Split(linkSeparator, 2)[0];
|
||||
if (link.Contains(".log", StringComparison.InvariantCultureIgnoreCase) || link.Contains("rpcs3.zip", StringComparison.CurrentCultureIgnoreCase))
|
||||
await args.Channel.SendMessageAsync("If you intended to upload a log file please reupload it directly to discord").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
CompatBot/EventHandlers/LogsAsTextMonitor.cs
Normal file
26
CompatBot/EventHandlers/LogsAsTextMonitor.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DSharpPlus.EventArgs;
|
||||
|
||||
namespace CompatBot.EventHandlers
|
||||
{
|
||||
internal class LogsAsTextMonitor
|
||||
{
|
||||
public static async Task OnMessageCreated(MessageCreateEventArgs args)
|
||||
{
|
||||
if (args.Author.IsBot)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(args.Message.Content) || args.Message.Content.StartsWith(Config.CommandPrefix))
|
||||
return;
|
||||
|
||||
if (!"help".Equals(args.Channel.Name, StringComparison.InvariantCultureIgnoreCase))
|
||||
return;
|
||||
|
||||
if (args.Message.Content.Contains('·'))
|
||||
if (args.Message.Content.Split('\n', StringSplitOptions.RemoveEmptyEntries).Any(l => l.StartsWith('·')))
|
||||
await args.Channel.SendMessageAsync($"{args.Message.Author.Mention} please upload the full log file instead of pasting some random lines that might be completely irrelevant").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
116
CompatBot/EventHandlers/ProductCodeLookup.cs
Normal file
116
CompatBot/EventHandlers/ProductCodeLookup.cs
Normal file
@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.POCOs;
|
||||
using CompatBot.ResultFormatters;
|
||||
using CompatBot.Utils;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.Entities;
|
||||
using DSharpPlus.EventArgs;
|
||||
|
||||
namespace CompatBot.EventHandlers
|
||||
{
|
||||
internal static class ProductCodeLookup
|
||||
{
|
||||
// see http://www.psdevwiki.com/ps3/Productcode
|
||||
private static readonly Regex ProductCode = new Regex(@"(?<letters>(?:[BPSUVX][CL]|P[ETU]|NP)[AEHJKPUIX][ABSM])[ \-]?(?<numbers>\d{5})", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly Client compatClient = new Client();
|
||||
|
||||
public static async Task OnMessageMention(MessageCreateEventArgs args)
|
||||
{
|
||||
if (args.Author.IsBot)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(args.Message.Content) || args.Message.Content.StartsWith(Config.CommandPrefix))
|
||||
return;
|
||||
|
||||
var lastBotMessages = await args.Channel.GetMessagesBeforeAsync(args.Message.Id, Config.ProductCodeLookupHistoryThrottle, DateTime.UtcNow.AddSeconds(-30)).ConfigureAwait(false);
|
||||
StringBuilder previousRepliesBuilder = null;
|
||||
foreach (var msg in lastBotMessages)
|
||||
{
|
||||
if (NeedToSilence(msg))
|
||||
return;
|
||||
|
||||
if (msg.Author.IsCurrent)
|
||||
{
|
||||
previousRepliesBuilder = previousRepliesBuilder ?? new StringBuilder();
|
||||
previousRepliesBuilder.AppendLine(msg.Content);
|
||||
var embeds = msg.Embeds;
|
||||
if (embeds?.Count > 0)
|
||||
foreach (var embed in embeds)
|
||||
previousRepliesBuilder.AppendLine(embed.Title).AppendLine(embed.Description);
|
||||
}
|
||||
}
|
||||
var previousReplies = previousRepliesBuilder?.ToString() ?? "";
|
||||
|
||||
var codesToLookup = ProductCode.Matches(args.Message.Content)
|
||||
.Select(match => (match.Groups["letters"].Value + match.Groups["numbers"]).ToUpper())
|
||||
.Distinct()
|
||||
.Where(c => !previousReplies.Contains(c, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Take(args.Channel.IsPrivate ? 50 : 5)
|
||||
.ToList();
|
||||
if (codesToLookup.Count == 0)
|
||||
return;
|
||||
|
||||
await args.Channel.TriggerTypingAsync().ConfigureAwait(false);
|
||||
var results = new List<(string code, Task<DiscordEmbed> task)>(codesToLookup.Count);
|
||||
foreach (var code in codesToLookup)
|
||||
results.Add((code, args.Client.LookupGameInfoAsync(code)));
|
||||
foreach (var result in results)
|
||||
try
|
||||
{
|
||||
await args.Channel.SendMessageAsync(embed: await result.task.ConfigureAwait(false)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
args.Client.DebugLogger.LogMessage(LogLevel.Warning, "", $"Couldn't post result for {result.code}: {e.Message}", DateTime.Now);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool NeedToSilence(DiscordMessage msg)
|
||||
{
|
||||
if (string.IsNullOrEmpty(msg.Content))
|
||||
return false;
|
||||
|
||||
if (!msg.Content.Contains("shut up") && !msg.Content.Contains("hush"))
|
||||
return false;
|
||||
|
||||
return msg.Content.Contains("bot") || (msg.MentionedUsers?.Any(u => u.IsCurrent) ?? false);
|
||||
|
||||
}
|
||||
|
||||
public static async Task<DiscordEmbed> LookupGameInfoAsync(this DiscordClient client, string code, bool footer = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(code))
|
||||
return TitleInfo.Unknown.AsEmbed(code, footer);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await compatClient.GetCompatResultAsync(RequestBuilder.Start().SetSearch(code), Config.Cts.Token).ConfigureAwait(false);
|
||||
if (result?.ReturnCode == -2)
|
||||
return TitleInfo.Maintenance.AsEmbed(null, footer);
|
||||
|
||||
if (result?.ReturnCode == -1)
|
||||
return TitleInfo.CommunicationError.AsEmbed(null, footer);
|
||||
|
||||
if (result?.Results == null)
|
||||
return TitleInfo.Unknown.AsEmbed(code, footer);
|
||||
|
||||
if (result.Results.TryGetValue(code, out var info))
|
||||
return info.AsEmbed(code, footer);
|
||||
|
||||
return TitleInfo.Unknown.AsEmbed(code, footer);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
client.DebugLogger.LogMessage(LogLevel.Warning, "", $"Couldn't get compat result for {code}: {e}", DateTime.Now);
|
||||
return TitleInfo.CommunicationError.AsEmbed(null, footer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
117
CompatBot/EventHandlers/Starbucks.cs
Normal file
117
CompatBot/EventHandlers/Starbucks.cs
Normal file
@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Utils;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.Entities;
|
||||
using DSharpPlus.EventArgs;
|
||||
|
||||
namespace CompatBot.EventHandlers
|
||||
{
|
||||
internal static class Starbucks
|
||||
{
|
||||
private static readonly TimeSpan ModerationTimeThreshold = TimeSpan.FromHours(12);
|
||||
|
||||
public static Task Handler(MessageReactionAddEventArgs args)
|
||||
{
|
||||
return CheckMessage(args.Client, args.Channel, args.User, args.Message, args.Emoji);
|
||||
}
|
||||
|
||||
public static async Task CheckBacklog(this DiscordClient client)
|
||||
{
|
||||
try
|
||||
{
|
||||
var after = DateTime.UtcNow - ModerationTimeThreshold;
|
||||
var checkTasks = new List<Task>();
|
||||
foreach (var channelId in Config.Moderation.Channels)
|
||||
{
|
||||
DiscordChannel channel;
|
||||
try
|
||||
{
|
||||
channel = await client.GetChannelAsync(channelId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
client.DebugLogger.LogMessage(LogLevel.Warning, "", $"Couldn't check channel {channelId} for starbucks: {e.Message}", DateTime.Now);
|
||||
continue;
|
||||
}
|
||||
|
||||
var messages = await channel.GetMessagesAsync().ConfigureAwait(false);
|
||||
|
||||
var messagesToCheck = from msg in messages
|
||||
where msg.CreationTimestamp > after && msg.Reactions.Any(r => r.Emoji == Config.Reactions.Starbucks && r.Count >= Config.Moderation.StarbucksThreshold)
|
||||
select msg;
|
||||
foreach (var message in messagesToCheck)
|
||||
{
|
||||
var reactionUsers = await message.GetReactionsAsync(Config.Reactions.Starbucks).ConfigureAwait(false);
|
||||
if (reactionUsers.Count > 0)
|
||||
checkTasks.Add(CheckMessage(client, channel, reactionUsers[0], message, Config.Reactions.Starbucks));
|
||||
}
|
||||
}
|
||||
await Task.WhenAll(checkTasks).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
client.DebugLogger.LogMessage(LogLevel.Error, "", e.ToString(), DateTime.Now);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static async Task CheckMessage(DiscordClient client, DiscordChannel channel, DiscordUser user, DiscordMessage message, DiscordEmoji emoji)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (user.IsBot || channel.IsPrivate)
|
||||
return;
|
||||
|
||||
if (emoji != Config.Reactions.Starbucks)
|
||||
return;
|
||||
|
||||
if (!Config.Moderation.Channels.Contains(channel.Id))
|
||||
return;
|
||||
|
||||
// message.Timestamp throws if it's not in the cache AND is in local time zone
|
||||
if (DateTime.UtcNow - message.CreationTimestamp > ModerationTimeThreshold)
|
||||
return;
|
||||
|
||||
if (message.Reactions.Any(r => r.Emoji == emoji && (r.IsMe || r.Count < Config.Moderation.StarbucksThreshold)))
|
||||
return;
|
||||
|
||||
// in case it's not in cache and doesn't contain any info, including Author
|
||||
message = await channel.GetMessageAsync(message.Id).ConfigureAwait(false);
|
||||
if ((message.Author as DiscordMember)?.Roles.Any(r => Config.Moderation.RoleWhiteList.Contains(r.Name)) ?? false)
|
||||
return;
|
||||
|
||||
var users = await message.GetReactionsAsync(emoji).ConfigureAwait(false);
|
||||
var members = users
|
||||
.Select(u => channel.Guild
|
||||
.GetMemberAsync(u.Id)
|
||||
.ContinueWith(ct => ct.IsCompletedSuccessfully ? ct : Task.FromResult((DiscordMember)null)))
|
||||
.ToList() //force eager task creation
|
||||
.Select(t => t.Unwrap().ConfigureAwait(false).GetAwaiter().GetResult())
|
||||
.Where(m => m != null)
|
||||
.ToList();
|
||||
var reporters = new List<DiscordMember>(Config.Moderation.StarbucksThreshold);
|
||||
foreach (var member in members)
|
||||
{
|
||||
if (member.IsCurrent)
|
||||
return;
|
||||
|
||||
if (member.Roles.Any())
|
||||
reporters.Add(member);
|
||||
}
|
||||
if (reporters.Count < Config.Moderation.StarbucksThreshold)
|
||||
return;
|
||||
|
||||
await message.CreateReactionAsync(emoji).ConfigureAwait(false);
|
||||
await client.ReportAsync("User moderation report ⭐💵", message, reporters).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
client.DebugLogger.LogMessage(LogLevel.Error, "", e.ToString(), DateTime.Now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
153
CompatBot/LogParsing/LogParser.LogSections.cs
Normal file
153
CompatBot/LogParsing/LogParser.LogSections.cs
Normal file
@ -0,0 +1,153 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Providers;
|
||||
|
||||
namespace CompatBot.LogParsing
|
||||
{
|
||||
internal partial class LogParser
|
||||
{
|
||||
private const RegexOptions DefaultOptions = RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture;
|
||||
|
||||
/*
|
||||
* Extractors are defined in terms of trigger-extractor
|
||||
*
|
||||
* Parser scans the log from section to section with a sliding window of up to 50 lines of text
|
||||
* Triggers are scanned for in the first line of said sliding window
|
||||
* If trigger is matched, then the associated reges will be run on THE WHOLE sliding window
|
||||
* If any data was captured, it will be stored in the current collection of items with the key of the explicit capture group of regex
|
||||
*
|
||||
* Due to limitations, REGEX can't contain anything other than ASCII (triggers CAN however)
|
||||
*
|
||||
*/
|
||||
private static readonly List<LogSection> LogSections = new List<LogSection>
|
||||
{
|
||||
new LogSection
|
||||
{
|
||||
Extractors = new Dictionary<string, Regex>
|
||||
{
|
||||
["RPCS3"] = new Regex(@"(?<build_and_specs>.*)\r?$", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture),
|
||||
},
|
||||
EndTrigger = "·",
|
||||
},
|
||||
new LogSection
|
||||
{
|
||||
Extractors = new Dictionary<string, Regex>
|
||||
{
|
||||
["Serial:"] = new Regex(@"Serial: (?<serial>[A-z]{4}\d{5})\r?$", DefaultOptions),
|
||||
["Path:"] = new Regex(@"Path: ((?<win_path>\w:/)|(?<lin_path>/[^/])).*?\r?$", DefaultOptions),
|
||||
["custom config:"] = new Regex("custom config: (?<custom_config>.*?)\r?$", DefaultOptions),
|
||||
},
|
||||
OnNewLineAsync = PiracyCheckAsync,
|
||||
EndTrigger = "Core:",
|
||||
},
|
||||
new LogSection
|
||||
{
|
||||
Extractors = new Dictionary<string, Regex>
|
||||
{
|
||||
["PPU Decoder:"] = new Regex("PPU Decoder: (?<ppu_decoder>.*?)\r?$", DefaultOptions),
|
||||
["PPU Threads:"] = new Regex("Threads: (?<ppu_threads>.*?)\r?$", DefaultOptions),
|
||||
["thread scheduler:"] = new Regex("scheduler: (?<thread_scheduler>.*?)\r?$", DefaultOptions),
|
||||
["SPU Decoder:"] = new Regex("SPU Decoder: (?<spu_decoder>.*?)\r?$", DefaultOptions),
|
||||
["secondary cores:"] = new Regex("secondary cores: (?<spu_secondary_cores>.*?)\r?$", DefaultOptions),
|
||||
["priority:"] = new Regex("priority: (?<spu_lower_thread_priority>.*?)\r?$", DefaultOptions),
|
||||
["SPU Threads:"] = new Regex("SPU Threads: (?<spu_threads>.*?)\r?$", DefaultOptions),
|
||||
["delay penalty:"] = new Regex("penalty: (?<spu_delay_penalty>.*?)\r?$", DefaultOptions),
|
||||
["loop detection:"] = new Regex("detection: (?<spu_loop_detection>.*?)\r?$", DefaultOptions),
|
||||
["Lib Loader:"] = new Regex("[Ll]oader: (?<lib_loader>.*?)\r?$", DefaultOptions),
|
||||
["static functions:"] = new Regex("functions: (?<hook_static_functions>.*?)\r?$", DefaultOptions),
|
||||
["Load libraries:"] = new Regex("libraries: (?<library_list>.*)", DefaultOptions),
|
||||
},
|
||||
EndTrigger = "VFS:",
|
||||
},
|
||||
new LogSection
|
||||
{
|
||||
EndTrigger = "Video:",
|
||||
},
|
||||
new LogSection
|
||||
{
|
||||
Extractors = new Dictionary<string, Regex>
|
||||
{
|
||||
["Renderer:"] = new Regex("Renderer: (?<renderer>.*?)\r?$", DefaultOptions),
|
||||
["Resolution:"] = new Regex("Resolution: (?<resolution>.*?)\r?$", DefaultOptions),
|
||||
["Aspect ratio:"] = new Regex("Aspect ratio: (?<aspect_ratio>.*?)\r?$", DefaultOptions),
|
||||
["Frame limit:"] = new Regex("Frame limit: (?<frame_limit>.*?)\r?$", DefaultOptions),
|
||||
["Write Color Buffers:"] = new Regex("Write Color Buffers: (?<write_color_buffers>.*?)\r?$", DefaultOptions),
|
||||
["VSync:"] = new Regex("VSync: (?<vsync>.*?)\r?$", DefaultOptions),
|
||||
["GPU texture scaling:"] = new Regex("Use GPU texture scaling: (?<gpu_texture_scaling>.*?)\r?$", DefaultOptions),
|
||||
["Strict Rendering Mode:"] = new Regex("Strict Rendering Mode: (?<strict_rendering_mode>.*?)\r?$", DefaultOptions),
|
||||
["Vertex Cache:"] = new Regex("Disable Vertex Cache: (?<vertex_cache>.*?)\r?$", DefaultOptions),
|
||||
["Blit:"] = new Regex("Blit: (?<cpu_blit>.*?)\r?$", DefaultOptions),
|
||||
["Resolution Scale:"] = new Regex("Resolution Scale: (?<resolution_scale>.*?)\r?$", DefaultOptions),
|
||||
["Anisotropic Filter"] = new Regex("Anisotropic Filter Override: (?<af_override>.*?)\r?$", DefaultOptions),
|
||||
["Scalable Dimension:"] = new Regex("Minimum Scalable Dimension: (?<texture_scale_threshold>.*?)\r?$", DefaultOptions),
|
||||
["12:"] = new Regex(@"(D3D12|DirectX 12):\s*\r?\n\s*Adapter: (?<d3d_gpu>.*?)\r?$", DefaultOptions),
|
||||
["Vulkan:"] = new Regex(@"Vulkan:\s*\r?\n\s*Adapter: (?<vulkan_gpu>.*?)\r?$", DefaultOptions),
|
||||
},
|
||||
EndTrigger = "Audio:",
|
||||
},
|
||||
new LogSection
|
||||
{
|
||||
EndTrigger = "Log:",
|
||||
OnSectionEnd = MarkAsComplete,
|
||||
},
|
||||
new LogSection
|
||||
{
|
||||
Extractors = new Dictionary<string, Regex>
|
||||
{
|
||||
["RSX:"] = new Regex(@"RSX:(\d|\.|\s|\w|-)* (?<driver_version>(\d+\.)*\d+)\r?\n[^\n]*?" +
|
||||
@"RSX: [^\n]+\r?\n[^\n]*?" +
|
||||
@"RSX: (?<driver_manuf>.*?)\r?\n[^\n]*?" +
|
||||
@"RSX: Supported texel buffer size", DefaultOptions),
|
||||
["GL RENDERER:"] = new Regex(@"GL RENDERER: (?<driver_manuf_new>.*?)\r?\n", DefaultOptions),
|
||||
["GL VERSION:"] = new Regex(@"GL VERSION:(\d|\.|\s|\w|-)* (?<driver_version_new>(\d+\.)*\d+)\r?\n", DefaultOptions),
|
||||
["texel buffer size reported:"] = new Regex(@"RSX: Supported texel buffer size reported: (?<texel_buffer_size_new>\d*?) bytes", DefaultOptions),
|
||||
["·F "] = new Regex(@"F \d+:\d+:\d+\.\d+ {.+?} (?<fatal_error>.*?)\r?$", DefaultOptions),
|
||||
},
|
||||
OnSectionEnd = MarkAsCompleteAndReset,
|
||||
EndTrigger = "Objects cleared...",
|
||||
}
|
||||
};
|
||||
|
||||
private static async Task PiracyCheckAsync(string line, LogParseState state)
|
||||
{
|
||||
if (await PiracyStringProvider.FindTriggerAsync(line).ConfigureAwait(false) is string match)
|
||||
{
|
||||
state.PiracyTrigger = match;
|
||||
state.PiracyContext = Utf8.GetString(Encoding.ASCII.GetBytes(line));
|
||||
state.Error = LogParseState.ErrorCode.PiracyDetected;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearResults(LogParseState state)
|
||||
{
|
||||
void Copy(params string[] keys)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
if (state.CompleteCollection?[key] is string value)
|
||||
state.WipCollection[key] = value;
|
||||
}
|
||||
state.WipCollection = new NameValueCollection();
|
||||
Copy(
|
||||
"build_and_specs",
|
||||
"vulkan_gpu", "d3d_gpu",
|
||||
"driver_version", "driver_manuf",
|
||||
"driver_manuf_new", "driver_version_new"
|
||||
);
|
||||
}
|
||||
|
||||
private static void MarkAsComplete(LogParseState state)
|
||||
{
|
||||
state.CompleteCollection = state.WipCollection;
|
||||
}
|
||||
|
||||
private static void MarkAsCompleteAndReset(LogParseState state)
|
||||
{
|
||||
MarkAsComplete(state);
|
||||
ClearResults(state);
|
||||
state.Id = -1;
|
||||
}
|
||||
}
|
||||
}
|
109
CompatBot/LogParsing/LogParser.PipeReader.cs
Normal file
109
CompatBot/LogParsing/LogParser.PipeReader.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Utils;
|
||||
|
||||
namespace CompatBot.LogParsing
|
||||
{
|
||||
internal static partial class LogParser
|
||||
{
|
||||
private static readonly byte[] Bom = {0xEF, 0xBB, 0xBF};
|
||||
|
||||
public static async Task<LogParseState> ReadPipeAsync(PipeReader reader)
|
||||
{
|
||||
var currentSectionLines = new LinkedList<ReadOnlySequence<byte>>();
|
||||
var state = new LogParseState();
|
||||
bool skippedBom = false;
|
||||
long totalReadBytes = 0;
|
||||
ReadResult result;
|
||||
do
|
||||
{
|
||||
result = await reader.ReadAsync(Config.Cts.Token).ConfigureAwait(false);
|
||||
var buffer = result.Buffer;
|
||||
if (!skippedBom)
|
||||
{
|
||||
if (buffer.Length < 3)
|
||||
continue;
|
||||
|
||||
var potentialBom = buffer.Slice(0, 3);
|
||||
if (potentialBom.ToArray().SequenceEqual(Bom))
|
||||
{
|
||||
reader.AdvanceTo(potentialBom.End);
|
||||
totalReadBytes += potentialBom.Length;
|
||||
skippedBom = true;
|
||||
continue;
|
||||
}
|
||||
skippedBom = true;
|
||||
}
|
||||
SequencePosition? lineEnd;
|
||||
do
|
||||
{
|
||||
if (currentSectionLines.Count > 0)
|
||||
buffer = buffer.Slice(buffer.GetPosition(1, currentSectionLines.Last.Value.End));
|
||||
lineEnd = buffer.PositionOf((byte)'\n');
|
||||
if (lineEnd != null)
|
||||
{
|
||||
await OnNewLineAsync(buffer.Slice(0, lineEnd.Value), result.Buffer, currentSectionLines, state).ConfigureAwait(false);
|
||||
if (state.Error != LogParseState.ErrorCode.None)
|
||||
return state;
|
||||
|
||||
buffer = buffer.Slice(buffer.GetPosition(1, lineEnd.Value));
|
||||
}
|
||||
} while (lineEnd != null);
|
||||
|
||||
if (result.IsCanceled || Config.Cts.IsCancellationRequested)
|
||||
state.Error = LogParseState.ErrorCode.SizeLimit;
|
||||
else if (result.IsCompleted)
|
||||
await FlushAllLinesAsync(result.Buffer, currentSectionLines, state).ConfigureAwait(false);
|
||||
var sectionStart = currentSectionLines.Count == 0 ? buffer : currentSectionLines.First.Value;
|
||||
reader.AdvanceTo(sectionStart.Start, buffer.End);
|
||||
totalReadBytes += result.Buffer.Slice(0, sectionStart.Start).Length;
|
||||
if (totalReadBytes >= Config.LogSizeLimit)
|
||||
{
|
||||
state.Error = LogParseState.ErrorCode.SizeLimit;
|
||||
break;
|
||||
}
|
||||
} while (!(result.IsCompleted || result.IsCanceled || Config.Cts.IsCancellationRequested));
|
||||
reader.Complete();
|
||||
return state;
|
||||
}
|
||||
|
||||
private static async Task OnNewLineAsync(ReadOnlySequence<byte> line, ReadOnlySequence<byte> buffer, LinkedList<ReadOnlySequence<byte>> sectionLines, LogParseState state)
|
||||
{
|
||||
var currentProcessor = SectionParsers[state.Id];
|
||||
if (line.AsString().Contains(currentProcessor.EndTrigger, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
await FlushAllLinesAsync(buffer, sectionLines, state).ConfigureAwait(false);
|
||||
SectionParsers[state.Id].OnSectionEnd?.Invoke(state);
|
||||
state.Id++;
|
||||
return;
|
||||
}
|
||||
if (sectionLines.Count == 50)
|
||||
await ProcessFirstLineInBufferAsync(buffer, sectionLines, state).ConfigureAwait(false);
|
||||
sectionLines.AddLast(line);
|
||||
}
|
||||
|
||||
private static async Task FlushAllLinesAsync(ReadOnlySequence<byte> buffer, LinkedList<ReadOnlySequence<byte>> sectionLines, LogParseState state)
|
||||
{
|
||||
while (sectionLines.Count > 0 && state.Error == LogParseState.ErrorCode.None)
|
||||
await ProcessFirstLineInBufferAsync(buffer, sectionLines, state).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task ProcessFirstLineInBufferAsync(ReadOnlySequence<byte> buffer, LinkedList<ReadOnlySequence<byte>> sectionLines, LogParseState state)
|
||||
{
|
||||
var currentProcessor = SectionParsers[state.Id];
|
||||
var firstSectionLine = sectionLines.First.Value.AsString();
|
||||
await currentProcessor.OnLineCheckAsync(firstSectionLine, state).ConfigureAwait(false);
|
||||
if (state.Error != LogParseState.ErrorCode.None)
|
||||
return;
|
||||
|
||||
var section = buffer.Slice(sectionLines.First.Value.Start, sectionLines.Last.Value.End).AsString();
|
||||
currentProcessor.OnExtract?.Invoke(firstSectionLine, section, state);
|
||||
sectionLines.RemoveFirst();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
70
CompatBot/LogParsing/LogParser.StateMachineGenerator.cs
Normal file
70
CompatBot/LogParsing/LogParser.StateMachineGenerator.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using NReco.Text;
|
||||
|
||||
namespace CompatBot.LogParsing
|
||||
{
|
||||
using SectionAction = KeyValuePair<string, Action<string, LogParseState>>;
|
||||
|
||||
internal partial class LogParser
|
||||
{
|
||||
private static readonly ReadOnlyCollection<LogSectionParser> SectionParsers;
|
||||
private static readonly Encoding Utf8 = new UTF8Encoding(false);
|
||||
|
||||
static LogParser()
|
||||
{
|
||||
var parsers = new List<LogSectionParser>(LogSections.Count);
|
||||
foreach (var sectionDescription in LogSections)
|
||||
{
|
||||
var parser = new LogSectionParser
|
||||
{
|
||||
OnLineCheckAsync = sectionDescription.OnNewLineAsync ?? ((l, s) => Task.CompletedTask),
|
||||
OnSectionEnd = sectionDescription.OnSectionEnd,
|
||||
EndTrigger = Encoding.ASCII.GetString(Utf8.GetBytes(sectionDescription.EndTrigger)),
|
||||
};
|
||||
// the idea here is to construct Aho-Corasick parser that will look for any data marker and run the associated regex to extract the data into state
|
||||
if (sectionDescription.Extractors?.Count > 0)
|
||||
{
|
||||
var act = new AhoCorasickDoubleArrayTrie<Action<string, LogParseState>>(sectionDescription.Extractors.Select(extractorPair =>
|
||||
new SectionAction(
|
||||
Encoding.ASCII.GetString(Utf8.GetBytes(extractorPair.Key)),
|
||||
(buffer, state) => OnExtractorHit(buffer, extractorPair.Value, state)
|
||||
)
|
||||
), true);
|
||||
parser.OnExtract = (line, buffer, state) => { act.ParseText(line, h => { h.Value(buffer, state); }); };
|
||||
}
|
||||
parsers.Add(parser);
|
||||
}
|
||||
SectionParsers = parsers.AsReadOnly();
|
||||
}
|
||||
|
||||
private static void OnExtractorHit(string buffer, Regex extractor, LogParseState state)
|
||||
{
|
||||
var matches = extractor.Matches(buffer);
|
||||
foreach (Match match in matches)
|
||||
foreach (Group group in match.Groups)
|
||||
if (!string.IsNullOrEmpty(group.Name) && group.Name != "0" && !string.IsNullOrWhiteSpace(group.Value))
|
||||
{
|
||||
#if DEBUG
|
||||
Console.WriteLine($"regex {group.Name} = {group.Value}");
|
||||
#endif
|
||||
state.WipCollection[group.Name] = Utf8.GetString(Encoding.ASCII.GetBytes(group.Value));
|
||||
}
|
||||
}
|
||||
|
||||
private delegate void OnNewLineDelegate(string line, string buffer, LogParseState state);
|
||||
|
||||
private class LogSectionParser
|
||||
{
|
||||
public OnNewLineDelegate OnExtract;
|
||||
public Func<string, LogParseState, Task> OnLineCheckAsync;
|
||||
public Action<LogParseState> OnSectionEnd;
|
||||
public string EndTrigger;
|
||||
}
|
||||
}
|
||||
}
|
21
CompatBot/LogParsing/POCOs/LogParseState.cs
Normal file
21
CompatBot/LogParsing/POCOs/LogParseState.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace CompatBot.LogParsing
|
||||
{
|
||||
internal class LogParseState
|
||||
{
|
||||
public NameValueCollection CompleteCollection = null;
|
||||
public NameValueCollection WipCollection = new NameValueCollection();
|
||||
public int Id = 0;
|
||||
public ErrorCode Error = ErrorCode.None;
|
||||
public string PiracyTrigger;
|
||||
public string PiracyContext;
|
||||
|
||||
public enum ErrorCode
|
||||
{
|
||||
None = 0,
|
||||
PiracyDetected = 1,
|
||||
SizeLimit = 2,
|
||||
}
|
||||
}
|
||||
}
|
15
CompatBot/LogParsing/POCOs/LogSection.cs
Normal file
15
CompatBot/LogParsing/POCOs/LogSection.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CompatBot.LogParsing
|
||||
{
|
||||
internal class LogSection
|
||||
{
|
||||
public string EndTrigger;
|
||||
public Dictionary<string, Regex> Extractors;
|
||||
public Func<string, LogParseState, Task> OnNewLineAsync;
|
||||
public Action<LogParseState> OnSectionEnd;
|
||||
}
|
||||
}
|
45
CompatBot/LogParsing/SourceHandlers/GzipHandler.cs
Normal file
45
CompatBot/LogParsing/SourceHandlers/GzipHandler.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.IO.Compression;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.LogParsing.SourceHandlers
|
||||
{
|
||||
public class GzipHandler: ISourceHandler
|
||||
{
|
||||
public Task<bool> CanHandleAsync(DiscordAttachment attachment)
|
||||
{
|
||||
return Task.FromResult(attachment.FileName.EndsWith(".log.gz", StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
public async Task FillPipeAsync(DiscordAttachment attachment, PipeWriter writer)
|
||||
{
|
||||
using (var client = HttpClientFactory.Create())
|
||||
using (var downloadStream = await client.GetStreamAsync(attachment.Url).ConfigureAwait(false))
|
||||
using (var gzipStream = new GZipStream(downloadStream, CompressionMode.Decompress))
|
||||
{
|
||||
try
|
||||
{
|
||||
int read;
|
||||
FlushResult flushed;
|
||||
do
|
||||
{
|
||||
var memory = writer.GetMemory(Config.MinimumBufferSize);
|
||||
read = await gzipStream.ReadAsync(memory, Config.Cts.Token);
|
||||
writer.Advance(read); //todo: test that .Advance(0) works as expected
|
||||
flushed = await writer.FlushAsync(Config.Cts.Token).ConfigureAwait(false);
|
||||
} while (read > 0 && !(flushed.IsCompleted || flushed.IsCanceled || Config.Cts.IsCancellationRequested));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
writer.Complete(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
writer.Complete();
|
||||
}
|
||||
}
|
||||
}
|
12
CompatBot/LogParsing/SourceHandlers/ISourceHandler.cs
Normal file
12
CompatBot/LogParsing/SourceHandlers/ISourceHandler.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading.Tasks;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.LogParsing.SourceHandlers
|
||||
{
|
||||
internal interface ISourceHandler
|
||||
{
|
||||
Task<bool> CanHandleAsync(DiscordAttachment attachment);
|
||||
Task FillPipeAsync(DiscordAttachment attachment, PipeWriter writer);
|
||||
}
|
||||
}
|
43
CompatBot/LogParsing/SourceHandlers/PlainText.cs
Normal file
43
CompatBot/LogParsing/SourceHandlers/PlainText.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.LogParsing.SourceHandlers
|
||||
{
|
||||
public class PlainTextHandler: ISourceHandler
|
||||
{
|
||||
public Task<bool> CanHandleAsync(DiscordAttachment attachment)
|
||||
{
|
||||
return Task.FromResult(attachment.FileName.EndsWith(".log", StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
public async Task FillPipeAsync(DiscordAttachment attachment, PipeWriter writer)
|
||||
{
|
||||
using (var client = HttpClientFactory.Create())
|
||||
using (var stream = await client.GetStreamAsync(attachment.Url).ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
int read;
|
||||
FlushResult flushed;
|
||||
do
|
||||
{
|
||||
var memory = writer.GetMemory(Config.MinimumBufferSize);
|
||||
read = await stream.ReadAsync(memory, Config.Cts.Token);
|
||||
writer.Advance(read); //todo: test that .Advance(0) works as expected
|
||||
flushed = await writer.FlushAsync(Config.Cts.Token).ConfigureAwait(false);
|
||||
} while (read > 0 && !(flushed.IsCompleted || flushed.IsCanceled || Config.Cts.IsCancellationRequested));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
writer.Complete(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
writer.Complete();
|
||||
}
|
||||
}
|
||||
}
|
84
CompatBot/LogParsing/SourceHandlers/ZipHandler.cs
Normal file
84
CompatBot/LogParsing/SourceHandlers/ZipHandler.cs
Normal file
@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Utils;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.LogParsing.SourceHandlers
|
||||
{
|
||||
public class ZipHandler: ISourceHandler
|
||||
{
|
||||
private static readonly ArrayPool<byte> bufferPool = ArrayPool<byte>.Create(1024, 16);
|
||||
|
||||
public async Task<bool> CanHandleAsync(DiscordAttachment attachment)
|
||||
{
|
||||
if (!attachment.FileName.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using (var client = HttpClientFactory.Create())
|
||||
using (var stream = await client.GetStreamAsync(attachment.Url).ConfigureAwait(false))
|
||||
{
|
||||
var buf = bufferPool.Rent(1024);
|
||||
var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
|
||||
var firstEntry = Encoding.ASCII.GetString(new ReadOnlySpan<byte>(buf, 0, read));
|
||||
var result = firstEntry.Contains(".log", StringComparison.InvariantCultureIgnoreCase);
|
||||
bufferPool.Return(buf);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task FillPipeAsync(DiscordAttachment attachment, PipeWriter writer)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var fileStream = new FileStream(Path.GetTempFileName(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 16384, FileOptions.Asynchronous | FileOptions.RandomAccess | FileOptions.DeleteOnClose))
|
||||
{
|
||||
using (var client = HttpClientFactory.Create())
|
||||
using (var downloadStream = await client.GetStreamAsync(attachment.Url).ConfigureAwait(false))
|
||||
await downloadStream.CopyToAsync(fileStream, 16384, Config.Cts.Token).ConfigureAwait(false);
|
||||
fileStream.Seek(0, SeekOrigin.Begin);
|
||||
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read))
|
||||
{
|
||||
var logEntry = zipArchive.Entries.FirstOrDefault(e => e.Name.EndsWith(".log", StringComparison.InvariantCultureIgnoreCase));
|
||||
if (logEntry == null)
|
||||
throw new InvalidOperationException("No zip entries that match the log criteria");
|
||||
|
||||
using (var zipStream = logEntry.Open())
|
||||
{
|
||||
int read;
|
||||
FlushResult flushed;
|
||||
do
|
||||
{
|
||||
var memory = writer.GetMemory(Config.MinimumBufferSize);
|
||||
read = await zipStream.ReadAsync(memory, Config.Cts.Token);
|
||||
writer.Advance(read); //todo: test that .Advance(0) works as expected
|
||||
flushed = await writer.FlushAsync(Config.Cts.Token).ConfigureAwait(false);
|
||||
} while (read > 0 && !(flushed.IsCompleted || flushed.IsCanceled || Config.Cts.IsCancellationRequested));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
writer.Complete(e);
|
||||
return;
|
||||
}
|
||||
writer.Complete();
|
||||
}
|
||||
}
|
||||
}
|
97
CompatBot/Program.cs
Normal file
97
CompatBot/Program.cs
Normal file
@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Commands;
|
||||
using CompatBot.Converters;
|
||||
using CompatBot.Database;
|
||||
using CompatBot.EventHandlers;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
internal static async Task Main(string[] args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Config.Token))
|
||||
{
|
||||
Console.WriteLine("No token was specified.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await DbImporter.UpgradeAsync(BotDb.Instance, Config.Cts.Token))
|
||||
return;
|
||||
|
||||
|
||||
var config = new DiscordConfiguration
|
||||
{
|
||||
Token = Config.Token,
|
||||
TokenType = TokenType.Bot,
|
||||
UseInternalLogHandler = true,
|
||||
//LogLevel = LogLevel.Debug,
|
||||
};
|
||||
|
||||
using (var client = new DiscordClient(config))
|
||||
{
|
||||
var commands = client.UseCommandsNext(new CommandsNextConfiguration {StringPrefixes = new[] {Config.CommandPrefix}});
|
||||
commands.RegisterConverter(new CustomDiscordChannelConverter());
|
||||
commands.RegisterCommands<Misc>();
|
||||
commands.RegisterCommands<CompatList>();
|
||||
commands.RegisterCommands<Sudo>();
|
||||
commands.RegisterCommands<Antipiracy>();
|
||||
commands.RegisterCommands<Warnings>();
|
||||
commands.RegisterCommands<Explain>();
|
||||
|
||||
client.Ready += async r =>
|
||||
{
|
||||
Console.WriteLine("Bot is ready to serve!");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Bot user id : {r.Client.CurrentUser.Id} ({r.Client.CurrentUser.Username})");
|
||||
Console.WriteLine($"Bot admin id : {Config.BotAdminId} ({(await r.Client.GetUserAsync(Config.BotAdminId)).Username})");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Checking starbucks backlog...");
|
||||
await r.Client.CheckBacklog().ConfigureAwait(false);
|
||||
Console.WriteLine("Starbucks checked.");
|
||||
};
|
||||
client.MessageReactionAdded += Starbucks.Handler;
|
||||
|
||||
client.MessageCreated += AntipiracyMonitor.OnMessageCreated; // should be first
|
||||
client.MessageCreated += ProductCodeLookup.OnMessageMention;
|
||||
client.MessageCreated += LogInfoHandler.OnMessageCreated;
|
||||
client.MessageCreated += LogsAsTextMonitor.OnMessageCreated;
|
||||
|
||||
client.MessageUpdated += AntipiracyMonitor.OnMessageEdit;
|
||||
|
||||
try
|
||||
{
|
||||
await client.ConnectAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine(e.Message);
|
||||
Console.ResetColor();
|
||||
Console.WriteLine("Terminating.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length > 1 && ulong.TryParse(args[1], out var channelId))
|
||||
{
|
||||
Console.WriteLine("Found channelId: " + args[1]);
|
||||
var channel = await client.GetChannelAsync(channelId).ConfigureAwait(false);
|
||||
await channel.SendMessageAsync("Bot is up and running").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
while (!Config.Cts.IsCancellationRequested)
|
||||
{
|
||||
if (client.Ping > 1000)
|
||||
await client.ReconnectAsync();
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), Config.Cts.Token).ContinueWith(dt => {/* in case it was cancelled */}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
Console.WriteLine("Exiting");
|
||||
}
|
||||
}
|
||||
}
|
15
CompatBot/Properties/launchSettings.json
Normal file
15
CompatBot/Properties/launchSettings.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"profiles": {
|
||||
"default": {
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"BotLogId": "",
|
||||
"BotSpamId": "",
|
||||
"BotAdminId": "",
|
||||
"BotChannelId": "",
|
||||
"BotRulesChannelId": "",
|
||||
"Token": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
78
CompatBot/Providers/ModProvider.cs
Normal file
78
CompatBot/Providers/ModProvider.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Database;
|
||||
|
||||
namespace CompatBot.Providers
|
||||
{
|
||||
internal static class ModProvider
|
||||
{
|
||||
private static readonly Dictionary<ulong, Moderator> mods;
|
||||
public static ReadOnlyDictionary<ulong, Moderator> Mods => new ReadOnlyDictionary<ulong, Moderator>(mods);
|
||||
|
||||
static ModProvider()
|
||||
{
|
||||
mods = BotDb.Instance.Moderator.ToDictionary(m => m.DiscordId, m => m);
|
||||
}
|
||||
|
||||
public static bool IsMod(ulong userId) => mods.ContainsKey(userId);
|
||||
|
||||
public static bool IsSudoer(ulong userId) => mods.TryGetValue(userId, out var mod) && mod.Sudoer;
|
||||
|
||||
public static async Task<bool> AddAsync(ulong userId)
|
||||
{
|
||||
if (IsMod(userId))
|
||||
return false;
|
||||
|
||||
var db = BotDb.Instance;
|
||||
var result = await db.Moderator.AddAsync(new Moderator {DiscordId = userId}).ConfigureAwait(false);
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
lock (mods)
|
||||
{
|
||||
if (IsMod(userId))
|
||||
return false;
|
||||
mods[userId] = result.Entity;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task<bool> RemoveAsync(ulong userId)
|
||||
{
|
||||
if (!mods.TryGetValue(userId, out var mod))
|
||||
return false;
|
||||
|
||||
var db = BotDb.Instance;
|
||||
db.Moderator.Remove(mod);
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
lock (mods)
|
||||
{
|
||||
if (IsMod(userId))
|
||||
mods.Remove(userId);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task<bool> MakeSudoerAsync(ulong userId)
|
||||
{
|
||||
if (!mods.TryGetValue(userId, out var mod) || mod.Sudoer)
|
||||
return false;
|
||||
|
||||
mod.Sudoer = true;
|
||||
await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task<bool> UnmakeSudoerAsync(ulong userId)
|
||||
{
|
||||
if (!mods.TryGetValue(userId, out var mod) || !mod.Sudoer)
|
||||
return false;
|
||||
|
||||
mod.Sudoer = false;
|
||||
await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
81
CompatBot/Providers/PiracyStringProvider.cs
Normal file
81
CompatBot/Providers/PiracyStringProvider.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NReco.Text;
|
||||
|
||||
namespace CompatBot.Providers
|
||||
{
|
||||
internal static class PiracyStringProvider
|
||||
{
|
||||
private static readonly object SyncObj = new object();
|
||||
private static readonly List<string> PiracyStrings;
|
||||
private static AhoCorasickDoubleArrayTrie<string> matcher;
|
||||
|
||||
static PiracyStringProvider()
|
||||
{
|
||||
PiracyStrings = BotDb.Instance.Piracystring.Select(ps => ps.String).ToList();
|
||||
RebuildMatcher();
|
||||
}
|
||||
|
||||
public static async Task<bool> AddAsync(string trigger)
|
||||
{
|
||||
if (PiracyStrings.Contains(trigger, StringComparer.InvariantCultureIgnoreCase))
|
||||
return false;
|
||||
|
||||
lock (SyncObj)
|
||||
{
|
||||
if (PiracyStrings.Contains(trigger, StringComparer.InvariantCultureIgnoreCase))
|
||||
return false;
|
||||
|
||||
PiracyStrings.Add(trigger);
|
||||
RebuildMatcher();
|
||||
}
|
||||
var db = BotDb.Instance;
|
||||
await db.Piracystring.AddAsync(new Piracystring {String = trigger}).ConfigureAwait(false);
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task<bool> RemoveAsync(int id)
|
||||
{
|
||||
var db = BotDb.Instance;
|
||||
var dbItem = await db.Piracystring.FirstOrDefaultAsync(ps => ps.Id == id).ConfigureAwait(false);
|
||||
if (dbItem == null)
|
||||
return false;
|
||||
|
||||
db.Piracystring.Remove(dbItem);
|
||||
if (!PiracyStrings.Contains(dbItem.String))
|
||||
return false;
|
||||
|
||||
lock (SyncObj)
|
||||
{
|
||||
if (!PiracyStrings.Remove(dbItem.String))
|
||||
return false;
|
||||
|
||||
RebuildMatcher();
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Task<string> FindTriggerAsync(string str)
|
||||
{
|
||||
string result = null;
|
||||
matcher?.ParseText(str, h =>
|
||||
{
|
||||
result = h.Value;
|
||||
return false;
|
||||
});
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private static void RebuildMatcher()
|
||||
{
|
||||
matcher = PiracyStrings.Count == 0 ? null : new AhoCorasickDoubleArrayTrie<string>(PiracyStrings.ToDictionary(s => s, s => s));
|
||||
}
|
||||
}
|
||||
}
|
221
CompatBot/ResultFormatters/LogParserResultFormatter.cs
Normal file
221
CompatBot/ResultFormatters/LogParserResultFormatter.cs
Normal file
@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.POCOs;
|
||||
using CompatBot.EventHandlers;
|
||||
using CompatBot.LogParsing;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.ResultFormatters
|
||||
{
|
||||
internal static class LogParserResult
|
||||
{
|
||||
private static readonly Client compatClient = new Client();
|
||||
|
||||
// RPCS3 v0.0.3-3-3499d08 Alpha | HEAD
|
||||
// RPCS3 v0.0.4-6422-95c6ac699 Alpha | HEAD
|
||||
// RPCS3 v0.0.5-7104-a19113025 Alpha | HEAD
|
||||
// RPCS3 v0.0.5-42b4ce13a Alpha | minor
|
||||
private static readonly Regex BuildInfoInLog = new Regex(@"RPCS3 v(?<version>(\d|\.)+)(-(?<build>\d+))?-(?<commit>[0-9a-f]+) (?<stage>\w+) \| (?<branch>.*?)\r?$",
|
||||
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.Multiline);
|
||||
|
||||
// rpcs3-v0.0.5-7105-064d0619_win64.7z
|
||||
// rpcs3-v0.0.5-7105-064d0619_linux64.AppImage
|
||||
private static readonly Regex BuildInfoInUpdate = new Regex(@"rpcs3-v(?<version>(\d|\.)+)(-(?<build>\d+))?-(?<commit>[0-9a-f]+)_",
|
||||
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||
|
||||
public static async Task<DiscordEmbed> AsEmbedAsync(this LogParseState state, DiscordClient client, DiscordMessage message)
|
||||
{
|
||||
DiscordEmbedBuilder builder;
|
||||
var collection = state.CompleteCollection ?? state.WipCollection;
|
||||
if (collection?.Count > 0)
|
||||
{
|
||||
var gameInfo = await client.LookupGameInfoAsync(collection["serial"], false).ConfigureAwait(false);
|
||||
builder = new DiscordEmbedBuilder(gameInfo);
|
||||
if (state.Error == LogParseState.ErrorCode.PiracyDetected)
|
||||
{
|
||||
state.PiracyContext = state.PiracyContext.Sanitize();
|
||||
var msg = $"{message.Author.Mention}, you are being denied further support until you legally dump the game!\n" +
|
||||
"Please note that the RPCS3 community and its developers do not support piracy!\n" +
|
||||
"Most of the issues caused by pirated dumps is because they have been tampered with in such a way " +
|
||||
"and therefore act unpredictably on RPCS3.\n" +
|
||||
"If you need help obtaining legal dumps please read <https://rpcs3.net/quickstart>";
|
||||
builder.WithColor(Config.Colors.LogAlert)
|
||||
.WithTitle("Pirated release detected")
|
||||
.WithDescription(msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
CleanupValues(collection);
|
||||
BuildInfoSection(builder, collection);
|
||||
BuildCpuSection(builder, collection);
|
||||
BuildGpuSection(builder, collection);
|
||||
BuildLibsSection(builder, collection);
|
||||
await BuildNotesSectionAsync(builder, state, collection).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder = new DiscordEmbedBuilder
|
||||
{
|
||||
Description = "Log analysis failed, most likely cause is an empty log. Try reuploading a new copy.",
|
||||
Color = Config.Colors.LogResultFailed,
|
||||
};
|
||||
}
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private static void CleanupValues(NameValueCollection items)
|
||||
{
|
||||
if (items["strict_rendering_mode"] == "true")
|
||||
items["resolution_scale"] = "Strict Mode";
|
||||
if (items["spu_threads"] == "0")
|
||||
items["spu_threads"] = "Auto";
|
||||
if (items["spu_secondary_cores"] != null)
|
||||
items["thread_scheduler"] = items["spu_secondary_cores"];
|
||||
if (items["driver_manuf_new"] != null)
|
||||
items["gpu_info"] = items["driver_manuf_new"];
|
||||
else if (items["vulkan_gpu"] != "\"\"")
|
||||
items["gpu_info"] = items["vulkan_gpu"];
|
||||
else if (items["d3d_gpu"] != "\"\"")
|
||||
items["gpu_info"] = items["d3d_gpu"];
|
||||
else if (items["driver_manuf"] != null)
|
||||
items["gpu_info"] = items["driver_manuf"];
|
||||
else
|
||||
items["gpu_info"] = "Unknown";
|
||||
if (items["driver_version_new"] != null)
|
||||
items["gpu_info"] = items["gpu_info"] + " (" + items["driver_version_new"] + ")";
|
||||
else if (items["driver_version"] != null)
|
||||
items["gpu_info"] = items["gpu_info"] + " (" + items["driver_version"] + ")";
|
||||
if (items["af_override"] is string af)
|
||||
{
|
||||
if (af == "0")
|
||||
items["af_override"] = "Auto";
|
||||
else if (af == "1")
|
||||
items["af_override"] = "Disabled";
|
||||
}
|
||||
if (items["lib_loader"] is string libLoader)
|
||||
{
|
||||
var auto = libLoader.Contains("auto", StringComparison.InvariantCultureIgnoreCase);
|
||||
var manual = libLoader.Contains("manual", StringComparison.InvariantCultureIgnoreCase);
|
||||
if (auto && manual)
|
||||
items["lib_loader"] = "Auto & manual select";
|
||||
else if (auto)
|
||||
items["lib_loader"] = "Auto";
|
||||
else if (manual)
|
||||
items["lib_loader"] = "Manual selection";
|
||||
}
|
||||
if (items["win_path"] != null)
|
||||
items["os_path"] = "Windows";
|
||||
else if (items["lin_path"] != null)
|
||||
items["os_path"] = "Linux";
|
||||
else
|
||||
items["os_path"] = "Unknown";
|
||||
if (items["library_list"] is string libs)
|
||||
{
|
||||
var libList = libs.Split('\n').Select(l => l.Trim(' ', '\t', '-', '\r', '[', ']')).Where(s => !string.IsNullOrEmpty(s)).ToList();
|
||||
items["library_list"] = libList.Count > 0 ? string.Join(", ", libList) : "None";
|
||||
}
|
||||
|
||||
foreach (var key in items.AllKeys)
|
||||
{
|
||||
var value = items[key];
|
||||
if ("true".Equals(value, StringComparison.CurrentCultureIgnoreCase))
|
||||
value = "[x]";
|
||||
else if ("false".Equals(value, StringComparison.CurrentCultureIgnoreCase))
|
||||
value = "[ ]";
|
||||
items[key] = value.Sanitize();
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildInfoSection(DiscordEmbedBuilder builder, NameValueCollection items)
|
||||
{
|
||||
builder.AddField("Build Info", $"{items["build_and_specs"]}{Environment.NewLine}GPU: {items["gpu_info"]}");
|
||||
}
|
||||
|
||||
private static void BuildCpuSection(DiscordEmbedBuilder builder, NameValueCollection items)
|
||||
{
|
||||
var content = new StringBuilder()
|
||||
.AppendLine($"`PPU Decoder: {items["ppu_decoder"],21}`")
|
||||
.AppendLine($"`SPU Decoder: {items["spu_decoder"],21}`")
|
||||
.AppendLine($"`SPU Lower Thread Priority: {items["spu_lower_thread_priority"],7}`")
|
||||
.AppendLine($"`SPU Loop Detection: {items["spu_loop_detection"],14}`")
|
||||
.AppendLine($"`Thread Scheduler: {items["thread_scheduler"],16}`")
|
||||
.AppendLine($"`Detected OS: {items["os_path"],21}`")
|
||||
.AppendLine($"`SPU Threads: {items["spu_threads"],21}`")
|
||||
.AppendLine($"`Force CPU Blit: {items["cpu_blit"] ?? "N/A",18}`")
|
||||
.AppendLine($"`Hook Static Functions: {items["hook_static_functions"],11}`")
|
||||
.AppendLine($"`Lib Loader: {items["lib_loader"],22}`")
|
||||
.ToString();
|
||||
builder.AddField(items["custom_config"] == null ? "CPU Settings" : "Per-game CPU Settings", content, true);
|
||||
}
|
||||
|
||||
private static void BuildGpuSection(DiscordEmbedBuilder builder, NameValueCollection items)
|
||||
{
|
||||
var content = new StringBuilder()
|
||||
.AppendLine($"`Renderer: {items["renderer"],24}`")
|
||||
.AppendLine($"`Aspect ratio: {items["aspect_ratio"],20}`")
|
||||
.AppendLine($"`Resolution: {items["resolution"],22}`")
|
||||
.AppendLine($"`Resolution Scale: {items["resolution_scale"] ?? "N/A",16}`")
|
||||
.AppendLine($"`Resolution Scale Threshold: {items["texture_scale_threshold"] ?? "N/A",6}`")
|
||||
.AppendLine($"`Write Color Buffers: {items["write_color_buffers"],13}`")
|
||||
.AppendLine($"`Use GPU texture scaling: {items["gpu_texture_scaling"],9}`")
|
||||
.AppendLine($"`Anisotropic Filter: {items["af_override"] ?? "N/A",14}`")
|
||||
.AppendLine($"`Frame Limit: {items["frame_limit"],21}`")
|
||||
.AppendLine($"`Disable Vertex Cache: {items["vertex_cache"],12}`")
|
||||
.ToString();
|
||||
builder.AddField(items["custom_config"] == null ? "GPU Settings" : "Per-game GPU Settings", content, true);
|
||||
}
|
||||
|
||||
private static void BuildLibsSection(DiscordEmbedBuilder builder, NameValueCollection items)
|
||||
{
|
||||
if (items["lib_loader"] is string libs && libs.Contains("manual", StringComparison.InvariantCultureIgnoreCase))
|
||||
builder.AddField("Selected Libraries", items["library_list"]);
|
||||
}
|
||||
|
||||
private static async Task BuildNotesSectionAsync(DiscordEmbedBuilder builder, LogParseState state, NameValueCollection items)
|
||||
{
|
||||
if (items["fatal_error"] is string fatalError)
|
||||
builder.AddField("Fatal Error", $"`{fatalError}`");
|
||||
string notes = null;
|
||||
if (state.Error == LogParseState.ErrorCode.SizeLimit)
|
||||
notes += "Log was too large, showing last processed run";
|
||||
|
||||
// should be last check here
|
||||
var updateInfo = await CheckForUpdateAsync(items).ConfigureAwait(false);
|
||||
if (updateInfo != null)
|
||||
notes += $"{Environment.NewLine}Outdated RPCS3 build detected";
|
||||
if (notes != null)
|
||||
builder.AddField("Notes", notes);
|
||||
|
||||
if (updateInfo != null)
|
||||
await updateInfo.AsEmbedAsync(builder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<UpdateInfo> CheckForUpdateAsync(NameValueCollection items)
|
||||
{
|
||||
if (!(items["build_and_specs"] is string buildAndSpecs))
|
||||
return null;
|
||||
|
||||
var buildInfo = BuildInfoInLog.Match(buildAndSpecs.ToLowerInvariant());
|
||||
if (!buildInfo.Success || buildInfo.Groups["branch"].Value != "head")
|
||||
return null;
|
||||
|
||||
var updateInfo = await compatClient.GetUpdateAsync(Config.Cts.Token).ConfigureAwait(false);
|
||||
var link = updateInfo.LatestBuild?.Windows?.Download ?? updateInfo.LatestBuild?.Linux?.Download;
|
||||
if (string.IsNullOrEmpty(link))
|
||||
return null;
|
||||
|
||||
var latestBuildInfo = BuildInfoInUpdate.Match(link.ToLowerInvariant());
|
||||
if (!latestBuildInfo.Success || buildInfo.Groups["commit"].Value == latestBuildInfo.Groups["commit"].Value)
|
||||
return null;
|
||||
|
||||
return updateInfo;
|
||||
}
|
||||
}
|
||||
}
|
90
CompatBot/ResultFormatters/TitleInfoFormatter.cs
Normal file
90
CompatBot/ResultFormatters/TitleInfoFormatter.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.POCOs;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.ResultFormatters
|
||||
{
|
||||
internal static class TitleInfoFormatter
|
||||
{
|
||||
private static readonly Dictionary<string, DiscordColor> StatusColors = new Dictionary<string, DiscordColor>(StringComparer.InvariantCultureIgnoreCase)
|
||||
{
|
||||
{"Nothing", Config.Colors.CompatStatusNothing},
|
||||
{"Loadable", Config.Colors.CompatStatusLoadable},
|
||||
{"Intro", Config.Colors.CompatStatusIntro},
|
||||
{"Ingame", Config.Colors.CompatStatusIngame},
|
||||
{"Playable", Config.Colors.CompatStatusPlayable},
|
||||
};
|
||||
|
||||
private static string ToUpdated(this TitleInfo info)
|
||||
{
|
||||
return DateTime.TryParseExact(info.Date, ApiConfig.DateInputFormat, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out var date) ? date.ToString(ApiConfig.DateOutputFormat) : null;
|
||||
}
|
||||
|
||||
private static string ToPrString(this TitleInfo info, string defaultString)
|
||||
{
|
||||
return (info.Pr ?? 0) == 0 ? defaultString : info.Pr.ToString();
|
||||
}
|
||||
|
||||
public static string AsString(this TitleInfo info, string titleId)
|
||||
{
|
||||
if (info.Status == TitleInfo.Maintenance.Status)
|
||||
return "API is undergoing maintenance, please try again later.";
|
||||
|
||||
if (info.Status == TitleInfo.CommunicationError.Status)
|
||||
return "Error communicating with compatibility API, please try again later.";
|
||||
|
||||
if (StatusColors.TryGetValue(info.Status, out _))
|
||||
{
|
||||
var title = info.Title.Trim(40);
|
||||
return $"ID:{titleId,-9} Title:{title,-40} PR:{info.ToPrString("???"),-4} Status:{info.Status,-8} Updated:{info.ToUpdated(),-10}";
|
||||
}
|
||||
|
||||
return $"Product code {titleId} was not found in compatibility database, possibly untested!";
|
||||
}
|
||||
|
||||
public static DiscordEmbed AsEmbed(this TitleInfo info, string titleId, bool footer = true)
|
||||
{
|
||||
if (info.Status == TitleInfo.Maintenance.Status)
|
||||
return new DiscordEmbedBuilder{Description = "API is undergoing maintenance, please try again later.", Color = Config.Colors.Maintenance}.Build();
|
||||
|
||||
if (info.Status == TitleInfo.CommunicationError.Status)
|
||||
return new DiscordEmbedBuilder{Description = "Error communicating with compatibility API, please try again later.", Color = Config.Colors.Maintenance}.Build();
|
||||
|
||||
if (StatusColors.TryGetValue(info.Status, out var color))
|
||||
{
|
||||
// apparently there's no formatting in the footer, but you need to escape everything in description; ugh
|
||||
var pr = info.ToPrString(footer ? @"¯\_(ツ)_ /¯" : @"¯\\\_(ツ)\_ /¯");
|
||||
var desc = $"Status: {info.Status}, PR: {pr}, Updated: {info.ToUpdated()}";
|
||||
return new DiscordEmbedBuilder
|
||||
{
|
||||
Title = $"[{titleId}] {info.Title.Trim(200)}",
|
||||
Url = $"https://forums.rpcs3.net/thread-{info.Thread}.html",
|
||||
Description = footer ? null : desc,
|
||||
Color = color,
|
||||
}.WithFooter(footer ? desc : null)
|
||||
.Build();
|
||||
}
|
||||
else
|
||||
{
|
||||
var desc = string.IsNullOrEmpty(titleId)
|
||||
? "No product id was found; log might be corrupted, please reupload a new copy"
|
||||
: $"Product code {titleId} was not found in compatibility database, possibly untested!";
|
||||
return new DiscordEmbedBuilder{Description = desc, Color = Config.Colors.CompatStatusUnknown};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static string AsString(this KeyValuePair<string, TitleInfo> resultInfo)
|
||||
{
|
||||
return resultInfo.Value.AsString(resultInfo.Key);
|
||||
}
|
||||
|
||||
public static DiscordEmbed AsEmbed(this KeyValuePair<string, TitleInfo> resultInfo)
|
||||
{
|
||||
return resultInfo.Value.AsEmbed(resultInfo.Key);
|
||||
}
|
||||
}
|
||||
}
|
52
CompatBot/ResultFormatters/UpdateInfoFormatter.cs
Normal file
52
CompatBot/ResultFormatters/UpdateInfoFormatter.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.POCOs;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.ResultFormatters
|
||||
{
|
||||
internal static class UpdateInfoFormatter
|
||||
{
|
||||
private static readonly Client client = new Client();
|
||||
|
||||
public static async Task<DiscordEmbedBuilder> AsEmbedAsync(this UpdateInfo info, DiscordEmbedBuilder builder = null)
|
||||
{
|
||||
var justAppend = builder != null;
|
||||
var build = info?.LatestBuild;
|
||||
var pr = build?.Pr ?? "0";
|
||||
string url = null;
|
||||
PrInfo prInfo = null;
|
||||
|
||||
if (justAppend)
|
||||
{
|
||||
if (pr == "0")
|
||||
pr = "PR #???";
|
||||
else
|
||||
{
|
||||
url = "https://github.com/RPCS3/rpcs3/pull/" + pr;
|
||||
prInfo = await client.GetPrInfoAsync(pr, Config.Cts.Token).ConfigureAwait(false);
|
||||
pr = $"PR #{pr} by {prInfo?.User?.login ?? "???"}";
|
||||
}
|
||||
}
|
||||
builder = builder ?? new DiscordEmbedBuilder {Title = pr, Url = url, Description = prInfo?.Title, Color = Config.Colors.DownloadLinks};
|
||||
return builder
|
||||
.AddField($"Windows ({build?.Windows?.Datetime})", GetLinkMessage(build?.Windows?.Download, justAppend), justAppend)
|
||||
.AddField($"Linux ({build?.Linux?.Datetime})", GetLinkMessage(build?.Linux?.Download, justAppend), justAppend);
|
||||
}
|
||||
|
||||
private static string GetLinkMessage(string link, bool simpleName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(link))
|
||||
return "No link available";
|
||||
|
||||
var text = new Uri(link).Segments?.Last();
|
||||
if (simpleName && text.Contains('_'))
|
||||
text = text.Split('_', 2)[0];
|
||||
|
||||
return $"⏬ [{text}]({link})";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
59
CompatBot/Utils/AutosplitResponseHelper.cs
Normal file
59
CompatBot/Utils/AutosplitResponseHelper.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.Utils
|
||||
{
|
||||
public static class AutosplitResponseHelper
|
||||
{
|
||||
public static Task SendAutosplitMessageAsync(this CommandContext ctx, StringBuilder message, int blockSize = 2000, string blockEnd = "```", string blockStart = "```")
|
||||
{
|
||||
return ctx.Channel.SendAutosplitMessageAsync(message, blockSize, blockEnd, blockStart);
|
||||
}
|
||||
|
||||
public static Task SendAutosplitMessageAsync(this CommandContext ctx, string message, int blockSize = 2000, string blockEnd = "```", string blockStart = "```")
|
||||
{
|
||||
return ctx.Channel.SendAutosplitMessageAsync(message, blockSize, blockEnd, blockStart);
|
||||
}
|
||||
|
||||
public static async Task SendAutosplitMessageAsync(this DiscordChannel channel, StringBuilder message, int blockSize = 2000, string blockEnd = "```", string blockStart = "```")
|
||||
{
|
||||
if (message == null)
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
|
||||
await SendAutosplitMessageAsync(channel, message.ToString(), blockSize, blockEnd, blockStart).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task SendAutosplitMessageAsync(this DiscordChannel channel, string message, int blockSize = 2000, string blockEnd = "```", string blockStart = "```")
|
||||
{
|
||||
if (channel == null)
|
||||
throw new ArgumentNullException(nameof(channel));
|
||||
|
||||
if (string.IsNullOrEmpty(message))
|
||||
return;
|
||||
|
||||
blockEnd = blockEnd ?? "";
|
||||
blockStart = blockStart ?? "";
|
||||
var maxContentSize = blockSize - blockEnd.Length - blockStart.Length;
|
||||
await channel.TriggerTypingAsync().ConfigureAwait(false);
|
||||
var buffer = new StringBuilder();
|
||||
foreach (var line in message.Split(Environment.NewLine).Select(l => l.Trim(maxContentSize)))
|
||||
{
|
||||
if (buffer.Length + line.Length + blockEnd.Length > blockSize)
|
||||
{
|
||||
await channel.SendMessageAsync(buffer.Append(blockEnd).ToString()).ConfigureAwait(false);
|
||||
await channel.TriggerTypingAsync().ConfigureAwait(false);
|
||||
buffer.Clear().Append(blockStart);
|
||||
}
|
||||
else
|
||||
buffer.AppendLine();
|
||||
buffer.Append(line);
|
||||
}
|
||||
await channel.SendMessageAsync(buffer.ToString()).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
96
CompatBot/Utils/DiscordClientExtensions.cs
Normal file
96
CompatBot/Utils/DiscordClientExtensions.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.Entities;
|
||||
using DSharpPlus.Exceptions;
|
||||
|
||||
namespace CompatBot.Utils
|
||||
{
|
||||
public static class DiscordClientExtensions
|
||||
{
|
||||
public static async Task<DiscordChannel> CreateDmAsync(this CommandContext ctx)
|
||||
{
|
||||
return ctx.Channel.IsPrivate ? ctx.Channel : await ctx.Member.CreateDmChannelAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static Task<string> GetUserNameAsync(this CommandContext ctx, ulong userId, bool? forDmPurposes = null, string defaultName = "Unknown user")
|
||||
{
|
||||
return ctx.Client.GetUserNameAsync(ctx.Channel, userId, forDmPurposes, defaultName);
|
||||
}
|
||||
|
||||
public static async Task<string> GetUserNameAsync(this DiscordClient client, DiscordChannel channel, ulong userId, bool? forDmPurposes = null, string defaultName = "Unknown user")
|
||||
{
|
||||
var isPrivate = forDmPurposes ?? channel.IsPrivate;
|
||||
if (userId == 0)
|
||||
return "";
|
||||
|
||||
try
|
||||
{
|
||||
return (await client.GetUserAsync(userId)).Username;
|
||||
}
|
||||
catch (NotFoundException)
|
||||
{
|
||||
return isPrivate ? $"@{userId}" : defaultName;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<IReadOnlyCollection<DiscordMessage>> GetMessagesBeforeAsync(this DiscordChannel channel, ulong beforeMessageId, int limit = 100, DateTime? timeLimit = null)
|
||||
{
|
||||
if (timeLimit > DateTime.UtcNow)
|
||||
throw new ArgumentException(nameof(timeLimit));
|
||||
|
||||
var afterTime = timeLimit ?? DateTime.UtcNow.AddSeconds(-30);
|
||||
var messages = await channel.GetMessagesBeforeAsync(beforeMessageId, limit).ConfigureAwait(false);
|
||||
return messages.TakeWhile(m => m.CreationTimestamp > afterTime).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
public static async Task<DiscordMessage> ReportAsync(this DiscordClient client, string infraction, DiscordMessage message, string trigger, string context, bool needsAttention = false)
|
||||
{
|
||||
var getLogChannelTask = client.GetChannelAsync(Config.BotLogId);
|
||||
var embedBuilder = MakeReportTemplate(infraction, message, needsAttention);
|
||||
var reportText = string.IsNullOrEmpty(trigger) ? "" : $"Triggered by: `{trigger}`{Environment.NewLine}";
|
||||
if (!string.IsNullOrEmpty(context))
|
||||
reportText += $"Triggered in: ```{context.Sanitize()}```{Environment.NewLine}";
|
||||
embedBuilder.Description = reportText + embedBuilder.Description;
|
||||
var logChannel = await getLogChannelTask.ConfigureAwait(false);
|
||||
return await logChannel.SendMessageAsync(embed: embedBuilder.Build()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task<DiscordMessage> ReportAsync(this DiscordClient client, string infraction, DiscordMessage message, IEnumerable<DiscordUser> reporters, bool needsAttention = true)
|
||||
{
|
||||
var getLogChannelTask = client.GetChannelAsync(Config.BotLogId);
|
||||
var embedBuilder = MakeReportTemplate(infraction, message, needsAttention);
|
||||
embedBuilder.AddField("Reporters", string.Join(Environment.NewLine, reporters.Select(r => r.Mention)));
|
||||
var logChannel = await getLogChannelTask.ConfigureAwait(false);
|
||||
return await logChannel.SendMessageAsync(embed: embedBuilder.Build()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static DiscordEmbedBuilder MakeReportTemplate(string infraction, DiscordMessage message, bool needsAttention){
|
||||
var content = message.Content;
|
||||
if (message.Attachments.Any())
|
||||
{
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
content += Environment.NewLine;
|
||||
content += string.Join(Environment.NewLine, message.Attachments.Select(a => "📎 " + a.FileName));
|
||||
}
|
||||
if (string.IsNullOrEmpty(content))
|
||||
content = "🤔 something fishy is going on here, there was no message or attachment";
|
||||
var result = new DiscordEmbedBuilder
|
||||
{
|
||||
Title = infraction,
|
||||
Description = needsAttention ? "Not removed, requires attention! @here" : "Removed, doesn't require attention",
|
||||
Color = needsAttention ? Config.Colors.LogAlert : Config.Colors.LogNotice
|
||||
}.AddField("Violator", message.Author.Mention, true)
|
||||
.AddField("Channel", message.Channel.Mention, true)
|
||||
.AddField("Time (UTC)", message.CreationTimestamp.ToString("yyyy-MM-dd HH:mm:ss"), true)
|
||||
.AddField("Content of the offending item", content);
|
||||
if (needsAttention)
|
||||
result.AddField("Link to the message", $"https://discordapp.com/channels/{message.Channel.Guild.Id}/{message.Channel.Id}/{message.Id}");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
113
CompatBot/Utils/EmbedPager.cs
Normal file
113
CompatBot/Utils/EmbedPager.cs
Normal file
@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using CompatApiClient;
|
||||
using DSharpPlus.Entities;
|
||||
using Remotion.Linq.Parsing;
|
||||
|
||||
namespace CompatBot.Utils
|
||||
{
|
||||
internal class EmbedPager
|
||||
{
|
||||
private const int MaxFieldLength = 1024;
|
||||
private const int MaxTitleSize = 256;
|
||||
private const int MaxFields = 25;
|
||||
|
||||
public IEnumerable<DiscordEmbed> BreakInEmbeds(DiscordEmbedBuilder builder, IEnumerable<string> lines, int maxLinesPerField = 10)
|
||||
{
|
||||
var fieldCount = 0;
|
||||
foreach (var field in BreakInFieldContent(lines, maxLinesPerField))
|
||||
{
|
||||
if (fieldCount == MaxFields)
|
||||
{
|
||||
yield return builder.Build();
|
||||
builder.ClearFields();
|
||||
fieldCount = 0;
|
||||
}
|
||||
builder.AddField(field.title.Trim(MaxTitleSize), field.content, true);
|
||||
fieldCount++;
|
||||
}
|
||||
if (fieldCount > 0)
|
||||
yield return builder.Build();
|
||||
}
|
||||
|
||||
private IEnumerable<(string title, string content)> BreakInFieldContent(IEnumerable<string> lines, int maxLinesPerField = 10)
|
||||
{
|
||||
if (maxLinesPerField < 1)
|
||||
throw new ArgumentException("Expected a number greater than 0, but was " + maxLinesPerField, nameof(maxLinesPerField));
|
||||
|
||||
var buffer = new StringBuilder();
|
||||
var lineCount = 0;
|
||||
string firstLine = null;
|
||||
string lastLine = null;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (string.IsNullOrEmpty(firstLine))
|
||||
firstLine = line;
|
||||
|
||||
if (lineCount == maxLinesPerField)
|
||||
{
|
||||
yield return (MakeTitle(firstLine, lastLine), buffer.ToString());
|
||||
buffer.Clear();
|
||||
lineCount = 0;
|
||||
firstLine = line;
|
||||
}
|
||||
|
||||
if (buffer.Length + line.Length + Environment.NewLine.Length > MaxFieldLength)
|
||||
{
|
||||
if (buffer.Length + line.Length > MaxFieldLength)
|
||||
{
|
||||
if (buffer.Length == 0)
|
||||
yield return (MakeTitle(line, line), line.Trim(MaxFieldLength));
|
||||
else
|
||||
{
|
||||
yield return (MakeTitle(firstLine, lastLine), buffer.ToString());
|
||||
buffer.Clear().Append(line);
|
||||
lineCount = 1;
|
||||
firstLine = line;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return (MakeTitle(firstLine, line), buffer.Append(line).ToString());
|
||||
buffer.Clear();
|
||||
lineCount = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
buffer.AppendLine();
|
||||
buffer.Append(line);
|
||||
lineCount++;
|
||||
lastLine = line;
|
||||
}
|
||||
}
|
||||
if (buffer.Length > 0)
|
||||
yield return (MakeTitle(firstLine, lastLine), buffer.ToString());
|
||||
}
|
||||
|
||||
private static string MakeTitle(string first, string last)
|
||||
{
|
||||
if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(last))
|
||||
return first + last;
|
||||
|
||||
if (first == last)
|
||||
return first;
|
||||
|
||||
if (last.StartsWith(first))
|
||||
return $"{first} - {last}";
|
||||
|
||||
var commonPrefix = "";
|
||||
var maxPrefixSize = Math.Min(Math.Min(first.Length, last.Length), MaxTitleSize/2);
|
||||
for (var i = 0; i < maxPrefixSize; i++)
|
||||
{
|
||||
if (first[i] == last[i])
|
||||
commonPrefix += first[i];
|
||||
else
|
||||
return $"{commonPrefix}{first[i]}-{commonPrefix}{last[i]}";
|
||||
}
|
||||
return commonPrefix;
|
||||
}
|
||||
}
|
||||
}
|
21
CompatBot/Utils/StreamExtensions.cs
Normal file
21
CompatBot/Utils/StreamExtensions.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CompatBot.Utils
|
||||
{
|
||||
internal static class StreamExtensions
|
||||
{
|
||||
public static async Task<int> ReadBytesAsync(this Stream stream, byte[] buffer)
|
||||
{
|
||||
var result = 0;
|
||||
int read;
|
||||
do
|
||||
{
|
||||
var remaining = buffer.Length - result;
|
||||
read = await stream.ReadAsync(buffer, result, remaining).ConfigureAwait(false);
|
||||
result += read;
|
||||
} while (read > 0 && result < buffer.Length);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
36
CompatBot/Utils/StringUtils.cs
Normal file
36
CompatBot/Utils/StringUtils.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Text;
|
||||
|
||||
namespace CompatBot.Utils
|
||||
{
|
||||
internal static class StringUtils
|
||||
{
|
||||
public static string StripQuotes(this string str)
|
||||
{
|
||||
if (str == null || str.Length < 2)
|
||||
return str;
|
||||
|
||||
if (str.StartsWith('"') && str.EndsWith('"'))
|
||||
return str.Substring(1, str.Length - 2);
|
||||
return str;
|
||||
}
|
||||
|
||||
public static string AsString(this ReadOnlySequence<byte> buffer, Encoding encoding = null)
|
||||
{
|
||||
encoding = encoding ?? Encoding.ASCII;
|
||||
if (buffer.IsSingleSegment)
|
||||
return encoding.GetString(buffer.First.Span);
|
||||
|
||||
void Splice(Span<char> span, ReadOnlySequence<byte> sequence)
|
||||
{
|
||||
foreach (var segment in sequence)
|
||||
{
|
||||
encoding.GetChars(segment.Span, span);
|
||||
span = span.Slice(segment.Length);
|
||||
}
|
||||
}
|
||||
return string.Create((int)buffer.Length, buffer, Splice);
|
||||
}
|
||||
}
|
||||
}
|
23
LICENSE
23
LICENSE
@ -6,9 +6,9 @@
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
(This is the first released version of the Lesser GPL. It also counts
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.)
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
@ -470,8 +470,9 @@ safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{description}
|
||||
Copyright (C) {year} {fullname}
|
||||
RPCS3 Compatibility Bot provides moderation and entertainment
|
||||
tools for discord channels of choice.
|
||||
Copyright (C) 2018 RPCS3 project
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
@ -488,17 +489,3 @@ convey the exclusion of warranty; and each file should have at least the
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random
|
||||
Hacker.
|
||||
|
||||
{signature of Ty Coon}, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
|
42
README.md
42
README.md
@ -1,20 +1,30 @@
|
||||
# discord-bot
|
||||
RPCS3 Compatibility Bot reimplemented in C# for .NET Core
|
||||
=========================================================
|
||||
|
||||
Dependencies:
|
||||
* python3.6 or newer
|
||||
* pip for python3
|
||||
* `$ pip install -U git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py[voice]`
|
||||
* pyparsing for python3 (distro package or through pip)
|
||||
* requests for python3 (distro package or through pip)
|
||||
* peewee for python3 (distro package or through pip)
|
||||
Development Requirements
|
||||
------------------------
|
||||
* [.NET Core 2.1 SDK](https://www.microsoft.com/net/download/windows) or newer
|
||||
* Any text editor, but Visual Studio 2017 or Visual Studio Code is recommended
|
||||
|
||||
Runtime Requirements
|
||||
--------------------
|
||||
* [.NET Core 2.1 Runtime](https://www.microsoft.com/net/download/windows) or newer for compiled version
|
||||
* [.NET Core 2.1 SDK](https://www.microsoft.com/net/download/windows) or newer to run from sources
|
||||
|
||||
Optional stuff for private testing:
|
||||
* [create an app](https://discordapp.com/developers/applications/me)
|
||||
* add a user bot to this new app (look at the bottom of the app page)
|
||||
* notice the Bot User Token
|
||||
* [add your new bot to your private server](https://discordapp.com/oauth2/authorize?client_id=BOTCLIENTID&scope=bot)
|
||||
* change IDs in `bot_config.py` for your channels and users
|
||||
How to Build
|
||||
------------
|
||||
* Change configuration for test server in `CompatBot/Properties/launchSettings.json`
|
||||
* Note that token could be set in the settings _or_ supplied as a launch argument (higher priority)
|
||||
* If you've changed the database model, add a migration
|
||||
* `$ cd CompatBot`
|
||||
* `$ dotnet ef migrations add NAME`
|
||||
* `$ cd ..`
|
||||
* `$ cd CompatBot`
|
||||
* `$ dotnet run [token]`
|
||||
|
||||
How to run:
|
||||
* `$ python3 bot.py bot_user_token`
|
||||
How to Run in Production
|
||||
------------------------
|
||||
* Change configuration if needed (probably just token)
|
||||
* Put `bot.db` in `CompatBot/`
|
||||
* `$ cd CompatBot`
|
||||
* `$ dotnet run -c Release [token]`
|
@ -1,9 +0,0 @@
|
||||
__title__ = 'CompatAPI Python Wrapper'
|
||||
__version__ = '1.0.0'
|
||||
__build__ = 0x1
|
||||
__author__ = 'Roberto Anic Banic'
|
||||
__license__ = 'GPLv3'
|
||||
__copyright__ = 'Copyright 2017 Roberto Anic Banic'
|
||||
|
||||
from .config import *
|
||||
from .utils import *
|
@ -1,92 +0,0 @@
|
||||
"""
|
||||
API Configuration File
|
||||
"""
|
||||
version = 1
|
||||
|
||||
datetime_input_format = "%Y-%m-%d"
|
||||
datetime_output_format = "%Y-%m-%d"
|
||||
datetime_compatlist_query_format = "%Y%m%d"
|
||||
base_url = "https://rpcs3.net/compatibility"
|
||||
newline_separator = "<newline>"
|
||||
|
||||
return_codes = {
|
||||
0: {
|
||||
"display_results": True,
|
||||
"override_all": False,
|
||||
"display_footer": True,
|
||||
"info": "Results successfully retrieved."
|
||||
},
|
||||
1: {
|
||||
"display_results": False,
|
||||
"override_all": False,
|
||||
"display_footer": True,
|
||||
"info": "No results."
|
||||
},
|
||||
2: {
|
||||
"display_results": True,
|
||||
"override_all": False,
|
||||
"display_footer": True,
|
||||
"info": "No match was found, displaying results for: ***{lehvenstein}***."
|
||||
},
|
||||
-1: {
|
||||
"display_results": False,
|
||||
"override_all": True,
|
||||
"display_footer": False,
|
||||
"info": "{requestor}: Internal error occurred, please contact Ani and Nicba1010"
|
||||
},
|
||||
-2: {
|
||||
"display_results": False,
|
||||
"override_all": True,
|
||||
"display_footer": False,
|
||||
"info": "{requestor}: API is undergoing maintenance, please try again later."
|
||||
},
|
||||
-3: {
|
||||
"display_results": False,
|
||||
"override_all": False,
|
||||
"display_footer": False,
|
||||
"info": "Illegal characters found, please try again with a different search term."
|
||||
}
|
||||
}
|
||||
|
||||
default_amount = 1
|
||||
request_result_amount = {
|
||||
1: 15,
|
||||
2: 25,
|
||||
3: 50,
|
||||
4: 100
|
||||
}
|
||||
|
||||
directions = {
|
||||
"a": ("a", "asc", "ascending"),
|
||||
"d": ("d", "desc", "descending")
|
||||
}
|
||||
|
||||
regions = {
|
||||
"j": ("j", "ja", "japan", "JPN"),
|
||||
"u": ("u", "us", "america", "USA"),
|
||||
"e": ("e", "eu", "europe", "EU"),
|
||||
"a": ("a", "asia", "ch", "china", "CHN"),
|
||||
"k": ("k", "kor", "korea", "KOR"),
|
||||
"h": ("h", "hk", "hong", "kong", "hongkong", "HK")
|
||||
}
|
||||
|
||||
statuses = {
|
||||
"all": 0,
|
||||
"playable": 1,
|
||||
"ingame": 2,
|
||||
"intro": 3,
|
||||
"loadable": 4,
|
||||
"nothing": 5
|
||||
}
|
||||
|
||||
sort_types = {
|
||||
"id": 1,
|
||||
"title": 2,
|
||||
"status": 3,
|
||||
"date": 4
|
||||
}
|
||||
|
||||
release_types = {
|
||||
"b": ("b", "d", "disc", "bluray", "Blu-Ray"),
|
||||
"n": ("n", "p", "psn", "PSN")
|
||||
}
|
200
api/request.py
200
api/request.py
@ -1,200 +0,0 @@
|
||||
"""
|
||||
ApiRequest class
|
||||
"""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
|
||||
import api
|
||||
from api import datetime_compatlist_query_format, datetime_input_format, base_url, version
|
||||
from api.response import ApiResponse
|
||||
|
||||
|
||||
class ApiRequest(object):
|
||||
"""
|
||||
API Request builder object
|
||||
"""
|
||||
|
||||
def __init__(self, requestor=None) -> None:
|
||||
self.requestor = requestor
|
||||
self.custom_header = None
|
||||
self.time_start = None
|
||||
self.search = None
|
||||
self.status = None
|
||||
self.start = None
|
||||
self.sort = None
|
||||
self.date = None
|
||||
self.release_type = None
|
||||
self.region = None
|
||||
self.amount = api.default_amount
|
||||
self.amount_wanted = api.request_result_amount[api.default_amount]
|
||||
|
||||
def set_search(self, search: str) -> 'ApiRequest':
|
||||
"""
|
||||
Adds the search string to the query.
|
||||
:param search: string to search for
|
||||
:return: ApiRequest object
|
||||
"""
|
||||
self.search = search
|
||||
return self
|
||||
|
||||
def set_custom_header(self, custom_header) -> 'ApiRequest':
|
||||
"""
|
||||
Sets a custom header.
|
||||
:param custom_header: custom hedaer
|
||||
:return: ApiRequest object
|
||||
"""
|
||||
self.custom_header = custom_header
|
||||
|
||||
def set_status(self, status: int) -> 'ApiRequest':
|
||||
"""
|
||||
Adds status filter to the query.
|
||||
:param status: status to filter by, see ApiConfig.statuses
|
||||
:return: ApiRequest object
|
||||
"""
|
||||
try:
|
||||
self.status = api.statuses[status]
|
||||
except KeyError:
|
||||
self.status = None
|
||||
|
||||
return self
|
||||
|
||||
def set_startswith(self, start: str) -> 'ApiRequest':
|
||||
"""
|
||||
Adds starting character filter to the query.
|
||||
:param start: character to filter by
|
||||
:return: ApiRequest object
|
||||
"""
|
||||
if len(start) != 1:
|
||||
if start in ("num", "09"):
|
||||
self.start = "09"
|
||||
elif start in ("sym", "#"):
|
||||
self.start = "sym"
|
||||
else:
|
||||
self.start = start
|
||||
|
||||
return self
|
||||
|
||||
def set_sort(self, sort_type, direction) -> 'ApiRequest':
|
||||
"""
|
||||
Adds sorting request to query.
|
||||
:param sort_type: element to sort by, see ApiConfig.sort_types
|
||||
:param direction: sorting direction, see ApiConfig.directions
|
||||
:return: ApiRequest object
|
||||
"""
|
||||
for k, v in api.directions.items():
|
||||
if direction in v:
|
||||
try:
|
||||
self.sort = str(api.sort_types[sort_type]) + k
|
||||
return self
|
||||
except KeyError:
|
||||
self.sort = None
|
||||
return self
|
||||
|
||||
return self
|
||||
|
||||
def set_date(self, date: str) -> 'ApiRequest':
|
||||
"""
|
||||
Adds date filter to query.
|
||||
:param date: date to filter by
|
||||
:return: ApiRequest object
|
||||
"""
|
||||
try:
|
||||
date = datetime.strptime(date, datetime_input_format)
|
||||
self.date = datetime.strftime(date, datetime_compatlist_query_format)
|
||||
except ValueError:
|
||||
self.date = None
|
||||
|
||||
return self
|
||||
|
||||
def set_release_type(self, release_type: str) -> 'ApiRequest':
|
||||
"""
|
||||
Adds release type filter to query.
|
||||
:param release_type: release type to filter by, see ApiConfig.release_type
|
||||
:return: ApiRequest object
|
||||
"""
|
||||
for k, v in api.release_types.items():
|
||||
if release_type in v:
|
||||
self.release_type = k
|
||||
return self
|
||||
|
||||
self.release_type = None
|
||||
return self
|
||||
|
||||
def set_region(self, region: str) -> 'ApiRequest':
|
||||
"""
|
||||
Adds region filter to query.
|
||||
:param region: region to filter by, see ApiConfig.regions
|
||||
:return: ApiRequest object
|
||||
"""
|
||||
for k, v in api.regions.items():
|
||||
if region in v:
|
||||
self.region = k
|
||||
return self
|
||||
|
||||
self.region = None
|
||||
return self
|
||||
|
||||
def set_amount(self, amount: int) -> 'ApiRequest':
|
||||
"""
|
||||
Sets the desired result count and gets the closest available.
|
||||
:param amount: desired result count, chooses closest available option, see ApiConfig.request_result_amount
|
||||
:return: ApiRequest object
|
||||
"""
|
||||
if max(api.request_result_amount.values()) >= amount >= 1:
|
||||
current_diff = -1
|
||||
|
||||
for k, v in api.request_result_amount.items():
|
||||
if v >= amount:
|
||||
diff = v - amount
|
||||
if diff < current_diff or current_diff == -1:
|
||||
self.amount = k
|
||||
current_diff = diff
|
||||
|
||||
if current_diff != -1:
|
||||
self.amount_wanted = amount
|
||||
else:
|
||||
self.amount_wanted = None
|
||||
self.amount = api.default_amount
|
||||
|
||||
return self
|
||||
|
||||
def build_query(self) -> str:
|
||||
"""
|
||||
Builds the search query.
|
||||
:return: the search query
|
||||
"""
|
||||
args = { "api": "v{}".format(version) }
|
||||
if self.search is not None:
|
||||
args["g"] = self.search
|
||||
if self.status is not None:
|
||||
args["s"] = self.status
|
||||
if self.start is not None:
|
||||
args["c"] = self.start
|
||||
if self.sort is not None:
|
||||
args["o"] = self.sort
|
||||
if self.date is not None:
|
||||
args["d"] = self.date
|
||||
if self.release_type is not None:
|
||||
args["t"] = self.release_type
|
||||
if self.region is not None:
|
||||
args["f"] = self.region
|
||||
|
||||
url = base_url + "?" + urlencode(args)
|
||||
return url
|
||||
|
||||
def request(self) -> ApiResponse:
|
||||
"""
|
||||
Makes an API request to the API with the current request configuration.
|
||||
:return: the API response
|
||||
"""
|
||||
print(self.build_query())
|
||||
self.time_start = api.system_time_millis()
|
||||
return ApiResponse(
|
||||
request=self,
|
||||
data=requests.get(self.build_query()).content,
|
||||
amount_wanted=self.amount_wanted,
|
||||
custom_header=self.custom_header
|
||||
)
|
@ -1,97 +0,0 @@
|
||||
"""
|
||||
ApiResponse class
|
||||
"""
|
||||
import json
|
||||
from typing import Dict, List
|
||||
|
||||
from api import newline_separator, return_codes, system_time_millis, regions, release_types, sanitize_string
|
||||
from bot_config import search_header
|
||||
from .result import ApiResult
|
||||
|
||||
|
||||
class ApiResponse(object):
|
||||
"""
|
||||
API Response object
|
||||
"""
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def __init__(self, request: 'ApiRequest', data: Dict, amount_wanted: int = None, custom_header: str = None) -> None:
|
||||
self.request = request
|
||||
self.results: List[ApiResult] = []
|
||||
|
||||
parsed_data = json.loads(data)
|
||||
self.code = parsed_data["return_code"]
|
||||
self.lehvenstein = parsed_data["search_term"] if self.code is 2 else ''
|
||||
if return_codes[self.code]["display_results"]:
|
||||
self.load_results(parsed_data["results"], amount=amount_wanted)
|
||||
|
||||
self.time_end = system_time_millis()
|
||||
self.custom_header = custom_header
|
||||
|
||||
def load_results(self, data: Dict, amount: int = None) -> None:
|
||||
"""
|
||||
Loads the result object from JSON
|
||||
:param data: data for the result objects
|
||||
:param amount: desired amount to load
|
||||
"""
|
||||
for game_id, result_data in data.items():
|
||||
if amount is None or len(self.results) < amount:
|
||||
self.results.append(ApiResult(game_id, result_data))
|
||||
else:
|
||||
break
|
||||
|
||||
def to_string(self) -> str:
|
||||
"""
|
||||
Makes a string representation of the object.
|
||||
:return: string representation of the object
|
||||
"""
|
||||
return self.build_string().format(
|
||||
requestor=self.request.requestor.mention,
|
||||
search_string=sanitize_string(self.request.search),
|
||||
request_url=self.request.build_query().replace("api=v1", "").replace("&&", "&").replace("?&","?"),
|
||||
milliseconds=self.time_end - self.request.time_start,
|
||||
amount=self.request.amount_wanted,
|
||||
region="" if self.request.region is None else regions[self.request.region][-1],
|
||||
media="" if self.request.release_type is None else release_types[self.request.release_type][-1],
|
||||
lehvenstein=self.lehvenstein
|
||||
)
|
||||
|
||||
def build_string(self) -> str:
|
||||
"""
|
||||
Builds a string representation of the object with placeholder.
|
||||
:return: string representation of the object with placeholder
|
||||
"""
|
||||
header_string = search_header if self.custom_header is None else self.custom_header
|
||||
results_string = ""
|
||||
|
||||
results_string_part = "```\n"
|
||||
for result in self.results:
|
||||
result_string = result.to_string()
|
||||
|
||||
if len(results_string_part) + len(result_string) + 4 > 2000:
|
||||
results_string_part += "```"
|
||||
results_string += results_string_part + newline_separator
|
||||
results_string_part = "```\n"
|
||||
|
||||
results_string_part += result_string + '\n'
|
||||
|
||||
if results_string_part != "```\n":
|
||||
results_string_part += "```"
|
||||
results_string += results_string_part
|
||||
|
||||
footer_string = "Retrieved from: *<{request_url}>* in {milliseconds} milliseconds!"
|
||||
if return_codes[self.code]["display_results"]:
|
||||
return "{}{}{}{}{}".format(
|
||||
header_string + '\n' + return_codes[self.code]["info"],
|
||||
newline_separator,
|
||||
results_string,
|
||||
newline_separator,
|
||||
footer_string
|
||||
)
|
||||
elif return_codes[self.code]["override_all"]:
|
||||
return return_codes[self.code]["info"]
|
||||
else:
|
||||
return "{}{}".format(
|
||||
header_string + '\n' + return_codes[self.code]["info"],
|
||||
(newline_separator + footer_string) if return_codes[self.code]["display_footer"] else ""
|
||||
)
|
@ -1,94 +0,0 @@
|
||||
"""
|
||||
ApiResult class
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
|
||||
from discord import Embed
|
||||
|
||||
from api import datetime_input_format, datetime_output_format, trim_string
|
||||
|
||||
|
||||
class ApiResult(object):
|
||||
"""
|
||||
API Result object
|
||||
"""
|
||||
|
||||
# Taken from https://rpcs3.net/compatibility
|
||||
STATUS_NOTHING = 0x455556
|
||||
STATUS_LOADABLE = 0xe74c3c
|
||||
STATUS_INTRO = 0xe08a1e
|
||||
STATUS_INGAME = 0xf9b32f
|
||||
STATUS_PLAYABLE = 0x1ebc61
|
||||
STATUS_UNKNOWN = 0x3198ff
|
||||
|
||||
status_map = dict({
|
||||
"Nothing" : STATUS_NOTHING,
|
||||
"Loadable": STATUS_LOADABLE,
|
||||
"Intro" : STATUS_INTRO,
|
||||
"Ingame" : STATUS_INGAME,
|
||||
"Playable": STATUS_PLAYABLE
|
||||
})
|
||||
|
||||
def __init__(self, game_id: str, data: Dict) -> None:
|
||||
self.game_id = game_id
|
||||
self.title = data["title"] if "title" in data else None
|
||||
self.status = data["status"] if "status" in data else None
|
||||
self.date = datetime.strptime(data["date"], datetime_input_format) if "date" in data else None
|
||||
self.thread = data["thread"] if "thread" in data else None
|
||||
self.commit = data["commit"] if "commit" in data else None
|
||||
self.pr = data["pr"] if "pr" in data and data["pr"] is not 0 else """???"""
|
||||
|
||||
def to_string(self) -> str:
|
||||
"""
|
||||
Makes a string representation of the object.
|
||||
:return: string representation of the object
|
||||
"""
|
||||
if self.status == "Maintenance":
|
||||
return "API is undergoing maintenance, please try again later."
|
||||
elif self.status in self.status_map:
|
||||
return ("ID:{:9s} Title:{:40s} PR:{:4s} Status:{:8s} Updated:{:10s}".format(
|
||||
self.game_id,
|
||||
trim_string(self.title, 40),
|
||||
self.pr,
|
||||
self.status,
|
||||
datetime.strftime(self.date, datetime_output_format)
|
||||
))
|
||||
else:
|
||||
return "Product code {} was not found in compatibility database, possibly untested!".format(self.game_id)
|
||||
|
||||
def to_embed(self, infoInFooter=True) -> Embed:
|
||||
"""
|
||||
Makes an Embed representation of the object.
|
||||
:return: Embed representation of the object
|
||||
"""
|
||||
if self.status == "Maintenance":
|
||||
return Embed(
|
||||
description="API is undergoing maintenance, please try again later.",
|
||||
color=0xffff00
|
||||
)
|
||||
elif self.status in self.status_map:
|
||||
desc = "Status: {}, PR: {}, Updated: {}".format(
|
||||
self.status,
|
||||
self.pr if self.pr != "???" else """¯\_(ツ)_/¯""",
|
||||
datetime.strftime(self.date, datetime_output_format))
|
||||
result = Embed(
|
||||
title="[{}] {}".format(self.game_id, trim_string(self.title, 200)),
|
||||
url="https://forums.rpcs3.net/thread-{}.html".format(self.thread),
|
||||
description=desc if not infoInFooter else None,
|
||||
color=self.status_map[self.status])
|
||||
if infoInFooter:
|
||||
return result.set_footer(text=desc)
|
||||
else:
|
||||
return result
|
||||
|
||||
else:
|
||||
desc = "No product id was found, log might be corrupted or tampered with"
|
||||
if self.game_id is not None:
|
||||
desc = "Product code {} was not found in compatibility database, possibly untested!".format(
|
||||
self.game_id)
|
||||
return Embed(
|
||||
description=desc,
|
||||
color=self.STATUS_UNKNOWN
|
||||
)
|
17
api/utils.py
17
api/utils.py
@ -1,17 +0,0 @@
|
||||
import time
|
||||
|
||||
|
||||
def trim_string(string: str, length: int) -> str:
|
||||
if len(string) > length:
|
||||
return string[:length - 3] + "..."
|
||||
else:
|
||||
return string
|
||||
|
||||
|
||||
def system_time_millis() -> int:
|
||||
return int(round(time.time() * 1000))
|
||||
|
||||
|
||||
def sanitize_string(s: str) -> str:
|
||||
return s.replace("`", "`\u200d").replace("@", "@\u200d") if s is not None else s
|
||||
|
@ -1,48 +0,0 @@
|
||||
"""
|
||||
Variables:
|
||||
{requestor} = Requestor Mention
|
||||
{search_string} = Search String
|
||||
{amount} = Amount Requested
|
||||
{region} = Region
|
||||
{media} = Blu-Ray/PSN
|
||||
"""
|
||||
search_header = "{requestor} searched for: ***{search_string}***"
|
||||
newest_header = "{requestor} requested top {amount} newest {region} {media} updated games"
|
||||
oldest_header = "{requestor} requested top {amount} oldest {region} {media} updated games"
|
||||
|
||||
invalid_command_text = "Invalid command!"
|
||||
|
||||
overflow_threshold = 1024 * 1024 * 256
|
||||
latest_limit = 15
|
||||
|
||||
boot_up_message = "Hello and welcome to CompatBot. \n" \
|
||||
"You can expect this message every time you crash this bot so please don't. \n" \
|
||||
"All jokes aside if you manage to crash it please message Nicba1010 with the cause of the crash. \n" \
|
||||
"Commands I would recommend using are:\n" \
|
||||
"```" \
|
||||
"!help\n" \
|
||||
"!help top\n" \
|
||||
"!top new 10 all playable\n" \
|
||||
"!filters (It's a MUST)\n" \
|
||||
"```" \
|
||||
"I wish everyone here all the best of luck with using this bot and I will strive to improve it as " \
|
||||
"often as possible.\n" \
|
||||
"*Roberto Anic Banic AKA Nicba1010\n" \
|
||||
"https://github.com/RPCS3/discord-bot"
|
||||
|
||||
bot_channel_id = 291679908067803136
|
||||
bot_spam_id = 319224795785068545
|
||||
bot_log_id = 436972161572536329
|
||||
bot_rules_channel_id = 311894275015049216
|
||||
bot_admin_id = 267367850706993152
|
||||
|
||||
user_moderation_character = '☕'
|
||||
user_moderatable_channel_ids = [272875751773306881, 319224795785068545]
|
||||
user_moderation_count_needed = 5
|
||||
user_moderation_excused_roles = ['Administrator', 'Community Manager', 'Web Developer', 'Moderator',
|
||||
'Lead Graphics Developer', 'Lead Core Developer', 'Developers', 'Affiliated',
|
||||
'Contributors'
|
||||
]
|
||||
piracy_strings = [
|
||||
|
||||
]
|
17
bot_utils.py
17
bot_utils.py
@ -1,17 +0,0 @@
|
||||
from api.request import ApiRequest
|
||||
from api.result import ApiResult
|
||||
|
||||
def get_code(code: str) -> ApiResult:
|
||||
"""
|
||||
Gets the game data for a certain game code or returns None
|
||||
:param code: code to get data for
|
||||
:return: data or None
|
||||
"""
|
||||
result = ApiRequest().set_search(code).set_amount(10).request()
|
||||
if result.code == -2:
|
||||
return ApiResult("", dict({"status": "Maintenance"}))
|
||||
elif len(result.results) >= 1:
|
||||
for result in result.results:
|
||||
if result.game_id == code:
|
||||
return result
|
||||
return ApiResult(code, dict({"status": "Unknown"}))
|
65
database.py
65
database.py
@ -1,65 +0,0 @@
|
||||
from peewee import *
|
||||
from playhouse.migrate import *
|
||||
|
||||
from bot_config import bot_admin_id
|
||||
|
||||
db = SqliteDatabase('bot.db')
|
||||
|
||||
|
||||
class BaseModel(Model):
|
||||
class Meta:
|
||||
database = db
|
||||
|
||||
|
||||
class Moderator(BaseModel):
|
||||
discord_id = IntegerField(unique=True)
|
||||
sudoer = BooleanField(default=False)
|
||||
|
||||
|
||||
class PiracyString(BaseModel):
|
||||
string = CharField(unique=True)
|
||||
|
||||
|
||||
class Warning(BaseModel):
|
||||
discord_id = IntegerField(index=True)
|
||||
issuer_id = IntegerField(default=0)
|
||||
reason = TextField()
|
||||
full_reason = TextField()
|
||||
|
||||
|
||||
class Explanation(BaseModel):
|
||||
keyword = TextField(unique=True)
|
||||
text = TextField()
|
||||
|
||||
def init():
|
||||
with db:
|
||||
with db.atomic() as tx:
|
||||
db.get_tables()
|
||||
db.create_tables([Moderator, PiracyString, Warning, Explanation])
|
||||
try:
|
||||
Moderator.get(discord_id=bot_admin_id)
|
||||
except DoesNotExist:
|
||||
Moderator(discord_id=bot_admin_id, sudoer=True).save()
|
||||
tx.commit()
|
||||
|
||||
migrator = SqliteMigrator(db)
|
||||
try:
|
||||
with db.atomic() as tx:
|
||||
migrate(
|
||||
migrator.add_column('warning', 'issuer_id', IntegerField(default=0)),
|
||||
)
|
||||
tx.commit()
|
||||
print("Updated [warning] columns")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
tx.rollback()
|
||||
try:
|
||||
with db.atomic() as tx:
|
||||
migrate(
|
||||
migrator.add_index('warning', ('discord_id',), False),
|
||||
)
|
||||
tx.commit()
|
||||
print("Updated [warning] indices")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
tx.rollback()
|
31
discord-bot-net.sln
Normal file
31
discord-bot-net.sln
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27703.2035
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatBot", "CompatBot\CompatBot.csproj", "{6D9CA448-60C1-4D66-91D6-EC6C586508E6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatApiClient", "CompatApiClient\CompatApiClient.csproj", "{8AF3C23B-D695-4391-A298-5BA4AAB8E13B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{6D9CA448-60C1-4D66-91D6-EC6C586508E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6D9CA448-60C1-4D66-91D6-EC6C586508E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6D9CA448-60C1-4D66-91D6-EC6C586508E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6D9CA448-60C1-4D66-91D6-EC6C586508E6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8AF3C23B-D695-4391-A298-5BA4AAB8E13B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8AF3C23B-D695-4391-A298-5BA4AAB8E13B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8AF3C23B-D695-4391-A298-5BA4AAB8E13B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8AF3C23B-D695-4391-A298-5BA4AAB8E13B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D7696F56-AEAC-4D83-9BD8-BE0C122A5DCE}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
379
log_analyzer.py
379
log_analyzer.py
@ -1,379 +0,0 @@
|
||||
import re
|
||||
|
||||
import itertools
|
||||
from collections import deque
|
||||
from api import sanitize_string, trim_string
|
||||
from api.result import ApiResult
|
||||
from bot_config import piracy_strings
|
||||
from bot_utils import get_code
|
||||
|
||||
SERIAL_PATTERN = re.compile('Serial: (?P<id>[A-z]{4}\\d{5})')
|
||||
LIBRARIES_PATTERN = re.compile('Load libraries:(?P<libraries>.*)', re.DOTALL | re.MULTILINE)
|
||||
|
||||
|
||||
class LogAnalyzer(object):
|
||||
ERROR_SUCCESS = 0
|
||||
ERROR_PIRACY = 1
|
||||
ERROR_STOP = 2
|
||||
ERROR_OVERFLOW = -1
|
||||
ERROR_FAIL = -2
|
||||
ERROR_DEFLATE_64 = -3
|
||||
|
||||
def clear_results(self):
|
||||
if self is None or self.parsed_data is None:
|
||||
return self.ERROR_SUCCESS
|
||||
if self.build_and_specs is None:
|
||||
self.build_and_specs = self.parsed_data["build_and_specs"]
|
||||
self.trigger = None
|
||||
self.trigger_context = None
|
||||
self.buffer = ''
|
||||
self.buffer_lines.clear()
|
||||
self.libraries = []
|
||||
self.parsed_data = { "build_and_specs": self.build_and_specs }
|
||||
return self.ERROR_SUCCESS
|
||||
|
||||
def piracy_check(self):
|
||||
for trigger in piracy_strings:
|
||||
lower_trigger = trigger.lower()
|
||||
if lower_trigger in self.buffer.lower():
|
||||
self.trigger = trigger
|
||||
for line in self.buffer_lines:
|
||||
if lower_trigger in line.lower():
|
||||
self.trigger_context = line
|
||||
return self.ERROR_PIRACY
|
||||
return self.ERROR_SUCCESS
|
||||
|
||||
def done(self):
|
||||
return self.ERROR_STOP
|
||||
|
||||
def done_and_reset(self):
|
||||
self.phase_index = 0
|
||||
return self.ERROR_STOP
|
||||
|
||||
def get_id(self):
|
||||
try:
|
||||
self.product_info = get_code(re.search(SERIAL_PATTERN, self.buffer).group('id'))
|
||||
return self.ERROR_SUCCESS
|
||||
except AttributeError:
|
||||
self.product_info = ApiResult(None, dict({"status": "Unknown"}))
|
||||
return self.ERROR_SUCCESS
|
||||
|
||||
def parse_rsx(self):
|
||||
if len(self.buffer_lines) < 4:
|
||||
return False
|
||||
if all(line.startswith('·!') for line in itertools.islice(self.buffer_lines, 4)) and \
|
||||
all('RSX: ' in line for line in itertools.islice(self.buffer_lines, 4)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_libraries(self):
|
||||
try:
|
||||
self.libraries = [lib.strip().replace('.sprx', '')
|
||||
for lib
|
||||
in re.search(LIBRARIES_PATTERN, self.buffer).group('libraries').strip()[1:].split('-')]
|
||||
except KeyError as ke:
|
||||
print(ke)
|
||||
pass
|
||||
return self.ERROR_SUCCESS
|
||||
|
||||
"""
|
||||
End Trigger
|
||||
Regex
|
||||
Message To Print
|
||||
Special Return
|
||||
"""
|
||||
phase = (
|
||||
{
|
||||
'end_trigger': '·',
|
||||
'regex': re.compile('(?P<build_and_specs>.*)', flags=re.DOTALL | re.MULTILINE),
|
||||
'function': clear_results
|
||||
},
|
||||
{
|
||||
'end_trigger': 'Core:',
|
||||
'regex': re.compile('Path: (?:(?P<win_path>\\w:/)|(?P<lin_path>/)).*?\n'
|
||||
'(?:.* custom config: (?P<custom_config>.*?)\n.*?)?',
|
||||
flags=re.DOTALL | re.MULTILINE),
|
||||
'function': [get_id, piracy_check]
|
||||
},
|
||||
{
|
||||
'end_trigger': 'VFS:',
|
||||
'regex': re.compile('Decoder: (?P<ppu_decoder>.*?)\n.*?'
|
||||
'Threads: (?P<ppu_threads>.*?)\n.*?'
|
||||
'(?:scheduler: (?P<thread_scheduler>.*?)\n.*?)?'
|
||||
'Decoder: (?P<spu_decoder>.*?)\n.*?'
|
||||
'(?:secondary cores: (?P<spu_secondary_cores>.*?)\n.*?)?'
|
||||
'priority: (?P<spu_lower_thread_priority>.*?)\n.*?'
|
||||
'SPU Threads: (?P<spu_threads>.*?)\n.*?'
|
||||
'penalty: (?P<spu_delay_penalty>.*?)\n.*?'
|
||||
'detection: (?P<spu_loop_detection>.*?)\n.*?'
|
||||
'[Ll]oader: (?P<lib_loader>.*?)\n.*?'
|
||||
'functions: (?P<hook_static_functions>.*?)\n.*',
|
||||
flags=re.DOTALL | re.MULTILINE),
|
||||
'function': get_libraries
|
||||
},
|
||||
{
|
||||
'end_trigger': 'Video:',
|
||||
'regex': None,
|
||||
'function': None
|
||||
},
|
||||
{
|
||||
'end_trigger': 'Audio:',
|
||||
'regex': re.compile('Renderer: (?P<renderer>.*?)\n.*?'
|
||||
'Resolution: (?P<resolution>.*?)\n.*?'
|
||||
'Aspect ratio: (?P<aspect_ratio>.*?)\n.*?'
|
||||
'Frame limit: (?P<frame_limit>.*?)\n.*?'
|
||||
'Write Color Buffers: (?P<write_color_buffers>.*?)\n.*?'
|
||||
'VSync: (?P<vsync>.*?)\n.*?'
|
||||
'Use GPU texture scaling: (?P<gpu_texture_scaling>.*?)\n.*?'
|
||||
'Strict Rendering Mode: (?P<strict_rendering_mode>.*?)\n.*?'
|
||||
'Disable Vertex Cache: (?P<vertex_cache>.*?)\n.*?'
|
||||
'Blit: (?P<cpu_blit>.*?)\n.*?'
|
||||
'Resolution Scale: (?P<resolution_scale>.*?)\n.*?'
|
||||
'Anisotropic Filter Override: (?P<af_override>.*?)\n.*?'
|
||||
'Minimum Scalable Dimension: (?P<texture_scale_threshold>.*?)\n.*?'
|
||||
'(?:D3D12|DirectX 12):\\s*\n\\s*Adapter: (?P<d3d_gpu>.*?)\n.*?'
|
||||
'Vulkan:\\s*\n\\s*Adapter: (?P<vulkan_gpu>.*?)\n.*?',
|
||||
flags=re.DOTALL | re.MULTILINE)
|
||||
},
|
||||
{
|
||||
'end_trigger': 'Log:',
|
||||
'regex': None,
|
||||
'function': done
|
||||
},
|
||||
{
|
||||
'end_trigger': 'Objects cleared...',
|
||||
'regex': re.compile('(?:'
|
||||
'RSX:(?:\\d|\\.|\\s|\\w|-)* (?P<driver_version>(?:\\d+\\.)*\\d+)\n[^\n]*?'
|
||||
'RSX: [^\n]+\n[^\n]*?'
|
||||
'RSX: (?P<driver_manuf>.*?)\n[^\n]*?'
|
||||
'RSX: Supported texel buffer size reported: (?P<texel_buffer_size>\\d*?) bytes'
|
||||
')|(?:'
|
||||
'GL RENDERER: (?P<driver_manuf_new>.*?)\n[^\n]*?'
|
||||
'GL VERSION:(?:\\d|\\.|\\s|\\w|-)* (?P<driver_version_new>(?:\\d+\\.)*\\d+)\n[^\n]*?'
|
||||
'RSX: [^\n]+\n[^\n]*?'
|
||||
'RSX: Supported texel buffer size reported: (?P<texel_buffer_size_new>\\d*?) bytes'
|
||||
')\n.*?',
|
||||
flags=re.DOTALL | re.MULTILINE),
|
||||
'on_buffer_flush': parse_rsx,
|
||||
'function': done_and_reset
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.buffer = ''
|
||||
self.buffer_lines = deque([])
|
||||
self.total_data_len = 0
|
||||
self.phase_index = 0
|
||||
self.build_and_specs = None
|
||||
self.trigger = None
|
||||
self.trigger_context = None
|
||||
self.libraries = []
|
||||
self.parsed_data = {}
|
||||
|
||||
def feed(self, data):
|
||||
self.total_data_len += len(data)
|
||||
if self.total_data_len > 32 * 1024 * 1024:
|
||||
return self.ERROR_OVERFLOW
|
||||
if self.phase[self.phase_index]['end_trigger'] in data \
|
||||
or self.phase[self.phase_index]['end_trigger'] is data.strip():
|
||||
error_code = self.process_data()
|
||||
self.buffer = ''
|
||||
self.buffer_lines.clear()
|
||||
if error_code == self.ERROR_SUCCESS or error_code == self.ERROR_STOP:
|
||||
self.phase_index += 1
|
||||
if error_code != self.ERROR_SUCCESS:
|
||||
self.sanitize()
|
||||
return error_code
|
||||
else:
|
||||
if len(self.buffer_lines) > 256:
|
||||
error_code = self.process_data(True)
|
||||
if error_code != self.ERROR_SUCCESS:
|
||||
self.sanitize()
|
||||
return error_code
|
||||
self.buffer_lines.popleft()
|
||||
self.buffer_lines.append(data)
|
||||
return self.ERROR_SUCCESS
|
||||
|
||||
def process_data(self, on_buffer_flush = False):
|
||||
current_phase = self.phase[self.phase_index]
|
||||
# if buffer was flushed and check function found relevant data, run regex
|
||||
if on_buffer_flush:
|
||||
try:
|
||||
if current_phase['on_buffer_flush'] is not None:
|
||||
if not current_phase['on_buffer_flush'](self):
|
||||
return self.ERROR_SUCCESS
|
||||
except KeyError:
|
||||
pass
|
||||
self.buffer = '\n'.join(self.buffer_lines)
|
||||
if current_phase['regex'] is not None:
|
||||
try:
|
||||
regex_result = re.search(current_phase['regex'], self.buffer.strip() + '\n')
|
||||
if regex_result is not None:
|
||||
group_args = regex_result.groupdict()
|
||||
self.parsed_data.update(group_args)
|
||||
except AttributeError as ae:
|
||||
print(ae)
|
||||
print("Regex failed!")
|
||||
return self.ERROR_FAIL
|
||||
try:
|
||||
# run funcitons only on end_trigger
|
||||
if current_phase['function'] is not None and not on_buffer_flush:
|
||||
if isinstance(current_phase['function'], list):
|
||||
for func in current_phase['function']:
|
||||
error_code = func(self)
|
||||
if error_code != self.ERROR_SUCCESS:
|
||||
return error_code
|
||||
return self.ERROR_SUCCESS
|
||||
else:
|
||||
return current_phase['function'](self)
|
||||
except KeyError:
|
||||
pass
|
||||
return self.ERROR_SUCCESS
|
||||
|
||||
def sanitize(self):
|
||||
result = {}
|
||||
for k, v in self.parsed_data.items():
|
||||
r = sanitize_string(v)
|
||||
if r is not None:
|
||||
if r == "true":
|
||||
r = "[x]"
|
||||
elif r == "false":
|
||||
r = "[ ]"
|
||||
result[k] = r
|
||||
self.parsed_data = result
|
||||
libs = []
|
||||
for l in self.libraries:
|
||||
libs.append(sanitize_string(l))
|
||||
self.libraries = libs
|
||||
if self.trigger is not None:
|
||||
self.trigger = sanitize_string(self.trigger)
|
||||
if self.trigger_context is not None:
|
||||
self.trigger_context = sanitize_string(trim_string(self.trigger_context, 256))
|
||||
|
||||
def get_trigger(self):
|
||||
return self.trigger
|
||||
|
||||
def get_trigger_context(self):
|
||||
return self.trigger_context
|
||||
|
||||
def process_final_data(self):
|
||||
group_args = self.parsed_data
|
||||
if 'strict_rendering_mode' in group_args and group_args['strict_rendering_mode'] == 'true':
|
||||
group_args['resolution_scale'] = "Strict Mode"
|
||||
if 'spu_threads' in group_args and group_args['spu_threads'] == '0':
|
||||
group_args['spu_threads'] = 'Auto'
|
||||
if 'spu_secondary_cores' in group_args and group_args['spu_secondary_cores'] is not None:
|
||||
group_args['thread_scheduler'] = group_args['spu_secondary_cores']
|
||||
if 'driver_manuf_new' in group_args and group_args['driver_manuf_new'] is not None:
|
||||
group_args['gpu_info'] = group_args['driver_manuf_new']
|
||||
elif 'vulkan_gpu' in group_args and group_args['vulkan_gpu'] != '""':
|
||||
group_args['gpu_info'] = group_args['vulkan_gpu']
|
||||
elif 'd3d_gpu' in group_args and group_args['d3d_gpu'] != '""':
|
||||
group_args['gpu_info'] = group_args['d3d_gpu']
|
||||
elif 'driver_manuf' in group_args and group_args['driver_manuf'] is not None:
|
||||
group_args['gpu_info'] = group_args['driver_manuf']
|
||||
else:
|
||||
group_args['gpu_info'] = 'Unknown'
|
||||
if 'driver_version_new' in group_args and group_args["driver_version_new"] is not None:
|
||||
group_args["gpu_info"] = group_args["gpu_info"] + " (" + group_args["driver_version_new"] + ")"
|
||||
elif 'driver_version' in group_args and group_args["driver_version"] is not None:
|
||||
group_args["gpu_info"] = group_args["gpu_info"] + " (" + group_args["driver_version"] + ")"
|
||||
if 'af_override' in group_args:
|
||||
if group_args['af_override'] == '0':
|
||||
group_args['af_override'] = 'Auto'
|
||||
elif group_args['af_override'] == '1':
|
||||
group_args['af_override'] = 'Disabled'
|
||||
|
||||
def get_text_report(self):
|
||||
self.process_final_data()
|
||||
additional_info = {
|
||||
'product_info': self.product_info.to_string(),
|
||||
'libs': ', '.join(self.libraries) if len(self.libraries) > 0 and self.libraries[0] != "]" else "None",
|
||||
'os': 'Windows' if 'win_path' in self.parsed_data and self.parsed_data['win_path'] is not None else 'Linux',
|
||||
'config': '\nUsing custom config!\n' if 'custom_config' in self.parsed_data and self.parsed_data['custom_config'] is not None else ''
|
||||
}
|
||||
additional_info.update(self.parsed_data)
|
||||
return (
|
||||
'```'
|
||||
'{product_info}\n'
|
||||
'\n'
|
||||
'{build_and_specs}'
|
||||
'GPU: {gpu_info}\n'
|
||||
'OS: {os}\n'
|
||||
'{config}\n'
|
||||
'PPU Decoder: {ppu_decoder:>21s} | Thread Scheduler: {thread_scheduler}\n'
|
||||
'SPU Decoder: {spu_decoder:>21s} | SPU Threads: {spu_threads}\n'
|
||||
'SPU Lower Thread Priority: {spu_lower_thread_priority:>7s} | Hook Static Functions: {hook_static_functions}\n'
|
||||
'SPU Loop Detection: {spu_loop_detection:>14s} | Lib Loader: {lib_loader}\n'
|
||||
'\n'
|
||||
'Selected Libraries: {libs}\n'
|
||||
'\n'
|
||||
'Renderer: {renderer:>24s} | Frame Limit: {frame_limit}\n'
|
||||
'Resolution: {resolution:>22s} | Write Color Buffers: {write_color_buffers}\n'
|
||||
'Resolution Scale: {resolution_scale:>16s} | Use GPU texture scaling: {gpu_texture_scaling}\n'
|
||||
'Resolution Scale Threshold: {texture_scale_threshold:>6s} | Anisotropic Filter Override: {af_override}\n'
|
||||
'VSync: {vsync:>27s} | Disable Vertex Cache: {vertex_cache}\n'
|
||||
'```'
|
||||
).format(**additional_info)
|
||||
|
||||
def get_embed_report(self):
|
||||
self.process_final_data()
|
||||
lib_loader = self.parsed_data['lib_loader']
|
||||
if lib_loader is not None:
|
||||
lib_loader = lib_loader.lower()
|
||||
lib_loader_auto = 'auto' in lib_loader
|
||||
lib_loader_manual = 'manual' in lib_loader
|
||||
if lib_loader_auto and lib_loader_manual:
|
||||
self.parsed_data['lib_loader'] = "Auto & manual select"
|
||||
elif lib_loader_auto:
|
||||
self.parsed_data['lib_loader'] = "Auto"
|
||||
elif lib_loader_manual:
|
||||
self.parsed_data['lib_loader'] = "Manual selection"
|
||||
custom_config = 'custom_config' in self.parsed_data and self.parsed_data['custom_config'] is not None
|
||||
self.parsed_data['os_path'] = 'Windows' if 'win_path' in self.parsed_data and self.parsed_data['win_path'] is not None else 'Linux'
|
||||
result = self.product_info.to_embed(False).add_field(
|
||||
name='Build Info',
|
||||
value=(
|
||||
'{build_and_specs}'
|
||||
'GPU: {gpu_info}'
|
||||
).format(**self.parsed_data),
|
||||
inline=False
|
||||
).add_field(
|
||||
name='CPU Settings' if not custom_config else 'Per-game CPU Settings',
|
||||
value=(
|
||||
'`PPU Decoder: {ppu_decoder:>21s}`\n'
|
||||
'`SPU Decoder: {spu_decoder:>21s}`\n'
|
||||
'`SPU Lower Thread Priority: {spu_lower_thread_priority:>7s}`\n'
|
||||
'`SPU Loop Detection: {spu_loop_detection:>14s}`\n'
|
||||
'`Thread Scheduler: {thread_scheduler:>16s}`\n'
|
||||
'`Detected OS: {os_path:>21s}`\n'
|
||||
'`SPU Threads: {spu_threads:>21s}`\n'
|
||||
'`Force CPU Blit: {cpu_blit:>18s}`\n'
|
||||
'`Hook Static Functions: {hook_static_functions:>11s}`\n'
|
||||
'`Lib Loader: {lib_loader:>22s}`\n'
|
||||
).format(**self.parsed_data),
|
||||
inline=True
|
||||
).add_field(
|
||||
name='GPU Settings' if not custom_config else 'Per-game GPU Settings',
|
||||
value=(
|
||||
'`Renderer: {renderer:>24s}`\n'
|
||||
'`Aspect ratio: {aspect_ratio:>20s}`\n'
|
||||
'`Resolution: {resolution:>22s}`\n'
|
||||
'`Resolution Scale: {resolution_scale:>16s}`\n'
|
||||
'`Resolution Scale Threshold: {texture_scale_threshold:>6s}`\n'
|
||||
'`Write Color Buffers: {write_color_buffers:>13s}`\n'
|
||||
'`Use GPU texture scaling: {gpu_texture_scaling:>9s}`\n'
|
||||
'`Anisotropic Filter: {af_override:>14s}`\n'
|
||||
'`Frame Limit: {frame_limit:>21s}`\n'
|
||||
# '`VSync: {vsync:>27s}`\n'
|
||||
'`Disable Vertex Cache: {vertex_cache:>12s}`\n'
|
||||
).format(**self.parsed_data),
|
||||
inline=True
|
||||
)
|
||||
if 'manual' in self.parsed_data['lib_loader'].lower():
|
||||
result = result.add_field(
|
||||
name="Selected Libraries",
|
||||
value=', '.join(self.libraries) if len(self.libraries) > 0 and self.libraries[0] != "]" else "None",
|
||||
inline=False
|
||||
)
|
||||
return result
|
129
math_parse.py
129
math_parse.py
@ -1,129 +0,0 @@
|
||||
from pyparsing import (Literal, CaselessLiteral, Word, Combine, Group, Optional,
|
||||
ZeroOrMore, Forward, nums, alphas, oneOf)
|
||||
import math
|
||||
import operator
|
||||
|
||||
__author__ = 'Paul McGuire & Roberto Anic Banic'
|
||||
__version__ = '$Revision: 0.1 $'
|
||||
__date__ = '$Date: 2018-02-04 $'
|
||||
__source__ = '''http://pyparsing.wikispaces.com/file/view/fourFn.py
|
||||
http://pyparsing.wikispaces.com/message/view/home/15549426
|
||||
'''
|
||||
__note__ = '''
|
||||
All I've done is rewrap Paul McGuire's fourFn.py as a class, so I can use it
|
||||
more easily in other places.
|
||||
Added cmp
|
||||
'''
|
||||
|
||||
|
||||
class NumericStringParser(object):
|
||||
'''
|
||||
Most of this code comes from the fourFn.py pyparsing example
|
||||
|
||||
'''
|
||||
|
||||
def pushFirst(self, strg, loc, toks):
|
||||
self.exprStack.append(toks[0])
|
||||
|
||||
def pushUMinus(self, strg, loc, toks):
|
||||
if toks and toks[0] == '-':
|
||||
self.exprStack.append('unary -')
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
expop :: '^'
|
||||
multop :: '*' | '/'
|
||||
addop :: '+' | '-'
|
||||
integer :: ['+' | '-'] '0'..'9'+
|
||||
atom :: PI | E | real | fn '(' expr ')' | '(' expr ')'
|
||||
factor :: atom [ expop factor ]*
|
||||
term :: factor [ multop factor ]*
|
||||
expr :: term [ addop term ]*
|
||||
"""
|
||||
point = Literal(".")
|
||||
e = CaselessLiteral("E")
|
||||
fnumber = Combine(Word("+-" + nums, nums) +
|
||||
Optional(point + Optional(Word(nums))) +
|
||||
Optional(e + Word("+-" + nums, nums)))
|
||||
ident = Word(alphas, alphas + nums + "_$")
|
||||
plus = Literal("+")
|
||||
minus = Literal("-")
|
||||
mult = Literal("*")
|
||||
div = Literal("/")
|
||||
lpar = Literal("(").suppress()
|
||||
rpar = Literal(")").suppress()
|
||||
addop = plus | minus
|
||||
multop = mult | div
|
||||
expop = Literal("^")
|
||||
pi = CaselessLiteral("PI")
|
||||
expr = Forward()
|
||||
atom = ((Optional(oneOf("- +")) +
|
||||
(ident + lpar + expr + rpar | pi | e | fnumber).setParseAction(self.pushFirst))
|
||||
| Optional(oneOf("- +")) + Group(lpar + expr + rpar)
|
||||
).setParseAction(self.pushUMinus)
|
||||
# by defining exponentiation as "atom [ ^ factor ]..." instead of
|
||||
# "atom [ ^ atom ]...", we get right-to-left exponents, instead of left-to-right
|
||||
# that is, 2^3^2 = 2^(3^2), not (2^3)^2.
|
||||
factor = Forward()
|
||||
factor << atom + \
|
||||
ZeroOrMore((expop + factor).setParseAction(self.pushFirst))
|
||||
term = factor + \
|
||||
ZeroOrMore((multop + factor).setParseAction(self.pushFirst))
|
||||
expr << term + \
|
||||
ZeroOrMore((addop + term).setParseAction(self.pushFirst))
|
||||
# addop_term = ( addop + term ).setParseAction( self.pushFirst )
|
||||
# general_term = term + ZeroOrMore( addop_term ) | OneOrMore( addop_term)
|
||||
# expr << general_term
|
||||
self.bnf = expr
|
||||
# map operator symbols to corresponding arithmetic operations
|
||||
epsilon = 1e-12
|
||||
self.opn = {"+": operator.add,
|
||||
"-": operator.sub,
|
||||
"*": operator.mul,
|
||||
"/": operator.truediv,
|
||||
"^": operator.pow}
|
||||
self.fn = {"sin": math.sin,
|
||||
"cos": math.cos,
|
||||
"tan": math.tan,
|
||||
"exp": math.exp,
|
||||
"abs": abs,
|
||||
"sqrt": lambda a: math.sqrt(a),
|
||||
"trunc": lambda a: int(a),
|
||||
"round": round,
|
||||
"sgn": lambda a: abs(a) > epsilon and self.cmp(a, 0) or 0}
|
||||
|
||||
def evaluateStack(self, s):
|
||||
op = s.pop()
|
||||
if op == 'unary -':
|
||||
return -self.evaluateStack(s)
|
||||
if op in "+-*/^":
|
||||
op2 = self.evaluateStack(s)
|
||||
op1 = self.evaluateStack(s)
|
||||
return self.opn[op](op1, op2)
|
||||
elif op == "PI":
|
||||
return math.pi # 3.1415926535
|
||||
elif op == "E":
|
||||
return math.e # 2.718281828
|
||||
elif op in self.fn:
|
||||
return self.fn[op](self.evaluateStack(s))
|
||||
elif op[0].isalpha():
|
||||
return 0
|
||||
else:
|
||||
return float(op)
|
||||
|
||||
def eval(self, num_string, parseAll=True):
|
||||
self.exprStack = []
|
||||
results = self.bnf.parseString(num_string, parseAll)
|
||||
val = self.evaluateStack(self.exprStack[:])
|
||||
return val
|
||||
|
||||
def cmp(self, x, y):
|
||||
"""
|
||||
Replacement for built-in function cmp that was removed in Python 3
|
||||
|
||||
Compare the two objects x and y and return an integer according to
|
||||
the outcome. The return value is negative if x < y, zero if x == y
|
||||
and strictly positive if x > y.
|
||||
"""
|
||||
|
||||
return (x > y) - (x < y)
|
@ -1,9 +0,0 @@
|
||||
def limit_int(amount: int, high: int, low: int = 1) -> int:
|
||||
"""
|
||||
Limits an integer.
|
||||
:param amount: amount
|
||||
:param high: high limit
|
||||
:param low: low limit
|
||||
:return: limited integer
|
||||
"""
|
||||
return low if amount < low else (high if amount > high else amount)
|
6
nuget.config
Normal file
6
nuget.config
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="DSharpPlus Nightlies" value="https://www.myget.org/F/dsharpplus-nightly/api/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
@ -1,64 +0,0 @@
|
||||
import os
|
||||
import uuid
|
||||
import zipfile
|
||||
import zlib
|
||||
|
||||
|
||||
def download_file(stream, ext='.zip'):
|
||||
filename = str(uuid.uuid4()) + ext
|
||||
with open(filename, 'wb') as f:
|
||||
for chunk in stream.iter_content(chunk_size=1024 * 1024):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
return filename
|
||||
|
||||
|
||||
def stream_text_log(stream):
|
||||
for chunk in stream.iter_content(chunk_size=1024):
|
||||
yield chunk
|
||||
|
||||
|
||||
def stream_gzip_decompress(stream):
|
||||
dec = zlib.decompressobj(32 + zlib.MAX_WBITS) # offset 32 to skip the header
|
||||
for chunk in stream:
|
||||
try:
|
||||
rv = dec.decompress(chunk)
|
||||
if rv:
|
||||
yield rv
|
||||
del rv
|
||||
except zlib.error as zlr:
|
||||
pass
|
||||
del dec
|
||||
|
||||
|
||||
def stream_zip_decompress(stream):
|
||||
filename = download_file(stream)
|
||||
with SelfDeletingFile(filename):
|
||||
try:
|
||||
with zipfile.ZipFile(filename) as z:
|
||||
for file in z.namelist():
|
||||
print(file)
|
||||
if str(file).endswith('.log'):
|
||||
with z.open(file) as f:
|
||||
for line in f:
|
||||
yield line
|
||||
del line
|
||||
except Exception as e:
|
||||
if e.args[0] == 'compression type 9 (deflate64)':
|
||||
raise Deflate64Exception
|
||||
|
||||
|
||||
class SelfDeletingFile(object):
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
|
||||
def __enter__(self):
|
||||
print('Opening self deleting temporary file: ' + self.filename)
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
print('Removing self deleting temporary file: ' + self.filename)
|
||||
os.remove(self.filename)
|
||||
|
||||
|
||||
class Deflate64Exception(Exception):
|
||||
pass
|
Loading…
Reference in New Issue
Block a user