Compare commits

..

1 Commits

Author SHA1 Message Date
Tom
aa89578297 Fixed TTS using StreamElements. Fixed several issues. 2026-01-03 05:19:33 +00:00
16 changed files with 275 additions and 135 deletions

View File

@@ -229,6 +229,7 @@ namespace TwitchChatTTS.Chat.Commands.Limits
var temp = usage.Uses.Skip(sizeDiff);
var tempSize = usage.Uses.Length - sizeDiff;
usage.Uses = temp.Union(new DateTime[Math.Max(0, Limit.Count - tempSize)]).ToArray();
}
finally
{
_rwls.ExitWriteLock();
@@ -256,7 +257,7 @@ namespace TwitchChatTTS.Chat.Commands.Limits
}
finally
{
_rwls.ExitWriteLock();
_rwls.ExitUpgradeableReadLock();
}
return true;

View File

@@ -1,5 +1,3 @@
using System.Collections.Concurrent;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
@@ -9,109 +7,199 @@ namespace TwitchChatTTS.Chat.Groups
{
private readonly IDictionary<string, Group> _groups;
private readonly IDictionary<long, ICollection<string>> _chatters;
private readonly ReaderWriterLockSlim _rwls;
private readonly ILogger _logger;
public ChatterGroupManager(ILogger logger)
{
_logger = logger;
_groups = new ConcurrentDictionary<string, Group>();
_chatters = new ConcurrentDictionary<long, ICollection<string>>();
_groups = new Dictionary<string, Group>();
_chatters = new Dictionary<long, ICollection<string>>();
_rwls = new ReaderWriterLockSlim();
}
public void Add(Group group)
{
_groups.Add(group.Id, group);
_rwls.EnterWriteLock();
try
{
_groups.Add(group.Id, group);
}
finally
{
_rwls.ExitWriteLock();
}
}
public void Add(long chatterId, string groupId)
{
if (_chatters.TryGetValue(chatterId, out var list))
_rwls.EnterWriteLock();
try
{
if (!list.Contains(groupId))
list.Add(groupId);
if (_chatters.TryGetValue(chatterId, out var list))
{
if (!list.Contains(groupId))
list.Add(groupId);
}
else
_chatters.Add(chatterId, new List<string>() { groupId });
}
finally
{
_rwls.ExitWriteLock();
}
else
_chatters.Add(chatterId, new List<string>() { groupId });
}
public void Add(long chatter, ICollection<string> groupIds)
{
if (_chatters.TryGetValue(chatter, out var list))
_rwls.EnterWriteLock();
try
{
foreach (var groupId in groupIds)
if (!list.Contains(groupId))
list.Add(groupId);
if (_chatters.TryGetValue(chatter, out var list))
{
foreach (var groupId in groupIds)
if (!list.Contains(groupId))
list.Add(groupId);
}
else
_chatters.Add(chatter, groupIds);
}
finally
{
_rwls.ExitWriteLock();
}
else
_chatters.Add(chatter, groupIds);
}
public void Clear()
{
_groups.Clear();
_chatters.Clear();
_rwls.EnterWriteLock();
try
{
_groups.Clear();
_chatters.Clear();
}
finally
{
_rwls.ExitWriteLock();
}
}
public Group? Get(string groupId)
{
if (_groups.TryGetValue(groupId, out var group))
return group;
return null;
_rwls.EnterReadLock();
try
{
if (_groups.TryGetValue(groupId, out var group))
return group;
return null;
}
finally
{
_rwls.ExitReadLock();
}
}
public IEnumerable<string> GetGroupNamesFor(long chatter)
{
if (_chatters.TryGetValue(chatter, out var groups))
return groups.Select(g => _groups.TryGetValue(g, out var group) ? group.Name : null)
.Where(g => g != null)
.Cast<string>();
_rwls.EnterReadLock();
try
{
if (_chatters.TryGetValue(chatter, out var groups))
return groups.Select(g => _groups.TryGetValue(g, out var group) ? group.Name : null)
.Where(g => g != null)
.Cast<string>();
return Array.Empty<string>();
return Array.Empty<string>();
}
finally
{
_rwls.ExitReadLock();
}
}
public int GetPriorityFor(long chatter)
{
if (!_chatters.TryGetValue(chatter, out var groups))
return 0;
_rwls.EnterReadLock();
try
{
if (!_chatters.TryGetValue(chatter, out var groups))
return 0;
return GetPriorityFor(groups);
return GetPriorityFor(groups);
}
finally
{
_rwls.ExitReadLock();
}
}
public int GetPriorityFor(IEnumerable<string> groupIds)
{
var values = groupIds.Select(g => _groups.TryGetValue(g, out var group) ? group : null).Where(g => g != null);
if (values.Any())
return values.Max(g => g!.Priority);
return 0;
_rwls.EnterReadLock();
try
{
var values = groupIds.Select(g => _groups.TryGetValue(g, out var group) ? group : null).Where(g => g != null);
if (values.Any())
return values.Max(g => g!.Priority);
return 0;
}
finally
{
_rwls.ExitReadLock();
}
}
public void Modify(Group group)
{
_groups[group.Id] = group;
_rwls.EnterWriteLock();
try
{
_groups[group.Id] = group;
}
finally
{
_rwls.ExitWriteLock();
}
}
public bool Remove(string groupId)
{
if (_groups.Remove(groupId))
_rwls.EnterWriteLock();
try
{
foreach (var entry in _chatters)
entry.Value.Remove(groupId);
return true;
if (_groups.Remove(groupId))
{
foreach (var entry in _chatters)
entry.Value.Remove(groupId);
return true;
}
return false;
}
finally
{
_rwls.ExitReadLock();
}
return false;
}
public bool Remove(long chatterId, string groupId)
{
if (_chatters.TryGetValue(chatterId, out var groups))
_rwls.EnterWriteLock();
try
{
groups.Remove(groupId);
_logger.Debug($"Removed chatter from group [chatter id: {chatterId}][group name: {_groups[groupId].Name}][group id: {groupId}]");
return true;
if (_chatters.TryGetValue(chatterId, out var groups))
{
groups.Remove(groupId);
_logger.Debug($"Removed chatter from group [chatter id: {chatterId}][group name: {_groups[groupId].Name}][group id: {groupId}]");
return true;
}
_logger.Debug($"Failed to remove chatter from group [chatter id: {chatterId}][group name: {_groups[groupId].Name}][group id: {groupId}]");
return false;
}
finally
{
_rwls.ExitReadLock();
}
_logger.Debug($"Failed to remove chatter from group [chatter id: {chatterId}][group name: {_groups[groupId].Name}][group id: {groupId}]");
return false;
}
}
}

