Fixed 7tv & Twitch reconnection. Added adbreak, follow, subscription handlers for Twitch. Added multi-chat support. Added support to unsubscribe from Twitch event subs.

This commit is contained in:
Tom 2024-08-06 19:29:29 +00:00
parent 75fcb8e0f8
commit 95d879f511
60 changed files with 1063 additions and 671 deletions

View File

@ -1,310 +0,0 @@
// using System.Text.RegularExpressions;
// using TwitchLib.Client.Events;
// using Serilog;
// using TwitchChatTTS;
// using TwitchChatTTS.Chat.Commands;
// using TwitchChatTTS.Hermes.Socket;
// using TwitchChatTTS.Chat.Groups.Permissions;
// using TwitchChatTTS.Chat.Groups;
// using TwitchChatTTS.Chat.Emotes;
// using Microsoft.Extensions.DependencyInjection;
// using CommonSocketLibrary.Common;
// using CommonSocketLibrary.Abstract;
// using TwitchChatTTS.OBS.Socket;
// public class ChatMessageHandler
// {
// private readonly User _user;
// private readonly TTSPlayer _player;
// private readonly CommandManager _commands;
// private readonly IGroupPermissionManager _permissionManager;
// private readonly IChatterGroupManager _chatterGroupManager;
// private readonly IEmoteDatabase _emotes;
// private readonly OBSSocketClient _obs;
// private readonly HermesSocketClient _hermes;
// private readonly Configuration _configuration;
// private readonly ILogger _logger;
// private Regex _sfxRegex;
// private HashSet<long> _chatters;
// public HashSet<long> Chatters { get => _chatters; set => _chatters = value; }
// public ChatMessageHandler(
// User user,
// TTSPlayer player,
// CommandManager commands,
// IGroupPermissionManager permissionManager,
// IChatterGroupManager chatterGroupManager,
// IEmoteDatabase emotes,
// [FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
// [FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
// Configuration configuration,
// ILogger logger
// )
// {
// _user = user;
// _player = player;
// _commands = commands;
// _permissionManager = permissionManager;
// _chatterGroupManager = chatterGroupManager;
// _emotes = emotes;
// _obs = (obs as OBSSocketClient)!;
// _hermes = (hermes as HermesSocketClient)!;
// _configuration = configuration;
// _logger = logger;
// _chatters = new HashSet<long>();
// _sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)");
// }
// public async Task<MessageResult> Handle(OnMessageReceivedArgs e)
// {
// var m = e.ChatMessage;
// if (_hermes.Connected && !_hermes.Ready)
// {
// _logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {m.Id}]");
// return new MessageResult(MessageStatus.NotReady, -1, -1);
// }
// if (_configuration.Twitch?.TtsWhenOffline != true && !_obs.Streaming)
// {
// _logger.Debug($"OBS is not streaming. Ignoring chat messages [message id: {m.Id}]");
// return new MessageResult(MessageStatus.NotReady, -1, -1);
// }
// var msg = e.ChatMessage.Message;
// var chatterId = long.Parse(m.UserId);
// var tasks = new List<Task>();
// var checks = new bool[] { true, m.IsSubscriber, m.IsVip, m.IsModerator, m.IsBroadcaster };
// var defaultGroups = new string[] { "everyone", "subscribers", "vip", "moderators", "broadcaster" };
// var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId);
// var groups = defaultGroups.Where((e, i) => checks[i]).Union(customGroups);
// try
// {
// var commandResult = await _commands.Execute(msg, m, groups);
// if (commandResult != ChatCommandResult.Unknown)
// return new MessageResult(MessageStatus.Command, -1, -1);
// }
// catch (Exception ex)
// {
// _logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {m.Username}][chatter id: {m.UserId}][message id: {m.Id}]");
// }
// var permissionPath = "tts.chat.messages.read";
// if (!string.IsNullOrWhiteSpace(m.CustomRewardId))
// permissionPath = "tts.chat.redemptions.read";
// var permission = chatterId == _user.OwnerId ? true : _permissionManager.CheckIfAllowed(groups, permissionPath);
// if (permission != true)
// {
// _logger.Debug($"Blocked message by {m.Username}: {msg}");
// return new MessageResult(MessageStatus.Blocked, -1, -1);
// }
// if (_obs.Streaming && !_chatters.Contains(chatterId))
// {
// tasks.Add(_hermes.SendChatterDetails(chatterId, m.Username));
// _chatters.Add(chatterId);
// }
// // Filter highly repetitive words (like emotes) from the message.
// int totalEmoteUsed = 0;
// var emotesUsed = new HashSet<string>();
// var words = msg.Split(' ');
// var wordCounter = new Dictionary<string, int>();
// string filteredMsg = string.Empty;
// var newEmotes = new Dictionary<string, string>();
// foreach (var w in words)
// {
// if (wordCounter.ContainsKey(w))
// wordCounter[w]++;
// else
// wordCounter.Add(w, 1);
// var emoteId = _emotes.Get(w);
// if (emoteId == null)
// {
// emoteId = m.EmoteSet.Emotes.FirstOrDefault(e => e.Name == w)?.Id;
// if (emoteId != null)
// {
// newEmotes.Add(emoteId, w);
// _emotes.Add(w, emoteId);
// }
// }
// if (emoteId != null)
// {
// emotesUsed.Add(emoteId);
// totalEmoteUsed++;
// }
// if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5))
// filteredMsg += w + " ";
// }
// if (_obs.Streaming && newEmotes.Any())
// tasks.Add(_hermes.SendEmoteDetails(newEmotes));
// msg = filteredMsg;
// // Replace filtered words.
// if (_user.RegexFilters != null)
// {
// foreach (var wf in _user.RegexFilters)
// {
// if (wf.Search == null || wf.Replace == null)
// continue;
// if (wf.IsRegex)
// {
// try
// {
// var regex = new Regex(wf.Search);
// msg = regex.Replace(msg, wf.Replace);
// continue;
// }
// catch (Exception)
// {
// wf.IsRegex = false;
// }
// }
// msg = msg.Replace(wf.Search, wf.Replace);
// }
// }
// // Determine the priority of this message
// int priority = _chatterGroupManager.GetPriorityFor(groups) + m.SubscribedMonthCount * (m.IsSubscriber ? 10 : 5);
// // Determine voice selected.
// string voiceSelected = _user.DefaultTTSVoice;
// if (_user.VoicesSelected?.ContainsKey(chatterId) == true)
// {
// var voiceId = _user.VoicesSelected[chatterId];
// if (_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null)
// {
// if (_user.VoicesEnabled.Contains(voiceName) || chatterId == _user.OwnerId || m.IsStaff)
// {
// voiceSelected = voiceName;
// }
// }
// }
// // Determine additional voices used
// var matches = _user.WordFilterRegex?.Matches(msg).ToArray();
// if (matches == null || matches.FirstOrDefault() == null || matches.First().Index < 0)
// {
// HandlePartialMessage(priority, voiceSelected, msg.Trim(), e);
// return new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed);
// }
// HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.First().Index).Trim(), e);
// foreach (Match match in matches)
// {
// var message = match.Groups[2].ToString();
// if (string.IsNullOrWhiteSpace(message))
// continue;
// var voice = match.Groups[1].ToString();
// voice = voice[0].ToString().ToUpper() + voice.Substring(1).ToLower();
// HandlePartialMessage(priority, voice, message.Trim(), e);
// }
// if (tasks.Any())
// await Task.WhenAll(tasks);
// return new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed);
// }
// private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e)
// {
// if (string.IsNullOrWhiteSpace(message))
// return;
// var m = e.ChatMessage;
// var parts = _sfxRegex.Split(message);
// var badgesString = string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value));
// if (parts.Length == 1)
// {
// _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; Reward Id: {m.CustomRewardId}; {badgesString}");
// _player.Add(new TTSMessage()
// {
// Voice = voice,
// Message = message,
// Timestamp = DateTime.UtcNow,
// Username = m.Username,
// //Bits = m.Bits,
// Badges = e.Badges,
// Priority = priority
// });
// return;
// }
// var sfxMatches = _sfxRegex.Matches(message);
// var sfxStart = sfxMatches.FirstOrDefault()?.Index ?? message.Length;
// for (var i = 0; i < sfxMatches.Count; i++)
// {
// var sfxMatch = sfxMatches[i];
// var sfxName = sfxMatch.Groups[1]?.ToString()?.ToLower();
// if (!File.Exists("sfx/" + sfxName + ".mp3"))
// {
// parts[i * 2 + 2] = parts[i * 2] + " (" + parts[i * 2 + 1] + ")" + parts[i * 2 + 2];
// continue;
// }
// if (!string.IsNullOrWhiteSpace(parts[i * 2]))
// {
// _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {badgesString}");
// _player.Add(new TTSMessage()
// {
// Voice = voice,
// Message = parts[i * 2],
// Moderator = m.IsModerator,
// Timestamp = DateTime.UtcNow,
// Username = m.Username,
// Bits = m.Bits,
// Badges = m.Badges,
// Priority = priority
// });
// }
// _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}");
// _player.Add(new TTSMessage()
// {
// Voice = voice,
// Message = sfxName,
// File = $"sfx/{sfxName}.mp3",
// Moderator = m.IsModerator,
// Timestamp = DateTime.UtcNow,
// Username = m.Username,
// Bits = m.Bits,
// Badges = m.Badges,
// Priority = priority
// });
// }
// if (!string.IsNullOrWhiteSpace(parts.Last()))
// {
// _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}");
// _player.Add(new TTSMessage()
// {
// Voice = voice,
// Message = parts.Last(),
// Moderator = m.IsModerator,
// Timestamp = DateTime.UtcNow,
// Username = m.Username,
// Bits = m.Bits,
// Badges = m.Badges,
// Priority = priority
// });
// }
// }
// }

View File

@ -13,6 +13,6 @@ namespace TwitchChatTTS.Chat.Commands
public interface IChatPartialCommand
{
bool AcceptCustomPermission { get; }
Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client);
Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes);
}
}

View File

@ -7,6 +7,7 @@ namespace TwitchChatTTS.Chat.Commands
Success = 2,
Permission = 3,
Syntax = 4,
Fail = 5
Fail = 5,
OtherRoom = 6,
}
}

View File

