2024-05-21 20:29:18 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using MAI2.Util;
|
|
|
|
|
using MAI2System;
|
|
|
|
|
using Manager;
|
|
|
|
|
using Manager.UserDatas;
|
2024-05-23 09:13:19 +00:00
|
|
|
|
using Rizu.Core.Models;
|
2024-05-21 20:29:18 +00:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.Networking;
|
|
|
|
|
using Path = System.IO.Path;
|
|
|
|
|
|
2024-05-23 09:13:19 +00:00
|
|
|
|
namespace Rizu.Core;
|
2024-05-21 20:29:18 +00:00
|
|
|
|
|
2024-05-23 09:13:19 +00:00
|
|
|
|
public class Exporter
|
2024-05-21 20:29:18 +00:00
|
|
|
|
{
|
2024-05-23 09:13:19 +00:00
|
|
|
|
public static readonly Exporter Instance = new();
|
|
|
|
|
|
|
|
|
|
private readonly Config _config = new();
|
|
|
|
|
|
|
|
|
|
public bool IsEnabled => _config.Enabled;
|
2024-05-21 20:29:18 +00:00
|
|
|
|
|
|
|
|
|
// 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;
|
2024-05-21 20:53:02 +00:00
|
|
|
|
req.certificateHandler = certHandler;
|
2024-05-21 20:29:18 +00:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-23 09:13:19 +00:00
|
|
|
|
private static void SaveFailedImport(string path, string import)
|
2024-05-21 20:29:18 +00:00
|
|
|
|
{
|
|
|
|
|
var parent = Path.GetDirectoryName(path);
|
|
|
|
|
|
|
|
|
|
if (!Directory.Exists(parent) && parent != null)
|
|
|
|
|
{
|
|
|
|
|
Directory.CreateDirectory(parent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
File.WriteAllText(path, import);
|
|
|
|
|
}
|
2024-05-23 09:13:19 +00:00
|
|
|
|
|
2024-05-21 20:29:18 +00:00
|
|
|
|
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" : "");
|
|
|
|
|
}
|
|
|
|
|
}
|