using System; using System.Collections; using System.Collections.Generic; using System.Text.RegularExpressions; using UnityEngine; using UnityEngine.Events; using UnityEngine.Networking; using WebSocketSharp; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace WefLab { /// /// Donation data structure /// [System.Serializable] public class DonationData { public string platform; public string donationType; public int amount; public string message; public string donorName; public long timestamp; } /// /// Unity event with donation data parameter /// [System.Serializable] public class DonationEvent : UnityEvent { } /// /// Donation event trigger based on amount range /// [System.Serializable] public class DonationEventTrigger { [Tooltip("Name for this donation trigger (e.g., 'Small Donation', 'Large Donation')")] public string triggerName = "New Trigger"; [Header("Amount Range")] [Tooltip("Minimum donation amount (inclusive)")] public int minAmount = 0; [Tooltip("Maximum donation amount (inclusive, -1 for unlimited)")] public int maxAmount = -1; [Header("Event Settings")] [Tooltip("Number of times to repeat the event (minimum 1)")] [Min(1)] public int repeatCount = 1; [Tooltip("Delay between each repeat in seconds (0 for immediate)")] [Min(0)] public float repeatDelay = 0f; [Header("Event")] [Tooltip("Unity event to trigger when donation amount is in range")] public DonationEvent onDonation; /// /// Check if donation amount is within range /// public bool IsInRange(int amount) { if (amount < minAmount) return false; if (maxAmount >= 0 && amount > maxAmount) return false; return true; } } /// /// WefLab WebSocket client for receiving donation events /// Implements Engine.IO v4 and Socket.IO protocol /// public class WefLabWebSocketClient : MonoBehaviour { [Header("WefLab Settings")] [Tooltip("WefLab page URL (e.g., https://weflab.com/page/guPK2ODAmmRvYG4)")] public string pageUrl = "https://weflab.com/page/guPK2ODAmmRvYG4"; [Header("Donation Event Triggers")] [Tooltip("Donation event triggers based on amount ranges")] public DonationEventTrigger[] donationTriggers = Array.Empty(); // Hidden connection info [HideInInspector] public bool isConnected = false; [HideInInspector] public string currentSid = ""; [HideInInspector] public string extractedUserIdx = ""; // WebSocket connection private WebSocket ws; private string userIdx = ""; // Will be extracted from page private string socketSid = ""; private bool isExtracting = false; // Ping/Pong settings private float pingInterval = 30f; private float lastPingTime = 0f; // Thread-safe action queue for main thread private Queue mainThreadActions = new Queue(); private object actionLock = new object(); void Start() { // Extract user idx from page URL and then connect StartCoroutine(ExtractUserIdxAndConnect()); } void Update() { // Execute queued actions on main thread lock (actionLock) { while (mainThreadActions.Count > 0) { mainThreadActions.Dequeue()?.Invoke(); } } } void OnDestroy() { Disconnect(); } /// /// Extract user index from page URL and connect /// private IEnumerator ExtractUserIdxAndConnect() { if (string.IsNullOrEmpty(pageUrl)) { Debug.LogError("[WefLab] Page URL is empty! Please set the page URL in Inspector."); yield break; } isExtracting = true; Debug.Log($"[WefLab] Fetching page URL: {pageUrl}"); // Fetch the page HTML using (UnityWebRequest request = UnityWebRequest.Get(pageUrl)) { yield return request.SendWebRequest(); if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($"[WefLab] Failed to fetch page: {request.error}"); isExtracting = false; yield break; } string htmlContent = request.downloadHandler.text; // Extract userIdx from JavaScript in the page // Look for pattern: loginData = {...} Match loginDataMatch = Regex.Match(htmlContent, @"loginData\s*=\s*(\{[^;]+\});", RegexOptions.Singleline); if (loginDataMatch.Success) { try { string loginDataJson = loginDataMatch.Groups[1].Value; var loginData = JsonConvert.DeserializeObject(loginDataJson); // Extract idx from loginData userIdx = loginData["idx"]?.ToString() ?? ""; if (!string.IsNullOrEmpty(userIdx)) { extractedUserIdx = userIdx; Debug.Log($"[WefLab] Successfully extracted userIdx: {userIdx}"); } else { Debug.LogError("[WefLab] userIdx not found in loginData"); } } catch (Exception ex) { Debug.LogError($"[WefLab] Error parsing loginData: {ex.Message}"); } } else { Debug.LogError("[WefLab] Could not find loginData in page HTML"); } } isExtracting = false; // Connect if we successfully extracted the userIdx if (!string.IsNullOrEmpty(userIdx)) { Connect(); } else { Debug.LogError("[WefLab] Cannot connect - userIdx extraction failed"); } } /// /// Connect to WefLab WebSocket server /// public void Connect() { if (ws != null && ws.IsAlive) { Debug.LogWarning("[WefLab] Already connected"); return; } // Build WebSocket URL string wsUrl = $"wss://ssmain.weflab.com/socket.io/?idx={userIdx}&type=page&page=alert&EIO=4&transport=websocket"; Debug.Log($"[WefLab] Connecting to: {wsUrl}"); ws = new WebSocket(wsUrl); // Event handlers ws.OnOpen += OnWebSocketOpen; ws.OnMessage += OnWebSocketMessage; ws.OnError += OnWebSocketError; ws.OnClose += OnWebSocketClose; // Connect ws.ConnectAsync(); } /// /// Disconnect from WebSocket /// public void Disconnect() { if (ws != null) { ws.Close(); ws = null; } isConnected = false; socketSid = ""; currentSid = ""; } #region WebSocket Event Handlers private void OnWebSocketOpen(object sender, EventArgs e) { EnqueueMainThreadAction(() => { Debug.Log("[WefLab] WebSocket connection opened"); }); } private void OnWebSocketMessage(object sender, MessageEventArgs e) { string data = e.Data; EnqueueMainThreadAction(() => { ProcessMessage(data); }); } private void OnWebSocketError(object sender, ErrorEventArgs e) { EnqueueMainThreadAction(() => { Debug.LogError($"[WefLab] WebSocket error: {e.Message}"); if (e.Exception != null) { Debug.LogError($"[WefLab] Exception: {e.Exception}"); } }); } private void OnWebSocketClose(object sender, CloseEventArgs e) { EnqueueMainThreadAction(() => { Debug.Log($"[WefLab] WebSocket closed. Code: {e.Code}, Reason: {e.Reason}"); isConnected = false; socketSid = ""; currentSid = ""; }); } #endregion #region Message Processing /// /// Process incoming WebSocket message /// private void ProcessMessage(string data) { if (string.IsNullOrEmpty(data)) return; // Get message type (first character or first two characters) string messageType = data.Length >= 2 && char.IsDigit(data[1]) ? data.Substring(0, 2) : data.Substring(0, 1); string payload = data.Substring(messageType.Length); switch (messageType) { case "0": // Engine.IO OPEN HandleEngineOpen(payload); break; case "2": // Engine.IO PING HandlePing(); break; case "3": // Engine.IO PONG Debug.Log("[WefLab] Received PONG"); break; case "40": // Socket.IO CONNECT HandleSocketConnect(payload); break; case "42": // Socket.IO EVENT HandleSocketEvent(payload); break; default: Debug.Log($"[WefLab] Unknown message type: {messageType} | Data: {data}"); break; } } /// /// Handle Engine.IO OPEN message (handshake) /// private void HandleEngineOpen(string payload) { try { var openData = JsonConvert.DeserializeObject(payload); currentSid = openData["sid"]?.ToString() ?? ""; if (openData["pingInterval"] != null) { pingInterval = openData["pingInterval"].Value() / 1000f; // Convert to seconds } Debug.Log($"[WefLab] Engine.IO OPEN - SID: {currentSid}, PingInterval: {pingInterval}s"); Debug.Log($"[WefLab] Full handshake data: {payload}"); // Send Socket.IO CONNECT SendSocketConnect(); } catch (Exception ex) { Debug.LogError($"[WefLab] Error parsing OPEN message: {ex.Message}"); } } /// /// Handle Engine.IO PING /// private void HandlePing() { Debug.Log("[WefLab] Received PING, sending PONG"); SendMessage("3"); // Send PONG } /// /// Handle Socket.IO CONNECT response /// private void HandleSocketConnect(string payload) { try { if (!string.IsNullOrEmpty(payload)) { var connectData = JsonConvert.DeserializeObject(payload); socketSid = connectData["sid"]?.ToString() ?? ""; Debug.Log($"[WefLab] Socket.IO CONNECT - SID: {socketSid}"); Debug.Log($"[WefLab] Full connect data: {payload}"); } isConnected = true; // Send join message SendJoinMessage(); } catch (Exception ex) { Debug.LogError($"[WefLab] Error parsing Socket.IO CONNECT: {ex.Message}"); } } /// /// Handle Socket.IO EVENT message (donation data) /// private void HandleSocketEvent(string payload) { try { var eventArray = JsonConvert.DeserializeObject(payload); if (eventArray == null || eventArray.Count < 2) { Debug.LogWarning($"[WefLab] Invalid event format: {payload}"); return; } string eventName = eventArray[0].ToString(); var eventData = eventArray[1] as JObject; Debug.Log($"[WefLab] ========== EVENT RECEIVED =========="); Debug.Log($"[WefLab] Event Name: {eventName}"); Debug.Log($"[WefLab] Event Data: {eventData?.ToString(Formatting.Indented)}"); Debug.Log($"[WefLab] ==================================="); if (eventName == "msg") { HandleMessageEvent(eventData); } } catch (Exception ex) { Debug.LogError($"[WefLab] Error parsing Socket.IO EVENT: {ex.Message}"); Debug.LogError($"[WefLab] Payload: {payload}"); } } /// /// Handle "msg" event (donation notifications) /// private void HandleMessageEvent(JObject msgData) { if (msgData == null) return; string msgType = msgData["type"]?.ToString() ?? ""; switch (msgType) { case "test_donation": HandleTestDonation(msgData); break; case "donation": case "SENDBALLOON": case "cheese": case "superchat": case "bits": HandleDonation(msgData); break; default: Debug.Log($"[WefLab] Unhandled message type: {msgType}"); Debug.Log($"[WefLab] Data: {msgData.ToString(Formatting.Indented)}"); break; } } /// /// Normalize donation amount to unified currency (Chzzk standard: 1 = 1 KRW) /// SOOP (Afreeca): 1 balloon = 100 KRW → multiply by 100 /// Chzzk: 1 cheese = 1 KRW → no conversion /// YouTube: Keep as-is (already in KRW equivalent) /// Twitch: Keep as-is /// private static int NormalizeDonationAmount(string platform, int rawAmount) { platform = platform.ToLower(); if (platform == "afreeca" || platform == "soop") { // SOOP: 1 balloon = 100 KRW return rawAmount * 100; } // Chzzk, YouTube, Twitch: no conversion needed return rawAmount; } /// /// Handle test donation event /// private void HandleTestDonation(JObject msgData) { if (msgData["data"] is not JObject donationDataJson) return; string platform = donationDataJson["platform"]?.ToString() ?? "unknown"; string type = donationDataJson["type"]?.ToString() ?? "unknown"; int rawAmount = 0; // Parse amount if (int.TryParse(donationDataJson["value"]?.ToString(), out int parsedAmount)) { rawAmount = parsedAmount; } // Normalize amount based on platform int amount = NormalizeDonationAmount(platform, rawAmount); string message = donationDataJson["msg"]?.ToString() ?? ""; long timestamp = donationDataJson["time"]?.Value() ?? 0; Debug.Log($"[WefLab] *** TEST DONATION ***"); Debug.Log($"[WefLab] Platform: {platform}"); Debug.Log($"[WefLab] Type: {type}"); Debug.Log($"[WefLab] Raw Amount: {rawAmount} → Normalized: {amount} KRW"); Debug.Log($"[WefLab] Message: {message}"); Debug.Log($"[WefLab] Timestamp: {timestamp}"); // Create donation data and trigger events DonationData donation = new() { platform = platform, donationType = type, amount = amount, message = message, donorName = "Test Donor", timestamp = timestamp }; TriggerDonationEvents(donation); } /// /// Handle real donation event /// private void HandleDonation(JObject msgData) { Debug.Log($"[WefLab] *** REAL DONATION ***"); Debug.Log($"[WefLab] Full donation data: {msgData.ToString(Formatting.Indented)}"); if (msgData["data"] is not JObject donationDataJson) return; string platform = msgData["platform"]?.ToString() ?? "unknown"; string type = msgData["type"]?.ToString() ?? "unknown"; int rawAmount = 0; // Parse amount from different possible fields if (donationDataJson["value"] != null && int.TryParse(donationDataJson["value"].ToString(), out int parsedValue)) { rawAmount = parsedValue; } else if (donationDataJson["amount"] != null && int.TryParse(donationDataJson["amount"].ToString(), out int parsedAmount)) { rawAmount = parsedAmount; } // Normalize amount based on platform int amount = NormalizeDonationAmount(platform, rawAmount); string message = donationDataJson["msg"]?.ToString() ?? donationDataJson["message"]?.ToString() ?? ""; string donorName = donationDataJson["nickname"]?.ToString() ?? donationDataJson["name"]?.ToString() ?? "Anonymous"; long timestamp = donationDataJson["time"]?.Value() ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); // Create donation data DonationData donation = new() { platform = platform, donationType = type, amount = amount, message = message, donorName = donorName, timestamp = timestamp }; Debug.Log($"[WefLab] Donation: {donorName} - {rawAmount} → {amount} KRW ({platform})"); // Trigger events TriggerDonationEvents(donation); } /// /// Trigger donation events based on amount range /// private void TriggerDonationEvents(DonationData donation) { if (donationTriggers == null || donationTriggers.Length == 0) { Debug.LogWarning("[WefLab] No donation triggers configured"); return; } bool triggeredAny = false; // Check each trigger foreach (var trigger in donationTriggers) { if (trigger == null) continue; // Check if donation amount is in range if (trigger.IsInRange(donation.amount)) { Debug.Log($"[WefLab] Triggering: {trigger.triggerName} (Amount: {donation.amount}, Range: {trigger.minAmount}-{(trigger.maxAmount >= 0 ? trigger.maxAmount.ToString() : "unlimited")}, Repeat: {trigger.repeatCount}x)"); // Start coroutine to handle repeated invocation StartCoroutine(InvokeRepeatedEvent(trigger, donation)); triggeredAny = true; } } if (!triggeredAny) { Debug.LogWarning($"[WefLab] No triggers matched for donation amount: {donation.amount}"); } } /// /// Coroutine to invoke event multiple times with delay /// private IEnumerator InvokeRepeatedEvent(DonationEventTrigger trigger, DonationData donation) { for (int i = 0; i < trigger.repeatCount; i++) { // Invoke the event trigger.onDonation?.Invoke(donation); // Wait for delay before next repetition (skip on last iteration) if (i < trigger.repeatCount - 1 && trigger.repeatDelay > 0) { yield return new WaitForSeconds(trigger.repeatDelay); } } } #endregion #region Send Messages /// /// Send Socket.IO CONNECT message (40) /// private void SendSocketConnect() { Debug.Log("[WefLab] Sending Socket.IO CONNECT (40)"); SendMessage("40"); } /// /// Send join message to subscribe to donation events /// private void SendJoinMessage() { var joinData = new { type = "join", page = "page", idx = userIdx, pageid = "alert", preset = "0" }; var message = new object[] { "msg", joinData }; string json = JsonConvert.SerializeObject(message); string fullMessage = "42" + json; Debug.Log($"[WefLab] Sending JOIN message: {fullMessage}"); SendMessage(fullMessage); } /// /// Send raw message through WebSocket /// private void SendMessage(string message) { if (ws != null && ws.IsAlive) { ws.Send(message); } else { Debug.LogWarning("[WefLab] Cannot send message - WebSocket not connected"); } } #endregion #region Thread Safety /// /// Enqueue action to be executed on main thread /// private void EnqueueMainThreadAction(Action action) { lock (actionLock) { mainThreadActions.Enqueue(action); } } #endregion #region Public Methods /// /// Reconnect to WebSocket /// public void Reconnect() { Disconnect(); Connect(); } /// /// Check if connected /// public bool IsConnected() { return isConnected && ws != null && ws.IsAlive; } #endregion } }