@ -50,7 +50,8 @@ namespace TwitchChatTTS.Chat.Commands
return this;
}
public ICommandBuilder AddAlias(string alias, string child) {
public ICommandBuilder AddAlias(string alias, string child)
{
if (_current == _root)
throw new Exception("Cannot add aliases without a command name.");
if (_current.Children == null || !_current.Children.Any())
@ -327,7 +328,8 @@ namespace TwitchChatTTS.Chat.Commands
Permissions = Permissions.Union([path]).ToArray();
}
public CommandNode AddAlias(string alias, string child) {
public CommandNode AddAlias(string alias, string child)
{
var target = _children.FirstOrDefault(c => c.Parameter.Name == child);
if (target == null)
throw new Exception($"Cannot find child parameter [parameter: {child}][alias: {alias}]");
@ -339,6 +341,8 @@ namespace TwitchChatTTS.Chat.Commands
var clone = target.MemberwiseClone() as CommandNode;
var node = new CommandNode(new StaticParameter(alias, alias, target.Parameter.Optional));
node._children = target._children;
node.Permissions = target.Permissions;
node.Command = target.Command;
_children.Add(node);
return this;
}

View File

@ -0,0 +1,41 @@
using Serilog;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
{
public class CommandFactory : ICommandFactory
{
private readonly IEnumerable<IChatCommand> _commands;
private readonly ICommandBuilder _builder;
private readonly ILogger _logger;
public CommandFactory(
IEnumerable<IChatCommand> commands,
ICommandBuilder builder,
ILogger logger
)
{
_commands = commands;
_builder = builder;
_logger = logger;
}
public ICommandSelector Build()
{
foreach (var command in _commands)
{
try
{
_logger.Debug($"Creating command tree for '{command.Name}'.");
command.Build(_builder);
}
catch (Exception e)
{
_logger.Error(e, $"Failed to properly load a chat command [command name: {command.Name}]");
}
}
return _builder.Build();
}
}
}

View File

@ -10,37 +10,30 @@ using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
{
public class CommandManager
public class CommandManager : ICommandManager
{
private readonly User _user;
private readonly ICommandSelector _commandSelector;
private ICommandSelector _commandSelector;
private readonly HermesSocketClient _hermes;
//private readonly TwitchWebsocketClient _twitch;
private readonly IGroupPermissionManager _permissionManager;
private readonly ILogger _logger;
private string CommandStartSign { get; } = "!";
public CommandManager(
IEnumerable<IChatCommand> commands,
ICommandBuilder commandBuilder,
User user,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> socketClient,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
//[FromKeyedServices("twitch")] SocketClient<TwitchWebsocketMessage> twitch,
IGroupPermissionManager permissionManager,
ILogger logger
)
{
_user = user;
_hermes = (socketClient as HermesSocketClient)!;
_hermes = (hermes as HermesSocketClient)!;
//_twitch = (twitch as TwitchWebsocketClient)!;
_permissionManager = permissionManager;
_logger = logger;
foreach (var command in commands)
{
_logger.Debug($"Creating command tree for '{command.Name}'.");
command.Build(commandBuilder);
}
_commandSelector = commandBuilder.Build();
}
@ -54,9 +47,13 @@ namespace TwitchChatTTS.Chat.Commands
if (!arg.StartsWith(CommandStartSign))
return ChatCommandResult.Unknown;
if (message.BroadcasterUserId != _user.TwitchUserId.ToString())
return ChatCommandResult.OtherRoom;
string[] parts = Regex.Matches(arg.Substring(CommandStartSign.Length), "(?<match>[^\"\\n\\s]+|\"[^\"\\n]*\")")
.Cast<Match>()
.Select(m => m.Groups["match"].Value)
.Where(m => !string.IsNullOrEmpty(m))
.Select(m => m.StartsWith('"') && m.EndsWith('"') ? m.Substring(1, m.Length - 2) : m)
.ToArray();
string[] args = parts.ToArray();
@ -65,7 +62,7 @@ namespace TwitchChatTTS.Chat.Commands
CommandSelectorResult selectorResult = _commandSelector.GetBestMatch(args, message);
if (selectorResult.Command == null)
{
_logger.Warning($"Could not match '{arg}' to any command.");
_logger.Warning($"Could not match '{arg}' to any command [chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
return ChatCommandResult.Missing;
}
@ -111,10 +108,24 @@ namespace TwitchChatTTS.Chat.Commands
return ChatCommandResult.Success;
}
public void Update(ICommandFactory factory)
{
_commandSelector = factory.Build();
}
private bool CanExecute(long chatterId, IEnumerable<string> groups, string path, string[]? additionalPaths)
{
_logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]{(additionalPaths != null ? "[paths: " + string.Join('|', additionalPaths) + "]" : string.Empty)}");
return _permissionManager.CheckIfAllowed(groups, path) != false && (additionalPaths == null || additionalPaths.All(p => _permissionManager.CheckIfAllowed(groups, p) != false));
if (_permissionManager.CheckIfAllowed(groups, path) != false)
{
if (additionalPaths == null)
return true;
// All direct allow must not be false and at least one of them must be true.
if (additionalPaths.All(p => _permissionManager.CheckIfDirectAllowed(groups, p) != false) && additionalPaths.Any(p => _permissionManager.CheckIfDirectAllowed(groups, p) == true))
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,9 @@
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
{
public interface ICommandFactory
{
ICommandSelector Build();
}
}

View File

@ -0,0 +1,9 @@
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Chat.Commands
{
public interface ICommandManager {
Task<ChatCommandResult> Execute(string arg, ChannelChatMessage message, IEnumerable<string> groups);
void Update(ICommandFactory factory);
}
}

View File

@ -5,6 +5,7 @@ using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.Twitch.Socket;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
@ -71,7 +72,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
string sceneName = values["sceneName"];
string sourceName = values["sourceName"];
@ -97,7 +98,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
string sceneName = values["sceneName"];
string sourceName = values["sourceName"];
@ -133,7 +134,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
string sceneName = values["sceneName"];
string sourceName = values["sourceName"];

View File

@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket;
using TwitchChatTTS.Twitch.Socket;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
@ -44,9 +45,9 @@ namespace TwitchChatTTS.Chat.Commands
{
public bool AcceptCustomPermission { get => true; }
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
await client.FetchEnabledTTSVoices();
await hermes.FetchEnabledTTSVoices();
}
}
@ -54,9 +55,9 @@ namespace TwitchChatTTS.Chat.Commands
{
public bool AcceptCustomPermission { get => true; }
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
await client.FetchTTSWordFilters();
await hermes.FetchTTSWordFilters();
}
}
@ -64,9 +65,9 @@ namespace TwitchChatTTS.Chat.Commands
{
public bool AcceptCustomPermission { get => true; }
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
await client.FetchTTSChatterVoices();
await hermes.FetchTTSChatterVoices();
}
}
@ -74,9 +75,9 @@ namespace TwitchChatTTS.Chat.Commands
{
public bool AcceptCustomPermission { get => true; }
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
await client.FetchDefaultTTSVoice();
await hermes.FetchDefaultTTSVoice();
}
}
@ -84,9 +85,9 @@ namespace TwitchChatTTS.Chat.Commands
{
public bool AcceptCustomPermission { get => true; }
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
await client.FetchRedemptions();
await hermes.FetchRedemptions();
}
}
@ -97,12 +98,13 @@ namespace TwitchChatTTS.Chat.Commands
public bool AcceptCustomPermission { get => true; }
public RefreshObs(OBSSocketClient obsManager, ILogger logger) {
public RefreshObs(OBSSocketClient obsManager, ILogger logger)
{
_obsManager = obsManager;
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
_obsManager.ClearCache();
_logger.Information("Cleared the cache used for OBS.");
@ -114,9 +116,9 @@ namespace TwitchChatTTS.Chat.Commands
public bool AcceptCustomPermission { get => true; }
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
await client.FetchPermissions();
await hermes.FetchPermissions();
}
}
}

View File

@ -1,5 +1,6 @@
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Twitch.Socket;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
@ -51,7 +52,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
if (_player.Playing == null)
return;
@ -78,7 +79,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
_player.RemoveAll();

View File

@ -1,5 +1,8 @@
using CommonSocketLibrary.Abstract;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Twitch.Socket;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
@ -7,13 +10,21 @@ namespace TwitchChatTTS.Chat.Commands
{
public class TTSCommand : IChatCommand
{
private readonly TwitchWebsocketClient _twitch;
private readonly User _user;
private readonly TwitchApiClient _client;
private readonly ILogger _logger;
public TTSCommand(User user, ILogger logger)
public TTSCommand(
[FromKeyedServices("twitch")] SocketClient<TwitchWebsocketMessage> twitch,
User user,
TwitchApiClient client,
ILogger logger)
{
_twitch = (twitch as TwitchWebsocketClient)!;
_user = user;
_client = client;
_logger = logger;
}
@ -51,7 +62,19 @@ namespace TwitchChatTTS.Chat.Commands
})
.AddAlias("off", "disable")
.AddAlias("disabled", "disable")
.AddAlias("false", "disable");
.AddAlias("false", "disable")
.CreateStaticInputParameter("join", b =>
{
b.CreateMentionParameter("mention", true)
.AddPermission("tts.commands.tts.join")
.CreateCommand(new JoinRoomCommand(_twitch, _client, _user, _logger));
})
.CreateStaticInputParameter("leave", b =>
{
b.CreateMentionParameter("mention", true)
.AddPermission("tts.commands.tts.leave")
.CreateCommand(new LeaveRoomCommand(_twitch, _client, _user, _logger));
});
});
}
@ -119,7 +142,8 @@ namespace TwitchChatTTS.Chat.Commands
}
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceNameLower).Key;
if (voiceId == null) {
if (voiceId == null)
{
_logger.Warning($"Could not find the identifier for the tts voice [voice name: {voiceName}]");
return;
}
@ -157,5 +181,94 @@ namespace TwitchChatTTS.Chat.Commands
_logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {_state}][invoker: {message.ChatterUserLogin}][id: {message.ChatterUserId}]");
}
}
private sealed class JoinRoomCommand : IChatPartialCommand
{
private readonly TwitchWebsocketClient _twitch;
private readonly TwitchApiClient _client;
private readonly User _user;
private ILogger _logger;
public bool AcceptCustomPermission { get => true; }
public JoinRoomCommand(
[FromKeyedServices("twitch")] SocketClient<TwitchWebsocketMessage> twitch,
TwitchApiClient client,
User user,
ILogger logger
)
{
_twitch = (twitch as TwitchWebsocketClient)!;
_client = client;
_user = user;
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
var mention = values["mention"].ToLower();
var fragment = message.Message.Fragments.FirstOrDefault(f => f.Mention != null && f.Text.ToLower() == mention);
if (fragment == null)
{
_logger.Warning("Cannot find the channel to join chat with.");
return;
}
await _client.CreateEventSubscription("channel.chat.message", "1", _twitch.SessionId, _user.TwitchUserId.ToString(), fragment.Mention!.UserId);
_logger.Information($"Joined chat room [channel: {fragment.Mention.UserLogin}][channel id: {fragment.Mention.UserId}][invoker: {message.ChatterUserLogin}][id: {message.ChatterUserId}]");
}
}
private sealed class LeaveRoomCommand : IChatPartialCommand
{
private readonly TwitchWebsocketClient _twitch;
private readonly TwitchApiClient _client;
private readonly User _user;
private ILogger _logger;
public bool AcceptCustomPermission { get => true; }
public LeaveRoomCommand(
[FromKeyedServices("twitch")] SocketClient<TwitchWebsocketMessage> twitch,
TwitchApiClient client,
User user,
ILogger logger
)
{
_twitch = (twitch as TwitchWebsocketClient)!;
_client = client;
_user = user;
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
var mention = values["mention"].ToLower();
var fragment = message.Message.Fragments.FirstOrDefault(f => f.Mention != null && f.Text.ToLower() == mention);
if (fragment?.Mention == null)
{
_logger.Warning("Cannot find the channel to leave chat from.");
return;
}
var subscriptionId = _twitch.GetSubscriptionId(_user.TwitchUserId.ToString(), "channel.chat.message");
if (subscriptionId == null)
{
_logger.Warning("Cannot find the subscription for that channel.");
return;
}
try
{
await _client.DeleteEventSubscription(subscriptionId);
_twitch.RemoveSubscription(fragment.Mention.UserId, "channel.chat.message");
_logger.Information($"Joined chat room [channel: {fragment.Mention.UserLogin}][channel id: {fragment.Mention.UserId}][invoker: {message.ChatterUserLogin}][id: {message.ChatterUserId}]");
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to delete the subscription from Twitch.");
}
}
}
}
}

