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.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 resp; try { resp = JsonUtility.FromJson>(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 pollResp; try { pollResp = JsonUtility.FromJson>(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" : ""); } }