From 72817921163b3868cd9579c30b1c30c0727bad09 Mon Sep 17 00:00:00 2001 From: Enrico Ludwig Date: Thu, 5 Sep 2024 16:39:26 +0200 Subject: [PATCH] Merged TwitchPubSubService and TwitchDataService to TwitchService --- App.axaml.cs | 4 +- Attributes/PubSubAttribute.cs | 17 - Attributes/PubSubType.cs | 13 - Misc/Tools.cs | 4 +- Models/PubSubListener.cs | 10 - Models/TwitchChannel.cs | 26 +- Services/ITwitchDataService.cs | 28 -- Services/ITwitchPubSubService.cs | 7 - Services/Implementations/TwitchDataService.cs | 206 ----------- .../Implementations/TwitchPubSubService.cs | 291 --------------- Services/TwitchService.cs | 349 ++++++++++++++++++ ViewModels/MainWindowViewModel.cs | 24 +- Views/MainWindow.axaml | 16 +- 13 files changed, 390 insertions(+), 605 deletions(-) delete mode 100644 Attributes/PubSubAttribute.cs delete mode 100644 Attributes/PubSubType.cs delete mode 100644 Models/PubSubListener.cs delete mode 100644 Services/ITwitchDataService.cs delete mode 100644 Services/ITwitchPubSubService.cs delete mode 100644 Services/Implementations/TwitchDataService.cs delete mode 100644 Services/Implementations/TwitchPubSubService.cs create mode 100644 Services/TwitchService.cs diff --git a/App.axaml.cs b/App.axaml.cs index 4817aa7..4fea7ed 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -5,7 +5,6 @@ using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using BetterRaid.Extensions; using BetterRaid.Services; -using BetterRaid.Services.Implementations; using BetterRaid.ViewModels; using BetterRaid.Views; using Microsoft.Extensions.DependencyInjection; @@ -28,8 +27,7 @@ public class App : Application private void InitializeServices() { - Services.AddSingleton(); - Services.AddSingleton(); + Services.AddSingleton(); Services.AddTransient(); _serviceProvider = Services.BuildServiceProvider(); diff --git a/Attributes/PubSubAttribute.cs b/Attributes/PubSubAttribute.cs deleted file mode 100644 index 3aa39a4..0000000 --- a/Attributes/PubSubAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace BetterRaid.Attributes; - -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)] -public class PubSubAttribute : Attribute -{ - public PubSubType Type { get; } - public string ChannelIdField { get; set; } - - public PubSubAttribute(PubSubType type, string channelIdField) - { - Type = type; - ChannelIdField = channelIdField; - } - -} \ No newline at end of file diff --git a/Attributes/PubSubType.cs b/Attributes/PubSubType.cs deleted file mode 100644 index ab6d59d..0000000 --- a/Attributes/PubSubType.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace BetterRaid.Attributes; - -public enum PubSubType -{ - VideoPlayback, - Follows, - Subscriptions, - ChannelPoints, - Bits, - Raids, - StreamUp, - StreamDown -} \ No newline at end of file diff --git a/Misc/Tools.cs b/Misc/Tools.cs index f92bdc5..75946e9 100644 --- a/Misc/Tools.cs +++ b/Misc/Tools.cs @@ -107,8 +107,8 @@ public static class Tools var accessToken = jsonData["access_token"]?.ToString(); - var dataService = App.ServiceProvider?.GetService(typeof(ITwitchDataService)); - if (dataService is ITwitchDataService twitchDataService) + var dataService = App.ServiceProvider?.GetService(typeof(ITwitchService)); + if (dataService is ITwitchService twitchDataService) { twitchDataService.ConnectApiAsync(Constants.TwitchClientId, accessToken!); } diff --git a/Models/PubSubListener.cs b/Models/PubSubListener.cs deleted file mode 100644 index 242537f..0000000 --- a/Models/PubSubListener.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Reflection; - -namespace BetterRaid.Models; - -public class PubSubListener -{ - public string ChannelId { get; set; } - public object? Instance { get; set; } - public MemberInfo? Listener { get; set; } -} \ No newline at end of file diff --git a/Models/TwitchChannel.cs b/Models/TwitchChannel.cs index d3cd13d..2438ab8 100644 --- a/Models/TwitchChannel.cs +++ b/Models/TwitchChannel.cs @@ -2,8 +2,8 @@ using System; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; -using BetterRaid.Attributes; using BetterRaid.Services; +using TwitchLib.PubSub.Events; namespace BetterRaid.Models; @@ -45,8 +45,6 @@ public class TwitchChannel : INotifyPropertyChanged } } - [PubSub(PubSubType.StreamUp, nameof(BroadcasterId))] - [PubSub(PubSubType.StreamDown, nameof(BroadcasterId))] public bool IsLive { get => _isLive; @@ -60,7 +58,6 @@ public class TwitchChannel : INotifyPropertyChanged } } - [PubSub(PubSubType.VideoPlayback, nameof(BroadcasterId))] public string? ViewerCount { get => _viewerCount; @@ -144,15 +141,15 @@ public class TwitchChannel : INotifyPropertyChanged Name = channelName; } - public void UpdateChannelData(ITwitchDataService dataService) + public void UpdateChannelData(ITwitchService service) { - var channel = dataService.TwitchApi.Helix.Search.SearchChannelsAsync(Name).Result.Channels + var channel = service.TwitchApi.Helix.Search.SearchChannelsAsync(Name).Result.Channels .FirstOrDefault(c => c.BroadcasterLogin.Equals(Name, StringComparison.CurrentCultureIgnoreCase)); if (channel == null) return; - var stream = dataService.TwitchApi.Helix.Streams.GetStreamsAsync(userLogins: [ Name ]).Result.Streams + var stream = service.TwitchApi.Helix.Streams.GetStreamsAsync(userLogins: [ Name ]).Result.Streams .FirstOrDefault(s => s.UserLogin.Equals(Name, StringComparison.CurrentCultureIgnoreCase)); BroadcasterId = channel.Id; @@ -166,6 +163,21 @@ public class TwitchChannel : INotifyPropertyChanged : $"{stream.ViewerCount}"; } + public void OnStreamUp(object? sender, OnStreamUpArgs args) + { + IsLive = true; + } + + public void OnStreamDown(object? sender, OnStreamDownArgs e) + { + IsLive = false; + } + + public void OnViewCount(object? sender, OnViewCountArgs e) + { + ViewerCount = $"{e.Viewers}"; + } + public event PropertyChangedEventHandler? PropertyChanged; private void OnPropertyChanged([CallerMemberName] string? propertyName = null) diff --git a/Services/ITwitchDataService.cs b/Services/ITwitchDataService.cs deleted file mode 100644 index 0ec8d96..0000000 --- a/Services/ITwitchDataService.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.ComponentModel; -using System.Threading.Tasks; -using BetterRaid.Models; -using TwitchLib.Api; - -namespace BetterRaid.Services; - -public interface ITwitchDataService -{ - public string? AccessToken { get; set; } - public TwitchChannel? UserChannel { get; set; } - public TwitchAPI TwitchApi { get; } - public bool IsRaidStarted { get; set; } - - public Task ConnectApiAsync(string clientId, string accessToken); - public void SaveAccessToken(string token); - public bool TryGetUserChannel(out TwitchChannel? userChannel); - public string GetOAuthUrl(); - public void StartRaid(string from, string to); - public bool CanStartRaidCommand(object? arg); - public void StartRaidCommand(object? arg); - public void StopRaid(); - public void StopRaidCommand(); - public void OpenChannelCommand(object? arg); - - public event PropertyChangingEventHandler? PropertyChanging; - public event PropertyChangedEventHandler? PropertyChanged; -} \ No newline at end of file diff --git a/Services/ITwitchPubSubService.cs b/Services/ITwitchPubSubService.cs deleted file mode 100644 index 03d8338..0000000 --- a/Services/ITwitchPubSubService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace BetterRaid.Services; - -public interface ITwitchPubSubService -{ - void RegisterReceiver(T receiver) where T : class; - void UnregisterReceiver(T receiver) where T : class; -} \ No newline at end of file diff --git a/Services/Implementations/TwitchDataService.cs b/Services/Implementations/TwitchDataService.cs deleted file mode 100644 index 8b89ce2..0000000 --- a/Services/Implementations/TwitchDataService.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using BetterRaid.Misc; -using BetterRaid.Models; -using TwitchLib.Api; - -namespace BetterRaid.Services.Implementations; - -public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged, INotifyPropertyChanging -{ - private bool _isRaidStarted; - private TwitchChannel? _userChannel; - - public string AccessToken { get; set; } = string.Empty; - - public bool IsRaidStarted - { - get => _isRaidStarted; - set => SetField(ref _isRaidStarted, value); - } - - public TwitchChannel? UserChannel - { - get => _userChannel; - set - { - if (_userChannel != null && _userChannel.Name?.Equals(value?.Name) == true) - return; - - SetField(ref _userChannel, value); - - _userChannel?.UpdateChannelData(this); - } - } - - public TwitchAPI TwitchApi { get; } - - public TwitchDataService() - { - TwitchApi = new TwitchAPI(); - - if (TryLoadAccessToken(out var token)) - { - Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Found access token."); - Task.Run(() => ConnectApiAsync(Constants.TwitchClientId, token)); - } - else - { - Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] No access token found."); - } - } - - public async Task ConnectApiAsync(string clientId, string accessToken) - { - Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Connecting to Twitch API ..."); - - AccessToken = accessToken; - - TwitchApi.Settings.ClientId = clientId; - TwitchApi.Settings.AccessToken = accessToken; - - if (TryGetUserChannel(out var channel)) - { - UserChannel = channel; - Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Connected to Twitch API as {channel?.Name}."); - } - else - { - UserChannel = null; - - Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] Could not get user channel."); - Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] Failed to connect to Twitch API."); - } - - await Task.CompletedTask; - } - - private bool TryLoadAccessToken(out string token) - { - token = string.Empty; - - if (!File.Exists(Constants.TwitchOAuthAccessTokenFilePath)) - return false; - - token = File.ReadAllText(Constants.TwitchOAuthAccessTokenFilePath); - return true; - } - - public void SaveAccessToken(string token) - { - File.WriteAllText(Constants.TwitchOAuthAccessTokenFilePath, token); - } - - public bool TryGetUserChannel(out TwitchChannel? userChannel) - { - userChannel = null; - - try - { - var user = TwitchApi.Helix.Users.GetUsersAsync().Result.Users[0]; - userChannel = new TwitchChannel(user.Login); - - return true; - } - catch (Exception e) - { - Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] {e.Message}"); - return false; - } - } - - public string GetOAuthUrl() - { - var scopes = string.Join("+", Constants.TwitchOAuthScopes); - - return $"https://id.twitch.tv/oauth2/authorize" - + $"?client_id={Constants.TwitchClientId}" - + $"&redirect_uri={Constants.TwitchOAuthRedirectUrl}" - + $"&response_type={Constants.TwitchOAuthResponseType}" - + $"&scope={scopes}"; - } - - public void StartRaid(string from, string to) - { - // TODO: Also check, if the logged in user is live - - TwitchApi.Helix.Raids.StartRaidAsync(from, to); - IsRaidStarted = true; - } - - public bool CanStartRaidCommand(object? arg) - { - return UserChannel?.IsLive == true && IsRaidStarted == false; - } - - public void StartRaidCommand(object? arg) - { - if (arg == null || UserChannel?.BroadcasterId == null) - { - return; - } - - var from = UserChannel.BroadcasterId!; - var to = arg.ToString()!; - - StartRaid(from, to); - } - - public void StopRaid() - { - if (UserChannel?.BroadcasterId == null) - return; - - if (IsRaidStarted == false) - return; - - TwitchApi.Helix.Raids.CancelRaidAsync(UserChannel.BroadcasterId); - IsRaidStarted = false; - } - - public void StopRaidCommand() - { - StopRaid(); - } - - public void OpenChannelCommand(object? arg) - { - var channelName = arg?.ToString(); - if (string.IsNullOrEmpty(channelName)) - return; - - var url = $"https://twitch.tv/{channelName}"; - - Tools.OpenUrl(url); - } - - public event PropertyChangingEventHandler? PropertyChanging; - public event PropertyChangedEventHandler? PropertyChanged; - - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - protected virtual void OnPropertyChanging([CallerMemberName] string? propertyName = null) - { - PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName)); - } - - protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) - { - if (EqualityComparer.Default.Equals(field, value)) - return false; - - OnPropertyChanging(propertyName); - field = value; - OnPropertyChanged(propertyName); - - return true; - } - -} \ No newline at end of file diff --git a/Services/Implementations/TwitchPubSubService.cs b/Services/Implementations/TwitchPubSubService.cs deleted file mode 100644 index c8056b3..0000000 --- a/Services/Implementations/TwitchPubSubService.cs +++ /dev/null @@ -1,291 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using BetterRaid.Attributes; -using BetterRaid.Extensions; -using BetterRaid.Models; -using TwitchLib.PubSub; -using TwitchLib.PubSub.Events; - -namespace BetterRaid.Services.Implementations; - -public class TwitchPubSubService : ITwitchPubSubService -{ - private readonly Dictionary> _targets = new(); - private readonly ITwitchDataService _dataService; - private TwitchPubSub? _sub; - - public TwitchPubSubService(ITwitchDataService dataService) - { - _dataService = dataService; - - Task.Run(InitializePubSubAsync); - } - - private async Task InitializePubSubAsync() - { - while (_dataService.UserChannel == null) - { - await Task.Delay(100); - } - - _sub = new TwitchPubSub(); - - _sub.OnPubSubServiceConnected += OnSubOnOnPubSubServiceConnected; - _sub.OnPubSubServiceError += OnSubOnOnPubSubServiceError; - _sub.OnPubSubServiceClosed += OnSubOnOnPubSubServiceClosed; - _sub.OnListenResponse += OnSubOnOnListenResponse; - _sub.OnViewCount += OnSubOnOnViewCount; - _sub.OnStreamUp += OnStreamUp; - _sub.OnStreamDown += OnStreamDown; - - _sub.Connect(); - - if (_dataService.UserChannel != null) - { - RegisterReceiver(_dataService.UserChannel); - } - - _dataService.PropertyChanging += (_, args) => - { - if (args.PropertyName != nameof(_dataService.UserChannel)) - return; - - if (_dataService.UserChannel != null) - UnregisterReceiver(_dataService.UserChannel); - }; - - _dataService.PropertyChanged += (_, args) => - { - if (args.PropertyName != nameof(_dataService.UserChannel)) - return; - - if (_dataService.UserChannel != null) - RegisterReceiver(_dataService.UserChannel); - }; - - await Task.CompletedTask; - } - - private void OnStreamDown(object? sender, OnStreamDownArgs args) - { - var listeners = _targets - .Where(x => x.Key == PubSubType.StreamDown) - .SelectMany(x => x.Value) - .Where(x => x.ChannelId == args.ChannelId) - .ToList(); - - foreach (var listener in listeners) - { - if (listener.Listener == null || listener.Instance == null) - continue; - - try - { - if (listener.Listener.SetValue(listener.Instance, false) == false) - { - Console.WriteLine( - $"[ERROR][{nameof(TwitchPubSubService)}] Failed to set {listener.Instance.GetType().Name}.{listener.Listener.Name} to true"); - } - else - { - Console.WriteLine( - $"[DEBUG][{nameof(TwitchPubSubService)}] Setting {listener.Instance.GetType().Name}.{listener.Listener.Name} to true"); - } - } - catch (Exception e) - { - Console.WriteLine( - $"[ERROR][{nameof(TwitchPubSubService)}] Exception while setting {listener.Instance?.GetType().Name}.{listener.Listener?.Name}: {e.Message}"); - } - } - } - - private void OnStreamUp(object? sender, OnStreamUpArgs args) - { - var listeners = _targets - .Where(x => x.Key == PubSubType.StreamUp) - .SelectMany(x => x.Value) - .Where(x => x.ChannelId == args.ChannelId) - .ToList(); - - foreach (var listener in listeners) - { - if (listener.Listener == null || listener.Instance == null) - continue; - - try - { - if (listener.Listener.SetValue(listener.Instance, true) == false) - { - Console.WriteLine( - $"[ERROR][{nameof(TwitchPubSubService)}] Failed to set {listener.Instance.GetType().Name}.{listener.Listener.Name} to true"); - } - else - { - Console.WriteLine( - $"[DEBUG][{nameof(TwitchPubSubService)}] Setting {listener.Instance.GetType().Name}.{listener.Listener.Name} to true"); - } - } - catch (Exception e) - { - Console.WriteLine( - $"[ERROR][{nameof(TwitchPubSubService)}] Exception while setting {listener.Instance?.GetType().Name}.{listener.Listener?.Name}: {e.Message}"); - } - } - } - - private void OnSubOnOnViewCount(object? sender, OnViewCountArgs args) - { - var listeners = _targets - .Where(x => x.Key == PubSubType.VideoPlayback) - .SelectMany(x => x.Value) - .Where(x => x.ChannelId == args.ChannelId) - .ToList(); - - foreach (var listener in listeners) - { - if (listener.Listener == null || listener.Instance == null) - continue; - - try - { - if (listener.Listener.SetValue(listener.Instance, args.Viewers) == false) - { - Console.WriteLine( - $"[ERROR][{nameof(TwitchPubSubService)}] Failed to set {listener.Instance.GetType().Name}.{listener.Listener.Name} to {args.Viewers}"); - } - else - { - Console.WriteLine( - $"[DEBUG][{nameof(TwitchPubSubService)}] Setting {listener.Instance.GetType().Name}.{listener.Listener.Name} to {args.Viewers}"); - } - } - catch (Exception e) - { - Console.WriteLine( - $"[ERROR][{nameof(TwitchPubSubService)}] Exception while setting {listener.Instance?.GetType().Name}.{listener.Listener?.Name}: {e.Message}"); - } - } - } - - private void OnSubOnOnListenResponse(object? sender, OnListenResponseArgs args) - { - Console.WriteLine($"Listen Response: {args.Topic}"); - } - - private void OnSubOnOnPubSubServiceClosed(object? sender, EventArgs args) - { - Console.WriteLine("PubSub Closed"); - } - - private void OnSubOnOnPubSubServiceError(object? sender, OnPubSubServiceErrorArgs args) - { - Console.WriteLine($"PubSub Error: {args.Exception.Message}"); - } - - private void OnSubOnOnPubSubServiceConnected(object? sender, EventArgs args) - { - Console.WriteLine("Connected to PubSub"); - } - - public void RegisterReceiver(T receiver) where T : class - { - ArgumentNullException.ThrowIfNull(receiver, nameof(receiver)); - - if (_sub == null) - { - Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] PubSub is not initialized"); - return; - } - - Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Registering {receiver.GetType().Name}"); - - var type = typeof(T); - var publicTargets = type - .GetProperties() - .Concat( - type.GetFields() as MemberInfo[] - ); - - foreach (var target in publicTargets) - { - if (target.GetCustomAttributes() is not { } attrs) - { - continue; - } - - foreach (var attr in attrs) - { - var channelId = - type.GetProperty(attr.ChannelIdField)?.GetValue(receiver)?.ToString() ?? - type.GetField(attr.ChannelIdField)?.GetValue(receiver)?.ToString(); - - if (channelId == null) - { - Console.WriteLine( - $"[ERROR][{nameof(TwitchPubSubService)}] {target.Name} is missing ChannelIdField named {attr.ChannelIdField}"); - continue; - } - - if (string.IsNullOrWhiteSpace(channelId)) - { - Console.WriteLine( - $"[ERROR][{nameof(TwitchPubSubService)}] {target.Name} ChannelIdField named {attr.ChannelIdField} is empty"); - continue; - } - - Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Registering {target.Name} for {attr.Type}"); - if (_targets.TryGetValue(attr.Type, out var listeners)) - { - listeners.Add(new PubSubListener - { - ChannelId = channelId, - Instance = receiver, - Listener = target - }); - } - else - { - _targets.Add(attr.Type, [ - new PubSubListener - { - ChannelId = channelId, - Instance = receiver, - Listener = target - } - ]); - } - - _sub.ListenToVideoPlayback(channelId); - _sub.SendTopics(_dataService.AccessToken, true); - } - } - } - - public void UnregisterReceiver(T receiver) where T : class - { - ArgumentNullException.ThrowIfNull(receiver, nameof(receiver)); - - if (_sub == null) - { - Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] PubSub is not initialized"); - return; - } - - foreach (var (topic, listeners) in _targets) - { - var listener = listeners.Where(x => x.Instance == receiver).ToList(); - - foreach (var l in listener) - { - _sub.ListenToVideoPlayback(l.ChannelId); - _sub.SendTopics(_dataService.AccessToken, true); - } - - _targets[topic].RemoveAll(x => x.Instance == receiver); - } - } -} \ No newline at end of file diff --git a/Services/TwitchService.cs b/Services/TwitchService.cs new file mode 100644 index 0000000..3485586 --- /dev/null +++ b/Services/TwitchService.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using BetterRaid.Misc; +using BetterRaid.Models; +using TwitchLib.Api; +using TwitchLib.Api.Helix.Models.Users.GetUsers; +using TwitchLib.PubSub; +using TwitchLib.PubSub.Events; +using OnEmoteOnlyArgs = TwitchLib.PubSub.Events.OnEmoteOnlyArgs; +using OnLogArgs = TwitchLib.PubSub.Events.OnLogArgs; + +namespace BetterRaid.Services; + +public interface ITwitchService +{ + public string? AccessToken { get; } + public TwitchChannel? UserChannel { get; set; } + public TwitchAPI TwitchApi { get; } + public bool IsRaidStarted { get; set; } + + public Task ConnectApiAsync(string clientId, string accessToken); + public string GetOAuthUrl(); + public void StartRaid(string from, string to); + public bool CanStartRaidCommand(object? arg); + public void StartRaidCommand(object? arg); + public void StopRaid(); + public void StopRaidCommand(); + public void OpenChannelCommand(object? arg); + public void RegisterForEvents(TwitchChannel channel); + public void UnregisterFromEvents(TwitchChannel channel); + + public event PropertyChangingEventHandler? PropertyChanging; + public event PropertyChangedEventHandler? PropertyChanged; +} + +public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INotifyPropertyChanging +{ + private bool _isRaidStarted; + private TwitchChannel? _userChannel; + private readonly List _registeredChannels; + private User? _user; + + public string AccessToken { get; private set; } = string.Empty; + + public bool IsRaidStarted + { + get => _isRaidStarted; + set => SetField(ref _isRaidStarted, value); + } + + public User? User + { + get => _user; + set => SetField(ref _user, value); + } + + public TwitchChannel? UserChannel + { + get => _userChannel; + set + { + if (_userChannel != null && _userChannel.Name?.Equals(value?.Name) == true) + return; + + SetField(ref _userChannel, value); + + _userChannel?.UpdateChannelData(this); + } + } + + public TwitchAPI TwitchApi { get; } + public TwitchPubSub TwitchEvents { get; } + + public TwitchService() + { + _registeredChannels = []; + + TwitchApi = new TwitchAPI(); + TwitchEvents = new TwitchPubSub(); + + if (TryLoadAccessToken(out var token)) + { + Console.WriteLine($"[INFO][{nameof(TwitchService)}] Found access token."); + Task.Run(() => ConnectApiAsync(Constants.TwitchClientId, token)) + .ContinueWith(_ => ConnectTwitchEvents(token)); + } + else + { + Console.WriteLine($"[INFO][{nameof(TwitchService)}] No access token found."); + } + } + + private async Task ConnectTwitchEvents(string token) + { + if (UserChannel == null || User == null) + return; + + Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connecting to Twitch Events ..."); + + TwitchEvents.OnRaidGo += OnUserRaidGo; + TwitchEvents.OnRaidUpdateV2 += OnUserRaidUpdate; + TwitchEvents.OnStreamUp += OnUserStreamUp; + TwitchEvents.OnStreamDown += OnUserStreamDown; + + TwitchEvents.ListenToRaid(UserChannel.BroadcasterId); + TwitchEvents.ListenToVideoPlayback(UserChannel.BroadcasterId); + + TwitchEvents.SendTopics(token); + TwitchEvents.Connect(); + + RegisterForEvents(UserChannel); + + Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connected to Twitch Events."); + + await Task.CompletedTask; + } + + public async Task ConnectApiAsync(string clientId, string accessToken) + { + Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connecting to Twitch API ..."); + + AccessToken = accessToken; + + TwitchApi.Settings.ClientId = clientId; + TwitchApi.Settings.AccessToken = accessToken; + + if (TryGetUser(out var user)) + { + User = user; + } + else + { + User = null; + + Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not get user."); + } + + if (TryGetUserChannel(out var channel)) + { + UserChannel = channel; + Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connected to Twitch API as {channel?.Name}."); + } + else + { + UserChannel = null; + + Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not get user channel."); + } + + if (User == null || UserChannel == null) + { + Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not connect to Twitch API."); + } + + await Task.CompletedTask; + } + + private bool TryLoadAccessToken(out string token) + { + token = string.Empty; + + if (!File.Exists(Constants.TwitchOAuthAccessTokenFilePath)) + return false; + + token = File.ReadAllText(Constants.TwitchOAuthAccessTokenFilePath); + return true; + } + + public void SaveAccessToken(string token) + { + File.WriteAllText(Constants.TwitchOAuthAccessTokenFilePath, token); + } + + public bool TryGetUser(out User? user) + { + user = null; + + try + { + var userResult = TwitchApi.Helix.Users.GetUsersAsync().Result.Users[0]; + + if (userResult == null) + { + return false; + } + + user = userResult; + return true; + } + catch (Exception e) + { + Console.WriteLine($"[ERROR][{nameof(TwitchService)}] {e.Message}"); + return false; + } + } + + public bool TryGetUserChannel(out TwitchChannel? channel) + { + channel = null; + + if (User == null) + return false; + + channel = new TwitchChannel(User.Login); + + return true; + } + + public void RegisterForEvents(TwitchChannel channel) + { + TwitchEvents.OnStreamUp += channel.OnStreamUp; + TwitchEvents.OnStreamDown += channel.OnStreamDown; + TwitchEvents.OnViewCount += channel.OnViewCount; + + TwitchEvents.ListenToVideoPlayback(channel.BroadcasterId); + + TwitchEvents.SendTopics(AccessToken); + + _registeredChannels.Add(channel); + } + + public void UnregisterFromEvents(TwitchChannel channel) + { + TwitchEvents.OnStreamUp -= channel.OnStreamUp; + TwitchEvents.OnStreamDown -= channel.OnStreamDown; + TwitchEvents.OnViewCount -= channel.OnViewCount; + + TwitchEvents.ListenToVideoPlayback(channel.BroadcasterId); + + TwitchEvents.SendTopics(AccessToken, true); + + _registeredChannels.Remove(channel); + } + + public string GetOAuthUrl() + { + var scopes = string.Join("+", Constants.TwitchOAuthScopes); + + return $"https://id.twitch.tv/oauth2/authorize" + + $"?client_id={Constants.TwitchClientId}" + + $"&redirect_uri={Constants.TwitchOAuthRedirectUrl}" + + $"&response_type={Constants.TwitchOAuthResponseType}" + + $"&scope={scopes}"; + } + + public void StartRaid(string from, string to) + { + // TODO: Also check, if the logged in user is live + + TwitchApi.Helix.Raids.StartRaidAsync(from, to); + IsRaidStarted = true; + } + + public bool CanStartRaidCommand(object? arg) + { + return UserChannel?.IsLive == true && IsRaidStarted == false; + } + + public void StartRaidCommand(object? arg) + { + if (arg == null || UserChannel?.BroadcasterId == null) + { + return; + } + + var from = UserChannel.BroadcasterId!; + var to = arg.ToString()!; + + StartRaid(from, to); + } + + public void StopRaid() + { + if (UserChannel?.BroadcasterId == null) + return; + + if (IsRaidStarted == false) + return; + + TwitchApi.Helix.Raids.CancelRaidAsync(UserChannel.BroadcasterId); + IsRaidStarted = false; + } + + public void StopRaidCommand() + { + StopRaid(); + } + + public void OpenChannelCommand(object? arg) + { + var channelName = arg?.ToString(); + if (string.IsNullOrEmpty(channelName)) + return; + + var url = $"https://twitch.tv/{channelName}"; + + Tools.OpenUrl(url); + } + + private void OnUserRaidUpdate(object? sender, OnRaidUpdateV2Args e) + { + + } + + private void OnUserRaidGo(object? sender, OnRaidGoArgs e) + { + IsRaidStarted = false; + } + + private void OnUserStreamDown(object? sender, OnStreamDownArgs e) + { + IsRaidStarted = false; + } + + private void OnUserStreamUp(object? sender, OnStreamUpArgs e) + { + IsRaidStarted = false; + } + + public event PropertyChangingEventHandler? PropertyChanging; + public event PropertyChangedEventHandler? PropertyChanged; + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private void OnPropertyChanging([CallerMemberName] string? propertyName = null) + { + PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName)); + } + + private bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + return false; + + OnPropertyChanging(propertyName); + field = value; + OnPropertyChanged(propertyName); + + return true; + } + +} \ No newline at end of file diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 57a1c19..9d7c292 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -18,7 +18,6 @@ public class MainWindowViewModel : ViewModelBase private string? _filter; private ObservableCollection _channels = []; private readonly BetterRaidDatabase? _db; - private readonly ITwitchPubSubService _pubSub; public BetterRaidDatabase? Database { @@ -40,7 +39,7 @@ public class MainWindowViewModel : ViewModelBase public ObservableCollection FilteredChannels => GetFilteredChannels(); - public ITwitchDataService DataService { get; } + public ITwitchService Twitch { get; } public string? Filter { @@ -48,13 +47,12 @@ public class MainWindowViewModel : ViewModelBase set => SetProperty(ref _filter, value); } - public bool IsLoggedIn => DataService.UserChannel != null; + public bool IsLoggedIn => Twitch.UserChannel != null; - public MainWindowViewModel(ITwitchPubSubService pubSub, ITwitchDataService dataService) + public MainWindowViewModel(ITwitchService twitch) { - _pubSub = pubSub; - DataService = dataService; - DataService.PropertyChanged += OnDataServicePropertyChanged; + Twitch = twitch; + Twitch.PropertyChanged += OnTwitchPropertyChanged; Database = BetterRaidDatabase.LoadFromFile(Constants.DatabaseFilePath); Database.PropertyChanged += OnDatabasePropertyChanged; @@ -75,7 +73,7 @@ public class MainWindowViewModel : ViewModelBase public void LoginWithTwitch() { - Tools.StartOAuthLogin(DataService.GetOAuthUrl(), OnTwitchLoginCallback, CancellationToken.None); + Tools.StartOAuthLogin(Twitch.GetOAuthUrl(), OnTwitchLoginCallback, CancellationToken.None); } private void OnTwitchLoginCallback() @@ -92,7 +90,7 @@ public class MainWindowViewModel : ViewModelBase foreach (var channel in Channels) { - _pubSub.UnregisterReceiver(channel); + Twitch.UnregisterFromEvents(channel); } Channels.Clear(); @@ -105,8 +103,8 @@ public class MainWindowViewModel : ViewModelBase { Task.Run(() => { - channel.UpdateChannelData(DataService); - _pubSub.RegisterReceiver(channel); + channel.UpdateChannelData(Twitch); + Twitch.RegisterForEvents(channel); }); Channels.Add(channel); @@ -123,9 +121,9 @@ public class MainWindowViewModel : ViewModelBase return new ObservableCollection(filteredChannels); } - private void OnDataServicePropertyChanged(object? sender, PropertyChangedEventArgs e) + private void OnTwitchPropertyChanged(object? sender, PropertyChangedEventArgs e) { - if (e.PropertyName != nameof(DataService.UserChannel)) + if (e.PropertyName != nameof(Twitch.UserChannel)) return; OnPropertyChanged(nameof(IsLoggedIn)); diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml index e8c63ac..90d9d0e 100644 --- a/Views/MainWindow.axaml +++ b/Views/MainWindow.axaml @@ -33,7 +33,7 @@ Width="40" Height="40" Margin="5" - Source="{Binding DataService.UserChannel.ThumbnailUrl, + Source="{Binding Twitch.UserChannel.ThumbnailUrl, FallbackValue={x:Static misc:Constants.ChannelPlaceholderImageUrl}, TargetNullValue={x:Static misc:Constants.ChannelPlaceholderImageUrl}}" /> @@ -42,9 +42,9 @@ FontWeight="Bold"> - - @@ -210,8 +210,8 @@ HorizontalContentAlignment="Center" VerticalContentAlignment="Center" IsEnabled="{Binding IsLive}" - IsVisible="{Binding !$parent[Window].((vm:MainWindowViewModel)DataContext).DataService.IsRaidStarted}" - Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.StartRaidCommand}" + IsVisible="{Binding !$parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.IsRaidStarted}" + Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.StartRaidCommand}" CommandParameter="{Binding BroadcasterId}" />