View File

@ -1,6 +1,7 @@
using HermesSocketLibrary.Socket.Data;
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Twitch.Socket;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
@ -37,11 +38,11 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
_logger.Information($"TTS Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");
await client.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}.");
await hermes.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}.");
}
}
}

View File

@ -8,8 +8,6 @@ namespace TwitchChatTTS.Chat.Commands
public class VoiceCommand : IChatCommand
{
private readonly User _user;
// TODO: get permissions
// TODO: validated parameter for username by including '@' and regex for username
private readonly ILogger _logger;
public VoiceCommand(User user, ILogger logger)
@ -26,7 +24,7 @@ namespace TwitchChatTTS.Chat.Commands
{
b.CreateVoiceNameParameter("voiceName", true)
.CreateCommand(new TTSVoiceSelector(_user, _logger))
.CreateUnvalidatedParameter("chatter", optional: true)
.CreateMentionParameter("chatter", enabled: true, optional: true)
.AddPermission("tts.command.voice.admin")
.CreateCommand(new TTSVoiceSelectorAdmin(_user, _logger));
});
@ -45,7 +43,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
if (_user == null || _user.VoicesSelected == null)
return;
@ -57,12 +55,12 @@ namespace TwitchChatTTS.Chat.Commands
if (_user.VoicesSelected.ContainsKey(chatterId))
{
await client.UpdateTTSUser(chatterId, voice.Key);
await hermes.UpdateTTSUser(chatterId, voice.Key);
_logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {message.ChatterUserLogin}][reason: command]");
}
else
{
await client.CreateTTSUser(chatterId, voice.Key);
await hermes.CreateTTSUser(chatterId, voice.Key);
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.ChatterUserLogin}][reason: command]");
}
}
@ -81,13 +79,12 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
if (_user == null || _user.VoicesSelected == null)
return;
var chatterLogin = values["chatter"].Substring(1);
var mention = message.Message.Fragments.FirstOrDefault(f => f.Mention != null && f.Mention.UserLogin == chatterLogin)?.Mention;
var mention = message.Message.Fragments.FirstOrDefault(f => f.Mention != null && f.Text == values["chatter"])?.Mention;
if (mention == null)
{
_logger.Warning("Failed to find the chatter to apply voice command to.");
@ -101,12 +98,12 @@ namespace TwitchChatTTS.Chat.Commands
if (_user.VoicesSelected.ContainsKey(chatterId))
{
await client.UpdateTTSUser(chatterId, voice.Key);
await hermes.UpdateTTSUser(chatterId, voice.Key);
_logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {mention.UserLogin}][reason: command]");
}
else
{
await client.CreateTTSUser(chatterId, voice.Key);
await hermes.CreateTTSUser(chatterId, voice.Key);
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {mention.UserLogin}][reason: command]");
}
}

View File

