2024-06-24 18:11:36 -04:00
using System.Reflection ;
2024-07-06 23:42:33 -04:00
using CommonSocketLibrary.Abstract ;
using CommonSocketLibrary.Common ;
2024-07-16 00:48:55 -04:00
using HermesSocketLibrary.Requests.Messages ;
2024-07-06 23:42:33 -04:00
using Microsoft.Extensions.DependencyInjection ;
2024-06-24 18:11:36 -04:00
using org.mariuszgromada.math.mxparser ;
using Serilog ;
2024-11-08 10:32:42 -05:00
using TwitchChatTTS.Bus ;
using TwitchChatTTS.Bus.Data ;
2024-07-16 00:48:55 -04:00
using TwitchChatTTS.Hermes.Socket ;
2024-07-19 12:56:41 -04:00
using TwitchChatTTS.OBS.Socket ;
2024-06-24 18:11:36 -04:00
using TwitchChatTTS.OBS.Socket.Data ;
namespace TwitchChatTTS.Twitch.Redemptions
{
2024-08-06 15:29:29 -04:00
public class RedemptionManager : IRedemptionManager
2024-06-24 18:11:36 -04:00
{
private readonly IDictionary < string , IList < RedeemableAction > > _store ;
2024-11-08 10:32:42 -05:00
private readonly ServiceBusCentral _bus ;
2024-07-06 23:42:33 -04:00
private readonly User _user ;
2024-07-19 12:56:41 -04:00
private readonly OBSSocketClient _obs ;
2024-07-16 00:48:55 -04:00
private readonly HermesSocketClient _hermes ;
2024-08-11 17:22:37 -04:00
private readonly NightbotApiClient _nightbot ;
2024-08-04 19:46:10 -04:00
private readonly AudioPlaybackEngine _playback ;
2024-06-24 18:11:36 -04:00
private readonly ILogger _logger ;
2024-07-06 23:42:33 -04:00
private readonly Random _random ;
2024-06-24 18:11:36 -04:00
private bool _isReady ;
2024-07-06 23:42:33 -04:00
public RedemptionManager (
2024-11-08 10:32:42 -05:00
ServiceBusCentral bus ,
2024-07-06 23:42:33 -04:00
User user ,
2024-07-19 12:56:41 -04:00
[FromKeyedServices("obs")] SocketClient < WebSocketMessage > obs ,
2024-07-16 00:48:55 -04:00
[FromKeyedServices("hermes")] SocketClient < WebSocketMessage > hermes ,
2024-08-11 17:22:37 -04:00
NightbotApiClient nightbot ,
2024-08-04 19:46:10 -04:00
AudioPlaybackEngine playback ,
2024-07-06 23:42:33 -04:00
ILogger logger )
2024-06-24 18:11:36 -04:00
{
_store = new Dictionary < string , IList < RedeemableAction > > ( ) ;
2024-11-08 10:32:42 -05:00
_bus = bus ;
2024-07-06 23:42:33 -04:00
_user = user ;
2024-07-19 12:56:41 -04:00
_obs = ( obs as OBSSocketClient ) ! ;
2024-07-16 00:48:55 -04:00
_hermes = ( hermes as HermesSocketClient ) ! ;
2024-08-11 17:22:37 -04:00
_nightbot = nightbot ;
2024-08-04 19:46:10 -04:00
_playback = playback ;
2024-06-24 18:11:36 -04:00
_logger = logger ;
2024-07-06 23:42:33 -04:00
_random = new Random ( ) ;
2024-06-24 18:11:36 -04:00
_isReady = false ;
2024-11-08 10:32:42 -05:00
var topic = _bus . GetTopic ( "redemptions_initiation" ) ;
topic . Subscribe ( new ServiceBusObserver ( data = > {
if ( data . Value is RedemptionInitiation obj )
Initialize ( obj . Redemptions , obj . Actions ) ;
} , _logger ) ) ;
2024-06-24 18:11:36 -04:00
}
private void Add ( string twitchRedemptionId , RedeemableAction action )
{
if ( ! _store . TryGetValue ( twitchRedemptionId , out var actions ) )
_store . Add ( twitchRedemptionId , actions = new List < RedeemableAction > ( ) ) ;
2024-07-06 23:42:33 -04:00
2024-06-24 18:11:36 -04:00
actions . Add ( action ) ;
2024-07-06 23:42:33 -04:00
_logger . Debug ( $"Added redemption action [name: {action.Name}][type: {action.Type}]" ) ;
2024-06-24 18:11:36 -04:00
}
2024-07-06 23:42:33 -04:00
public async Task Execute ( RedeemableAction action , string senderDisplayName , long senderId )
2024-06-24 18:11:36 -04:00
{
2024-07-16 00:48:55 -04:00
_logger . Debug ( $"Executing an action for a redemption [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
if ( action . Data = = null )
{
_logger . Warning ( $"No data was provided for an action, caused by redemption [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
return ;
}
2024-06-24 18:11:36 -04:00
try
{
switch ( action . Type )
{
case "WRITE_TO_FILE" :
Directory . CreateDirectory ( Path . GetDirectoryName ( action . Data [ "file_path" ] ) ) ;
2024-07-06 23:42:33 -04:00
await File . WriteAllTextAsync ( action . Data [ "file_path" ] , ReplaceContentText ( action . Data [ "file_content" ] , senderDisplayName ) ) ;
2024-07-16 00:48:55 -04:00
_logger . Debug ( $"Overwritten text to file [file: {action.Data[" file_path "]}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-06-24 18:11:36 -04:00
break ;
case "APPEND_TO_FILE" :
Directory . CreateDirectory ( Path . GetDirectoryName ( action . Data [ "file_path" ] ) ) ;
2024-07-06 23:42:33 -04:00
await File . AppendAllTextAsync ( action . Data [ "file_path" ] , ReplaceContentText ( action . Data [ "file_content" ] , senderDisplayName ) ) ;
2024-07-16 00:48:55 -04:00
_logger . Debug ( $"Appended text to file [file: {action.Data[" file_path "]}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-06-24 18:11:36 -04:00
break ;
case "OBS_TRANSFORM" :
var type = typeof ( OBSTransformationData ) ;
2024-07-19 12:56:41 -04:00
await _obs . UpdateTransformation ( action . Data [ "scene_name" ] , action . Data [ "scene_item_name" ] , ( d ) = >
2024-06-24 18:11:36 -04:00
{
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 )
{
2024-07-16 00:48:55 -04:00
_logger . Warning ( $"Failed to find property for OBS transformation [scene: {action.Data[" scene_name "]}][source: {action.Data[" scene_item_name "]}][property: {propertyName}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-06-24 18:11:36 -04:00
continue ;
}
var currentValue = prop . GetValue ( d ) ;
if ( currentValue = = null )
{
2024-07-16 00:48:55 -04:00
_logger . Warning ( $"Found a null value from OBS transformation [scene: {action.Data[" scene_name "]}][source: {action.Data[" scene_item_name "]}][property: {propertyName}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
continue ;
2024-06-24 18:11:36 -04:00
}
Expression expression = new Expression ( expressionString ) ;
expression . addConstants ( new Constant ( "x" , ( double? ) currentValue ? ? 0.0d ) ) ;
if ( ! expression . checkSyntax ( ) )
{
2024-07-16 00:48:55 -04:00
_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}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-06-24 18:11:36 -04:00
continue ;
}
var newValue = expression . calculate ( ) ;
prop . SetValue ( d , newValue ) ;
2024-07-16 00:48:55 -04:00
_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}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-06-24 18:11:36 -04:00
}
2024-07-16 00:48:55 -04:00
_logger . Debug ( $"Finished applying the OBS transformation property changes [scene: {action.Data[" scene_name "]}][source: {action.Data[" scene_item_name "]}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-06-24 18:11:36 -04:00
} ) ;
break ;
2024-07-06 23:42:33 -04:00
case "TOGGLE_OBS_VISIBILITY" :
2024-07-19 12:56:41 -04:00
await _obs . ToggleSceneItemVisibility ( action . Data [ "scene_name" ] , action . Data [ "scene_item_name" ] ) ;
2024-07-06 23:42:33 -04:00
break ;
case "SPECIFIC_OBS_VISIBILITY" :
2024-07-19 12:56:41 -04:00
await _obs . UpdateSceneItemVisibility ( action . Data [ "scene_name" ] , action . Data [ "scene_item_name" ] , action . Data [ "obs_visible" ] . ToLower ( ) = = "true" ) ;
2024-07-06 23:42:33 -04:00
break ;
case "SPECIFIC_OBS_INDEX" :
2024-07-19 12:56:41 -04:00
await _obs . UpdateSceneItemIndex ( action . Data [ "scene_name" ] , action . Data [ "scene_item_name" ] , int . Parse ( action . Data [ "obs_index" ] ) ) ;
2024-07-06 23:42:33 -04:00
break ;
case "SLEEP" :
_logger . Debug ( "Sleeping on thread due to redemption for OBS." ) ;
await Task . Delay ( int . Parse ( action . Data [ "sleep" ] ) ) ;
break ;
case "SPECIFIC_TTS_VOICE" :
2024-07-16 00:48:55 -04:00
case "RANDOM_TTS_VOICE" :
string voiceId = string . Empty ;
bool specific = action . Type = = "SPECIFIC_TTS_VOICE" ;
var voicesEnabled = _user . VoicesEnabled . ToList ( ) ;
if ( specific )
voiceId = _user . VoicesAvailable . Keys . First ( id = > _user . VoicesAvailable [ id ] . ToLower ( ) = = action . Data [ "tts_voice" ] . ToLower ( ) ) ;
else
{
if ( ! voicesEnabled . Any ( ) )
{
_logger . Warning ( $"There are no TTS voices enabled [voice pool size: {voicesEnabled.Count}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
return ;
}
if ( voicesEnabled . Count < = 1 )
{
_logger . Warning ( $"There are not enough TTS voices enabled to randomize [voice pool size: {voicesEnabled.Count}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
return ;
}
string? selectedId = null ;
if ( ! _user . VoicesSelected . ContainsKey ( senderId ) )
selectedId = _user . VoicesAvailable . Keys . First ( id = > _user . VoicesAvailable [ id ] = = _user . DefaultTTSVoice ) ;
else
selectedId = _user . VoicesSelected [ senderId ] ;
do
{
var randomVoice = voicesEnabled [ _random . Next ( voicesEnabled . Count ) ] ;
voiceId = _user . VoicesAvailable . Keys . First ( id = > _user . VoicesAvailable [ id ] = = randomVoice ) ;
} while ( voiceId = = selectedId ) ;
}
if ( string . IsNullOrEmpty ( voiceId ) )
2024-07-06 23:42:33 -04:00
{
2024-07-16 00:48:55 -04:00
_logger . Warning ( $"Voice is not valid [voice: {action.Data[" tts_voice "]}][voice pool size: {voicesEnabled.Count}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-07-06 23:42:33 -04:00
return ;
}
var voiceName = _user . VoicesAvailable [ voiceId ] ;
if ( ! _user . VoicesEnabled . Contains ( voiceName ) )
{
2024-07-16 00:48:55 -04:00
_logger . Warning ( $"Voice is not enabled [voice: {action.Data[" tts_voice "]}][voice pool size: {voicesEnabled.Count}][voice id: {voiceId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-07-06 23:42:33 -04:00
return ;
}
2024-07-16 00:48:55 -04:00
if ( _user . VoicesSelected . ContainsKey ( senderId ) )
2024-07-06 23:42:33 -04:00
{
2024-07-16 00:48:55 -04:00
await _hermes . UpdateTTSUser ( senderId , voiceId ) ;
_logger . Debug ( $"Sent request to create chat TTS voice [voice: {voiceName}][chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-07-06 23:42:33 -04:00
}
2024-07-16 00:48:55 -04:00
else
2024-07-06 23:42:33 -04:00
{
2024-07-16 00:48:55 -04:00
await _hermes . CreateTTSUser ( senderId , voiceId ) ;
_logger . Debug ( $"Sent request to update chat TTS voice [voice: {voiceName}][chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-07-06 23:42:33 -04:00
}
break ;
2024-06-24 18:11:36 -04:00
case "AUDIO_FILE" :
2024-07-06 23:42:33 -04:00
if ( ! File . Exists ( action . Data [ "file_path" ] ) )
{
2024-07-16 00:48:55 -04:00
_logger . Warning ( $"Cannot find audio file for Twitch channel point redeem [file: {action.Data[" file_path "]}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-06-24 18:11:36 -04:00
return ;
}
2024-08-04 19:46:10 -04:00
_playback . PlaySound ( action . Data [ "file_path" ] ) ;
2024-07-16 00:48:55 -04:00
_logger . Debug ( $"Played an audio file for channel point redeem [file: {action.Data[" file_path "]}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-06-24 18:11:36 -04:00
break ;
2024-08-11 17:22:37 -04:00
case "NIGHTBOT_PLAY" :
await _nightbot . Play ( ) ;
break ;
case "NIGHTBOT_PAUSE" :
await _nightbot . Pause ( ) ;
break ;
case "NIGHTBOT_SKIP" :
await _nightbot . Skip ( ) ;
break ;
case "NIGHTBOT_CLEAR_PLAYLIST" :
await _nightbot . ClearPlaylist ( ) ;
break ;
case "NIGHTBOT_CLEAR_QUEUE" :
await _nightbot . ClearQueue ( ) ;
break ;
2024-06-24 18:11:36 -04:00
default :
2024-07-16 00:48:55 -04:00
_logger . Warning ( $"Unknown redeemable action has occured. Update needed? [type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-06-24 18:11:36 -04:00
break ;
}
}
catch ( Exception ex )
{
2024-07-16 00:48:55 -04:00
_logger . Error ( ex , $"Failed to execute a redemption action [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]" ) ;
2024-06-24 18:11:36 -04:00
}
}
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 ) ;
}
2024-07-06 23:42:33 -04:00
public void Initialize ( IEnumerable < Redemption > redemptions , IDictionary < string , RedeemableAction > actions )
2024-06-24 18:11:36 -04:00
{
_store . Clear ( ) ;
2024-07-16 00:48:55 -04:00
var ordered = redemptions . Where ( r = > r ! = null ) . OrderBy ( r = > r . Order ) ;
2024-06-24 18:11:36 -04:00
foreach ( var redemption in ordered )
2024-07-06 23:42:33 -04:00
{
2024-07-16 00:48:55 -04:00
if ( redemption . ActionName = = null )
{
_logger . Warning ( "Null value found for the action name of a redemption." ) ;
continue ;
}
2024-07-06 23:42:33 -04:00
try
{
if ( actions . TryGetValue ( redemption . ActionName , out var action ) & & action ! = null )
{
_logger . Debug ( $"Fetched a redemption action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]" ) ;
Add ( redemption . RedemptionId , action ) ;
}
else
_logger . Warning ( $"Could not find redemption action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]" ) ;
}
catch ( Exception e )
{
_logger . Error ( e , $"Failed to add a redemption [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]" ) ;
}
}
2024-06-24 18:11:36 -04:00
_isReady = true ;
2024-07-06 23:42:33 -04:00
_logger . Debug ( "All redemptions added. Redemption Manager is ready." ) ;
2024-06-24 18:11:36 -04:00
}
2024-07-06 23:42:33 -04:00
private string ReplaceContentText ( string content , string username )
{
return content . Replace ( "%USER%" , username )
. Replace ( "\\n" , "\n" ) ;
2024-06-24 18:11:36 -04:00
}
}
}