Merge branch 'ui-rework' into dispatcher-service
This commit is contained in:
commit
b2252557a6
@ -7,7 +7,6 @@ using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using BetterRaid.Extensions;
|
||||
using BetterRaid.Services;
|
||||
using BetterRaid.Services.Implementations;
|
||||
using BetterRaid.ViewModels;
|
||||
using BetterRaid.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -27,8 +26,7 @@ public class App : Application
|
||||
private ServiceProvider InitializeServices()
|
||||
{
|
||||
var Services = new ServiceCollection();
|
||||
Services.AddSingleton<ITwitchDataService, TwitchDataService>();
|
||||
Services.AddSingleton<ITwitchPubSubService, TwitchPubSubService>();
|
||||
Services.AddSingleton<ITwitchService, TwitchService>();
|
||||
Services.AddSingleton<ISynchronizaionService, DispatcherService>(serviceProvider => new DispatcherService(Dispatcher.UIThread));
|
||||
Services.AddTransient<IMainViewModelFactory, MainWindowViewModelFactory>();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
namespace BetterRaid.Attributes;
|
||||
|
||||
public enum PubSubType
|
||||
{
|
||||
VideoPlayback,
|
||||
Follows,
|
||||
Subscriptions,
|
||||
ChannelPoints,
|
||||
Bits,
|
||||
Raids,
|
||||
StreamUp,
|
||||
StreamDown
|
||||
}
|
@ -15,7 +15,7 @@ public static class Tools
|
||||
private static HttpListener? _oauthListener;
|
||||
|
||||
// Source: https://stackoverflow.com/a/43232486
|
||||
public static void StartOAuthLogin(string url, ITwitchDataService twitchDataService, Action? callback = null, CancellationToken token = default)
|
||||
public static void StartOAuthLogin(ITwitchService twitchService, Action? callback = null, CancellationToken token = default)
|
||||
{
|
||||
if (_oauthListener == null)
|
||||
{
|
||||
@ -23,13 +23,13 @@ public static class Tools
|
||||
_oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl + "/");
|
||||
_oauthListener.Start();
|
||||
|
||||
Task.Run(() => WaitForCallback(callback, token, twitchDataService), token);
|
||||
Task.Run(() => WaitForCallback(callback, token, twitchService), token);
|
||||
}
|
||||
|
||||
OpenUrl(url);
|
||||
OpenUrl(twitchService.GetOAuthUrl());
|
||||
}
|
||||
|
||||
private static async Task WaitForCallback(Action? callback, CancellationToken token, ITwitchDataService twitchDataService)
|
||||
private static async Task WaitForCallback(Action? callback, CancellationToken token, ITwitchService twitchService)
|
||||
{
|
||||
if (_oauthListener == null)
|
||||
return;
|
||||
@ -107,7 +107,7 @@ public static class Tools
|
||||
|
||||
var accessToken = jsonData["access_token"]?.ToString();
|
||||
|
||||
twitchDataService.ConnectApiAsync(Constants.TwitchClientId, accessToken!);
|
||||
twitchService.ConnectApiAsync(Constants.TwitchClientId, accessToken!);
|
||||
|
||||
res.StatusCode = 200;
|
||||
res.Close();
|
||||
|
@ -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; }
|
||||
}
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
namespace BetterRaid.Services;
|
||||
|
||||
public interface ITwitchPubSubService
|
||||
{
|
||||
void RegisterReceiver<T>(T receiver) where T : class;
|
||||
void UnregisterReceiver<T>(T receiver) where T : class;
|
||||
}
|
@ -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<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
return false;
|
||||
|
||||
OnPropertyChanging(propertyName);
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -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<PubSubType, List<PubSubListener>> _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>(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<PubSubAttribute>() 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>(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);
|
||||
}
|
||||
}
|
||||
}
|
349
Services/TwitchService.cs
Normal file
349
Services/TwitchService.cs
Normal file
@ -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<TwitchChannel> _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<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
return false;
|
||||
|
||||
OnPropertyChanging(propertyName);
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -18,7 +18,6 @@ public class MainWindowViewModel : ViewModelBase
|
||||
private string? _filter;
|
||||
private ObservableCollection<TwitchChannel> _channels = [];
|
||||
private readonly BetterRaidDatabase? _db;
|
||||
private readonly ITwitchPubSubService _pubSub;
|
||||
private readonly ISynchronizaionService _synchronizaionService;
|
||||
|
||||
public BetterRaidDatabase? Database
|
||||
@ -41,7 +40,7 @@ public class MainWindowViewModel : ViewModelBase
|
||||
|
||||
public ObservableCollection<TwitchChannel> FilteredChannels => GetFilteredChannels();
|
||||
|
||||
public ITwitchDataService DataService { get; }
|
||||
public ITwitchService Twitch { get; }
|
||||
|
||||
public string? Filter
|
||||
{
|
||||
@ -49,14 +48,14 @@ 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, ISynchronizaionService synchronizaionService)
|
||||
public MainWindowViewModel(ITwitchService twitch, ISynchronizaionService synchronizaionService)
|
||||
{
|
||||
_pubSub = pubSub;
|
||||
DataService = dataService;
|
||||
_synchronizaionService = synchronizaionService;
|
||||
DataService.PropertyChanged += OnDataServicePropertyChanged;
|
||||
|
||||
Twitch = twitch;
|
||||
Twitch.PropertyChanged += OnTwitchPropertyChanged;
|
||||
|
||||
Database = BetterRaidDatabase.LoadFromFile(Constants.DatabaseFilePath);
|
||||
Database.PropertyChanged += OnDatabasePropertyChanged;
|
||||
@ -77,7 +76,7 @@ public class MainWindowViewModel : ViewModelBase
|
||||
|
||||
public void LoginWithTwitch()
|
||||
{
|
||||
Tools.StartOAuthLogin(DataService.GetOAuthUrl(), DataService, OnTwitchLoginCallback, CancellationToken.None);
|
||||
Tools.StartOAuthLogin(Twitch, OnTwitchLoginCallback, CancellationToken.None);
|
||||
}
|
||||
|
||||
private void OnTwitchLoginCallback()
|
||||
@ -94,7 +93,7 @@ public class MainWindowViewModel : ViewModelBase
|
||||
|
||||
foreach (var channel in Channels)
|
||||
{
|
||||
_pubSub.UnregisterReceiver(channel);
|
||||
Twitch.UnregisterFromEvents(channel);
|
||||
}
|
||||
|
||||
Channels.Clear();
|
||||
@ -107,8 +106,8 @@ public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
channel.UpdateChannelData(DataService);
|
||||
_pubSub.RegisterReceiver(channel);
|
||||
channel.UpdateChannelData(Twitch);
|
||||
Twitch.RegisterForEvents(channel);
|
||||
});
|
||||
|
||||
Channels.Add(channel);
|
||||
@ -125,9 +124,9 @@ public class MainWindowViewModel : ViewModelBase
|
||||
return new ObservableCollection<TwitchChannel>(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));
|
||||
|
@ -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">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0} ({1})">
|
||||
<Binding Path="DataService.UserChannel.DisplayName"
|
||||
<Binding Path="Twitch.UserChannel.DisplayName"
|
||||
FallbackValue="-" />
|
||||
<Binding Path="DataService.UserChannel.ViewerCount"
|
||||
<Binding Path="Twitch.UserChannel.ViewerCount"
|
||||
FallbackValue="Offline"
|
||||
TargetNullValue="Offline" />
|
||||
</MultiBinding>
|
||||
@ -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}" />
|
||||
|
||||
<Button Content="Cancel Raid"
|
||||
@ -223,8 +223,8 @@
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
IsVisible="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.IsRaidStarted}"
|
||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.StopRaidCommand}" />
|
||||
IsVisible="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.IsRaidStarted}"
|
||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.StopRaidCommand}" />
|
||||
|
||||
<Button Content="View Channel"
|
||||
Height="50"
|
||||
@ -234,7 +234,7 @@
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.OpenChannelCommand}"
|
||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.OpenChannelCommand}"
|
||||
CommandParameter="{Binding Name}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
Reference in New Issue
Block a user