@ -12,62 +12,75 @@ namespace TwitchChatTTS.Chat.Groups
private readonly ILogger _logger;
public ChatterGroupManager(ILogger logger) {
public ChatterGroupManager(ILogger logger)
{
_logger = logger;
_groups = new ConcurrentDictionary<string, Group>();
_chatters = new ConcurrentDictionary<long, ICollection<string>>();
}
public void Add(Group group) {
public void Add(Group group)
{
_groups.Add(group.Name, group);
}
public void Add(long chatter, string groupName) {
public void Add(long chatter, string groupName)
{
_chatters.Add(chatter, new List<string>() { groupName });
}
public void Add(long chatter, ICollection<string> groupNames) {
if (_chatters.TryGetValue(chatter, out var list)) {
public void Add(long chatter, ICollection<string> groupNames)
{
if (_chatters.TryGetValue(chatter, out var list))
{
foreach (var group in groupNames)
list.Add(group);
} else
}
else
_chatters.Add(chatter, groupNames);
}
public void Clear() {
public void Clear()
{
_groups.Clear();
_chatters.Clear();
}
public Group? Get(string groupName) {
public Group? Get(string groupName)
{
if (_groups.TryGetValue(groupName, out var group))
return group;
return null;
}
public IEnumerable<string> GetGroupNamesFor(long chatter) {
public IEnumerable<string> GetGroupNamesFor(long chatter)
{
if (_chatters.TryGetValue(chatter, out var groups))
return groups.Select(g => _groups[g].Name);
return Array.Empty<string>();
}
public int GetPriorityFor(long chatter) {
public int GetPriorityFor(long chatter)
{
if (!_chatters.TryGetValue(chatter, out var groups))
return 0;
return GetPriorityFor(groups);
}
public int GetPriorityFor(IEnumerable<string> groupNames) {
public int GetPriorityFor(IEnumerable<string> groupNames)
{
var values = groupNames.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;
}
public bool Remove(long chatterId, string groupId) {
if (_chatters.TryGetValue(chatterId, out var groups)) {
public bool Remove(long chatterId, string groupId)
{
if (_chatters.TryGetValue(chatterId, out var groups))
{
groups.Remove(groupId);
_logger.Debug($"Removed chatter from group [chatter id: {chatterId}][group name: {_groups[groupId]}][group id: {groupId}]");
return true;

View File

@ -23,6 +23,13 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
return res;
}
public bool? CheckIfDirectAllowed(string path)
{
var res = Get(path)?.DirectAllow;
_logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"} [direct]");
return res;
}
public bool? CheckIfAllowed(IEnumerable<string> groups, string path)
{
bool overall = false;
@ -37,6 +44,20 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
return overall ? true : null;
}
public bool? CheckIfDirectAllowed(IEnumerable<string> groups, string path)
{
bool overall = false;
foreach (var group in groups)
{
var result = CheckIfDirectAllowed($"{group}.{path}");
if (result == false)
return false;
if (result == true)
overall = true;
}
return overall ? true : null;
}
public void Clear()
{
_root.Clear();
@ -104,6 +125,7 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
}
set => _allow = value;
}
public bool? DirectAllow { get => _allow; }
internal PermissionNode? Parent { get => _parent; }
public IList<PermissionNode>? Children { get => _children == null ? null : new ReadOnlyCollection<PermissionNode>(_children); }

View File

@ -5,6 +5,8 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
void Set(string path, bool? allow);
bool? CheckIfAllowed(string path);
bool? CheckIfAllowed(IEnumerable<string> groups, string path);
bool? CheckIfDirectAllowed(string path);
bool? CheckIfDirectAllowed(IEnumerable<string> groups, string path);
void Clear();
bool Remove(string path);
}

View File

@ -101,13 +101,14 @@ public class TTSPlayer
}
}
public void RemoveAll(long chatterId)
public void RemoveAll(long broadcasterId, long chatterId)
{
try
{
_mutex2.WaitOne();
if (_buffer.UnorderedItems.Any(i => i.Element.ChatterId == chatterId)) {
var list = _buffer.UnorderedItems.Where(i => i.Element.ChatterId != chatterId).ToArray();
if (_buffer.UnorderedItems.Any(i => i.Element.RoomId == broadcasterId && i.Element.ChatterId == chatterId))
{
var list = _buffer.UnorderedItems.Where(i => i.Element.RoomId == broadcasterId && i.Element.ChatterId != chatterId).ToArray();
_buffer.Clear();
foreach (var item in list)
_buffer.Enqueue(item.Element, item.Element.Priority);
@ -121,8 +122,9 @@ public class TTSPlayer
try
{
_mutex.WaitOne();
if (_messages.UnorderedItems.Any(i => i.Element.ChatterId == chatterId)) {
var list = _messages.UnorderedItems.Where(i => i.Element.ChatterId != chatterId).ToArray();
if (_messages.UnorderedItems.Any(i => i.Element.RoomId == broadcasterId && i.Element.ChatterId == chatterId))
{
var list = _messages.UnorderedItems.Where(i => i.Element.RoomId == broadcasterId && i.Element.ChatterId != chatterId).ToArray();
_messages.Clear();
foreach (var item in list)
_messages.Enqueue(item.Element, item.Element.Priority);
@ -139,7 +141,8 @@ public class TTSPlayer
try
{
_mutex2.WaitOne();
if (_buffer.UnorderedItems.Any(i => i.Element.MessageId == messageId)) {
if (_buffer.UnorderedItems.Any(i => i.Element.MessageId == messageId))
{
var list = _buffer.UnorderedItems.Where(i => i.Element.MessageId != messageId).ToArray();
_buffer.Clear();
foreach (var item in list)
@ -155,7 +158,8 @@ public class TTSPlayer
try
{
_mutex.WaitOne();
if (_messages.UnorderedItems.Any(i => i.Element.MessageId == messageId)) {
if (_messages.UnorderedItems.Any(i => i.Element.MessageId == messageId))
{
var list = _messages.UnorderedItems.Where(i => i.Element.MessageId != messageId).ToArray();
_messages.Clear();
foreach (var item in list)
@ -182,6 +186,7 @@ public class TTSPlayer
public class TTSMessage
{
public string? Voice { get; set; }
public long RoomId { get; set; }
public long ChatterId { get; set; }
public string MessageId { get; set; }
public string? Message { get; set; }

View File

@ -43,5 +43,15 @@ namespace TwitchChatTTS.Helpers
{
return await _client.PostAsJsonAsync(uri, new object(), _options);
}
public async Task<T?> Delete<T>(string uri)
{
return await _client.DeleteFromJsonAsync<T>(uri, _options);
}
public async Task<HttpResponseMessage> Delete(string uri)
{
return await _client.DeleteAsync(uri);
}
}
}

View File

@ -1,4 +1,6 @@
public class Account {
public string Id { get; set; }
public string Username { get; set; }
public string Role { get; set; }
public string? BroadcasterId { get; set; }
}

View File

@ -1,6 +1,7 @@
namespace TwitchChatTTS.Hermes
{
public interface ICustomDataManager {
public interface ICustomDataManager
{
void Add(string key, object value, string type);
void Change(string key, object value);
void Delete(string key);
@ -11,7 +12,8 @@ namespace TwitchChatTTS.Hermes
{
private IDictionary<string, DataInfo> _data;
public CustomDataManager() {
public CustomDataManager()
{
_data = new Dictionary<string, DataInfo>();
}
@ -38,7 +40,8 @@ namespace TwitchChatTTS.Hermes
}
// type: text (string), whole number (int), number (double), boolean, formula (string, data type of number)
public struct DataInfo {
public struct DataInfo
{
public string Id { get; set; }
public string Type { get; set; }
public object Value { get; set; }

View File

@ -31,7 +31,7 @@ public class HermesApiClient
}
public async Task<bool> AuthorizeTwitch()
public async Task<TwitchBotAuth?> AuthorizeTwitch()
{
try
{
@ -51,10 +51,9 @@ public class HermesApiClient
else if (authorize != null)
{
_logger.Error("Twitch API Authorization failed: " + authorize.AccessToken + " | " + authorize.RefreshToken + " | " + authorize.UserId + " | " + authorize.BroadcasterId);
return false;
return null;
}
_logger.Debug($"Authorized Twitch API.");
return true;
return _token;
}
catch (JsonException)
{
@ -64,7 +63,7 @@ public class HermesApiClient
{
_logger.Error(e, "Failed to authorize to Twitch API.");
}
return false;
return null;
}
public async Task<TTSVersion?> GetLatestTTSVersion()
@ -74,7 +73,10 @@ public class HermesApiClient
public async Task<Account> FetchHermesAccountDetails()
{
var account = await _web.GetJson<Account>($"https://{BASE_URL}/api/account");
var account = await _web.GetJson<Account>($"https://{BASE_URL}/api/account", new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
if (account == null || account.Id == null || account.Username == null)
throw new NullReferenceException("Invalid value found while fetching for hermes account data.");
return account;

View File

@ -18,7 +18,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
public class RequestAckHandler : IWebSocketHandler
{
private User _user;
//private readonly RedemptionManager _redemptionManager;
private readonly ICallbackManager<HermesRequestData> _callbackManager;
private readonly IServiceProvider _serviceProvider;
private readonly JsonSerializerOptions _options;
@ -30,16 +29,16 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
public RequestAckHandler(
User user,
ICallbackManager<HermesRequestData> callbackManager,
IServiceProvider serviceProvider,
User user,
JsonSerializerOptions options,
ILogger logger
)
{
_user = user;
_callbackManager = callbackManager;
_serviceProvider = serviceProvider;
_user = user;
_options = options;
_logger = logger;
}
@ -263,15 +262,15 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
re.Match(string.Empty);
filter.Regex = re;
}
catch (Exception e) { }
catch (Exception) { }
}
_user.RegexFilters = filters;
_logger.Information($"TTS word filters [count: {_user.RegexFilters.Count()}] have been refreshed.");
}
else if (message.Request.Type == "update_tts_voice_state")
{
string voiceId = message.Request.Data["voice"].ToString();
bool state = message.Request.Data["state"].ToString().ToLower() == "true";
string voiceId = message.Request.Data?["voice"].ToString()!;
bool state = message.Request.Data?["state"].ToString()!.ToLower() == "true";
if (!_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) || voiceName == null)
{
@ -305,14 +304,14 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
_logger.Warning("Failed to read the redeemable actions for redemptions.");
return;
}
if (hermesRequestData?.Data == null || !(hermesRequestData.Data["redemptions"] is IEnumerable<Redemption> redemptions))
if (hermesRequestData?.Data == null || hermesRequestData.Data["redemptions"] is not IEnumerable<Redemption> redemptions)
{
_logger.Warning("Failed to read the redemptions while updating redemption actions.");
return;
}
_logger.Information($"Redeemable actions [count: {actions.Count()}] loaded.");
var redemptionManager = _serviceProvider.GetRequiredService<RedemptionManager>();
var redemptionManager = _serviceProvider.GetRequiredService<IRedemptionManager>();
redemptionManager.Initialize(redemptions, actions.ToDictionary(a => a.Name, a => a));
}
else if (message.Request.Type == "get_default_tts_voice")

View File

@ -104,7 +104,8 @@ namespace TwitchChatTTS.Hermes.Socket
});
}
public async Task FetchChatterIdentifiers() {
public async Task FetchChatterIdentifiers()
{
await Send(3, new RequestMessage()
{
Type = "get_chatter_ids",
@ -112,7 +113,8 @@ namespace TwitchChatTTS.Hermes.Socket
});
}
public async Task FetchDefaultTTSVoice() {
public async Task FetchDefaultTTSVoice()
{
await Send(3, new RequestMessage()
{
Type = "get_default_tts_voice",
@ -120,7 +122,8 @@ namespace TwitchChatTTS.Hermes.Socket
});
}
public async Task FetchEmotes() {
public async Task FetchEmotes()
{
await Send(3, new RequestMessage()
{
Type = "get_emotes",
@ -128,7 +131,8 @@ namespace TwitchChatTTS.Hermes.Socket
});
}
public async Task FetchEnabledTTSVoices() {
public async Task FetchEnabledTTSVoices()
{
await Send(3, new RequestMessage()
{
Type = "get_enabled_tts_voices",
@ -136,7 +140,8 @@ namespace TwitchChatTTS.Hermes.Socket
});
}
public async Task FetchTTSVoices() {
public async Task FetchTTSVoices()
{
await Send(3, new RequestMessage()
{
Type = "get_tts_voices",
@ -144,7 +149,8 @@ namespace TwitchChatTTS.Hermes.Socket
});
}
public async Task FetchTTSChatterVoices() {
public async Task FetchTTSChatterVoices()
{
await Send(3, new RequestMessage()
{
Type = "get_tts_users",
@ -152,7 +158,8 @@ namespace TwitchChatTTS.Hermes.Socket
});
}
public async Task FetchTTSWordFilters() {
public async Task FetchTTSWordFilters()
{
await Send(3, new RequestMessage()
{
Type = "get_tts_word_filters",

View File

@ -104,7 +104,8 @@ namespace TwitchChatTTS.OBS.Socket
}
}
public async Task ExecuteRequest(RequestResponseMessage message) {
public async Task ExecuteRequest(RequestResponseMessage message)
{
if (!_handlers.TryGetValue(7, out var handler) || handler == null)
{
_logger.Error("Failed to find the request response handler for OBS.");

View File

@ -54,7 +54,8 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
{
if (removing)
{
if (_emotes.Get(o.Name) != o.Id) {
if (_emotes.Get(o.Name) != o.Id)
{
_logger.Warning("Mismatched emote found while removing a 7tv emote.");
continue;
}
@ -63,7 +64,8 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
}
else if (updater != null)
{
if (_emotes.Get(o.Name) != o.Id) {
if (_emotes.Get(o.Name) != o.Id)
{
_logger.Warning("Mismatched emote found while updating a 7tv emote.");
continue;
}

View File

@ -131,7 +131,8 @@ namespace TwitchChatTTS.Seven.Socket
else if (_reconnectDelay[code] > 0)
await Task.Delay(_reconnectDelay[code]);
}
else {
else
{
_logger.Warning("Unknown 7tv disconnection.");
await Task.Delay(TimeSpan.FromSeconds(30));
}

View File

@ -28,6 +28,7 @@ using static TwitchChatTTS.Chat.Commands.TTSCommands;
using TwitchChatTTS.Twitch.Socket;
using TwitchChatTTS.Twitch.Socket.Messages;
using TwitchChatTTS.Twitch.Socket.Handlers;
using CommonSocketLibrary.Backoff;
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true
@ -43,13 +44,10 @@ var deserializer = new DeserializerBuilder()
var configContent = File.ReadAllText("tts.config.yml");
var configuration = deserializer.Deserialize<Configuration>(configContent);
s.AddSingleton<Configuration>(configuration);
s.AddSingleton(configuration);
var logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
//.MinimumLevel.Override("TwitchLib.Communication.Clients.WebSocketClient", LogEventLevel.Warning)
//.MinimumLevel.Override("TwitchLib.PubSub.TwitchPubSub", LogEventLevel.Warning)
.MinimumLevel.Override("TwitchLib", LogEventLevel.Warning)
.MinimumLevel.Override("mariuszgromada", LogEventLevel.Error)
.Enrich.FromLogContext()
.WriteTo.File("logs/log-.log", restrictedToMinimumLevel: LogEventLevel.Debug, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 3)
@ -57,11 +55,11 @@ var logger = new LoggerConfiguration()
.CreateLogger();
s.AddSerilog(logger);
s.AddSingleton<User>(new User());
s.AddSingleton<User>();
s.AddSingleton<AudioPlaybackEngine>();
s.AddSingleton<ICallbackManager<HermesRequestData>, CallbackManager<HermesRequestData>>();
s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
s.AddSingleton(new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
@ -77,10 +75,11 @@ s.AddSingleton<IChatCommand, VersionCommand>();
s.AddSingleton<ICommandBuilder, CommandBuilder>();
s.AddSingleton<IChatterGroupManager, ChatterGroupManager>();
s.AddSingleton<IGroupPermissionManager, GroupPermissionManager>();
s.AddSingleton<CommandManager>();
s.AddSingleton<ICommandFactory, CommandFactory>();
s.AddSingleton<ICommandManager, CommandManager>();
s.AddSingleton<TTSPlayer>();
s.AddSingleton<RedemptionManager>();
s.AddSingleton<IRedemptionManager, RedemptionManager>();
s.AddSingleton<HermesApiClient>();
s.AddSingleton<TwitchBotAuth>();
s.AddSingleton<TwitchApiClient>();
@ -109,19 +108,33 @@ s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, SevenMessageTypeManag
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");
// twitch websocket
s.AddKeyedSingleton<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>("twitch");
s.AddKeyedSingleton<IBackoff>("twitch", new ExponentialBackoff(1000, 120 * 1000));
s.AddSingleton<ITwitchConnectionManager, TwitchConnectionManager>();
s.AddKeyedTransient<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>("twitch", (sp, _) =>
{
var factory = sp.GetRequiredService<ITwitchConnectionManager>();
var client = factory.GetWorkingClient();
client.Connect().Wait();
return client;
});
s.AddKeyedTransient<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>("twitch-create");
s.AddKeyedSingleton<ITwitchSocketHandler, SessionKeepAliveHandler>("twitch");
s.AddKeyedSingleton<ITwitchSocketHandler, SessionWelcomeHandler>("twitch");
s.AddKeyedSingleton<ITwitchSocketHandler, SessionReconnectHandler>("twitch");
s.AddKeyedSingleton<ITwitchSocketHandler, NotificationHandler>("twitch");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelAdBreakHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelBanHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatMessageHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatClearUserHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatClearHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatDeleteMessageHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelCustomRedemptionHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelFollowHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelResubscriptionHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionGiftHandler>("twitch-notifications");
// hermes websocket
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");

83
TTS.cs
View File

@ -13,6 +13,7 @@ using CommonSocketLibrary.Common;
using TwitchChatTTS.OBS.Socket;
using TwitchChatTTS.Twitch.Socket.Messages;
using TwitchChatTTS.Twitch.Socket;
using TwitchChatTTS.Chat.Commands;
namespace TwitchChatTTS
{
@ -24,45 +25,51 @@ namespace TwitchChatTTS
private readonly User _user;
private readonly HermesApiClient _hermesApiClient;
private readonly SevenApiClient _sevenApiClient;
private readonly TwitchApiClient _twitchApiClient;
private readonly HermesSocketClient _hermes;
private readonly OBSSocketClient _obs;
private readonly SevenSocketClient _seven;
private readonly TwitchWebsocketClient _twitch;
private readonly ICommandFactory _commandFactory;
private readonly ICommandManager _commandManager;
private readonly IEmoteDatabase _emotes;
private readonly Configuration _configuration;
private readonly TTSPlayer _player;
private readonly AudioPlaybackEngine _playback;
private readonly IServiceProvider _serviceProvider;
private readonly Configuration _configuration;
private readonly ILogger _logger;
public TTS(
User user,
HermesApiClient hermesApiClient,
SevenApiClient sevenApiClient,
TwitchApiClient twitchApiClient,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
[FromKeyedServices("7tv")] SocketClient<WebSocketMessage> seven,
[FromKeyedServices("twitch")] SocketClient<TwitchWebsocketMessage> twitch,
ICommandFactory commandFactory,
ICommandManager commandManager,
IEmoteDatabase emotes,
Configuration configuration,
TTSPlayer player,
AudioPlaybackEngine playback,
IServiceProvider serviceProvider,
Configuration configuration,
ILogger logger
)
{
_user = user;
_hermesApiClient = hermesApiClient;
_sevenApiClient = sevenApiClient;
_twitchApiClient = twitchApiClient;
_hermes = (hermes as HermesSocketClient)!;
_obs = (obs as OBSSocketClient)!;
_seven = (seven as SevenSocketClient)!;
_twitch = (twitch as TwitchWebsocketClient)!;
_commandFactory = commandFactory;
_commandManager = commandManager;
_emotes = emotes;
_configuration = configuration;
_player = player;
_playback = playback;
_serviceProvider = serviceProvider;
_logger = logger;
}
@ -91,6 +98,7 @@ namespace TwitchChatTTS
await Task.Delay(15 * 1000);
}
await _twitch.Connect();
await InitializeHermesWebsocket();
try
{
@ -98,29 +106,33 @@ namespace TwitchChatTTS
_user.HermesUserId = hermesAccount.Id;
_user.HermesUsername = hermesAccount.Username;
_user.TwitchUsername = hermesAccount.Username;
_user.TwitchUserId = long.Parse(hermesAccount.BroadcasterId);
}
catch (ArgumentNullException)
{
_logger.Error("Ensure you have your Twitch account linked to TTS.");
await Task.Delay(TimeSpan.FromSeconds(30));
return;
}
catch (FormatException)
{
_logger.Error("Ensure you have your Twitch account linked to TTS.");
await Task.Delay(TimeSpan.FromSeconds(30));
return;
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to initialize properly. Restart app please.");
await Task.Delay(30 * 1000);
await Task.Delay(TimeSpan.FromSeconds(30));
return;
}
await _hermesApiClient.AuthorizeTwitch();
var twitchBotToken = await _hermesApiClient.FetchTwitchBotToken();
_user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId!);
_logger.Information($"Username: {_user.TwitchUsername} [id: {_user.TwitchUserId}]");
var twitchapiclient2 = _serviceProvider.GetRequiredService<TwitchApiClient>();
twitchapiclient2.Initialize(twitchBotToken);
_twitch.Initialize();
await _twitch.Connect();
var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId.ToString());
if (emoteSet != null)
_user.SevenEmoteSetId = emoteSet.Id;
_commandManager.Update(_commandFactory);
await InitializeEmotes(_sevenApiClient, emoteSet);
await InitializeSevenTv();
await InitializeObs();
@ -265,43 +277,6 @@ namespace TwitchChatTTS
}
}
// private async Task<TwitchApiClient?> InitializeTwitchApiClient(string username)
// {
// _logger.Debug("Initializing twitch client.");
// var hermesapiclient = _serviceProvider.GetRequiredService<HermesApiClient>();
// if (!await hermesapiclient.AuthorizeTwitch())
// {
// _logger.Error("Cannot connect to Twitch API.");
// return null;
// }
// var twitchapiclient = _serviceProvider.GetRequiredService<TwitchApiClient>();
// var channels = _configuration.Twitch?.Channels ?? [username];
// _logger.Information("Twitch channels: " + string.Join(", ", channels));
// twitchapiclient.InitializeClient(username, channels);
// twitchapiclient.InitializePublisher();
// var handler = _serviceProvider.GetRequiredService<ChatMessageHandler>();
// 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;
// await _hermes.SendEmoteUsage(e.ChatMessage.Id, result.ChatterId, result.Emotes);
// }
// catch (Exception ex)
// {
// _logger.Error(ex, "Unable to either execute a command or to send emote usage message.");
// }
// });
// return twitchapiclient;
// }
private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes)
{
var globalEmotes = await sevenapi.FetchGlobalSevenEmotes();

View File

@ -0,0 +1,11 @@
using HermesSocketLibrary.Requests.Messages;
namespace TwitchChatTTS.Twitch.Redemptions
{
public interface IRedemptionManager
{
Task Execute(RedeemableAction action, string senderDisplayName, long senderId);
IList<RedeemableAction> Get(string twitchRedemptionId);
void Initialize(IEnumerable<Redemption> redemptions, IDictionary<string, RedeemableAction> actions);
}
}

View File

@ -11,7 +11,7 @@ using TwitchChatTTS.OBS.Socket.Data;
namespace TwitchChatTTS.Twitch.Redemptions
{
public class RedemptionManager
public class RedemptionManager : IRedemptionManager
{
private readonly IDictionary<string, IList<RedeemableAction>> _store;
private readonly User _user;

View File

@ -0,0 +1,57 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class ChannelAdBreakHandler : ITwitchSocketHandler
{
public string Name => "channel.ad_break.begin";
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelAdBreakHandler(IRedemptionManager redemptionManager, ILogger logger)
{
_redemptionManager = redemptionManager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelAdBreakMessage message)
return;
bool isAutomatic = message.IsAutomatic == "true";
if (isAutomatic)
_logger.Information($"Ad break has begun [duration: {message.DurationSeconds} seconds][automatic: {isAutomatic}]");
else
_logger.Information($"Ad break has begun [duration: {message.DurationSeconds} seconds][requester: {message.RequesterUserLogin}][requester id: {message.RequesterUserId}]");
try
{
var actions = _redemptionManager.Get("adbreak");
if (!actions.Any())
{
_logger.Debug($"No redemable actions for ad break was found");
return;
}
_logger.Debug($"Found {actions.Count} actions for this Twitch ad break");
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.RequesterUserLogin, long.Parse(message.RequesterUserId));
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: ad break]");
}
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to fetch the redeemable actions for ad break");
}
}
}
}

View File

@ -14,7 +14,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger = logger;
}
public Task Execute(TwitchWebsocketClient sender, object? data)
public Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelBanMessage message)
return Task.CompletedTask;

View File

@ -18,7 +18,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger = logger;
}
public Task Execute(TwitchWebsocketClient sender, object? data)
public Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelChatClearMessage message)
return Task.CompletedTask;

View File

@ -18,14 +18,16 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger = logger;
}
public Task Execute(TwitchWebsocketClient sender, object? data)
public Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelChatClearUserMessage message)
return Task.CompletedTask;
long broadcasterId = long.Parse(message.BroadcasterUserId);
long chatterId = long.Parse(message.TargetUserId);
_player.RemoveAll(chatterId);
if (_player.Playing?.ChatterId == chatterId) {
_player.RemoveAll(broadcasterId, chatterId);
if (_player.Playing != null && _player.Playing.RoomId == broadcasterId && _player.Playing.ChatterId == chatterId)
{
_playback.RemoveMixerInput(_player.Playing.Audio!);
_player.Playing = null;
}

View File

@ -18,7 +18,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger = logger;
}
public Task Execute(TwitchWebsocketClient sender, object? data)
public Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelChatDeleteMessage message)
return Task.CompletedTask;

View File

@ -19,7 +19,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
private readonly User _user;
private readonly TTSPlayer _player;
private readonly CommandManager _commands;
private readonly ICommandManager _commands;
private readonly IGroupPermissionManager _permissionManager;
private readonly IChatterGroupManager _chatterGroupManager;
private readonly IEmoteDatabase _emotes;
@ -34,7 +34,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
public ChannelChatMessageHandler(
User user,
TTSPlayer player,
CommandManager commands,
ICommandManager commands,
IGroupPermissionManager permissionManager,
IChatterGroupManager chatterGroupManager,
IEmoteDatabase emotes,
@ -59,15 +59,10 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object? data)
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (sender == null)
return;
if (data == null)
{
_logger.Warning("Twitch websocket message data is null.");
return;
}
if (data is not ChannelChatMessage message)
return;
@ -231,6 +226,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
var parts = _sfxRegex.Split(message);
var chatterId = long.Parse(e.ChatterUserId);
var broadcasterId = long.Parse(e.BroadcasterUserId);
var badgesString = string.Join(", ", e.Badges.Select(b => b.SetId + '|' + b.Id + '=' + b.Info));
if (parts.Length == 1)
@ -241,6 +237,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
Voice = voice,
Message = message,
Timestamp = DateTime.UtcNow,
RoomId = broadcasterId,
ChatterId = chatterId,
MessageId = e.MessageId,
Badges = e.Badges,
@ -271,6 +268,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
Voice = voice,
Message = parts[i * 2],
Timestamp = DateTime.UtcNow,
RoomId = broadcasterId,
ChatterId = chatterId,
MessageId = e.MessageId,
Badges = e.Badges,
@ -284,6 +282,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
Voice = voice,
File = $"sfx/{sfxName}.mp3",
Timestamp = DateTime.UtcNow,
RoomId = broadcasterId,
ChatterId = chatterId,
MessageId = e.MessageId,
Badges = e.Badges,
@ -299,6 +298,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
Voice = voice,
Message = parts.Last(),
Timestamp = DateTime.UtcNow,
RoomId = broadcasterId,
ChatterId = chatterId,
MessageId = e.MessageId,
Badges = e.Badges,

View File

@ -8,11 +8,11 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public string Name => "channel.channel_points_custom_reward_redemption.add";
private readonly RedemptionManager _redemptionManager;
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelCustomRedemptionHandler(
RedemptionManager redemptionManager,
IRedemptionManager redemptionManager,
ILogger logger
)
{
@ -20,7 +20,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object? data)
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelCustomRedemptionMessage message)
return;

View File

@ -0,0 +1,52 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class ChannelFollowHandler : ITwitchSocketHandler
{
public string Name => "channel.follow";
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelFollowHandler(IRedemptionManager redemptionManager, ILogger logger)
{
_redemptionManager = redemptionManager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelFollowMessage message)
return;
_logger.Information($"User followed [chatter: {message.UserLogin}][chatter id: {message.UserId}]");
try
{
var actions = _redemptionManager.Get("follow");
if (!actions.Any())
{
_logger.Debug($"No redemable actions for follow was found");
return;
}
_logger.Debug($"Found {actions.Count} actions for this Twitch follow");
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId));
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: follow]");
}
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to fetch the redeemable actions for follow");
}
}
}
}

View File

@ -0,0 +1,52 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class ChannelResubscriptionHandler : ITwitchSocketHandler
{
public string Name => "channel.subscription.message";
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelResubscriptionHandler(IRedemptionManager redemptionManager, ILogger logger)
{
_redemptionManager = redemptionManager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelResubscriptionMessage message)
return;
_logger.Debug("Resubscription occured.");
try
{
var actions = _redemptionManager.Get("subscription");
if (!actions.Any())
{
_logger.Debug($"No redemable actions for this subscription was found [message: {message.Message.Text}]");
return;
}
_logger.Debug($"Found {actions.Count} actions for this Twitch subscription [message: {message.Message.Text}]");
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId));
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: subscription][message: {message.Message.Text}]");
}
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to fetch the redeemable actions for subscription [message: {message.Message.Text}]");
}
}
}
}

View File

@ -0,0 +1,52 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class ChannelSubscriptionGiftHandler : ITwitchSocketHandler
{
public string Name => "channel.subscription.gift";
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelSubscriptionGiftHandler(IRedemptionManager redemptionManager, ILogger logger)
{
_redemptionManager = redemptionManager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelSubscriptionGiftMessage message)
return;
_logger.Debug("Gifted subscription occured.");
try
{
var actions = _redemptionManager.Get("subscription.gift");
if (!actions.Any())
{
_logger.Debug($"No redemable actions for this gifted subscription was found");
return;
}
_logger.Debug($"Found {actions.Count} actions for this Twitch gifted subscription [gifted: {message.UserLogin}][gifted id: {message.UserId}][Anonymous: {message.IsAnonymous}][cumulative: {message.CumulativeTotal ?? -1}]");
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId));
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: gifted subscription]");
}
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to fetch the redeemable actions for gifted subscription");
}
}
}
}

View File

@ -1,33 +1,54 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class ChannelSubscriptionHandler : ITwitchSocketHandler
{
public string Name => "channel.subscription.message";
public string Name => "channel.subscription";
private readonly TTSPlayer _player;
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelSubscriptionHandler(TTSPlayer player, ILogger logger) {
_player = player;
public ChannelSubscriptionHandler(IRedemptionManager redemptionManager, ILogger logger)
{
_redemptionManager = redemptionManager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object? data)
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (sender == null)
return;
if (data == null)
{
_logger.Warning("Twitch websocket message data is null.");
return;
}
if (data is not ChannelSubscriptionMessage message)
return;
if (message.IsGifted)
return;
_logger.Debug("Subscription occured.");
try
{
var actions = _redemptionManager.Get("subscription");
if (!actions.Any())
{
_logger.Debug($"No redemable actions for this subscription was found [subscriber: {message.UserLogin}][subscriber id: {message.UserId}]");
return;
}
_logger.Debug($"Found {actions.Count} actions for this Twitch subscription [subscriber: {message.UserLogin}][subscriber id: {message.UserId}]");
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId));
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: subscription][subscriber: {message.UserLogin}][subscriber id: {message.UserId}]");
}
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to fetch the redeemable actions for subscription [subscriber: {message.UserLogin}][subscriber id: {message.UserId}]");
}
}
}
}

