Rizu/Rizu.Core/Exporter.cs
Adele Reed 3ed7191acf Use System.Text.Json with BepInEx (#1)
When running via BepInEx, Unity's JsonUtility produces and reads empty documents.

As a bit of a hacky fix, this builds using System.Text.Json. granted, this pulls new deps in. All of the deps listed in the csproj will need to be included in the install package for BepInEx.

Reviewed-on: #1
Co-authored-by: Adele Reed <virepri2k@gmail.com>
Co-committed-by: Adele Reed <virepri2k@gmail.com>
2024-08-06 17:26:31 +00:00

253 lines
8.0 KiB
C#

using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Threading;
using MAI2.Util;
using MAI2System;
using Manager;
using Manager.UserDatas;
using Rizu.Core.Models;
using UnityEngine;
using UnityEngine.Networking;
using Path = System.IO.Path;
namespace Rizu.Core;
public class Exporter
{
public static readonly Exporter Instance = new();
private readonly Config _config = new();
public bool IsEnabled => _config.Enabled;
// ReSharper disable Unity.PerformanceAnalysis
public IEnumerator ExportScore(GameScoreList score)
{
if (!_config.Enabled)
{
yield break;
}
var user = Singleton<UserDataManager>.Instance.GetUserData(score.PlayerIndex);
string import = "";
try
{
import = ScoreConversion.CreateScoreBatchManual(score);
}
catch (Exception exception)
{
Logger.Error(exception.ToString());
}
if (import != "")
yield return SubmitImport(import, user.Detail.AccessCode);
}
public IEnumerator ExportDan(UserDetail userDetail)
{
if (!_config.Enabled)
{
yield break;
}
var import = ScoreConversion.CreateDanBatchManual((int)userDetail.CourseRank);
if (string.IsNullOrEmpty(import))
{
yield break;
}
yield return SubmitImport(import, userDetail.AccessCode);
}
private string GetTokenForAccessCode(string accessCode)
{
if (accessCode != null && _config.AccessTokens.TryGetValue(accessCode, out var accessToken))
{
return accessToken;
}
if (_config.AccessTokens.TryGetValue("default", out accessToken))
{
return accessToken;
}
if (accessCode == null)
{
Logger.Error("No `default` token was set for guest plays. Not sending import.");
return null;
}
Logger.Error(
"Access code {0}******** does not have an associated API key, and no `default` token was set. Not sending import.",
accessCode.Substring(0, 12));
return null;
}
private IEnumerator SubmitImport(string import, string accessCode)
{
var accessToken = GetTokenForAccessCode(accessCode);
if (accessToken == null)
{
yield break;
}
Logger.Debug("Sending import to Tachi: {0}", import);
using var req = new UnityWebRequest(_config.TachiBaseUrl + _config.TachiImportEndpoint);
using var certHandler = new ForceAcceptAllCertificateHandler();
req.method = UnityWebRequest.kHttpVerbPOST;
req.timeout = _config.NetworkTimeout;
req.certificateHandler = certHandler;
req.uploadHandler = new UploadHandlerRaw(new UTF8Encoding().GetBytes(import));
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json");
req.SetRequestHeader("X-User-Intent", "false");
req.SetRequestHeader("Authorization", $"Bearer {accessToken}");
yield return req.SendWebRequest();
if (req.isNetworkError || req.isHttpError)
{
Logger.Error("Could not send score to Tachi: {0}", req.error);
if (string.IsNullOrEmpty(_config.FailedImportsFolder))
{
yield break;
}
var filename = $"{accessCode ?? "GUEST"}_{DateTime.Now:yyyyMMdd'_'HHmmss}.json";
var path = Path.Combine(_config.FailedImportsFolder, filename);
ThreadPool.QueueUserWorkItem(_ => SaveFailedImport(path, import));
Logger.Info("Saved failed import to {0}", path);
yield break;
}
TachiResponse<BatchManualResponseBody> resp;
Logger.Debug("Received response from Tachi (Response code {1}): {0}", req.downloadHandler.text, req.responseCode);
try
{
resp = JsonShim.Deserialize<TachiResponse<BatchManualResponseBody>>(req.downloadHandler.text);
}
catch (Exception e)
{
Logger.Error("Could not parse response from Tachi: {0}", e);
yield break;
}
if (!resp.success)
{
Logger.Error("Score import not successful: {0}", resp.description);
yield break;
}
var pollUrl = resp.body?.url;
if (string.IsNullOrEmpty(pollUrl))
{
Logger.Debug("Received no poll uri!");
yield break;
}
Logger.Info("Poll URL: {0}", pollUrl);
yield return PollImport(pollUrl, accessToken);
}
private IEnumerator PollImport(string pollUrl, string accessToken)
{
while (true)
{
using var pollReq = UnityWebRequest.Get(pollUrl);
using var certHandler = new ForceAcceptAllCertificateHandler();
pollReq.timeout = _config.NetworkTimeout;
pollReq.certificateHandler = certHandler;
pollReq.downloadHandler = new DownloadHandlerBuffer();
pollReq.SetRequestHeader("Authorization", $"Bearer {accessToken}");
yield return pollReq.SendWebRequest();
TachiResponse<ImportStatusResponseBody> pollResp;
try
{
pollResp = JsonShim.Deserialize<TachiResponse<ImportStatusResponseBody>>(pollReq.downloadHandler.text);
}
catch (Exception e)
{
Logger.Error("Could not parse response from Tachi: {0}", e);
yield break;
}
if (!pollResp.success)
{
Logger.Error("Import failed: {0}", pollResp.description);
yield break;
}
if (pollResp.body.importStatus == "completed")
{
Logger.Info(
"{0} ({1} scores, {2} sessions, {3} errors)",
pollResp.description,
pollResp.body.import.scoreIDs.Length,
pollResp.body.import.createdSessions.Length,
pollResp.body.import.errors.Length);
yield break;
}
yield return new WaitForSeconds(1);
}
}
private static void SaveFailedImport(string path, string import)
{
var parent = Path.GetDirectoryName(path);
if (!Directory.Exists(parent) && parent != null)
{
Directory.CreateDirectory(parent);
}
File.WriteAllText(path, import);
}
public void LoadConfig()
{
using var ini = new IniFile(".\\Rizu.cfg");
_config.Enabled = ini.getValue("General", "Enable", true);
_config.NetworkTimeout = ini.getValue("General", "NetworkTimeout", 30);
_config.FailedImportsFolder = ini.getValue("General", "FailedImportsFolder", "");
_config.TachiBaseUrl = ini.getValue("Tachi", "BaseUrl", "https://kamai.tachi.ac");
_config.TachiImportEndpoint = ini.getValue("Tachi", "Import", "/ir/direct-manual/import");
var keysSection = ini.findSection("Keys");
if (keysSection == null)
{
return;
}
for (var section = keysSection.childHead; section != null; section = section.next)
{
_config.AccessTokens[section.name] = section.value;
}
Logger.Info("Loaded configuration");
Logger.Info("> Enabled: {0}", _config.Enabled);
Logger.Info("> Network timeout: {0}", _config.NetworkTimeout);
Logger.Info("> Tachi import URL: {0}{1}", _config.TachiBaseUrl, _config.TachiImportEndpoint);
Logger.Info("> {0} API key{1} loaded", _config.AccessTokens.Count, _config.AccessTokens.Count != 1 ? "s" : "");
}
}