push notifications

This commit is contained in:
2026-11-03 20:26:24 +05:30
parent a1b2b41912
commit 3ee348c09f
221 changed files with 9440 additions and 4899 deletions
+146 -13
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
@@ -13,11 +14,13 @@ public static class SupportClient
{
public const string DefaultBaseUrl = "https://kkadmin.playpoolstudios.com";
public const int MaxMessageLength = 4000;
public const int MaxLogsLength = 2000000;
const string TokenPrefsKey = "WalkInvest.SupportPlayerToken";
const string UserIdPrefsKey = "WalkInvest.SupportPlayerUserId";
const string UsernamePrefsKey = "WalkInvest.SupportPlayerUsername";
const string MessageLengthError = "Message must be between 1 and 4000 characters";
const string LogsLengthError = "Logs must be at most 2000000 characters";
const string PlayerConnectionError = "Please check your internet connection and try again.";
const int RequestTimeoutSeconds = 20;
@@ -141,8 +144,6 @@ public static class SupportClient
password = password ?? ""
};
string json = JsonUtility.ToJson(body);
Debug.Log("[SupportClient] login user='" + body.username +
"' passwordLength=" + body.password.Length);
HttpOutcome http = null;
yield return Request("POST", "/api/support/player/login", json, false, r => http = r);
@@ -157,14 +158,55 @@ public static class SupportClient
onComplete?.Invoke(result);
}
/// <summary>POST /api/support/player/devices. Stores this install's Firebase Installation ID.</summary>
public static IEnumerator RegisterDevice(string fid, string platform, Action<SupportCallResult<SupportDeviceResponse>> onComplete)
{
yield return SendDevice("POST", fid, platform, onComplete);
}
/// <summary>DELETE /api/support/player/devices. Drops this install's Firebase Installation ID.</summary>
public static IEnumerator UnregisterDevice(string fid, Action<SupportCallResult<SupportDeviceResponse>> onComplete)
{
yield return SendDevice("DELETE", fid, "android", onComplete);
}
static IEnumerator SendDevice(string method, string fid, string platform, Action<SupportCallResult<SupportDeviceResponse>> onComplete)
{
fid = fid != null ? fid.Trim() : "";
if (string.IsNullOrEmpty(fid))
{
var invalid = Fail<SupportDeviceResponse>(400, "Invalid installation id");
LogFailure("/api/support/player/devices", invalid);
onComplete?.Invoke(invalid);
yield break;
}
string json = JsonUtility.ToJson(new DeviceRequest
{
fid = fid,
platform = string.IsNullOrEmpty(platform) ? "android" : platform.Trim()
});
HttpOutcome http = null;
yield return Request(method, "/api/support/player/devices", json, true, r => http = r);
if (http != null && http.statusCode == 401 && IsSignedIn)
ClearSession();
SupportCallResult<SupportDeviceResponse> result = ParseAck<SupportDeviceResponse>(http);
LogFailure("/api/support/player/devices", result);
onComplete?.Invoke(result);
}
/// <summary>GET /api/support/player/tickets. Open tickets first, then newest message.</summary>
public static IEnumerator ListTickets(Action<SupportCallResult<SupportTicketListResponse>> onComplete)
{
yield return SendAuthorized("/api/support/player/tickets", "GET", null, onComplete, NormalizeTickets);
}
/// <summary>POST /api/support/player/tickets. Each call creates a new open ticket.</summary>
public static IEnumerator CreateTicket(string category, string body, Action<SupportCallResult<SupportThread>> onComplete)
/// <summary>
/// POST /api/support/player/tickets. Each call creates a new open ticket.
/// <paramref name="logs"/> is the Unity player log, sent on create only. Null or whitespace omits it.
/// </summary>
public static IEnumerator CreateTicket(string category, string body, string logs, Action<SupportCallResult<SupportThread>> onComplete)
{
category = category != null ? category.Trim() : "";
if (!IsCategory(category))
@@ -184,10 +226,71 @@ public static class SupportClient
yield break;
}
string json = JsonUtility.ToJson(new CreateTicketRequest { category = category, body = body });
logs = string.IsNullOrWhiteSpace(logs) ? null : logs.Trim();
if (logs != null && logs.Length > MaxLogsLength)
{
var invalid = Fail<SupportThread>(400, LogsLengthError);
LogFailure("/api/support/player/tickets", invalid);
onComplete?.Invoke(invalid);
yield break;
}
string json = JsonUtility.ToJson(new CreateTicketRequest { category = category, body = body, logs = logs });
yield return SendAuthorized("/api/support/player/tickets", "POST", json, onComplete, NormalizeThread);
}
/// <summary>
/// Reads <see cref="Application.consoleLogPath"/> for a new ticket.
/// Returns null when the log is missing or empty. Keeps the newest text when the file is longer than <see cref="MaxLogsLength"/>.
/// </summary>
public static string ReadUnityPlayerLog()
{
string path = Application.consoleLogPath;
if (string.IsNullOrWhiteSpace(path))
return null;
try
{
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
bool truncated = false;
long maxBytes = (long)MaxLogsLength * 4;
if (stream.Length > maxBytes)
{
stream.Seek(-maxBytes, SeekOrigin.End);
truncated = true;
}
using (var reader = new StreamReader(stream, Encoding.UTF8, true, 4096, true))
{
string text;
if (truncated)
{
string partial = reader.ReadLine();
string rest = reader.ReadToEnd();
text = string.IsNullOrEmpty(rest) ? partial : rest;
}
else
{
text = reader.ReadToEnd();
}
if (string.IsNullOrWhiteSpace(text))
return null;
text = text.Trim();
if (text.Length > MaxLogsLength)
text = text.Substring(text.Length - MaxLogsLength);
return text;
}
}
}
catch (Exception e)
{
Debug.LogWarning("[SupportClient] could not read Unity log: " + e.Message);
return null;
}
}
/// <summary>GET /api/support/player/tickets/{ticketId}. Messages are oldest first.</summary>
public static IEnumerator GetThread(string ticketId, Action<SupportCallResult<SupportThread>> onComplete)
{
@@ -279,17 +382,16 @@ public static class SupportClient
}
string url = RootUrl() + path;
Debug.Log("[SupportClient] " + method + " " + url);
using (var request = new UnityWebRequest(url, method))
{
request.downloadHandler = new DownloadHandlerBuffer();
request.timeout = RequestTimeoutSeconds;
if (method == UnityWebRequest.kHttpVerbPOST)
{
byte[] raw = Encoding.UTF8.GetBytes(json ?? "{}");
request.uploadHandler = new UploadHandlerRaw(raw);
request.SetRequestHeader("Content-Type", "application/json");
}
request.timeout = json != null && json.Length > 64 * 1024 ? 120 : RequestTimeoutSeconds;
if (json != null || method == UnityWebRequest.kHttpVerbPOST)
{
byte[] raw = Encoding.UTF8.GetBytes(json ?? "{}");
request.uploadHandler = new UploadHandlerRaw(raw);
request.SetRequestHeader("Content-Type", "application/json");
}
if (authorize)
request.SetRequestHeader("Authorization", "Bearer " + Token);
@@ -347,6 +449,23 @@ public static class SupportClient
};
}
/// <summary>2xx with an empty body still counts as success.</summary>
static SupportCallResult<T> ParseAck<T>(HttpOutcome http) where T : class, new()
{
SupportCallResult<T> parsed = ParseOk<T>(http);
if (parsed.ok || http == null || http.statusCode < 200 || http.statusCode >= 300)
return parsed;
if (parsed.error != "Invalid response from server")
return parsed;
return new SupportCallResult<T>
{
ok = true,
statusCode = http.statusCode,
data = new T()
};
}
static SupportCallResult<T> Fail<T>(long statusCode, string error, int retryAfterSeconds = 0)
{
return new SupportCallResult<T>
@@ -467,6 +586,7 @@ public static class SupportClient
{
public string category;
public string body;
public string logs;
}
[Serializable]
@@ -475,6 +595,13 @@ public static class SupportClient
public string body;
}
[Serializable]
class DeviceRequest
{
public string fid;
public string platform;
}
[Serializable]
public class SupportErrorBody
{
@@ -519,6 +646,7 @@ public class SupportTicket
public string updatedAt;
public string lastMessageAt;
public string lastMessagePreview;
public bool hasLogs;
}
[Serializable]
@@ -544,3 +672,8 @@ public class SupportTicketListResponse
{
public SupportTicket[] tickets;
}
[Serializable]
public class SupportDeviceResponse
{
}