View File

@ -3,6 +3,6 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
public interface ITwitchSocketHandler
{
string Name { get; }
Task Execute(TwitchWebsocketClient sender, object? data);
Task Execute(TwitchWebsocketClient sender, object data);
}
}

View File

@ -23,30 +23,30 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_handlers = handlers.ToDictionary(h => h.Name, h => h);
_logger = logger;
_options = new JsonSerializerOptions() {
_options = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
_messageTypes = new Dictionary<string, Type>();
_messageTypes.Add("channel.adbreak.begin", typeof(ChannelAdBreakMessage));
_messageTypes.Add("channel.ban", typeof(ChannelBanMessage));
_messageTypes.Add("channel.chat.message", typeof(ChannelChatMessage));
_messageTypes.Add("channel.chat.clear_user_messages", typeof(ChannelChatClearUserMessage));
_messageTypes.Add("channel.chat.clear", typeof(ChannelChatClearMessage));
_messageTypes.Add("channel.chat.message_delete", typeof(ChannelChatDeleteMessage));
_messageTypes.Add("channel.channel_points_custom_reward_redemption.add", typeof(ChannelCustomRedemptionMessage));
_messageTypes.Add("channel.follow", typeof(ChannelFollowMessage));
_messageTypes.Add("channel.resubscription", typeof(ChannelResubscriptionMessage));
_messageTypes.Add("channel.subscription.message", typeof(ChannelSubscriptionMessage));
_messageTypes.Add("channel.subscription.gift", typeof(ChannelSubscriptionGiftMessage));
}
public async Task Execute(TwitchWebsocketClient sender, object? data)
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (sender == null)
return;
if (data == null)
{
_logger.Warning("Twitch websocket message data is null.");
return;
}
if (data is not NotificationMessage message)
return;