View File

@@ -5,33 +5,51 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
{
public class GroupPermissionManager : IGroupPermissionManager
{
private PermissionNode _root;
private ILogger _logger;
private readonly PermissionNode _root;
private readonly ILogger _logger;
private readonly ReaderWriterLockSlim _rwls;
public GroupPermissionManager(ILogger logger)
{
_logger = logger;
_root = new PermissionNode(string.Empty, null, null);
_rwls = new ReaderWriterLockSlim();
}
public bool? CheckIfAllowed(string path)
{
var res = Get(path)!.Allow;
_logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"}");
return res;
_rwls.EnterReadLock();
try
{
var res = Get(path)!.Allow;
_logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"}");
return res;
}
finally
{
_rwls.ExitReadLock();
}
}
public bool? CheckIfDirectAllowed(string path)
{
var node = Get(path, nullIfMissing: true);
if (node == null)
return null;
var res = node.DirectAllow;
_logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"} [direct]");
return res;
_rwls.EnterReadLock();
try
{
var node = Get(path, nullIfMissing: true);
if (node == null)
return null;
var res = node.DirectAllow;
_logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"} [direct]");
return res;
}
finally
{
_rwls.ExitReadLock();
}
}
public bool? CheckIfAllowed(IEnumerable<string> groups, string path)
@@ -64,31 +82,63 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
public void Clear()
{
_root.Clear();
_rwls.EnterWriteLock();
try
{
_root.Clear();
}
finally
{
_rwls.ExitWriteLock();
}
}
public bool Remove(string path)
{
var node = Get(path);
if (node == null || node.Parent == null)
return false;
var parts = path.Split('.');
var last = parts.Last();
if (parts.Length > 1 && parts[parts.Length - 1] == node.Parent.Name || parts.Length == 1 && node.Parent.Name == null)
_rwls.EnterUpgradeableReadLock();
try
{
node.Parent.Remove(last);
_logger.Debug($"Permission Node REMOVE priv {path}");
return true;
var node = Get(path);
if (node == null || node.Parent == null)
return false;
_rwls.EnterWriteLock();
try
{
var parts = path.Split('.');
var last = parts.Last();
if (parts.Length > 1 && parts[parts.Length - 1] == node.Parent.Name || parts.Length == 1 && node.Parent.Name == null)
{
node.Parent.Remove(last);
_logger.Debug($"Permission Node REMOVE priv {path}");
return true;
}
return false;
}
finally
{
_rwls.ExitWriteLock();
}
}
finally
{
_rwls.ExitUpgradeableReadLock();
}
return false;
}
public void Set(string path, bool? allow)
{
var node = Get(path, true);
node!.Allow = allow;
_logger.Debug($"Permission Node ADD {path} = {allow?.ToString() ?? "null"}");
_rwls.EnterWriteLock();
try
{
var node = Get(path, true);
node!.Allow = allow;
_logger.Debug($"Permission Node ADD {path} = {allow?.ToString() ?? "null"}");
}
finally
{
_rwls.ExitWriteLock();
}
}
private PermissionNode? Get(string path, bool edit = false, bool nullIfMissing = false)

