using System.Net; using System.Net.Http.Json; using System.Text.Json; using KfChatDotNetBot.Models.DbModels; using KfChatDotNetBot.Settings; using Microsoft.EntityFrameworkCore; using NLog; namespace KfChatDotNetBot.Services; public class TwitchGraphQl(string? proxy = null, CancellationToken cancellationToken = default) : IDisposable { private Logger _logger = LogManager.GetCurrentClassLogger(); private Uri _gqlEndpoint = new Uri("https://gql.twitch.tv/gql"); private string _gqlClientId = "kimne78kx3ncx6brgo4mv6wki5h1ko"; private string? _proxy = proxy; private CancellationToken _cancellationToken = cancellationToken; private Task? _liveStatusCheckTask; private CancellationTokenSource _liveStatusCheckTaskCts = new(); public delegate void OnStreamStateUpdateEventHandler(object sender, string channelName, bool isLive); public event OnStreamStateUpdateEventHandler? OnStreamStateUpdated; public void StartLiveStatusCheck() { _liveStatusCheckTaskCts = new CancellationTokenSource(); _liveStatusCheckTask = Task.Run(LiveStatusCheckTask, _liveStatusCheckTaskCts.Token); } public bool IsTaskRunning() { if (_liveStatusCheckTask?.Status is not (TaskStatus.Running or TaskStatus.WaitingForActivation)) return false; return !_liveStatusCheckTaskCts.IsCancellationRequested; } private async Task LiveStatusCheckTask() { var interval = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.TwitchGraphQlCheckInterval)).ToType(); using var timer = new PeriodicTimer(TimeSpan.FromSeconds(interval)); while (await timer.WaitForNextTickAsync(_liveStatusCheckTaskCts.Token)) { var ct = _liveStatusCheckTaskCts.Token; _logger.Debug("Going to check if Bossman is live right now"); var settings = await SettingsProvider.GetMultipleValuesAsync([ BuiltIn.Keys.TwitchBossmanJackUsername, BuiltIn.Keys.TwitchGraphQlPersistedCurrentlyLive ]); TwitchGraphQlModel stream; try { stream = await GetStream(settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value!); } catch (Exception e) { _logger.Error("Caught exception when trying to check if Bossman is live on Twitch"); _logger.Error(e); continue; } if (stream.IsLive) { await using var db = new ApplicationDbContext(); var mostRecentViewCount = await db.TwitchViewCounts.OrderBy(x => x.Id).LastOrDefaultAsync(ct); if (mostRecentViewCount == null || mostRecentViewCount.Viewers != stream.ViewerCount!.Value || mostRecentViewCount.Topic != stream.Id) { _logger.Info("Updating DB with fresh view count"); await db.TwitchViewCounts.AddAsync(new TwitchViewCountDbModel { Topic = stream.Id!, Viewers = stream.ViewerCount!.Value, Time = DateTimeOffset.UtcNow, ServerTime = 0 }, ct); await db.SaveChangesAsync(ct); } } var persistedLive = settings[BuiltIn.Keys.TwitchGraphQlPersistedCurrentlyLive].ToBoolean(); if (stream.IsLive == persistedLive) continue; OnStreamStateUpdated?.Invoke(this, settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value!, stream.IsLive); await SettingsProvider.SetValueAsBooleanAsync(BuiltIn.Keys.TwitchGraphQlPersistedCurrentlyLive, stream.IsLive); } } public async Task GetStream(string channel) { var graphQl = "query {\n user(login: \"" + channel + "\") {\n stream {\n id\n viewersCount\n }\n }\n}"; _logger.Debug($"Built GraphQL query string: {graphQl}"); var jsonBody = new Dictionary { { "query", graphQl }, { "variables", new object() } }; _logger.Debug("Created dictionary object for the JSON payload, should serialize to following value:"); _logger.Debug(JsonSerializer.Serialize(jsonBody)); var handler = new HttpClientHandler {AutomaticDecompression = DecompressionMethods.All}; if (_proxy != null) { handler.UseProxy = true; handler.Proxy = new WebProxy(_proxy); _logger.Debug($"Configured to use proxy {_proxy}"); } using var client = new HttpClient(handler); client.DefaultRequestHeaders.Add("client-id", _gqlClientId); var postBody = JsonContent.Create(jsonBody); var response = await client.PostAsync(_gqlEndpoint, postBody, _cancellationToken); var responseContent = await response.Content.ReadFromJsonAsync(cancellationToken: _cancellationToken); _logger.Debug("Twitch API returned following JSON"); _logger.Debug(responseContent.GetRawText); var returnData = new TwitchGraphQlModel { IsLive = false}; if (responseContent.GetProperty("data").GetProperty("user").ValueKind == JsonValueKind.Null) { _logger.Error("data.user was null"); return returnData; } if (responseContent.GetProperty("data").GetProperty("user").GetProperty("stream").ValueKind == JsonValueKind.Null) { _logger.Debug("stream property was null. Means streamer is not live"); return returnData; } returnData.IsLive = true; returnData.Id = responseContent.GetProperty("data").GetProperty("user").GetProperty("stream").GetProperty("id") .GetString(); returnData.ViewerCount = responseContent.GetProperty("data").GetProperty("user").GetProperty("stream").GetProperty("viewersCount") .GetInt32(); return returnData; } public void Dispose() { _liveStatusCheckTaskCts.Cancel(); _liveStatusCheckTask?.Dispose(); GC.SuppressFinalize(this); } } public class TwitchGraphQlModel { /// /// Whether the streamer is live /// public required bool IsLive { get; set; } /// /// Viewer count, null if not live /// public int? ViewerCount { get; set; } /// /// Stream ID returned by the GraphQL endpoint. null if none /// public string? Id { get; set; } }