View File

@ -0,0 +1,12 @@
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class SessionKeepAliveHandler : ITwitchSocketHandler
{
public string Name => "session_keepalive";
public Task Execute(TwitchWebsocketClient sender, object data)
{
return Task.CompletedTask;
}
}
}

View File

@ -8,40 +8,45 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public string Name => "session_reconnect";
private readonly TwitchApiClient _api;
private readonly ITwitchConnectionManager _manager;
private readonly ILogger _logger;
public SessionReconnectHandler(TwitchApiClient api, ILogger logger)
public SessionReconnectHandler(ITwitchConnectionManager manager, ILogger logger)
{
_api = api;
_manager = manager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object? data)
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (sender == null)
return;
if (data == null)
{
_logger.Warning("Twitch websocket message data is null.");
return;
}
if (data is not SessionWelcomeMessage message)
return;
if (_api == null)
return;
if (string.IsNullOrEmpty(message.Session.Id))
{
_logger.Warning($"No session info provided by Twitch [status: {message.Session.Status}]");
_logger.Warning($"No session id provided by Twitch [status: {message.Session.Status}]");
return;
}
// TODO: Be able to handle multiple websocket connections.
sender.URL = message.Session.ReconnectUrl;
await Task.Delay(TimeSpan.FromSeconds(29));
await sender.DisconnectAsync(new SocketDisconnectionEventArgs("Close", "Twitch asking to reconnect."));
await sender.Connect();
if (message.Session.ReconnectUrl == null)
{
_logger.Warning($"No reconnection info provided by Twitch [status: {message.Session.Status}]");
return;
}
sender.ReceivedReconnecting = true;
var backup = _manager.GetBackupClient();
var identified = _manager.GetWorkingClient();
if (identified != null && backup != identified)
{
await identified.DisconnectAsync(new SocketDisconnectionEventArgs("Closed", "Reconnection from another client."));
}
backup.URL = message.Session.ReconnectUrl;
await backup.Connect();
}
}
}

View File