View File

@@ -62,15 +62,15 @@ namespace TwitchChatTTS.Chat.Messaging
var emoteUsage = GetEmoteUsage(fragments);
var tasks = new List<Task>();
if (_obs.Streaming && !_user.Slave)
if ((!_obs.Connected || _obs.Streaming) && !_user.Slave)
{
if (emoteUsage.NewEmotes.Any())
tasks.Add(_hermes.SendEmoteDetails(emoteUsage.NewEmotes));
if (emoteUsage.EmotesUsed.Any() && messageId != null && chatterId != null)
tasks.Add(_hermes.SendEmoteUsage(messageId, chatterId.Value, emoteUsage.EmotesUsed));
if (!string.IsNullOrEmpty(chatterLogin) && chatterId != null && !_user.Chatters.Contains(chatterId.Value))
if (chatterId.HasValue && !_user.Chatters.Contains(chatterId.Value))
{
tasks.Add(_hermes.SendChatterDetails(chatterId.Value, chatterLogin));
tasks.Add(_hermes.SendChatterDetails(chatterId.Value, chatterLogin!));
_user.Chatters.Add(chatterId.Value);
}
}

View File

@@ -33,8 +33,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (message.AnotherClient)
{
if (client.LoggedIn)
_logger.Warning($"Another client has connected to the same account via {(message.WebLogin ? "web login" : "application")}.");
_logger.Warning($"Another client has connected to the same account via {(message.WebLogin ? "web login" : "application")}.");
return;
}
if (client.LoggedIn)
@@ -51,6 +50,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
_user.TwitchUsername = message.UserName;
_user.TwitchUserId = long.Parse(message.ProviderAccountId);
_user.OwnerId = message.OwnerId;
_user.StreamElementsOverlayKey = message.StreamElementsOverlayKey;
_user.DefaultTTSVoice = message.DefaultTTSVoice;
_user.VoicesAvailable = new ConcurrentDictionary<string, string>(message.TTSVoicesAvailable);
_user.VoicesEnabled = new HashSet<string>(message.EnabledTTSVoices);
@@ -58,7 +58,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
_user.NightbotConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "nightbot") ?? message.Connections.FirstOrDefault(c => c.Type == "nightbot");
if (_user.TwitchConnection != null)
{
_logger.Information("Twitch connection: " + _user.TwitchConnection.Name + " / " + _user.TwitchConnection.AccessToken);
_logger.Debug("Twitch connection: " + _user.TwitchConnection.Name + " / " + _user.TwitchConnection.AccessToken);
}
var filters = message.WordFilters.Where(f => f.Search != null && f.Replace != null).ToList();
@@ -101,8 +101,9 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
}
_logger.Information("TTS is now ready.");
_bus.Send(this, "tts_connected", _user);
client.Ready = true;
_bus.Send(this, "tts_connected", _user);
}
}
}

View File

@@ -82,35 +82,36 @@ namespace TwitchChatTTS.Hermes.Socket
public override async Task Connect()
{
_rwls.EnterWriteLock();
_rwls.EnterReadLock();
try
{
if (Connected)
return;
_logger.Debug($"Attempting to connect to {URL}");
await ConnectAsync(URL);
}
finally
{
_rwls.ExitWriteLock();
_rwls.ExitReadLock();
}
_logger.Debug($"Attempting to connect to {URL}");
await ConnectAsync(URL);
}
private async Task Disconnect()
{
_rwls.EnterWriteLock();
_rwls.EnterReadLock();
try
{
if (!Connected)
return;
await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed"));
}
finally
{
_rwls.ExitWriteLock();
_rwls.ExitReadLock();
}
await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed"));
}
public async Task CreateTTSVoice(string voiceName)
@@ -437,13 +438,13 @@ namespace TwitchChatTTS.Hermes.Socket
_logger.Warning("Tom to Speech websocket client is not connected. Not sending a message.");
return;
}
await base.Send(opcode, message);
}
finally
{
_rwls.ExitReadLock();
}
await base.Send(opcode, message);
}
}
}

