2024-03-12 14:05:27 -04:00
using System.Runtime.InteropServices ;
using System.Web ;
using CommonSocketLibrary.Abstract ;
using CommonSocketLibrary.Common ;
2024-03-15 08:27:35 -04:00
using HermesSocketLibrary.Socket.Data ;
2024-03-12 14:05:27 -04:00
using Microsoft.Extensions.DependencyInjection ;
using Microsoft.Extensions.Hosting ;
2024-06-16 20:19:31 -04:00
using Serilog ;
2024-03-12 14:05:27 -04:00
using NAudio.Wave.SampleProviders ;
2024-06-16 20:19:31 -04:00
using TwitchChatTTS.Seven ;
2024-03-12 14:05:27 -04:00
using TwitchLib.Client.Events ;
2024-06-24 18:11:36 -04:00
using TwitchChatTTS.Twitch.Redemptions ;
using org.mariuszgromada.math.mxparser ;
2024-03-12 14:05:27 -04:00
namespace TwitchChatTTS
{
public class TTS : IHostedService
{
2024-06-16 20:19:31 -04:00
public const int MAJOR_VERSION = 3 ;
2024-07-06 23:42:33 -04:00
public const int MINOR_VERSION = 6 ;
2024-06-16 20:19:31 -04:00
2024-07-06 23:42:33 -04:00
private readonly User _user ;
private readonly HermesApiClient _hermesApiClient ;
private readonly SevenApiClient _sevenApiClient ;
2024-06-24 18:11:36 -04:00
private readonly RedemptionManager _redemptionManager ;
2024-03-15 08:27:35 -04:00
private readonly Configuration _configuration ;
private readonly TTSPlayer _player ;
private readonly IServiceProvider _serviceProvider ;
2024-06-24 18:11:36 -04:00
private readonly ILogger _logger ;
2024-03-12 14:05:27 -04:00
2024-06-24 18:11:36 -04:00
public TTS (
User user ,
HermesApiClient hermesApiClient ,
SevenApiClient sevenApiClient ,
RedemptionManager redemptionManager ,
Configuration configuration ,
TTSPlayer player ,
IServiceProvider serviceProvider ,
ILogger logger
)
2024-06-16 20:19:31 -04:00
{
2024-07-06 23:42:33 -04:00
_user = user ;
_hermesApiClient = hermesApiClient ;
_sevenApiClient = sevenApiClient ;
2024-06-24 18:11:36 -04:00
_redemptionManager = redemptionManager ;
2024-03-15 08:27:35 -04:00
_configuration = configuration ;
_player = player ;
_serviceProvider = serviceProvider ;
2024-06-24 18:11:36 -04:00
_logger = logger ;
2024-03-12 14:05:27 -04:00
}
2024-06-16 20:19:31 -04:00
public async Task StartAsync ( CancellationToken cancellationToken )
{
2024-03-12 14:05:27 -04:00
Console . Title = "TTS - Twitch Chat" ;
2024-07-06 23:42:33 -04:00
License . iConfirmCommercialUse ( "abcdef" ) ;
2024-06-16 20:19:31 -04:00
2024-07-06 23:42:33 -04:00
if ( string . IsNullOrWhiteSpace ( _configuration . Hermes . Token ) ) {
_logger . Error ( "Hermes API token not set in the configuration file." ) ;
return ;
}
2024-03-15 08:27:35 -04:00
2024-07-06 23:42:33 -04:00
var hermesVersion = await _hermesApiClient . GetTTSVersion ( ) ;
2024-06-16 20:19:31 -04:00
if ( hermesVersion . MajorVersion > TTS . MAJOR_VERSION | | hermesVersion . MajorVersion = = TTS . MAJOR_VERSION & & hermesVersion . MinorVersion > TTS . MINOR_VERSION )
{
_logger . Information ( $"A new update for TTS is avaiable! Version {hermesVersion.MajorVersion}.{hermesVersion.MinorVersion} is available at {hermesVersion.Download}" ) ;
var changes = hermesVersion . Changelog . Split ( "\n" ) ;
if ( changes ! = null & & changes . Any ( ) )
_logger . Information ( "Changelogs:\n - " + string . Join ( "\n - " , changes ) + "\n\n" ) ;
await Task . Delay ( 15 * 1000 ) ;
}
2024-03-15 08:27:35 -04:00
2024-06-16 20:19:31 -04:00
try
{
2024-07-06 23:42:33 -04:00
await FetchUserData ( _user , _hermesApiClient , _sevenApiClient ) ;
2024-06-16 20:19:31 -04:00
}
catch ( Exception ex )
{
_logger . Error ( ex , "Failed to initialize properly." ) ;
await Task . Delay ( 30 * 1000 ) ;
}
2024-03-15 08:27:35 -04:00
2024-07-06 23:42:33 -04:00
var twitchapiclient = await InitializeTwitchApiClient ( _user . TwitchUsername , _user . TwitchUserId . ToString ( ) ) ;
2024-06-16 20:19:31 -04:00
if ( twitchapiclient = = null )
{
await Task . Delay ( 30 * 1000 ) ;
return ;
}
2024-03-15 08:27:35 -04:00
2024-07-06 23:42:33 -04:00
var emoteSet = await _sevenApiClient . FetchChannelEmoteSet ( _user . TwitchUserId . ToString ( ) ) ;
await InitializeEmotes ( _sevenApiClient , emoteSet ) ;
2024-06-16 20:19:31 -04:00
await InitializeHermesWebsocket ( ) ;
await InitializeSevenTv ( emoteSet . Id ) ;
2024-03-12 14:05:27 -04:00
await InitializeObs ( ) ;
2024-06-16 20:19:31 -04:00
AudioPlaybackEngine . Instance . AddOnMixerInputEnded ( ( object? s , SampleProviderEventArgs e ) = >
{
if ( e . SampleProvider = = _player . Playing )
{
_player . Playing = null ;
}
} ) ;
Task . Run ( async ( ) = >
{
while ( true )
{
try
{
if ( cancellationToken . IsCancellationRequested )
{
2024-07-06 23:42:33 -04:00
_logger . Warning ( "TTS Buffer -Cancellation requested." ) ;
2024-06-16 20:19:31 -04:00
return ;
2024-03-12 14:05:27 -04:00
}
2024-06-16 20:19:31 -04:00
var m = _player . ReceiveBuffer ( ) ;
if ( m = = null )
{
await Task . Delay ( 200 ) ;
continue ;
}
string url = $"https://api.streamelements.com/kappa/v2/speech?voice={m.Voice}&text={HttpUtility.UrlEncode(m.Message)}" ;
var sound = new NetworkWavSound ( url ) ;
var provider = new CachedWavProvider ( sound ) ;
var data = AudioPlaybackEngine . Instance . ConvertSound ( provider ) ;
var resampled = new WdlResamplingSampleProvider ( data , AudioPlaybackEngine . Instance . SampleRate ) ;
2024-06-24 18:11:36 -04:00
_logger . Verbose ( "Fetched TTS audio data." ) ;
2024-06-16 20:19:31 -04:00
m . Audio = resampled ;
_player . Ready ( m ) ;
2024-03-12 14:05:27 -04:00
}
2024-06-16 20:19:31 -04:00
catch ( COMException e )
{
2024-06-24 18:11:36 -04:00
_logger . Error ( e , "Failed to send request for TTS [HResult: " + e . HResult + "]" ) ;
2024-06-16 20:19:31 -04:00
}
catch ( Exception e )
{
_logger . Error ( e , "Failed to send request for TTS." ) ;
}
}
} ) ;
2024-03-12 14:05:27 -04:00
2024-06-16 20:19:31 -04:00
Task . Run ( async ( ) = >
{
while ( true )
{
try
{
if ( cancellationToken . IsCancellationRequested )
{
2024-07-06 23:42:33 -04:00
_logger . Warning ( "TTS Queue - Cancellation requested." ) ;
2024-06-16 20:19:31 -04:00
return ;
2024-03-12 14:05:27 -04:00
}
2024-06-16 20:19:31 -04:00
while ( _player . IsEmpty ( ) | | _player . Playing ! = null )
{
await Task . Delay ( 200 ) ;
continue ;
}
var m = _player . ReceiveReady ( ) ;
if ( m = = null )
{
continue ;
}
if ( ! string . IsNullOrWhiteSpace ( m . File ) & & File . Exists ( m . File ) )
{
2024-07-06 23:42:33 -04:00
_logger . Debug ( "Playing audio file via TTS: " + m . File ) ;
2024-06-16 20:19:31 -04:00
AudioPlaybackEngine . Instance . PlaySound ( m . File ) ;
continue ;
}
2024-07-06 23:42:33 -04:00
_logger . Debug ( "Playing message via TTS: " + m . Message ) ;
2024-06-16 20:19:31 -04:00
_player . Playing = m . Audio ;
if ( m . Audio ! = null )
AudioPlaybackEngine . Instance . AddMixerInput ( m . Audio ) ;
2024-03-12 14:05:27 -04:00
}
2024-06-16 20:19:31 -04:00
catch ( Exception e )
{
_logger . Error ( e , "Failed to play a TTS audio message" ) ;
}
}
} ) ;
2024-03-12 14:05:27 -04:00
2024-06-16 20:19:31 -04:00
_logger . Information ( "Twitch websocket client connecting..." ) ;
await twitchapiclient . Connect ( ) ;
2024-03-12 14:05:27 -04:00
}
public async Task StopAsync ( CancellationToken cancellationToken )
{
if ( cancellationToken . IsCancellationRequested )
2024-06-16 20:19:31 -04:00
_logger . Warning ( "Application has stopped due to cancellation token." ) ;
2024-03-12 14:05:27 -04:00
else
2024-06-16 20:19:31 -04:00
_logger . Warning ( "Application has stopped." ) ;
2024-03-15 08:27:35 -04:00
}
2024-06-16 20:19:31 -04:00
private async Task FetchUserData ( User user , HermesApiClient hermes , SevenApiClient seven )
{
var hermesAccount = await hermes . FetchHermesAccountDetails ( ) ;
if ( hermesAccount = = null )
throw new Exception ( "Cannot connect to Hermes. Ensure your token is valid." ) ;
user . HermesUserId = hermesAccount . Id ;
user . HermesUsername = hermesAccount . Username ;
user . TwitchUsername = hermesAccount . Username ;
var twitchBotToken = await hermes . FetchTwitchBotToken ( ) ;
user . TwitchUserId = long . Parse ( twitchBotToken . BroadcasterId ) ;
2024-06-24 18:11:36 -04:00
_logger . Information ( $"Username: {user.TwitchUsername} [id: {user.TwitchUserId}]" ) ;
2024-06-16 20:19:31 -04:00
user . DefaultTTSVoice = await hermes . FetchTTSDefaultVoice ( ) ;
2024-07-06 23:42:33 -04:00
_logger . Information ( "TTS Default Voice: " + user . DefaultTTSVoice ) ;
2024-06-16 20:19:31 -04:00
var wordFilters = await hermes . FetchTTSWordFilters ( ) ;
user . RegexFilters = wordFilters . ToList ( ) ;
_logger . Information ( $"{user.RegexFilters.Count()} TTS word filters." ) ;
2024-03-15 08:27:35 -04:00
2024-06-16 20:19:31 -04:00
var usernameFilters = await hermes . FetchTTSUsernameFilters ( ) ;
user . ChatterFilters = usernameFilters . ToDictionary ( e = > e . Username , e = > e ) ;
_logger . Information ( $"{user.ChatterFilters.Where(f => f.Value.Tag == " blacklisted ").Count()} username(s) have been blocked." ) ;
_logger . Information ( $"{user.ChatterFilters.Where(f => f.Value.Tag == " priority ").Count()} user(s) have been prioritized." ) ;
var voicesSelected = await hermes . FetchTTSChatterSelectedVoices ( ) ;
user . VoicesSelected = voicesSelected . ToDictionary ( s = > s . ChatterId , s = > s . Voice ) ;
_logger . Information ( $"{user.VoicesSelected.Count} TTS voices have been selected for specific chatters." ) ;
var voicesEnabled = await hermes . FetchTTSEnabledVoices ( ) ;
if ( voicesEnabled = = null | | ! voicesEnabled . Any ( ) )
2024-06-24 18:11:36 -04:00
user . VoicesEnabled = new HashSet < string > ( [ "Brian" ] ) ;
2024-06-16 20:19:31 -04:00
else
user . VoicesEnabled = new HashSet < string > ( voicesEnabled . Select ( v = > v ) ) ;
_logger . Information ( $"{user.VoicesEnabled.Count} TTS voices have been enabled." ) ;
var defaultedChatters = voicesSelected . Where ( item = > item . Voice = = null | | ! user . VoicesEnabled . Contains ( item . Voice ) ) ;
2024-06-24 18:11:36 -04:00
if ( defaultedChatters . Any ( ) )
_logger . Information ( $"{defaultedChatters.Count()} chatter(s) will have their TTS voice set to default due to having selected a disabled TTS voice." ) ;
var redemptionActions = await hermes . FetchRedeemableActions ( ) ;
var redemptions = await hermes . FetchRedemptions ( ) ;
2024-07-06 23:42:33 -04:00
_redemptionManager . Initialize ( redemptions , redemptionActions . ToDictionary ( a = > a . Name , a = > a ) ) ;
_logger . Information ( $"Redemption Manager has been initialized with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions." ) ;
2024-06-16 20:19:31 -04:00
}
private async Task InitializeHermesWebsocket ( )
{
try
{
_logger . Information ( "Initializing hermes websocket client." ) ;
2024-06-24 18:11:36 -04:00
var hermesClient = _serviceProvider . GetRequiredKeyedService < SocketClient < WebSocketMessage > > ( "hermes" ) ;
2024-03-15 08:27:35 -04:00
var url = "wss://hermes-ws.goblincaves.com" ;
2024-06-16 20:19:31 -04:00
_logger . Debug ( $"Attempting to connect to {url}" ) ;
2024-03-15 08:27:35 -04:00
await hermesClient . ConnectAsync ( url ) ;
2024-06-16 20:19:31 -04:00
hermesClient . Connected = true ;
await hermesClient . Send ( 1 , new HermesLoginMessage ( )
{
2024-07-06 23:42:33 -04:00
ApiKey = _configuration . Hermes . Token ,
MajorVersion = TTS . MAJOR_VERSION ,
MinorVersion = TTS . MINOR_VERSION ,
2024-03-15 08:27:35 -04:00
} ) ;
}
2024-06-16 20:19:31 -04:00
catch ( Exception )
{
_logger . Warning ( "Connecting to hermes failed. Skipping hermes websockets." ) ;
2024-03-15 08:27:35 -04:00
}
2024-06-16 20:19:31 -04:00
}
2024-03-15 08:27:35 -04:00
2024-06-16 20:19:31 -04:00
private async Task InitializeSevenTv ( string emoteSetId )
{
try
{
_logger . Information ( "Initializing 7tv websocket client." ) ;
2024-03-15 08:27:35 -04:00
var sevenClient = _serviceProvider . GetRequiredKeyedService < SocketClient < WebSocketMessage > > ( "7tv" ) ;
2024-06-16 20:19:31 -04:00
if ( string . IsNullOrWhiteSpace ( emoteSetId ) )
{
_logger . Warning ( "Could not fetch 7tv emotes." ) ;
return ;
}
var url = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*<object_id={emoteSetId}>" ;
_logger . Debug ( $"Attempting to connect to {url}" ) ;
2024-03-15 08:27:35 -04:00
await sevenClient . ConnectAsync ( $"{url}" ) ;
2024-06-16 20:19:31 -04:00
}
catch ( Exception )
{
_logger . Warning ( "Connecting to 7tv failed. Skipping 7tv websockets." ) ;
2024-03-12 14:05:27 -04:00
}
}
2024-06-16 20:19:31 -04:00
private async Task InitializeObs ( )
{
if ( _configuration . Obs = = null | | string . IsNullOrWhiteSpace ( _configuration . Obs . Host ) | | ! _configuration . Obs . Port . HasValue | | _configuration . Obs . Port . Value < 0 )
{
_logger . Warning ( "Lacking OBS connection info. Skipping OBS websockets." ) ;
2024-03-15 08:27:35 -04:00
return ;
}
2024-06-16 20:19:31 -04:00
try
{
2024-03-15 08:27:35 -04:00
var obsClient = _serviceProvider . GetRequiredKeyedService < SocketClient < WebSocketMessage > > ( "obs" ) ;
var url = $"ws://{_configuration.Obs.Host.Trim()}:{_configuration.Obs.Port}" ;
2024-06-16 20:19:31 -04:00
_logger . Debug ( $"Initializing OBS websocket client. Attempting to connect to {url}" ) ;
2024-03-15 08:27:35 -04:00
await obsClient . ConnectAsync ( url ) ;
2024-06-16 20:19:31 -04:00
}
catch ( Exception )
{
_logger . Warning ( "Connecting to obs failed. Skipping obs websockets." ) ;
2024-03-12 14:05:27 -04:00
}
}
2024-06-16 20:19:31 -04:00
private async Task < TwitchApiClient ? > InitializeTwitchApiClient ( string username , string broadcasterId )
{
_logger . Debug ( "Initializing twitch client." ) ;
2024-03-15 08:27:35 -04:00
var twitchapiclient = _serviceProvider . GetRequiredService < TwitchApiClient > ( ) ;
2024-06-16 20:19:31 -04:00
if ( ! await twitchapiclient . Authorize ( broadcasterId ) )
{
_logger . Error ( "Cannot connect to Twitch API." ) ;
return null ;
}
2024-03-12 14:05:27 -04:00
2024-03-15 08:27:35 -04:00
var channels = _configuration . Twitch . Channels ? ? [ username ] ;
2024-06-16 20:19:31 -04:00
_logger . Information ( "Twitch channels: " + string . Join ( ", " , channels ) ) ;
2024-03-15 08:27:35 -04:00
twitchapiclient . InitializeClient ( username , channels ) ;
2024-03-12 14:05:27 -04:00
twitchapiclient . InitializePublisher ( ) ;
2024-03-15 08:27:35 -04:00
var handler = _serviceProvider . GetRequiredService < ChatMessageHandler > ( ) ;
2024-06-16 20:19:31 -04:00
twitchapiclient . AddOnNewMessageReceived ( async ( object? s , OnMessageReceivedArgs e ) = >
{
try
{
var result = await handler . Handle ( e ) ;
if ( result . Status ! = MessageStatus . None | | result . Emotes = = null | | ! result . Emotes . Any ( ) )
return ;
var ws = _serviceProvider . GetRequiredKeyedService < SocketClient < WebSocketMessage > > ( "hermes" ) ;
await ws . Send ( 8 , new EmoteUsageMessage ( )
{
MessageId = e . ChatMessage . Id ,
DateTime = DateTime . UtcNow ,
BroadcasterId = result . BroadcasterId ,
ChatterId = result . ChatterId ,
Emotes = result . Emotes
} ) ;
}
catch ( Exception ex )
{
_logger . Error ( ex , "Unable to send emote usage message." ) ;
}
2024-03-12 14:05:27 -04:00
} ) ;
return twitchapiclient ;
}
2024-06-16 20:19:31 -04:00
2024-07-06 23:42:33 -04:00
private async Task InitializeEmotes ( SevenApiClient sevenapi , EmoteSet channelEmotes )
2024-06-16 20:19:31 -04:00
{
var emotes = _serviceProvider . GetRequiredService < EmoteDatabase > ( ) ;
var globalEmotes = await sevenapi . FetchGlobalSevenEmotes ( ) ;
if ( channelEmotes ! = null & & channelEmotes . Emotes . Any ( ) )
{
_logger . Information ( $"Loaded {channelEmotes.Emotes.Count()} 7tv channel emotes." ) ;
foreach ( var entry in channelEmotes . Emotes )
emotes . Add ( entry . Name , entry . Id ) ;
}
if ( globalEmotes ! = null & & globalEmotes . Any ( ) )
{
_logger . Information ( $"Loaded {globalEmotes.Count()} 7tv global emotes." ) ;
foreach ( var entry in globalEmotes )
emotes . Add ( entry . Name , entry . Id ) ;
}
2024-03-12 14:05:27 -04:00
}
}
}