using System.Net.Http; using System.Text.Json; using ytLive.Models; namespace ytLive.Services; /// /// Polls YouTube Live Chat API for messages. /// YouTube doesn't have WebSocket for chat — polling is the only option. /// public class YouTubeChatService : IDisposable { private readonly YouTubeAuthService _auth; private readonly HttpClient _http = new(); private Timer? _pollTimer; private string? _nextPageToken; private string? _liveChatId; private bool _isRunning; public event Action? MessageReceived; private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true }; public YouTubeChatService(YouTubeAuthService auth) { _auth = auth; } public void Start(string liveChatId, int pollIntervalMs = 2000) { _liveChatId = liveChatId; _isRunning = true; _nextPageToken = null; _pollTimer = new(async _ => await Poll(), null, 0, pollIntervalMs); } public void Stop() { _isRunning = false; _pollTimer?.Dispose(); _pollTimer = null; } /// TASK 41: injects a synthetic message through the exact seam the real /// poll uses (MessageReceived), so a simulated member / SuperChat / subscriber /// event renders through the whole overlay pipeline. YouTube's insert API only /// creates text messages — these events are local-only by design and never /// reach YouTube (marked ). public void InjectSimulatedMessage(ChatMessage message) => MessageReceived?.Invoke(message); private async Task Poll() { if (!_isRunning || _liveChatId == null || _auth.CurrentChannel == null) return; try { if (_auth.CurrentChannel.TokenExpiry <= DateTime.UtcNow.AddMinutes(5)) await _auth.RefreshToken(); var url = $"https://www.googleapis.com/youtube/v3/liveChat/messages?liveChatId={_liveChatId}&part=snippet,authorDetails&maxResults=50"; if (_nextPageToken != null) url += $"&pageToken={_nextPageToken}"; _http.DefaultRequestHeaders.Authorization = new("Bearer", _auth.CurrentChannel.AccessToken); var response = await _http.GetAsync(url); if (!response.IsSuccessStatusCode) return; var json = await response.Content.ReadAsStringAsync(); var data = JsonSerializer.Deserialize(json); if (data.TryGetProperty("nextPageToken", out var token)) _nextPageToken = token.GetString(); if (!data.TryGetProperty("items", out var items)) return; foreach (var item in items.EnumerateArray()) { var snippet = item.GetProperty("snippet"); var author = item.GetProperty("authorDetails"); var type = snippet.GetProperty("type").GetString() ?? "textMessageEvent"; var message = new ChatMessage { Id = item.GetProperty("id").GetString()!, AuthorName = author.GetProperty("displayName").GetString()!, AuthorChannelId = author.GetProperty("channelId").GetString()!, AuthorImageUrl = author.GetProperty("profileImageUrl").GetString()!, Timestamp = DateTime.Parse(snippet.GetProperty("publishedAt").GetString()!), IsMember = author.GetProperty("isChatSponsor").GetBoolean(), MembershipLevel = author.TryGetProperty("badgeText", out var badgeText) ? badgeText.GetString() : null }; switch (type) { case "superChatEvent": message.IsSuperChat = true; if (snippet.TryGetProperty("superChatDetails", out var scDetails)) { message.SuperChatAmount = scDetails.TryGetProperty("amountMicros", out var amt) ? amt.GetDouble() / 1_000_000.0 : 0; message.SuperChatCurrency = scDetails.TryGetProperty("currency", out var cur) ? cur.GetString() : null; message.Message = scDetails.TryGetProperty("userComment", out var comment) ? comment.GetString() ?? string.Empty : string.Empty; } break; case "newSponsorEvent": message.Message = $"🎉 {message.AuthorName} became a member!"; if (snippet.TryGetProperty("membershipDetails", out var memDetails)) { message.MembershipLevel = memDetails.TryGetProperty("memberLevelName", out var level) ? level.GetString() : message.MembershipLevel; } break; default: // textMessageEvent message.Message = snippet.TryGetProperty("textMessageDetails", out var textDetails) ? textDetails.TryGetProperty("messageText", out var text) ? text.GetString() ?? string.Empty : string.Empty : string.Empty; break; } MessageReceived?.Invoke(message); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Chat poll error: {ex.Message}"); } } public void Dispose() { Stop(); _http.Dispose(); } }