View File

@@ -1,4 +1,3 @@
using System.Text.Json;
using Serilog;
using TwitchChatTTS.Chat.Groups;
@@ -8,13 +7,11 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
{
public string Name => "delete_group";
private readonly IChatterGroupManager _groups;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public DeleteGroupAck(IChatterGroupManager groups, JsonSerializerOptions options, ILogger logger)
public DeleteGroupAck(IChatterGroupManager groups, ILogger logger)
{
_groups = groups;
_options = options;
_logger = logger;
}

View File

@@ -35,7 +35,13 @@ public class SevenApiClient
{
_logger.Debug($"Fetching 7tv information using Twitch Id [twitch id: {twitchId}]");
var details = await _web.GetJson<UserDetails>($"{API_URL}/users/twitch/" + twitchId);
return details?.EmoteSet;
_logger.Information($"Fetched 7tv emotes [count: {details?.EmoteSet.EmoteCount ?? -1}]");
if (details?.EmoteSet == null)
{
_logger.Warning("Could not find 7tv emotes linked to your Twitch account.");
return null;
}
return details.EmoteSet;
}
catch (JsonException e)
{

10
TTS.cs
View File

@@ -22,8 +22,8 @@ namespace TwitchChatTTS
public class TTS : IHostedService
{
public const int MAJOR_VERSION = 4;
public const int MINOR_VERSION = 8;
public const int PATCH_VERSION = 2;
public const int MINOR_VERSION = 9;
public const int PATCH_VERSION = 3;
private readonly User _user;
private readonly HermesApiClient _hermesApiClient;
@@ -84,7 +84,7 @@ namespace TwitchChatTTS
if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token))
{
_logger.Error("Tom to Speech API token not set in the yml file.");
_logger.Error("Tom to Speech API token not set in the configuration file.");
return;
}
@@ -96,7 +96,7 @@ namespace TwitchChatTTS
_logger.Error("Failed to fetch latest TTS version. Something went wrong.");
return;
}
if (hermesVersion.MajorVersion > TTS.MAJOR_VERSION || hermesVersion.MajorVersion == TTS.MAJOR_VERSION && (hermesVersion.MinorVersion > TTS.MINOR_VERSION || hermesVersion.MinorVersion == TTS.MINOR_VERSION && (hermesVersion.PatchVersion == null || hermesVersion.PatchVersion > TTS.PATCH_VERSION)))
if (hermesVersion.MajorVersion > MAJOR_VERSION || hermesVersion.MajorVersion == MAJOR_VERSION && (hermesVersion.MinorVersion > MINOR_VERSION || hermesVersion.MinorVersion == MINOR_VERSION && (hermesVersion.PatchVersion == null || hermesVersion.PatchVersion > PATCH_VERSION)))
{
_logger.Information($"A new update for TTS is avaiable! Version {hermesVersion.MajorVersion}.{hermesVersion.MinorVersion}.{hermesVersion.PatchVersion} is available at {hermesVersion.Download}");
var changes = hermesVersion.Changelog.Split("\n");
@@ -111,9 +111,9 @@ namespace TwitchChatTTS
}
var disposables = new List<IDisposable>();
var connected = _bus.GetTopic("tts_connected");
// 7tv
var connected = _bus.GetTopic("tts_connected");
disposables.Add(connected.FirstAsync().Subscribe(async (data) =>
{
if (data.Value is not User user)

View File

@@ -11,19 +11,23 @@ namespace TwitchChatTTS
{
public class TTSListening : IHostedService
{
private const string TTS_API_URL = "https://api.streamelements.com/kappa/v2/speech";
private readonly AudioPlaybackEngine _playback;
private readonly TTSPlayer _player;
private readonly TTSConsumer _consumer;
private readonly IDisposable _subscription;
private readonly User _user;
private readonly ILogger _logger;
public TTSListening(AudioPlaybackEngine playback, TTSPlayer player, TTSPublisher publisher, TTSConsumer consumer, ILogger logger)
public TTSListening(AudioPlaybackEngine playback, TTSPlayer player, TTSPublisher publisher, TTSConsumer consumer, User user, ILogger logger)
{
_playback = playback;
_player = player;
_consumer = consumer;
_subscription = publisher.Subscribe(consumer);
_user = user;
_logger = logger;
}
@@ -115,7 +119,7 @@ namespace TwitchChatTTS
try
{
string url = $"https://api.streamelements.com/kappa/v2/speech?voice={message.Voice}&text={HttpUtility.UrlEncode(message.Message.Trim())}";
string url = $"{TTS_API_URL}?key={_user.StreamElementsOverlayKey}&voice={message.Voice}&text={HttpUtility.UrlEncode(message.Message.Trim())}";
var nws = new NetworkWavSound(url);
var provider = new CachedWavProvider(nws);
var data = _playback.ConvertSound(provider);

View File

@@ -110,37 +110,29 @@ namespace TwitchChatTTS.Twitch.Redemptions
private void Add(string twitchRedemptionId, string redemptionId)
{
_rwls.EnterWriteLock();
try
if (!_redeems.TryGetValue(twitchRedemptionId, out var redeems))
_redeems.Add(twitchRedemptionId, redeems = new List<string>());
var item = _redemptions.TryGetValue(redemptionId, out var r) ? r : null;
if (item == null)
return;
var redemptions = redeems.Select(r => _redemptions.TryGetValue(r, out var rr) ? rr : null);
bool added = false;
for (int i = 0; i < redeems.Count; i++)
{
if (!_redeems.TryGetValue(twitchRedemptionId, out var redeems))
_redeems.Add(twitchRedemptionId, redeems = new List<string>());
var item = _redemptions.TryGetValue(redemptionId, out var r) ? r : null;
if (item == null)
return;
var redemptions = redeems.Select(r => _redemptions.TryGetValue(r, out var rr) ? rr : null);
bool added = false;
for (int i = 0; i < redeems.Count; i++)
if (redeems[i] != null && _redemptions.TryGetValue(redeems[i], out var rr))
{
if (redeems[i] != null && _redemptions.TryGetValue(redeems[i], out var rr))
if (item.Order > rr.Order)
{
if (item.Order > rr.Order)
{
redeems.Insert(i, redemptionId);
added = true;
break;
}
redeems.Insert(i, redemptionId);
added = true;
break;
}
}
if (!added)
redeems.Add(redemptionId);
}
finally
{
_rwls.ExitWriteLock();
}
if (!added)
redeems.Add(redemptionId);
_logger.Debug($"Added redemption action [redemption id: {redemptionId}][twitch redemption id: {twitchRedemptionId}]");
}

View File

@@ -120,8 +120,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
private int GetTotalBits(TwitchChatFragment[] fragments)
{
return fragments.Where(f => f.Type == "cheermote" && f.Cheermote != null)
.Select(f => f.Cheermote!.Bits)
.Sum();
.Sum(f => f.Cheermote!.Bits);
}
private string GetPermissionPath(string? customRewardId, int bits)

View File

@@ -142,14 +142,14 @@ namespace TwitchChatTTS.Twitch.Socket
_logger.Warning($"Twitch client has been identified, but isn't main or backup [client: {client.UID}][main: {_identified.UID}][backup: {_backup?.UID}]");
clientDisconnect = true;
}
if (clientDisconnect)
client.DisconnectAsync(new SocketDisconnectionEventArgs("Closed", "No need for a tertiary client.")).Wait();
}
finally
{
_mutex.ReleaseMutex();
}
if (clientDisconnect)
await client.DisconnectAsync(new SocketDisconnectionEventArgs("Closed", "No need for a tertiary client."));
}
}
}

View File

@@ -183,7 +183,7 @@ namespace TwitchChatTTS.Twitch.Socket
while (current < total)
{
var size = Encoding.UTF8.GetBytes(content.Substring(current), array);
await _socket!.SendAsync(array, WebSocketMessageType.Text, current + size >= total, _cts!.Token);
await _socket!.SendAsync(array, WebSocketMessageType.Text, current + size >= total, CancellationToken.None);
current += size;
}
_logger.Debug("Twitch TX #" + type + ": " + content);

View File

@@ -15,6 +15,7 @@ namespace TwitchChatTTS
public required string TwitchUsername { get; set; }
public required string SevenEmoteSetId { get; set; }
public long? OwnerId { get; set; }
public required string StreamElementsOverlayKey { get; set; }
public Connection? TwitchConnection { get; set; }
public Connection? NightbotConnection { get; set; }

View File

@@ -178,7 +178,7 @@ namespace TwitchChatTTS.Veadotube
while (current < total)
{
var size = Encoding.UTF8.GetBytes(content.Substring(current), array);
await _socket.SendAsync(array, WebSocketMessageType.Text, current + size >= total, _cts!.Token);
await _socket.SendAsync(array, WebSocketMessageType.Text, current + size >= total, CancellationToken.None);
current += size;
}
_logger.Debug($"Veado TX [message type: {typeof(T).Name}]: " + content);