@ -1,4 +1,3 @@
using CommonSocketLibrary.Abstract;
using Serilog;
using TwitchChatTTS.Twitch.Socket.Messages;
@ -8,26 +7,23 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public string Name => "session_welcome";
private readonly HermesApiClient _hermes;
private readonly TwitchApiClient _api;
private readonly User _user;
private readonly ILogger _logger;
public SessionWelcomeHandler(TwitchApiClient api, User user, ILogger logger)
public SessionWelcomeHandler(HermesApiClient hermes, TwitchApiClient api, User user, ILogger logger)
{
_hermes = hermes;
_api = api;
_user = user;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object? data)
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (sender == null)
return;
if (data == null)
{
_logger.Warning("Twitch websocket message data is null.");
return;
}
if (data is not SessionWelcomeMessage message)
return;
if (_api == null)
@ -39,6 +35,11 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
return;
}
await _hermes.AuthorizeTwitch();
var token = await _hermes.FetchTwitchBotToken();
_api.Initialize(token);
string broadcasterId = _user.TwitchUserId.ToString();
string[] subscriptionsv1 = [
"channel.chat.message",
"channel.chat.message_delete",
@ -53,17 +54,36 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
string[] subscriptionsv2 = [
"channel.follow",
];
string broadcasterId = _user.TwitchUserId.ToString();
foreach (var subscription in subscriptionsv1)
await Subscribe(subscription, message.Session.Id, broadcasterId, "1");
foreach (var subscription in subscriptionsv2)
await Subscribe(subscription, message.Session.Id, broadcasterId, "2");
sender.SessionId = message.Session.Id;
sender.Identified = sender.SessionId != null;
string? pagination = null;
int size = 0;
do
{
var subscriptionsData = await _api.GetSubscriptions(status: "enabled", broadcasterId: broadcasterId, after: pagination);
var subscriptionNames = subscriptionsData?.Data == null ? [] : subscriptionsData.Data.Select(s => s.Type).ToArray();
if (subscriptionNames.Length == 0)
break;
foreach (var d in subscriptionsData!.Data!)
sender.AddSubscription(broadcasterId, d.Type, d.Id);
subscriptionsv1 = subscriptionsv1.Except(subscriptionNames).ToArray();
subscriptionsv2 = subscriptionsv2.Except(subscriptionNames).ToArray();
pagination = subscriptionsData?.Pagination?.Cursor;
size = subscriptionNames.Length;
} while (size >= 100 && pagination != null && subscriptionsv1.Length + subscriptionsv2.Length > 0);
foreach (var subscription in subscriptionsv1)
await Subscribe(sender, subscription, message.Session.Id, broadcasterId, "1");
foreach (var subscription in subscriptionsv2)
await Subscribe(sender, subscription, message.Session.Id, broadcasterId, "2");
sender.Identify(message.Session.Id);
}
private async Task Subscribe(string subscriptionName, string sessionId, string broadcasterId, string version)
private async Task Subscribe(TwitchWebsocketClient sender, string subscriptionName, string sessionId, string broadcasterId, string version)
{
try
{
@ -83,6 +103,10 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger.Error($"Failed to create an event subscription [subscription type: {subscriptionName}][reason: data is empty]");
return;
}
foreach (var d in response.Data)
sender.AddSubscription(broadcasterId, d.Type, d.Id);
_logger.Information($"Sucessfully added subscription to Twitch websockets [subscription type: {subscriptionName}]");
}
catch (Exception ex)

View File

@ -0,0 +1,15 @@
namespace TwitchChatTTS.Twitch.Socket.Messages
{
public class ChannelAdBreakMessage
{
public string DurationSeconds { get; set; }
public DateTime StartedAt { get; set; }
public string IsAutomatic { get; set; }
public string BroadcasterUserId { get; set; }
public string BroadcasterUserLogin { get; set; }
public string BroadcasterUserName { get; set; }
public string RequesterUserId { get; set; }
public string RequesterUserLogin { get; set; }
public string RequesterUserName { get; set; }
}
}

View File

@ -0,0 +1,13 @@
namespace TwitchChatTTS.Twitch.Socket.Messages
{
public class ChannelFollowMessage
{
public string BroadcasterUserId { get; set; }
public string BroadcasterUserLogin { get; set; }
public string BroadcasterUserName { get; set; }
public string UserId { get; set; }
public string UserLogin { get; set; }
public string UserName { get; set; }
public DateTime FollowedAt { get; set; }
}
}

View File

