mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-05-02 04:22:04 -04:00
Super experimental replacement for the dead Twitch WS PubSub service
This commit is contained in:
@@ -22,7 +22,7 @@ public class BotServices
|
||||
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
internal KickWsClient.KickWsClient? KickClient;
|
||||
private Twitch? _twitch;
|
||||
private TwitchGraphQl? _twitch;
|
||||
private Shuffle? _shuffle;
|
||||
private DiscordService? _discord;
|
||||
private TwitchChat? _twitchChat;
|
||||
@@ -214,18 +214,18 @@ public class BotServices
|
||||
|
||||
private async Task BuildTwitch()
|
||||
{
|
||||
var settings = await SettingsProvider.GetMultipleValuesAsync([BuiltIn.Keys.TwitchBossmanJackId, BuiltIn.Keys.Proxy]);
|
||||
if (settings[BuiltIn.Keys.TwitchBossmanJackId].Value == null)
|
||||
var settings = await SettingsProvider.GetMultipleValuesAsync([BuiltIn.Keys.TwitchBossmanJackUsername, BuiltIn.Keys.Proxy]);
|
||||
if (settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value == null)
|
||||
{
|
||||
_twitchDisabled = true;
|
||||
_logger.Debug($"Ignoring Twitch client as {BuiltIn.Keys.TwitchBossmanJackId} is not defined");
|
||||
_logger.Debug($"Ignoring Twitch client as {BuiltIn.Keys.TwitchBossmanJackUsername} is not defined");
|
||||
return;
|
||||
}
|
||||
_twitch = new Twitch([settings[BuiltIn.Keys.TwitchBossmanJackId].ToType<int>()], settings[BuiltIn.Keys.Proxy].Value, _cancellationToken);
|
||||
_twitch = new TwitchGraphQl(settings[BuiltIn.Keys.Proxy].Value, _cancellationToken);
|
||||
_twitch.OnStreamStateUpdated += OnTwitchStreamStateUpdated;
|
||||
_twitch.OnStreamCommercial += OnTwitchStreamCommercial;
|
||||
_twitch.OnStreamTosStrike += OnTwitchStreamTosStrike;
|
||||
await _twitch.StartWsClient();
|
||||
//_twitch.OnStreamCommercial += OnTwitchStreamCommercial;
|
||||
//_twitch.OnStreamTosStrike += OnTwitchStreamTosStrike;
|
||||
//await _twitch.StartWsClient();
|
||||
_logger.Info("Built Twitch Websocket connection for livestream notifications");
|
||||
}
|
||||
|
||||
@@ -378,7 +378,7 @@ public class BotServices
|
||||
await BuildDiscord();
|
||||
}
|
||||
|
||||
if (!_twitchDisabled && _twitch != null && !_twitch.IsConnected())
|
||||
if (!_twitchDisabled && _twitch != null && !_twitch.IsTaskRunning())
|
||||
{
|
||||
_logger.Error("Twitch died, recreating it");
|
||||
_twitch.Dispose();
|
||||
@@ -878,19 +878,26 @@ public class BotServices
|
||||
_chatBot.SendChatMessage($"🚨🚨 Shufflebros! 🚨🚨 {bet.Username} just bet {bet.Amount} {bet.Currency} which paid out [color={payoutColor}]{bet.Payout} {bet.Currency}[/color] ({bet.Multiplier}x) on {bet.GameName} 💰💰", true);
|
||||
}
|
||||
|
||||
private void OnTwitchStreamStateUpdated(object sender, int channelId, bool isLive)
|
||||
private void OnTwitchStreamStateUpdated(object sender, string channelName, bool isLive)
|
||||
{
|
||||
_logger.Info($"BossmanJack stream event came in. isLive => {isLive}");
|
||||
var settings = SettingsProvider.GetMultipleValuesAsync([BuiltIn.Keys.RestreamUrl, BuiltIn.Keys.TwitchBossmanJackUsername]).Result;
|
||||
var settings = SettingsProvider.GetMultipleValuesAsync([
|
||||
BuiltIn.Keys.RestreamUrl, BuiltIn.Keys.TwitchBossmanJackUsername, BuiltIn.Keys.CaptureEnabled
|
||||
]).Result;
|
||||
|
||||
if (isLive)
|
||||
{
|
||||
_chatBot.SendChatMessage($"{settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value} just went live on Twitch! https://www.twitch.tv/{settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value}\r\n" +
|
||||
settings[BuiltIn.Keys.RestreamUrl].Value);
|
||||
settings[BuiltIn.Keys.RestreamUrl].Value, true);
|
||||
IsBmjLive = true;
|
||||
if (settings[BuiltIn.Keys.CaptureEnabled].ToBoolean())
|
||||
{
|
||||
_logger.Info("Capturing Bossman's stream");
|
||||
_ = new StreamCapture($"https://www.twitch.tv/{settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value}", StreamCaptureMethods.Streamlink, _cancellationToken).CaptureAsync();
|
||||
}
|
||||
return;
|
||||
}
|
||||
_chatBot.SendChatMessage($"{settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value} is no longer live! :lossmanjack:");
|
||||
_chatBot.SendChatMessage($"{settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value} is no longer live! :lossmanjack:", true);
|
||||
IsBmjLive = false;
|
||||
}
|
||||
|
||||
@@ -1137,6 +1144,7 @@ public class BotServices
|
||||
$"{identity} is no longer live! :lossmanjack:", true);
|
||||
}
|
||||
|
||||
// TODO: Fix this so it aligns with the new Persisted Live setting instead of tracking separately
|
||||
public async Task<bool> CheckBmjIsLive(string bmjUsername)
|
||||
{
|
||||
if (IsBmjLive)
|
||||
@@ -1158,7 +1166,7 @@ public class BotServices
|
||||
_logger.Error("Twitch client has not been built!");
|
||||
throw new Exception("Twitch client not initialized");
|
||||
}
|
||||
IsBmjLive = await _twitch.IsStreamLive(bmjUsername);
|
||||
IsBmjLive = (await _twitch.GetStream(bmjUsername)).IsLive;
|
||||
_isBmjLiveSynced = true;
|
||||
}
|
||||
if (IsBmjLive)
|
||||
|
||||
@@ -15,7 +15,7 @@ public class StreamCapture(string streamUrl, StreamCaptureMethods captureMethod,
|
||||
.GetMultipleValuesAsync([BuiltIn.Keys.CaptureYtDlpBinaryPath, BuiltIn.Keys.CaptureYtDlpWorkingDirectory,
|
||||
BuiltIn.Keys.CaptureYtDlpCookiesFromBrowser, BuiltIn.Keys.CaptureYtDlpOutputFormat, BuiltIn.Keys.CaptureYtDlpParentTerminal,
|
||||
BuiltIn.Keys.CaptureYtDlpScriptPath, BuiltIn.Keys.CaptureYtDlpUserAgent, BuiltIn.Keys.CaptureStreamlinkBinaryPath,
|
||||
BuiltIn.Keys.CaptureStreamlinkOutputFormat, BuiltIn.Keys.CaptureStreamlinkRemuxScript]).Result;
|
||||
BuiltIn.Keys.CaptureStreamlinkOutputFormat, BuiltIn.Keys.CaptureStreamlinkRemuxScript, BuiltIn.Keys.CaptureStreamlinkTwitchOptions]).Result;
|
||||
|
||||
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
@@ -111,12 +111,17 @@ public class StreamCapture(string streamUrl, StreamCaptureMethods captureMethod,
|
||||
}
|
||||
else if (captureMethod == StreamCaptureMethods.Streamlink)
|
||||
{
|
||||
captureLine = $"{_settings[BuiltIn.Keys.CaptureStreamlinkBinaryPath].Value} --output \"{_settings[BuiltIn.Keys.CaptureStreamlinkOutputFormat].Value}\" " +
|
||||
var twitchOpts = string.Empty;
|
||||
if (streamUrl.Contains("twitch.tv"))
|
||||
{
|
||||
twitchOpts = _settings[BuiltIn.Keys.CaptureStreamlinkTwitchOptions].Value;
|
||||
}
|
||||
captureLine = $"{_settings[BuiltIn.Keys.CaptureStreamlinkBinaryPath].Value} {twitchOpts} --output \"{_settings[BuiltIn.Keys.CaptureStreamlinkOutputFormat].Value}\" " +
|
||||
$"--retry-streams 15 --retry-max 10 {streamUrl} best";
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Error($"We were given a straem capture method that doesn't exist: {captureMethod}");
|
||||
_logger.Error($"We were given a stream capture method that doesn't exist: {captureMethod}");
|
||||
throw new UnsupportedStreamCaptureMethodException();
|
||||
}
|
||||
|
||||
|
||||
150
KfChatDotNetBot/Services/TwitchGraphQl.cs
Normal file
150
KfChatDotNetBot/Services/TwitchGraphQl.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using KfChatDotNetBot.Models.DbModels;
|
||||
using KfChatDotNetBot.Settings;
|
||||
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<int>();
|
||||
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)
|
||||
{
|
||||
_logger.Info("Updating DB with fresh view count");
|
||||
await using var db = new ApplicationDbContext();
|
||||
await db.TwitchViewCounts.AddAsync(new TwitchViewCountDbModel
|
||||
{
|
||||
Topic = stream.Id.ToString()!,
|
||||
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<TwitchGraphQlModel> 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<string, object>
|
||||
{
|
||||
{ "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<JsonElement>(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")
|
||||
.GetInt64();
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the streamer is live
|
||||
/// </summary>
|
||||
public required bool IsLive { get; set; }
|
||||
/// <summary>
|
||||
/// Viewer count, null if not live
|
||||
/// </summary>
|
||||
public int? ViewerCount { get; set; }
|
||||
/// <summary>
|
||||
/// Stream ID returned by the GraphQL endpoint. null if none
|
||||
/// </summary>
|
||||
public long? Id { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user