Revised the redeem system, activated via channel point redeems. Added OBS transformation to redeems. Logs changed & writes to logs folder as well. Removed most use of IServiceProvider.
This commit is contained in:
9
Twitch/Redemptions/Action.cs
Normal file
9
Twitch/Redemptions/Action.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace TwitchChatTTS.Twitch.Redemptions
|
||||
{
|
||||
public class RedeemableAction
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public IDictionary<string, string> Data { get; set; }
|
||||
}
|
||||
}
|
11
Twitch/Redemptions/Redemption.cs
Normal file
11
Twitch/Redemptions/Redemption.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace TwitchChatTTS.Twitch.Redemptions
|
||||
{
|
||||
public class Redemption
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string RedemptionId { get; set; }
|
||||
public string ActionName { get; set; }
|
||||
public int Order { get; set; }
|
||||
public bool State { get; set; }
|
||||
}
|
||||
}
|
148
Twitch/Redemptions/RedemptionManager.cs
Normal file
148
Twitch/Redemptions/RedemptionManager.cs
Normal file
@ -0,0 +1,148 @@
|
||||
using System.Reflection;
|
||||
using org.mariuszgromada.math.mxparser;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
using TwitchChatTTS.OBS.Socket.Manager;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Redemptions
|
||||
{
|
||||
public class RedemptionManager
|
||||
{
|
||||
private readonly IList<Redemption> _redemptions;
|
||||
private readonly IDictionary<string, RedeemableAction> _actions;
|
||||
private readonly IDictionary<string, IList<RedeemableAction>> _store;
|
||||
private readonly OBSManager _obsManager;
|
||||
private readonly ILogger _logger;
|
||||
private bool _isReady;
|
||||
|
||||
|
||||
public RedemptionManager(OBSManager obsManager, ILogger logger)
|
||||
{
|
||||
_redemptions = new List<Redemption>();
|
||||
_actions = new Dictionary<string, RedeemableAction>();
|
||||
_store = new Dictionary<string, IList<RedeemableAction>>();
|
||||
_obsManager = obsManager;
|
||||
_logger = logger;
|
||||
_isReady = false;
|
||||
}
|
||||
|
||||
public void AddTwitchRedemption(Redemption redemption)
|
||||
{
|
||||
_redemptions.Add(redemption);
|
||||
}
|
||||
|
||||
public void AddAction(RedeemableAction action)
|
||||
{
|
||||
_actions.Add(action.Name, action);
|
||||
}
|
||||
|
||||
private void Add(string twitchRedemptionId, RedeemableAction action)
|
||||
{
|
||||
if (!_store.TryGetValue(twitchRedemptionId, out var actions))
|
||||
_store.Add(twitchRedemptionId, actions = new List<RedeemableAction>());
|
||||
actions.Add(action);
|
||||
_store[twitchRedemptionId] = actions.OrderBy(a => a).ToList();
|
||||
}
|
||||
|
||||
public async Task Execute(RedeemableAction action, string sender)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (action.Type)
|
||||
{
|
||||
case "WRITE_TO_FILE":
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"]));
|
||||
await File.WriteAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], sender));
|
||||
_logger.Debug($"Overwritten text to file [file: {action.Data["file_path"]}]");
|
||||
break;
|
||||
case "APPEND_TO_FILE":
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"]));
|
||||
await File.AppendAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], sender));
|
||||
_logger.Debug($"Appended text to file [file: {action.Data["file_path"]}]");
|
||||
break;
|
||||
case "OBS_TRANSFORM":
|
||||
var type = typeof(OBSTransformationData);
|
||||
await _obsManager.UpdateTransformation(action.Data["scene_name"], action.Data["scene_item_name"], (d) =>
|
||||
{
|
||||
string[] properties = ["rotation", "position_x", "position_y"];
|
||||
foreach (var property in properties)
|
||||
{
|
||||
if (!action.Data.TryGetValue(property, out var expressionString) || expressionString == null)
|
||||
continue;
|
||||
|
||||
var propertyName = string.Join("", property.Split('_').Select(p => char.ToUpper(p[0]) + p.Substring(1)));
|
||||
PropertyInfo? prop = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (prop == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find property for OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}]");
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentValue = prop.GetValue(d);
|
||||
if (currentValue == null)
|
||||
{
|
||||
_logger.Warning($"Found a null value from OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}]");
|
||||
}
|
||||
|
||||
Expression expression = new Expression(expressionString);
|
||||
expression.addConstants(new Constant("x", (double?)currentValue ?? 0.0d));
|
||||
if (!expression.checkSyntax())
|
||||
{
|
||||
_logger.Warning($"Could not parse math expression for OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][expression: {expressionString}][property: {propertyName}]");
|
||||
continue;
|
||||
}
|
||||
|
||||
var newValue = expression.calculate();
|
||||
prop.SetValue(d, newValue);
|
||||
_logger.Debug($"OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}][old value: {currentValue}][new value: {newValue}][expression: {expressionString}]");
|
||||
}
|
||||
_logger.Debug($"Finished applying the OBS transformation property changes [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}]");
|
||||
});
|
||||
break;
|
||||
case "AUDIO_FILE":
|
||||
if (!File.Exists(action.Data["file_path"])) {
|
||||
_logger.Warning($"Cannot find audio file for Twitch channel point redeem [file: {action.Data["file_path"]}]");
|
||||
return;
|
||||
}
|
||||
AudioPlaybackEngine.Instance.PlaySound(action.Data["file_path"]);
|
||||
_logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}]");
|
||||
break;
|
||||
default:
|
||||
_logger.Warning($"Unknown redeemable action has occured [type: {action.Type}]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to execute a redemption action.");
|
||||
}
|
||||
}
|
||||
|
||||
public IList<RedeemableAction> Get(string twitchRedemptionId)
|
||||
{
|
||||
if (!_isReady)
|
||||
throw new InvalidOperationException("Not ready");
|
||||
|
||||
if (_store.TryGetValue(twitchRedemptionId, out var actions))
|
||||
return actions;
|
||||
return new List<RedeemableAction>(0);
|
||||
}
|
||||
|
||||
public void Ready()
|
||||
{
|
||||
var ordered = _redemptions.OrderBy(r => r.Order);
|
||||
_store.Clear();
|
||||
|
||||
foreach (var redemption in ordered)
|
||||
if (_actions.TryGetValue(redemption.ActionName, out var action) && action != null)
|
||||
Add(redemption.RedemptionId, action);
|
||||
|
||||
_isReady = true;
|
||||
_logger.Debug("Redemption Manager is ready.");
|
||||
}
|
||||
|
||||
private string ReplaceContentText(string content, string username) {
|
||||
return content.Replace("%USER%", username);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,28 +6,32 @@ using TwitchLib.Api.Core.Exceptions;
|
||||
using TwitchLib.Client.Events;
|
||||
using TwitchLib.Client.Models;
|
||||
using TwitchLib.Communication.Events;
|
||||
using static TwitchChatTTS.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using TwitchLib.PubSub.Interfaces;
|
||||
using TwitchLib.Client.Interfaces;
|
||||
using TwitchChatTTS.OBS.Socket;
|
||||
using TwitchChatTTS.Twitch.Redemptions;
|
||||
|
||||
public class TwitchApiClient
|
||||
{
|
||||
private readonly RedemptionManager _redemptionManager;
|
||||
private readonly HermesApiClient _hermesApiClient;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly ILogger _logger;
|
||||
private TwitchBotAuth _token;
|
||||
private readonly TwitchBotAuth _token;
|
||||
private readonly ITwitchClient _client;
|
||||
private readonly ITwitchPubSub _publisher;
|
||||
private readonly WebClientWrap _web;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger _logger;
|
||||
private bool _initialized;
|
||||
private string _broadcasterId;
|
||||
|
||||
|
||||
public TwitchApiClient(
|
||||
RedemptionManager redemptionManager,
|
||||
HermesApiClient hermesApiClient,
|
||||
Configuration configuration,
|
||||
TwitchBotAuth token,
|
||||
ITwitchClient twitchClient,
|
||||
@ -36,6 +40,8 @@ public class TwitchApiClient
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_redemptionManager = redemptionManager;
|
||||
_hermesApiClient = hermesApiClient;
|
||||
_configuration = configuration;
|
||||
_token = token;
|
||||
_client = twitchClient;
|
||||
@ -43,6 +49,7 @@ public class TwitchApiClient
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
_initialized = false;
|
||||
_broadcasterId = string.Empty;
|
||||
|
||||
_web = new WebClientWrap(new JsonSerializerOptions()
|
||||
{
|
||||
@ -126,6 +133,9 @@ public class TwitchApiClient
|
||||
|
||||
_logger.Information("Attempting to re-authorize.");
|
||||
await Authorize(_broadcasterId);
|
||||
await _client.DisconnectAsync();
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
await _client.ConnectAsync();
|
||||
};
|
||||
|
||||
_client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) =>
|
||||
@ -159,67 +169,49 @@ public class TwitchApiClient
|
||||
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
|
||||
return;
|
||||
|
||||
_logger.Information("Follow: " + e.DisplayName);
|
||||
_logger.Information($"New Follower [name: {e.DisplayName}][username: {e.Username}]");
|
||||
};
|
||||
|
||||
_publisher.OnChannelPointsRewardRedeemed += (s, e) =>
|
||||
_publisher.OnChannelPointsRewardRedeemed += async (s, e) =>
|
||||
{
|
||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient;
|
||||
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
|
||||
return;
|
||||
|
||||
_logger.Information($"Channel Point Reward Redeemed [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
||||
_logger.Information($"Channel Point Reward Redeemed [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
||||
|
||||
if (_configuration.Twitch?.Redeems == null)
|
||||
return;
|
||||
|
||||
var redeemName = e.RewardRedeemed.Redemption.Reward.Title.ToLower().Trim().Replace(" ", "-");
|
||||
if (!_configuration.Twitch.Redeems.TryGetValue(redeemName, out RedeemConfiguration? redeem))
|
||||
return;
|
||||
|
||||
if (redeem == null)
|
||||
return;
|
||||
|
||||
// Write or append to file if needed.
|
||||
var outputFile = string.IsNullOrWhiteSpace(redeem.OutputFilePath) ? null : redeem.OutputFilePath.Trim();
|
||||
if (outputFile == null)
|
||||
var actions = _redemptionManager.Get(e.RewardRedeemed.Redemption.Reward.Id);
|
||||
if (!actions.Any())
|
||||
{
|
||||
_logger.Debug($"No output file was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
||||
_logger.Debug($"No redemable actions for this redeem was found [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
var outputContent = string.IsNullOrWhiteSpace(redeem.OutputContent) ? null : redeem.OutputContent.Trim().Replace("%USER%", e.RewardRedeemed.Redemption.User.DisplayName).Replace("\\n", "\n");
|
||||
if (outputContent == null)
|
||||
_logger.Debug($"Found {actions.Count} actions for this Twitch channel point redemption [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
||||
|
||||
foreach (var action in actions)
|
||||
try
|
||||
{
|
||||
_logger.Warning($"No output content was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
||||
await _redemptionManager.Execute(action, e.RewardRedeemed.Redemption.User.DisplayName);
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (redeem.OutputAppend == true)
|
||||
{
|
||||
File.AppendAllText(outputFile, outputContent + "\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
File.WriteAllText(outputFile, outputContent);
|
||||
}
|
||||
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Play audio file if needed.
|
||||
var audioFile = string.IsNullOrWhiteSpace(redeem.AudioFilePath) ? null : redeem.AudioFilePath.Trim();
|
||||
if (audioFile == null)
|
||||
_publisher.OnPubSubServiceClosed += async (s, e) =>
|
||||
{
|
||||
_logger.Warning("Twitch PubSub ran into a service close. Attempting to connect again.");
|
||||
//await Task.Delay(Math.Min(3000 + (1 << psConnectionFailures), 120000));
|
||||
var authorized = await Authorize(_broadcasterId);
|
||||
|
||||
var twitchBotData = await _hermesApiClient.FetchTwitchBotToken();
|
||||
if (twitchBotData == null)
|
||||
{
|
||||
_logger.Debug($"No audio file was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
||||
}
|
||||
else if (!File.Exists(audioFile))
|
||||
{
|
||||
_logger.Warning($"Cannot find audio file [location: {audioFile}] for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
||||
}
|
||||
else
|
||||
{
|
||||
AudioPlaybackEngine.Instance.PlaySound(audioFile);
|
||||
Console.WriteLine("The API is down. Contact the owner.");
|
||||
return;
|
||||
}
|
||||
await _publisher.ConnectAsync();
|
||||
};
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user