@ -0,0 +1,10 @@
namespace TwitchChatTTS.Twitch.Socket.Messages
{
public class ChannelResubscriptionMessage : ChannelSubscriptionData
{
public TwitchChatMessageInfo Message { get; set; }
public int CumulativeMonths { get; set; }
public int StreakMonths { get; set; }
public int DurationMonths { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace TwitchChatTTS.Twitch.Socket.Messages
{
public class ChannelSubscriptionGiftMessage : ChannelSubscriptionData
{
public int Total { get; set; }
public int? CumulativeTotal { get; set; }
public bool IsAnonymous { get; set; }
}
}

View File

@ -1,17 +1,18 @@
namespace TwitchChatTTS.Twitch.Socket.Messages
{
public class ChannelSubscriptionMessage
public class ChannelSubscriptionData
{
public string UserId { get; set; }
public string UserLogin { get; set; }
public string UserName { get; set; }
public string BroadcasterUserId { get; set; }
public string BroadcasterUserLogin { get; set; }
public string BroadcasterUserName { get; set; }
public string ChatterUserId { get; set; }
public string ChatterUserLogin { get; set; }
public string ChatterUserName { get; set; }
public string Tier { get; set; }
public TwitchChatMessageInfo Message { get; set; }
public int CumulativeMonths { get; set; }
public int StreakMonths { get; set; }
public int DurationMonths { get; set; }
}
public class ChannelSubscriptionMessage : ChannelSubscriptionData
{
public bool IsGifted { get; set; }
}
}

View File

@ -6,5 +6,10 @@ namespace TwitchChatTTS.Twitch.Socket.Messages
public int Total { get; set; }
public int TotalCost { get; set; }
public int MaxTotalCost { get; set; }
public EventResponsePagination? Pagination { get; set; }
}
public class EventResponsePagination {
public string Cursor { get; set; }
}
}

View File

@ -11,7 +11,8 @@ namespace TwitchChatTTS.Twitch.Socket.Messages
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Cost { get; set; }
public EventSubscriptionMessage() {
public EventSubscriptionMessage()
{
Type = string.Empty;
Version = string.Empty;
Condition = new Dictionary<string, string>();
@ -45,7 +46,8 @@ namespace TwitchChatTTS.Twitch.Socket.Messages
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? SessionId { get; }
public EventSubTransport() {
public EventSubTransport()
{
Method = string.Empty;
}

View File

@ -11,6 +11,6 @@ namespace TwitchChatTTS.Twitch.Socket.Messages
public string Id { get; set; }
public string Status { get; set; }
public DateTime CreatedAt { get; set; }
public object Event { get; set; }
public object? Event { get; set; }
}
}

View File

@ -8,7 +8,7 @@ namespace TwitchChatTTS.Twitch.Socket.Messages
public string Id { get; set; }
public string Status { get; set; }
public DateTime ConnectedAt { get; set; }
public int KeepaliveTimeoutSeconds { get; set; }
public int? KeepaliveTimeoutSeconds { get; set; }
public string? ReconnectUrl { get; set; }
public string? RecoveryUrl { get; set; }
}

View File

@ -0,0 +1,119 @@
using CommonSocketLibrary.Abstract;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Socket
{
public interface ITwitchConnectionManager
{
TwitchWebsocketClient GetWorkingClient();
TwitchWebsocketClient GetBackupClient();
}
public class TwitchConnectionManager : ITwitchConnectionManager
{
private TwitchWebsocketClient? _identified;
private TwitchWebsocketClient? _backup;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
private readonly object _lock;
public TwitchConnectionManager(IServiceProvider serviceProvider, ILogger logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
_lock = new object();
}
public TwitchWebsocketClient GetBackupClient()
{
lock (_lock)
{
if (_identified == null)
throw new InvalidOperationException("Cannot get backup Twitch client yet. Waiting for identification.");
if (_backup != null)
return _backup;
return CreateNewClient();
}
}
public TwitchWebsocketClient GetWorkingClient()
{
lock (_lock)
{
if (_identified == null)
{
return CreateNewClient();
}
return _identified;
}
}
private TwitchWebsocketClient CreateNewClient()
{
if (_backup != null)
return _backup;
var client = (_serviceProvider.GetRequiredKeyedService<SocketClient<TwitchWebsocketMessage>>("twitch-create") as TwitchWebsocketClient)!;
client.Initialize();
_backup = client;
client.OnIdentified += async (s, e) =>
{
bool clientDisconnect = false;
lock (_lock)
{
if (_identified == client)
{
_logger.Error("Twitch client has been re-identified.");
return;
}
if (_backup != client)
{
_logger.Warning("Twitch client has been identified, but isn't backup. Disconnecting.");
clientDisconnect = true;
return;
}
if (_identified != null)
{
return;
}
_identified = _backup;
_backup = null;
}
if (clientDisconnect)
await client.DisconnectAsync(new SocketDisconnectionEventArgs("Closed", "No need for a tertiary client."));
_logger.Information("Twitch client has been identified.");
};
client.OnDisconnected += (s, e) =>
{
lock (_lock)
{
if (_identified == client)
{
_identified = null;
}
else if (_backup == client)
{
_backup = null;
}
else
_logger.Error("Twitch client disconnection from unknown source.");
}
};
_logger.Debug("Created a Twitch websocket client.");
return client;
}
}
}

View File

@ -6,26 +6,32 @@ using System.Net.WebSockets;
using TwitchChatTTS.Twitch.Socket.Messages;
using System.Text;
using TwitchChatTTS.Twitch.Socket.Handlers;
using CommonSocketLibrary.Backoff;
namespace TwitchChatTTS.Twitch.Socket
{
public class TwitchWebsocketClient : SocketClient<TwitchWebsocketMessage>
{
private readonly IDictionary<string, ITwitchSocketHandler> _handlers;
private readonly IDictionary<string, Type> _messageTypes;
private readonly IDictionary<string, string> _subscriptions;
private readonly IBackoff _backoff;
private DateTime _lastReceivedMessageTimestamp;
private bool _disconnected;
private readonly object _lock;
public event EventHandler<EventArgs> OnIdentified;
public string URL;
private IDictionary<string, ITwitchSocketHandler> _handlers;
private IDictionary<string, Type> _messageTypes;
private readonly Configuration _configuration;
private System.Timers.Timer _reconnectTimer;
public bool Connected { get; set; }
public bool Identified { get; set; }
public string SessionId { get; set; }
public bool Connected { get; private set; }
public bool Identified { get; private set; }
public string SessionId { get; private set; }
public bool ReceivedReconnecting { get; set; }
public TwitchWebsocketClient(
Configuration configuration,
[FromKeyedServices("twitch")] IEnumerable<ITwitchSocketHandler> handlers,
[FromKeyedServices("twitch")] IBackoff backoff,
ILogger logger
) : base(logger, new JsonSerializerOptions()
{
@ -34,14 +40,12 @@ namespace TwitchChatTTS.Twitch.Socket
})
{
_handlers = handlers.ToDictionary(h => h.Name, h => h);
_configuration = configuration;
_reconnectTimer = new System.Timers.Timer(TimeSpan.FromSeconds(30));
_reconnectTimer.AutoReset = false;
_reconnectTimer.Elapsed += async (sender, e) => await Reconnect();
_reconnectTimer.Enabled = false;
_backoff = backoff;
_subscriptions = new Dictionary<string, string>();
_lock = new object();
_messageTypes = new Dictionary<string, Type>();
_messageTypes.Add("session_keepalive", typeof(object));
_messageTypes.Add("session_welcome", typeof(SessionWelcomeMessage));
_messageTypes.Add("session_reconnect", typeof(SessionWelcomeMessage));
_messageTypes.Add("notification", typeof(NotificationMessage));
@ -50,23 +54,56 @@ namespace TwitchChatTTS.Twitch.Socket
}
public void AddSubscription(string broadcasterId, string type, string id)
{
if (_subscriptions.ContainsKey(broadcasterId + '|' + type))
_subscriptions[broadcasterId + '|' + type] = id;
else
_subscriptions.Add(broadcasterId + '|' + type, id);
}
public string? GetSubscriptionId(string broadcasterId, string type)
{
if (_subscriptions.TryGetValue(broadcasterId + '|' + type, out var id))
return id;
return null;
}
public void RemoveSubscription(string broadcasterId, string type)
{
_subscriptions.Remove(broadcasterId + '|' + type);
}
public void Initialize()
{
_logger.Information($"Initializing OBS websocket client.");
OnConnected += (sender, e) =>
{
Connected = true;
_reconnectTimer.Enabled = false;
_logger.Information("Twitch websocket client connected.");
_disconnected = false;
};
OnDisconnected += (sender, e) =>
OnDisconnected += async (sender, e) =>
{
_reconnectTimer.Enabled = Identified;
_logger.Information($"Twitch websocket client disconnected [status: {e.Status}][reason: {e.Reason}] " + (Identified ? "Will be attempting to reconnect every 30 seconds." : "Will not be attempting to reconnect."));
lock (_lock)
{
if (_disconnected)
return;
_disconnected = true;
}
_logger.Information($"Twitch websocket client disconnected [status: {e.Status}][reason: {e.Reason}]");
Connected = false;
Identified = false;
if (!ReceivedReconnecting)
{
_logger.Information("Attempting to reconnect to Twitch websocket server.");
await Reconnect(_backoff, async () => await Connect());
}
};
}
@ -79,42 +116,14 @@ namespace TwitchChatTTS.Twitch.Socket
}
_logger.Debug($"Twitch websocket client attempting to connect to {URL}");
try
{
await ConnectAsync(URL);
}
catch (Exception)
{
_logger.Warning("Connecting to twitch failed. Skipping Twitch websockets.");
}
}
private async Task Reconnect()
public void Identify(string sessionId)
{
if (Connected)
{
try
{
await DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), ""));
}
catch (Exception)
{
_logger.Error("Failed to disconnect from Twitch websocket server.");
}
}
try
{
await Connect();
}
catch (WebSocketException wse) when (wse.Message.Contains("502"))
{
_logger.Error("Twitch websocket server cannot be found.");
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to reconnect to Twitch websocket server.");
}
Identified = true;
SessionId = sessionId;
OnIdentified?.Invoke(this, EventArgs.Empty);
}
protected TwitchWebsocketMessage GenerateMessage<T>(string messageType, T data)
@ -134,14 +143,17 @@ namespace TwitchChatTTS.Twitch.Socket
protected override async Task OnResponseReceived(TwitchWebsocketMessage? message)
{
if (message == null || message.Metadata == null) {
if (message == null || message.Metadata == null)
{
_logger.Information("Twitch message is null");
return;
}
_lastReceivedMessageTimestamp = DateTime.UtcNow;
string content = message.Payload?.ToString() ?? string.Empty;
if (message.Metadata.MessageType != "session_keepalive")
_logger.Information("Twitch RX #" + message.Metadata.MessageType + ": " + content);
_logger.Debug("Twitch RX #" + message.Metadata.MessageType + ": " + content);
if (!_messageTypes.TryGetValue(message.Metadata.MessageType, out var type) || type == null)
{
@ -156,6 +168,11 @@ namespace TwitchChatTTS.Twitch.Socket
}
var data = JsonSerializer.Deserialize(content, type, _options);
if (data == null)
{
_logger.Warning("Twitch websocket message payload is null.");
return;
}
await handler.Execute(this, data);
}
@ -180,7 +197,7 @@ namespace TwitchChatTTS.Twitch.Socket
await _socket!.SendAsync(array, WebSocketMessageType.Text, current + size >= total, _cts!.Token);
current += size;
}
_logger.Information("TX #" + type + ": " + content);
_logger.Debug("Twitch TX #" + type + ": " + content);
}
catch (Exception e)
{

View File

@ -1,29 +0,0 @@
// using System.Text.RegularExpressions;
// using HermesSocketLibrary.Request.Message;
// using TwitchChatTTS.Hermes;
// namespace TwitchChatTTS.Twitch
// {
// public class TTSContext
// {
// public string DefaultVoice;
// public IEnumerable<TTSVoice>? EnabledVoices;
// public IDictionary<string, TTSUsernameFilter>? UsernameFilters;
// public IEnumerable<TTSWordFilter>? WordFilters;
// public IList<VoiceDetails>? AvailableVoices { get => _availableVoices; set { _availableVoices = value; EnabledVoicesRegex = GenerateEnabledVoicesRegex(); } }
// public IDictionary<long, string>? SelectedVoices;
// public Regex? EnabledVoicesRegex;
// private IList<VoiceDetails>? _availableVoices;
// private Regex? GenerateEnabledVoicesRegex() {
// if (AvailableVoices == null || AvailableVoices.Count() <= 0) {
// return null;
// }
// var enabledVoicesString = string.Join("|", AvailableVoices.Select(v => v.Name));
// return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase);
// }
// }
// }

View File

@ -24,35 +24,40 @@ public class TwitchApiClient
});
}
public async Task<EventResponse<EventSubscriptionMessage>?> CreateEventSubscription(string type, string version, string userId)
public async Task<EventResponse<NotificationInfo>?> CreateEventSubscription(string type, string version, string sessionId, string userId, string? broadcasterId = null)
{
var conditions = new Dictionary<string, string>() { { "user_id", userId }, { "broadcaster_user_id", userId }, { "moderator_user_id", userId } };
var subscriptionData = new EventSubscriptionMessage(type, version, "https://hermes.goblincaves.com/api/account/authorize", "isdnmjfopsdfmsf4390", conditions);
var response = await _web.Post("https://api.twitch.tv/helix/eventsub/subscriptions", subscriptionData);
if (response.StatusCode == HttpStatusCode.Accepted)
{
_logger.Debug("Twitch API call [type: create event subscription]: " + await response.Content.ReadAsStringAsync());
return await response.Content.ReadFromJsonAsync(typeof(EventResponse<EventSubscriptionMessage>)) as EventResponse<EventSubscriptionMessage>;
}
_logger.Warning("Twitch api failed to create event subscription: " + await response.Content.ReadAsStringAsync());
return null;
}
public async Task<EventResponse<EventSubscriptionMessage>?> CreateEventSubscription(string type, string version, string sessionId, string userId)
{
var conditions = new Dictionary<string, string>() { { "user_id", userId }, { "broadcaster_user_id", userId }, { "moderator_user_id", userId } };
var conditions = new Dictionary<string, string>() { { "user_id", userId }, { "broadcaster_user_id", broadcasterId ?? userId }, { "moderator_user_id", broadcasterId ?? userId } };
var subscriptionData = new EventSubscriptionMessage(type, version, sessionId, conditions);
var response = await _web.Post("https://api.twitch.tv/helix/eventsub/subscriptions", subscriptionData);
if (response.StatusCode == HttpStatusCode.Accepted)
{
_logger.Debug("Twitch API call [type: create event subscription]: " + await response.Content.ReadAsStringAsync());
return await response.Content.ReadFromJsonAsync(typeof(EventResponse<EventSubscriptionMessage>)) as EventResponse<EventSubscriptionMessage>;
return await response.Content.ReadFromJsonAsync(typeof(EventResponse<NotificationInfo>)) as EventResponse<NotificationInfo>;
}
_logger.Error("Twitch api failed to create event subscription: " + await response.Content.ReadAsStringAsync());
_logger.Error("Twitch api failed to create event subscription for websocket: " + await response.Content.ReadAsStringAsync());
return null;
}
public void Initialize(TwitchBotToken token) {
public async Task DeleteEventSubscription(string subscriptionId)
{
await _web.Delete("https://api.twitch.tv/helix/eventsub/subscriptions?id=" + subscriptionId);
}
public async Task<EventResponse<NotificationInfo>?> GetSubscriptions(string? status = null, string? broadcasterId = null, string? after = null)
{
List<string> queryParams = new List<string>();
if (!string.IsNullOrWhiteSpace(status))
queryParams.Add("status=" + status);
if (!string.IsNullOrWhiteSpace(broadcasterId))
queryParams.Add("user_id=" + broadcasterId);
if (!string.IsNullOrWhiteSpace(after))
queryParams.Add("after=" + after);
var query = queryParams.Any() ? '?' + string.Join('&', queryParams) : string.Empty;
return await _web.GetJson<EventResponse<NotificationInfo>>("https://api.twitch.tv/helix/eventsub/subscriptions" + query);
}
public void Initialize(TwitchBotToken token)
{
_web.AddHeader("Authorization", "Bearer " + token.AccessToken);
_web.AddHeader("Client-Id", token.ClientId);
}