Rizu/Rizu.Core/Exporter.cs

243 lines
7.7 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);
var import = ScoreConversion.CreateScoreBatchManual(score);
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;
try
{
resp = JsonUtility.FromJson<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;
}
Logger.Info("{0}", resp.description);
var pollUrl = resp.body?.url;
if (string.IsNullOrEmpty(pollUrl))
{
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 = JsonUtility.FromJson<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" : "");
}
}