Added several redemption actions. Added certain login features. Fixed OBS command. Added more logging.
This commit is contained in:
parent
706eecf2d2
commit
af3763a837
@ -186,7 +186,10 @@ public class ChatMessageHandler
|
||||
var voiceId = _user.VoicesSelected[userId];
|
||||
if (_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null)
|
||||
{
|
||||
voiceSelected = voiceName;
|
||||
if (_user.VoicesEnabled.Contains(voiceName) || chatterId == _user.OwnerId || m.IsStaff)
|
||||
{
|
||||
voiceSelected = voiceName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,9 +222,7 @@ public class ChatMessageHandler
|
||||
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);
|
||||
|
@ -30,7 +30,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
|
||||
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchLib.Client.Models;
|
||||
@ -8,14 +9,16 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
private IDictionary<string, ChatCommand> _commands;
|
||||
private readonly TwitchBotAuth _token;
|
||||
private readonly User _user;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger _logger;
|
||||
private string CommandStartSign { get; } = "!";
|
||||
|
||||
|
||||
public ChatCommandManager(TwitchBotAuth token, IServiceProvider serviceProvider, ILogger logger)
|
||||
public ChatCommandManager(TwitchBotAuth token, User user, IServiceProvider serviceProvider, ILogger logger)
|
||||
{
|
||||
_token = token;
|
||||
_user = user;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
@ -65,7 +68,11 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
if (!arg.StartsWith(CommandStartSign))
|
||||
return ChatCommandResult.Unknown;
|
||||
|
||||
string[] parts = arg.Split(" ");
|
||||
string[] parts = Regex.Matches(arg, "(?<match>[^\"\\n\\s]+|\"[^\"\\n]*\")")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups["match"].Value)
|
||||
.Select(m => m.StartsWith('"') && m.EndsWith('"') ? m.Substring(1, m.Length - 2) : m)
|
||||
.ToArray();
|
||||
string com = parts.First().Substring(CommandStartSign.Length).ToLower();
|
||||
string[] args = parts.Skip(1).ToArray();
|
||||
long broadcasterId = long.Parse(_token.BroadcasterId);
|
||||
@ -77,7 +84,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
return ChatCommandResult.Missing;
|
||||
}
|
||||
|
||||
if (!await command.CheckPermissions(message, broadcasterId) && message.UserId != "126224566" && !message.IsStaff)
|
||||
if (!await command.CheckPermissions(message, broadcasterId) && message.UserId != _user.OwnerId?.ToString() && !message.IsStaff)
|
||||
{
|
||||
_logger.Warning($"Chatter is missing permission to execute command named '{com}' [args: {arg}][chatter: {message.Username}][cid: {message.UserId}]");
|
||||
return ChatCommandResult.Permission;
|
||||
@ -108,7 +115,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
return ChatCommandResult.Fail;
|
||||
}
|
||||
|
||||
_logger.Information($"Executed the {com} command with the following args: " + string.Join(" ", args));
|
||||
_logger.Information($"Executed the {com} command [arguments: {arg}]");
|
||||
return ChatCommandResult.Success;
|
||||
}
|
||||
}
|
||||
|
@ -40,27 +40,45 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
if (_user == null || _user.VoicesAvailable == null)
|
||||
return;
|
||||
|
||||
var voiceName = args[0].ToLower();
|
||||
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
||||
var action = args[1].ToLower();
|
||||
var action = args[0].ToLower();
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "sleep":
|
||||
await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary<string, object>() { { "sleepMillis", 10000 } }));
|
||||
break;
|
||||
case "get_scene_item_id":
|
||||
await _manager.Send(new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" }, { "rotation", 90 } }));
|
||||
if (args.Count < 3)
|
||||
return;
|
||||
|
||||
_logger.Debug($"Getting scene item id via chat command [args: {string.Join(" ", args)}]");
|
||||
await _manager.Send(new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", args[1] }, { "sourceName", args[2] } }));
|
||||
break;
|
||||
case "transform":
|
||||
if (args.Count < 5)
|
||||
return;
|
||||
|
||||
_logger.Debug($"Getting scene item transformation data via chat command [args: {string.Join(" ", args)}]");
|
||||
await _manager.UpdateTransformation(args[1], args[2], (d) =>
|
||||
{
|
||||
|
||||
if (args[3].ToLower() == "rotation")
|
||||
d.Rotation = int.Parse(args[4]);
|
||||
else if (args[3].ToLower() == "x")
|
||||
d.Rotation = int.Parse(args[4]);
|
||||
else if (args[3].ToLower() == "y")
|
||||
d.PositionY = int.Parse(args[4]);
|
||||
});
|
||||
await _manager.Send(new RequestMessage("Transform", string.Empty, new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sceneItemId", 90 }, { "rotation", 90 } }));
|
||||
break;
|
||||
case "remove":
|
||||
await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary<string, object>() { { "sleepMillis", 10000 } }));
|
||||
case "sleep":
|
||||
if (args.Count < 2)
|
||||
return;
|
||||
|
||||
_logger.Debug($"Sending OBS to sleep via chat command [args: {string.Join(" ", args)}]");
|
||||
await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary<string, object>() { { "sleepMillis", int.Parse(args[1]) } }));
|
||||
break;
|
||||
case "visibility":
|
||||
if (args.Count < 4)
|
||||
return;
|
||||
|
||||
_logger.Debug($"Updating scene item visibility via chat command [args: {string.Join(" ", args)}]");
|
||||
await _manager.UpdateSceneItemVisibility(args[1], args[2], args[3].ToLower() == "true");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Twitch.Redemptions;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
@ -6,13 +7,15 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
public class RefreshTTSDataCommand : ChatCommand
|
||||
{
|
||||
private readonly User _user;
|
||||
private readonly RedemptionManager _redemptionManager;
|
||||
private readonly HermesApiClient _hermesApi;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public RefreshTTSDataCommand(User user, HermesApiClient hermesApi, ILogger logger)
|
||||
public RefreshTTSDataCommand(User user, RedemptionManager redemptionManager, HermesApiClient hermesApi, ILogger logger)
|
||||
: base("refresh", "Refreshes certain TTS related data on the client.")
|
||||
{
|
||||
_user = user;
|
||||
_redemptionManager = redemptionManager;
|
||||
_hermesApi = hermesApi;
|
||||
_logger = logger;
|
||||
}
|
||||
@ -51,7 +54,13 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
break;
|
||||
case "default_voice":
|
||||
_user.DefaultTTSVoice = await _hermesApi.FetchTTSDefaultVoice();
|
||||
_logger.Information("Default Voice: " + _user.DefaultTTSVoice);
|
||||
_logger.Information("TTS Default Voice: " + _user.DefaultTTSVoice);
|
||||
break;
|
||||
case "redemptions":
|
||||
var redemptionActions = await _hermesApi.FetchRedeemableActions();
|
||||
var redemptions = await _hermesApi.FetchRedemptions();
|
||||
_redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a));
|
||||
_logger.Information($"Redemption Manager has been refreshed with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
|
||||
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||
{
|
||||
return message.IsBroadcaster;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||
|
@ -32,7 +32,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
|
||||
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||
{
|
||||
return message.IsBroadcaster;
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||
@ -52,6 +52,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
Type = "update_tts_voice_state",
|
||||
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", true } }
|
||||
});
|
||||
_logger.Information($"Enabled a TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]");
|
||||
break;
|
||||
case "disable":
|
||||
await _hermesClient.Send(3, new RequestMessage()
|
||||
@ -59,17 +60,9 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
Type = "update_tts_voice_state",
|
||||
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", false } }
|
||||
});
|
||||
break;
|
||||
case "remove":
|
||||
await _hermesClient.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "delete_tts_voice",
|
||||
Data = new Dictionary<string, object>() { { "voice", voiceId } }
|
||||
});
|
||||
_logger.Information($"Disabled a TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]");
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.Information($"Added a new TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]");
|
||||
}
|
||||
}
|
||||
}
|
@ -35,19 +35,23 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||
{
|
||||
if (_user == null || _user.VoicesSelected == null || _user.VoicesAvailable == null)
|
||||
if (_user == null || _user.VoicesSelected == null || _user.VoicesEnabled == null)
|
||||
return;
|
||||
|
||||
long chatterId = long.Parse(message.UserId);
|
||||
var voiceName = args.First().ToLower();
|
||||
var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceName);
|
||||
var enabled = _user.VoicesEnabled.Contains(voice.Value);
|
||||
|
||||
await _hermesClient.Send(3, new RequestMessage()
|
||||
if (enabled)
|
||||
{
|
||||
Type = _user.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user",
|
||||
Data = new Dictionary<string, object>() { { "chatter", chatterId }, { "voice", voice.Key } }
|
||||
});
|
||||
_logger.Information($"Updated chat TTS voice [voice: {voice.Value}][username: {message.Username}].");
|
||||
await _hermesClient.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = _user.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user",
|
||||
Data = new Dictionary<string, object>() { { "chatter", chatterId }, { "voice", voice.Key } }
|
||||
});
|
||||
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.Username}].");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
if (message.AnotherClient)
|
||||
{
|
||||
_logger.Warning("Another client has connected to the same account.");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -36,6 +37,8 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
_logger.Information($"Logged in as {_user.TwitchUsername}.");
|
||||
}
|
||||
|
||||
_user.OwnerId = message.OwnerId;
|
||||
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_tts_voices",
|
||||
|
@ -12,6 +12,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
{
|
||||
public class RequestAckHandler : IWebSocketHandler
|
||||
{
|
||||
private User _user;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly JsonSerializerOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
@ -20,8 +21,9 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
|
||||
public int OperationCode { get; } = 4;
|
||||
|
||||
public RequestAckHandler(IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger)
|
||||
public RequestAckHandler(User user, IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger)
|
||||
{
|
||||
_user = user;
|
||||
_serviceProvider = serviceProvider;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
@ -33,8 +35,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
return;
|
||||
if (message.Request == null)
|
||||
return;
|
||||
var context = _serviceProvider.GetRequiredService<User>();
|
||||
if (context == null)
|
||||
if (_user == null)
|
||||
return;
|
||||
|
||||
if (message.Request.Type == "get_tts_voices")
|
||||
@ -46,7 +47,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
|
||||
lock (_voicesAvailableLock)
|
||||
{
|
||||
context.VoicesAvailable = voices.ToDictionary(e => e.Id, e => e.Name);
|
||||
_user.VoicesAvailable = voices.ToDictionary(e => e.Id, e => e.Name);
|
||||
}
|
||||
_logger.Information("Updated all available voices for TTS.");
|
||||
}
|
||||
@ -54,22 +55,28 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
{
|
||||
_logger.Verbose("Adding new tts voice for user.");
|
||||
if (!long.TryParse(message.Request.Data["user"].ToString(), out long chatterId))
|
||||
{
|
||||
_logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]");
|
||||
return;
|
||||
}
|
||||
string userId = message.Request.Data["user"].ToString();
|
||||
string voice = message.Request.Data["voice"].ToString();
|
||||
|
||||
context.VoicesSelected.Add(chatterId, voice);
|
||||
_user.VoicesSelected.Add(chatterId, voice);
|
||||
_logger.Information($"Added new TTS voice [voice: {voice}] for user [user id: {userId}]");
|
||||
}
|
||||
else if (message.Request.Type == "update_tts_user")
|
||||
{
|
||||
_logger.Verbose("Updating user's voice");
|
||||
if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId))
|
||||
{
|
||||
_logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]");
|
||||
return;
|
||||
}
|
||||
string userId = message.Request.Data["user"].ToString();
|
||||
string voice = message.Request.Data["voice"].ToString();
|
||||
|
||||
context.VoicesSelected[chatterId] = voice;
|
||||
_user.VoicesSelected[chatterId] = voice;
|
||||
_logger.Information($"Updated TTS voice [voice: {voice}] for user [user id: {userId}]");
|
||||
}
|
||||
else if (message.Request.Type == "create_tts_voice")
|
||||
@ -82,9 +89,9 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
|
||||
lock (_voicesAvailableLock)
|
||||
{
|
||||
var list = context.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value);
|
||||
var list = _user.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value);
|
||||
list.Add(voiceId, voice);
|
||||
context.VoicesAvailable = list;
|
||||
_user.VoicesAvailable = list;
|
||||
}
|
||||
_logger.Information($"Created new tts voice [voice: {voice}][id: {voiceId}].");
|
||||
}
|
||||
@ -92,14 +99,14 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
{
|
||||
_logger.Verbose("Deleting tts voice.");
|
||||
var voice = message.Request.Data["voice"].ToString();
|
||||
if (!context.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null)
|
||||
if (!_user.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null)
|
||||
return;
|
||||
|
||||
lock (_voicesAvailableLock)
|
||||
{
|
||||
var dict = context.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value);
|
||||
var dict = _user.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value);
|
||||
dict.Remove(voice);
|
||||
context.VoicesAvailable.Remove(voice);
|
||||
_user.VoicesAvailable.Remove(voice);
|
||||
}
|
||||
_logger.Information($"Deleted a voice [voice: {voiceName}]");
|
||||
}
|
||||
@ -109,10 +116,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
string voiceId = message.Request.Data["idd"].ToString();
|
||||
string voice = message.Request.Data["voice"].ToString();
|
||||
|
||||
if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null)
|
||||
if (!_user.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null)
|
||||
return;
|
||||
|
||||
context.VoicesAvailable[voiceId] = voice;
|
||||
_user.VoicesAvailable[voiceId] = voice;
|
||||
_logger.Information($"Updated TTS voice [voice: {voice}][id: {voiceId}]");
|
||||
}
|
||||
else if (message.Request.Type == "get_tts_users")
|
||||
@ -125,7 +132,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
var temp = new ConcurrentDictionary<long, string>();
|
||||
foreach (var entry in users)
|
||||
temp.TryAdd(entry.Key, entry.Value);
|
||||
context.VoicesSelected = temp;
|
||||
_user.VoicesSelected = temp;
|
||||
_logger.Information($"Updated {temp.Count()} chatters' selected voice.");
|
||||
}
|
||||
else if (message.Request.Type == "get_chatter_ids")
|
||||
@ -162,18 +169,18 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
{
|
||||
_logger.Verbose("Updating TTS voice states.");
|
||||
string voiceId = message.Request.Data["voice"].ToString();
|
||||
bool state = message.Request.Data["state"].ToString() == "true";
|
||||
bool state = message.Request.Data["state"].ToString().ToLower() == "true";
|
||||
|
||||
if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null)
|
||||
if (!_user.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find voice [id: {voiceId}]");
|
||||
_logger.Warning($"Failed to find voice by id [id: {voiceId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state)
|
||||
context.VoicesEnabled.Add(voiceId);
|
||||
_user.VoicesEnabled.Add(voiceId);
|
||||
else
|
||||
context.VoicesEnabled.Remove(voiceId);
|
||||
_user.VoicesEnabled.Remove(voiceId);
|
||||
_logger.Information($"Updated voice state [voice: {voiceName}][new state: {(state ? "enabled" : "disabled")}]");
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +100,12 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
LastHeartbeatReceived = DateTime.UtcNow;
|
||||
|
||||
if (_configuration.Hermes?.Token != null)
|
||||
await Send(1, new HermesLoginMessage() { ApiKey = _configuration.Hermes.Token });
|
||||
await Send(1, new HermesLoginMessage()
|
||||
{
|
||||
ApiKey = _configuration.Hermes.Token,
|
||||
MajorVersion = TTS.MAJOR_VERSION,
|
||||
MinorVersion = TTS.MINOR_VERSION,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,21 @@
|
||||
public class TwitchBotAuth {
|
||||
public class TwitchBotAuth
|
||||
{
|
||||
public string? UserId { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
public string? BroadcasterId { get; set; }
|
||||
public long? ExpiresIn
|
||||
{
|
||||
get => _expiresIn;
|
||||
set
|
||||
{
|
||||
_expiresIn = value;
|
||||
if (value != null)
|
||||
ExpiresAt = DateTime.UtcNow + TimeSpan.FromSeconds((double) value);
|
||||
}
|
||||
}
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
private long? _expiresIn;
|
||||
}
|
@ -22,14 +22,6 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
|
||||
sender.Connected = true;
|
||||
_logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + ".");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
|
||||
/*var messages = new RequestMessage[] {
|
||||
//new RequestMessage("Sleep", string.Empty, new Dictionary<string, object>() { { "sleepMillis", 5000 } }),
|
||||
new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" } }),
|
||||
};
|
||||
await _manager.Send(messages);*/
|
||||
}
|
||||
}
|
||||
}
|
@ -23,12 +23,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
if (data is not RequestResponseMessage message || message == null)
|
||||
return;
|
||||
|
||||
_logger.Debug($"Received an OBS request response [response id: {message.RequestId}]");
|
||||
_logger.Debug($"Received an OBS request response [obs request id: {message.RequestId}]");
|
||||
|
||||
var requestData = _manager.Take(message.RequestId);
|
||||
if (requestData == null)
|
||||
{
|
||||
_logger.Warning($"OBS Request Response not being processed: request not stored [response id: {message.RequestId}]");
|
||||
_logger.Warning($"OBS Request Response not being processed: request not stored [obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -44,54 +44,149 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
if (sender is not OBSSocketClient client)
|
||||
return;
|
||||
|
||||
if (message.RequestId == "stream")
|
||||
{
|
||||
client.Live = message.ResponseData["outputActive"].ToString() == "True";
|
||||
_logger.Warning($"Updated stream's live status to {client.Live} [response id: {message.RequestId}]");
|
||||
}
|
||||
_logger.Debug($"Fetched stream's live status [live: {client.Live}][obs request id: {message.RequestId}]");
|
||||
break;
|
||||
case "GetSceneItemId":
|
||||
if (!request.RequestData.TryGetValue("sceneName", out object sceneName))
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene name that was requested [response id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (!request.RequestData.TryGetValue("sourceName", out object sourceName))
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][response id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (!message.ResponseData.TryGetValue("sceneItemId", out object sceneItemId)) {
|
||||
_logger.Warning($"Failed to fetch the scene item id [scene: {sceneName}][scene item: {sourceName}][response id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (!request.RequestData.TryGetValue("sourceName", out object? sourceName) || sourceName == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (message.ResponseData == null)
|
||||
{
|
||||
_logger.Warning($"OBS Response is null [scene: {sceneName}][scene item: {sourceName}][obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (!message.ResponseData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null)
|
||||
{
|
||||
_logger.Warning($"Failed to fetch the scene item id [scene: {sceneName}][scene item: {sourceName}][obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Information($"Added scene item id [scene: {sceneName}][source: {sourceName}][id: {sceneItemId}][response id: {message.RequestId}].");
|
||||
_manager.AddSourceId(sceneName.ToString(), sourceName.ToString(), long.Parse(sceneItemId.ToString()));
|
||||
_logger.Debug($"Found the scene item by name [scene: {sceneName}][source: {sourceName}][id: {sceneItemId}][obs request id: {message.RequestId}].");
|
||||
//_manager.AddSourceId(sceneName.ToString(), sourceName.ToString(), (long) sceneItemId);
|
||||
|
||||
requestData.ResponseValues = new Dictionary<string, object>
|
||||
{
|
||||
{ "sceneItemId", sceneItemId }
|
||||
};
|
||||
break;
|
||||
requestData.ResponseValues = new Dictionary<string, object>
|
||||
{
|
||||
{ "sceneItemId", sceneItemId }
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "GetSceneItemTransform":
|
||||
if (!message.ResponseData.TryGetValue("sceneItemTransform", out object? transformData))
|
||||
{
|
||||
_logger.Warning($"Failed to find the OBS scene item [response id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (message.ResponseData == null)
|
||||
{
|
||||
_logger.Warning($"OBS Response is null [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (!message.ResponseData.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null)
|
||||
{
|
||||
_logger.Warning($"Failed to fetch the OBS transformation data [obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Verbose("Fetching OBS transformation data: " + transformData?.ToString());
|
||||
requestData.ResponseValues = new Dictionary<string, object>
|
||||
_logger.Debug($"Fetched OBS transformation data [scene: {sceneName}][scene item id: {sceneItemId}][transformation: {transformData}][obs request id: {message.RequestId}]");
|
||||
requestData.ResponseValues = new Dictionary<string, object>
|
||||
{
|
||||
{ "sceneItemTransform", transformData }
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "GetSceneItemEnabled":
|
||||
{
|
||||
{ "sceneItemTransform", transformData }
|
||||
};
|
||||
break;
|
||||
if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (message.ResponseData == null)
|
||||
{
|
||||
_logger.Warning($"OBS Response is null [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (!message.ResponseData.TryGetValue("sceneItemEnabled", out object? sceneItemVisibility) || sceneItemVisibility == null)
|
||||
{
|
||||
_logger.Warning($"Failed to fetch the scene item visibility [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug($"Fetched OBS scene item visibility [scene: {sceneName}][scene item id: {sceneItemId}][visibility: {sceneItemVisibility}][obs request id: {message.RequestId}]");
|
||||
requestData.ResponseValues = new Dictionary<string, object>
|
||||
{
|
||||
{ "sceneItemEnabled", sceneItemVisibility }
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "SetSceneItemTransform":
|
||||
{
|
||||
if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
_logger.Debug($"Received response from OBS for updating scene item transformation [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]");
|
||||
break;
|
||||
}
|
||||
case "SetSceneItemEnabled":
|
||||
{
|
||||
if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
_logger.Debug($"Received response from OBS for updating scene item visibility [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]");
|
||||
break;
|
||||
}
|
||||
case "Sleep":
|
||||
{
|
||||
if (!request.RequestData.TryGetValue("sleepMillis", out object? sleepMillis) || sleepMillis == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]");
|
||||
return;
|
||||
}
|
||||
_logger.Debug($"Received response from OBS for sleeping [sleep: {sleepMillis}][obs request id: {message.RequestId}]");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
_logger.Warning($"OBS Request Response not being processed [type: {request.RequestType}][{string.Join(Environment.NewLine, message.ResponseData?.Select(kvp => kvp.Key + " = " + kvp.Value?.ToString()) ?? new string[0])}]");
|
||||
_logger.Warning($"OBS Request Response not being processed [type: {request.RequestType}][{string.Join(Environment.NewLine, message.ResponseData?.Select(kvp => kvp.Key + " = " + kvp.Value?.ToString()) ?? [])}]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, $"Failed to process the response from OBS for a request [type: {request.RequestType}]");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (requestData.Callback != null)
|
||||
|
@ -43,7 +43,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager
|
||||
public async Task Send(IEnumerable<RequestMessage> messages)
|
||||
{
|
||||
string uid = GenerateUniqueIdentifier();
|
||||
_logger.Debug($"Sending OBS request batch of {messages.Count()} messages [obsid: {uid}].");
|
||||
_logger.Debug($"Sending OBS request batch of {messages.Count()} messages [obs request id: {uid}].");
|
||||
|
||||
// Keep track of requests to know what we requested.
|
||||
foreach (var message in messages)
|
||||
@ -52,7 +52,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager
|
||||
var data = new RequestData(message, uid);
|
||||
_requests.Add(message.RequestId, data);
|
||||
}
|
||||
_logger.Debug($"Generated uid for all OBS request messages in batch [obsid: {uid}]: {string.Join(", ", messages.Select(m => m.RequestType + "=" + m.RequestId))}");
|
||||
_logger.Debug($"Generated uid for all OBS request messages in batch [obs request id: {uid}][obs request ids: {string.Join(", ", messages.Select(m => m.RequestType + "=" + m.RequestId))}]");
|
||||
|
||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
|
||||
await client.Send(8, new RequestBatchMessage(uid, messages));
|
||||
@ -61,7 +61,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager
|
||||
public async Task Send(RequestMessage message, Action<Dictionary<string, object>>? callback = null)
|
||||
{
|
||||
string uid = GenerateUniqueIdentifier();
|
||||
_logger.Debug($"Sending an OBS request [obsid: {uid}]");
|
||||
_logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]");
|
||||
|
||||
// Keep track of requests to know what we requested.
|
||||
message.RequestId = GenerateUniqueIdentifier();
|
||||
@ -87,22 +87,18 @@ namespace TwitchChatTTS.OBS.Socket.Manager
|
||||
|
||||
public async Task UpdateTransformation(string sceneName, string sceneItemName, Action<OBSTransformationData> action)
|
||||
{
|
||||
var m1 = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sourceName", sceneItemName } });
|
||||
await Send(m1, async (d) =>
|
||||
{
|
||||
if (!d.TryGetValue("sceneItemId", out object value) || !long.TryParse(value.ToString(), out long sceneItemId))
|
||||
return;
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
_logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][obsid: {m1.RequestId}]: {sceneItemId}");
|
||||
await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) =>
|
||||
{
|
||||
var m2 = new RequestMessage("GetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
|
||||
await Send(m2, async (d) =>
|
||||
{
|
||||
if (d == null)
|
||||
return;
|
||||
if (!d.TryGetValue("sceneItemTransform", out object transformData))
|
||||
if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null)
|
||||
return;
|
||||
|
||||
_logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obsid: {m2.RequestId}]: {transformData}");
|
||||
_logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]");
|
||||
var transform = JsonSerializer.Deserialize<OBSTransformationData>(transformData.ToString(), new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
@ -110,88 +106,121 @@ namespace TwitchChatTTS.OBS.Socket.Manager
|
||||
});
|
||||
if (transform == null)
|
||||
{
|
||||
_logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obsid: {m2.RequestId}].");
|
||||
_logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m2.RequestId}].");
|
||||
return;
|
||||
}
|
||||
|
||||
//double fr = (transform.Rotation + rotation) % 360;
|
||||
double w = transform.Width;
|
||||
double h = transform.Height;
|
||||
|
||||
// double ox = w * Math.Cos(r) - h * Math.Sin(r);
|
||||
// double oy = w * Math.Sin(r) + h * Math.Cos(r);
|
||||
//var oo = (fr > 45 && fr < 225 ? 0 : 1);
|
||||
// var ww = fr >= 135 && fr < 225 ? h : w;
|
||||
// var hh = fr >= 315 || fr < 45 ? h : w;
|
||||
//double dx = h * Math.Sin(r);
|
||||
//double dy = w * Math.Cos(fr > 90 && fr < 270 ? Math.PI - r : r); // * (fr >= 135 && fr < 225 || fr >= 315 || fr <= 45 ? -1 : 1);
|
||||
|
||||
int a = transform.Alignment;
|
||||
bool hasBounds = transform.BoundsType != "OBS_BOUNDS_NONE";
|
||||
|
||||
if (hasBounds)
|
||||
{
|
||||
// Take care of bounds, for most cases.
|
||||
// 'Crop to Bounding Box' might be unsupported.
|
||||
w = transform.BoundsWidth;
|
||||
h = transform.BoundsHeight;
|
||||
a = transform.BoundsAlignment;
|
||||
}
|
||||
else if (transform.CropBottom + transform.CropLeft + transform.CropRight + transform.CropTop > 0)
|
||||
{
|
||||
w -= transform.CropLeft + transform.CropRight;
|
||||
h -= transform.CropTop + transform.CropBottom;
|
||||
}
|
||||
|
||||
if (a != (int)OBSAlignment.Center)
|
||||
{
|
||||
if (hasBounds)
|
||||
transform.BoundsAlignment = a = (int)OBSAlignment.Center;
|
||||
else
|
||||
transform.Alignment = a = (int)OBSAlignment.Center;
|
||||
|
||||
transform.PositionX = transform.PositionX + w / 2;
|
||||
transform.PositionY = transform.PositionY + h / 2;
|
||||
}
|
||||
|
||||
// if (hasBounds)
|
||||
// {
|
||||
// // Take care of bounds, for most cases.
|
||||
// // 'Crop to Bounding Box' might be unsupported.
|
||||
// w = transform.BoundsWidth;
|
||||
// h = transform.BoundsHeight;
|
||||
// a = transform.BoundsAlignment;
|
||||
// }
|
||||
// else if (transform.CropBottom + transform.CropLeft + transform.CropRight + transform.CropTop > 0)
|
||||
// {
|
||||
// w -= transform.CropLeft + transform.CropRight;
|
||||
// h -= transform.CropTop + transform.CropBottom;
|
||||
// }
|
||||
|
||||
action?.Invoke(transform);
|
||||
|
||||
// double ax = w * Math.Cos(ir) - h * Math.Sin(ir);
|
||||
// double ay = w * Math.Sin(ir) + h * Math.Cos(ir);
|
||||
// _logger.Information($"ax: {ax} ay: {ay}");
|
||||
|
||||
// double bx = w * Math.Cos(r) - h * Math.Sin(r);
|
||||
// double by = w * Math.Sin(r) + h * Math.Cos(r);
|
||||
// _logger.Information($"bx: {bx} by: {by}");
|
||||
|
||||
// double ddx = bx - ax;
|
||||
// double ddy = by - ay;
|
||||
// _logger.Information($"dx: {ddx} dy: {ddy}");
|
||||
|
||||
// double arctan = Math.Atan(ddy / ddx);
|
||||
// _logger.Information("Angle: " + arctan);
|
||||
|
||||
// var xs = new int[] { 0, 0, 1, 1 };
|
||||
// var ys = new int[] { 0, 1, 1, 0 };
|
||||
// int i = ((int)Math.Floor(fr / 90) + 8) % 4;
|
||||
// double dx = xs[i] * w * Math.Cos(rad) - ys[i] * h * Math.Sin(rad);
|
||||
// double dy = xs[i] * w * Math.Sin(rad) + ys[i] * h * Math.Cos(rad);
|
||||
|
||||
|
||||
//transform.Rotation = fr;
|
||||
//_logger.Information($"w: {w} h: {h} fr: {fr} r: {r} rot: {rotation}");
|
||||
//_logger.Information($"dx: {dx} ox: {ox} oox: {oox}");
|
||||
//_logger.Information($"dy: {dy} oy: {oy} ooy: {ooy}");
|
||||
|
||||
var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } });
|
||||
await Send(m3);
|
||||
_logger.Debug($"New transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ToggleSceneItemVisibility(string sceneName, string sceneItemName)
|
||||
{
|
||||
LogExceptions(async () =>
|
||||
{
|
||||
await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) =>
|
||||
{
|
||||
var m1 = new RequestMessage("GetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
|
||||
await Send(m1, async (d) =>
|
||||
{
|
||||
if (d == null || !d.TryGetValue("sceneItemEnabled", out object? visible) || visible == null)
|
||||
return;
|
||||
|
||||
var m2 = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", visible.ToString().ToLower() == "true" ? false : true } });
|
||||
await Send(m2);
|
||||
});
|
||||
});
|
||||
}, "Failed to toggle OBS scene item visibility.");
|
||||
}
|
||||
|
||||
public async Task UpdateSceneItemVisibility(string sceneName, string sceneItemName, bool isVisible)
|
||||
{
|
||||
LogExceptions(async () =>
|
||||
{
|
||||
await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) =>
|
||||
{
|
||||
var m = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", isVisible } });
|
||||
await Send(m);
|
||||
});
|
||||
}, "Failed to update OBS scene item visibility.");
|
||||
}
|
||||
|
||||
public async Task UpdateSceneItemIndex(string sceneName, string sceneItemName, int index)
|
||||
{
|
||||
LogExceptions(async () =>
|
||||
{
|
||||
await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) =>
|
||||
{
|
||||
var m = new RequestMessage("SetSceneItemIndex", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } });
|
||||
await Send(m);
|
||||
});
|
||||
}, "Failed to update OBS scene item index.");
|
||||
}
|
||||
|
||||
private async Task GetSceneItemById(string sceneName, string sceneItemName, Action<long> action)
|
||||
{
|
||||
var m1 = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sourceName", sceneItemName } });
|
||||
await Send(m1, async (d) =>
|
||||
{
|
||||
if (d == null || !d.TryGetValue("sceneItemId", out object? value) || value == null || !long.TryParse(value.ToString(), out long sceneItemId))
|
||||
return;
|
||||
|
||||
_logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sceneItemId}][obs request id: {m1.RequestId}]");
|
||||
action.Invoke(sceneItemId);
|
||||
});
|
||||
}
|
||||
|
||||
private string GenerateUniqueIdentifier()
|
||||
{
|
||||
return Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
private void LogExceptions(Action action, string description)
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class RequestData
|
||||
|
@ -2,12 +2,11 @@ using System.Text.Json;
|
||||
using TwitchChatTTS.Helpers;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Seven;
|
||||
using TwitchChatTTS;
|
||||
|
||||
public class SevenApiClient
|
||||
{
|
||||
public static readonly string API_URL = "https://7tv.io/v3";
|
||||
public static readonly string WEBSOCKET_URL = "wss://events.7tv.io/v3";
|
||||
public const string API_URL = "https://7tv.io/v3";
|
||||
public const string WEBSOCKET_URL = "wss://events.7tv.io/v3";
|
||||
|
||||
private readonly WebClientWrap _web;
|
||||
private readonly ILogger _logger;
|
||||
@ -32,11 +31,11 @@ public class SevenApiClient
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
_logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON.");
|
||||
_logger.Error(e, "Failed to fetch channel emotes from 7tv due to improper JSON.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to fetch emotes from 7tv.");
|
||||
_logger.Error(e, "Failed to fetch channel emotes from 7tv.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -50,11 +49,11 @@ public class SevenApiClient
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
_logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON.");
|
||||
_logger.Error(e, "Failed to fetch global emotes from 7tv due to improper JSON.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to fetch emotes from 7tv.");
|
||||
_logger.Error(e, "Failed to fetch global emotes from 7tv.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
55
TTS.cs
55
TTS.cs
@ -17,8 +17,11 @@ namespace TwitchChatTTS
|
||||
public class TTS : IHostedService
|
||||
{
|
||||
public const int MAJOR_VERSION = 3;
|
||||
public const int MINOR_VERSION = 3;
|
||||
public const int MINOR_VERSION = 6;
|
||||
|
||||
private readonly User _user;
|
||||
private readonly HermesApiClient _hermesApiClient;
|
||||
private readonly SevenApiClient _sevenApiClient;
|
||||
private readonly RedemptionManager _redemptionManager;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly TTSPlayer _player;
|
||||
@ -36,6 +39,9 @@ namespace TwitchChatTTS
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_user = user;
|
||||
_hermesApiClient = hermesApiClient;
|
||||
_sevenApiClient = sevenApiClient;
|
||||
_redemptionManager = redemptionManager;
|
||||
_configuration = configuration;
|
||||
_player = player;
|
||||
@ -46,12 +52,14 @@ namespace TwitchChatTTS
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Console.Title = "TTS - Twitch Chat";
|
||||
License.iConfirmCommercialUse("abcdef");
|
||||
|
||||
var user = _serviceProvider.GetRequiredService<User>();
|
||||
var hermes = _serviceProvider.GetRequiredService<HermesApiClient>();
|
||||
var seven = _serviceProvider.GetRequiredService<SevenApiClient>();
|
||||
if (string.IsNullOrWhiteSpace(_configuration.Hermes.Token)) {
|
||||
_logger.Error("Hermes API token not set in the configuration file.");
|
||||
return;
|
||||
}
|
||||
|
||||
var hermesVersion = await hermes.GetTTSVersion();
|
||||
var hermesVersion = await _hermesApiClient.GetTTSVersion();
|
||||
if (hermesVersion.MajorVersion > TTS.MAJOR_VERSION || hermesVersion.MajorVersion == TTS.MAJOR_VERSION && hermesVersion.MinorVersion > TTS.MINOR_VERSION)
|
||||
{
|
||||
_logger.Information($"A new update for TTS is avaiable! Version {hermesVersion.MajorVersion}.{hermesVersion.MinorVersion} is available at {hermesVersion.Download}");
|
||||
@ -63,7 +71,7 @@ namespace TwitchChatTTS
|
||||
|
||||
try
|
||||
{
|
||||
await FetchUserData(user, hermes, seven);
|
||||
await FetchUserData(_user, _hermesApiClient, _sevenApiClient);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -71,19 +79,15 @@ namespace TwitchChatTTS
|
||||
await Task.Delay(30 * 1000);
|
||||
}
|
||||
|
||||
var twitchapiclient = await InitializeTwitchApiClient(user.TwitchUsername, user.TwitchUserId.ToString());
|
||||
var twitchapiclient = await InitializeTwitchApiClient(_user.TwitchUsername, _user.TwitchUserId.ToString());
|
||||
if (twitchapiclient == null)
|
||||
{
|
||||
await Task.Delay(30 * 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
var emoteSet = await seven.FetchChannelEmoteSet(user.TwitchUserId.ToString());
|
||||
user.SevenEmoteSetId = emoteSet?.Id;
|
||||
|
||||
License.iConfirmCommercialUse("abcdef");
|
||||
|
||||
await InitializeEmotes(seven, emoteSet);
|
||||
var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId.ToString());
|
||||
await InitializeEmotes(_sevenApiClient, emoteSet);
|
||||
await InitializeHermesWebsocket();
|
||||
await InitializeSevenTv(emoteSet.Id);
|
||||
await InitializeObs();
|
||||
@ -104,7 +108,7 @@ namespace TwitchChatTTS
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.Warning("TTS Buffer - Cancellation token was canceled.");
|
||||
_logger.Warning("TTS Buffer -Cancellation requested.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -144,7 +148,7 @@ namespace TwitchChatTTS
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.Warning("TTS Queue - Cancellation token was canceled.");
|
||||
_logger.Warning("TTS Queue - Cancellation requested.");
|
||||
return;
|
||||
}
|
||||
while (_player.IsEmpty() || _player.Playing != null)
|
||||
@ -160,12 +164,12 @@ namespace TwitchChatTTS
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File))
|
||||
{
|
||||
_logger.Information("Playing message: " + m.File);
|
||||
_logger.Debug("Playing audio file via TTS: " + m.File);
|
||||
AudioPlaybackEngine.Instance.PlaySound(m.File);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.Information("Playing message: " + m.Message);
|
||||
_logger.Debug("Playing message via TTS: " + m.Message);
|
||||
_player.Playing = m.Audio;
|
||||
if (m.Audio != null)
|
||||
AudioPlaybackEngine.Instance.AddMixerInput(m.Audio);
|
||||
@ -204,7 +208,7 @@ namespace TwitchChatTTS
|
||||
_logger.Information($"Username: {user.TwitchUsername} [id: {user.TwitchUserId}]");
|
||||
|
||||
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
|
||||
_logger.Information("Default Voice: " + user.DefaultTTSVoice);
|
||||
_logger.Information("TTS Default Voice: " + user.DefaultTTSVoice);
|
||||
|
||||
var wordFilters = await hermes.FetchTTSWordFilters();
|
||||
user.RegexFilters = wordFilters.ToList();
|
||||
@ -232,12 +236,8 @@ namespace TwitchChatTTS
|
||||
|
||||
var redemptionActions = await hermes.FetchRedeemableActions();
|
||||
var redemptions = await hermes.FetchRedemptions();
|
||||
foreach (var action in redemptionActions)
|
||||
_redemptionManager.AddAction(action);
|
||||
foreach (var redemption in redemptions)
|
||||
_redemptionManager.AddTwitchRedemption(redemption);
|
||||
_redemptionManager.Ready();
|
||||
_logger.Information($"Redemption Manager is ready with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
|
||||
_redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a));
|
||||
_logger.Information($"Redemption Manager has been initialized with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
|
||||
}
|
||||
|
||||
private async Task InitializeHermesWebsocket()
|
||||
@ -252,7 +252,9 @@ namespace TwitchChatTTS
|
||||
hermesClient.Connected = true;
|
||||
await hermesClient.Send(1, new HermesLoginMessage()
|
||||
{
|
||||
ApiKey = _configuration.Hermes.Token
|
||||
ApiKey = _configuration.Hermes.Token,
|
||||
MajorVersion = TTS.MAJOR_VERSION,
|
||||
MinorVersion = TTS.MINOR_VERSION,
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
@ -346,10 +348,9 @@ namespace TwitchChatTTS
|
||||
return twitchapiclient;
|
||||
}
|
||||
|
||||
private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet emoteSet)
|
||||
private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet channelEmotes)
|
||||
{
|
||||
var emotes = _serviceProvider.GetRequiredService<EmoteDatabase>();
|
||||
var channelEmotes = emoteSet;
|
||||
var globalEmotes = await sevenapi.FetchGlobalSevenEmotes();
|
||||
|
||||
if (channelEmotes != null && channelEmotes.Emotes.Any())
|
||||
|
@ -1,4 +1,7 @@
|
||||
using System.Reflection;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using org.mariuszgromada.math.mxparser;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
@ -8,43 +11,40 @@ namespace TwitchChatTTS.Twitch.Redemptions
|
||||
{
|
||||
public class RedemptionManager
|
||||
{
|
||||
private readonly IList<Redemption> _redemptions;
|
||||
private readonly IDictionary<string, RedeemableAction> _actions;
|
||||
private readonly IDictionary<string, IList<RedeemableAction>> _store;
|
||||
private readonly User _user;
|
||||
private readonly OBSManager _obsManager;
|
||||
private readonly SocketClient<WebSocketMessage> _hermesClient;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Random _random;
|
||||
private bool _isReady;
|
||||
|
||||
|
||||
public RedemptionManager(OBSManager obsManager, ILogger logger)
|
||||
public RedemptionManager(
|
||||
User user,
|
||||
OBSManager obsManager,
|
||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
|
||||
ILogger logger)
|
||||
{
|
||||
_redemptions = new List<Redemption>();
|
||||
_actions = new Dictionary<string, RedeemableAction>();
|
||||
_store = new Dictionary<string, IList<RedeemableAction>>();
|
||||
_user = user;
|
||||
_obsManager = obsManager;
|
||||
_hermesClient = hermesClient;
|
||||
_logger = logger;
|
||||
_random = new Random();
|
||||
_isReady = false;
|
||||
}
|
||||
|
||||
public void AddTwitchRedemption(Redemption redemption)
|
||||
{
|
||||
_redemptions.Add(redemption);
|
||||
}
|
||||
|
||||
public void AddAction(RedeemableAction action)
|
||||
{
|
||||
_actions.Add(action.Name, action);
|
||||
}
|
||||
|
||||
private void Add(string twitchRedemptionId, RedeemableAction action)
|
||||
{
|
||||
if (!_store.TryGetValue(twitchRedemptionId, out var actions))
|
||||
_store.Add(twitchRedemptionId, actions = new List<RedeemableAction>());
|
||||
|
||||
actions.Add(action);
|
||||
_store[twitchRedemptionId] = actions.OrderBy(a => a).ToList();
|
||||
_logger.Debug($"Added redemption action [name: {action.Name}][type: {action.Type}]");
|
||||
}
|
||||
|
||||
public async Task Execute(RedeemableAction action, string sender)
|
||||
public async Task Execute(RedeemableAction action, string senderDisplayName, long senderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -52,12 +52,12 @@ namespace TwitchChatTTS.Twitch.Redemptions
|
||||
{
|
||||
case "WRITE_TO_FILE":
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"]));
|
||||
await File.WriteAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], sender));
|
||||
await File.WriteAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], senderDisplayName));
|
||||
_logger.Debug($"Overwritten text to file [file: {action.Data["file_path"]}]");
|
||||
break;
|
||||
case "APPEND_TO_FILE":
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"]));
|
||||
await File.AppendAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], sender));
|
||||
await File.AppendAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], senderDisplayName));
|
||||
_logger.Debug($"Appended text to file [file: {action.Data["file_path"]}]");
|
||||
break;
|
||||
case "OBS_TRANSFORM":
|
||||
@ -99,8 +99,63 @@ namespace TwitchChatTTS.Twitch.Redemptions
|
||||
_logger.Debug($"Finished applying the OBS transformation property changes [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}]");
|
||||
});
|
||||
break;
|
||||
case "TOGGLE_OBS_VISIBILITY":
|
||||
await _obsManager.ToggleSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"]);
|
||||
break;
|
||||
case "SPECIFIC_OBS_VISIBILITY":
|
||||
await _obsManager.UpdateSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"], action.Data["obs_visible"].ToLower() == "true");
|
||||
break;
|
||||
case "SPECIFIC_OBS_INDEX":
|
||||
await _obsManager.UpdateSceneItemIndex(action.Data["scene_name"], action.Data["scene_item_name"], int.Parse(action.Data["obs_index"]));
|
||||
break;
|
||||
case "SLEEP":
|
||||
_logger.Debug("Sleeping on thread due to redemption for OBS.");
|
||||
await Task.Delay(int.Parse(action.Data["sleep"]));
|
||||
break;
|
||||
case "SPECIFIC_TTS_VOICE":
|
||||
var voiceId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id].ToLower() == action.Data["tts_voice"].ToLower());
|
||||
if (voiceId == null)
|
||||
{
|
||||
_logger.Warning($"Voice specified is not valid [voice: {action.Data["tts_voice"]}]");
|
||||
return;
|
||||
}
|
||||
var voiceName = _user.VoicesAvailable[voiceId];
|
||||
if (!_user.VoicesEnabled.Contains(voiceName))
|
||||
{
|
||||
_logger.Warning($"Voice specified is not enabled [voice: {action.Data["tts_voice"]}][voice id: {voiceId}]");
|
||||
return;
|
||||
}
|
||||
await _hermesClient.Send(3, new HermesSocketLibrary.Socket.Data.RequestMessage()
|
||||
{
|
||||
Type = _user.VoicesSelected.ContainsKey(senderId) ? "update_tts_user" : "create_tts_user",
|
||||
Data = new Dictionary<string, object>() { { "chatter", senderId }, { "voice", voiceId } }
|
||||
});
|
||||
_logger.Debug($"Changed the TTS voice of a chatter [voice: {action.Data["tts_voice"]}][display name: {senderDisplayName}][chatter id: {senderId}]");
|
||||
break;
|
||||
case "RANDOM_TTS_VOICE":
|
||||
var voicesEnabled = _user.VoicesEnabled.ToList();
|
||||
if (!voicesEnabled.Any())
|
||||
{
|
||||
_logger.Warning($"There are no TTS voices enabled [voice pool size: {voicesEnabled.Count}]");
|
||||
return;
|
||||
}
|
||||
if (voicesEnabled.Count <= 1)
|
||||
{
|
||||
_logger.Warning($"There are not enough TTS voices enabled to randomize [voice pool size: {voicesEnabled.Count}]");
|
||||
return;
|
||||
}
|
||||
var randomVoice = voicesEnabled[_random.Next(voicesEnabled.Count)];
|
||||
var randomVoiceId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id] == randomVoice);
|
||||
await _hermesClient.Send(3, new HermesSocketLibrary.Socket.Data.RequestMessage()
|
||||
{
|
||||
Type = _user.VoicesSelected.ContainsKey(senderId) ? "update_tts_user" : "create_tts_user",
|
||||
Data = new Dictionary<string, object>() { { "chatter", senderId }, { "voice", randomVoiceId } }
|
||||
});
|
||||
_logger.Debug($"Randomly changed the TTS voice of a chatter [voice: {randomVoice}][display name: {senderDisplayName}][chatter id: {senderId}]");
|
||||
break;
|
||||
case "AUDIO_FILE":
|
||||
if (!File.Exists(action.Data["file_path"])) {
|
||||
if (!File.Exists(action.Data["file_path"]))
|
||||
{
|
||||
_logger.Warning($"Cannot find audio file for Twitch channel point redeem [file: {action.Data["file_path"]}]");
|
||||
return;
|
||||
}
|
||||
@ -108,7 +163,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
|
||||
_logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}]");
|
||||
break;
|
||||
default:
|
||||
_logger.Warning($"Unknown redeemable action has occured [type: {action.Type}]");
|
||||
_logger.Warning($"Unknown redeemable action has occured. Update needed? [type: {action.Type}]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -128,21 +183,37 @@ namespace TwitchChatTTS.Twitch.Redemptions
|
||||
return new List<RedeemableAction>(0);
|
||||
}
|
||||
|
||||
public void Ready()
|
||||
public void Initialize(IEnumerable<Redemption> redemptions, IDictionary<string, RedeemableAction> actions)
|
||||
{
|
||||
var ordered = _redemptions.OrderBy(r => r.Order);
|
||||
_store.Clear();
|
||||
|
||||
var ordered = redemptions.OrderBy(r => r.Order);
|
||||
foreach (var redemption in ordered)
|
||||
if (_actions.TryGetValue(redemption.ActionName, out var action) && action != null)
|
||||
Add(redemption.RedemptionId, action);
|
||||
{
|
||||
try
|
||||
{
|
||||
if (actions.TryGetValue(redemption.ActionName, out var action) && action != null)
|
||||
{
|
||||
_logger.Debug($"Fetched a redemption action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]");
|
||||
Add(redemption.RedemptionId, action);
|
||||
}
|
||||
else
|
||||
_logger.Warning($"Could not find redemption action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, $"Failed to add a redemption [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]");
|
||||
}
|
||||
}
|
||||
|
||||
_isReady = true;
|
||||
_logger.Debug("Redemption Manager is ready.");
|
||||
_logger.Debug("All redemptions added. Redemption Manager is ready.");
|
||||
}
|
||||
|
||||
private string ReplaceContentText(string content, string username) {
|
||||
return content.Replace("%USER%", username);
|
||||
private string ReplaceContentText(string content, string username)
|
||||
{
|
||||
return content.Replace("%USER%", username)
|
||||
.Replace("\\n", "\n");
|
||||
}
|
||||
}
|
||||
}
|
@ -64,6 +64,7 @@ public class TwitchApiClient
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Debug($"Attempting to authorize Twitch API [id: {broadcasterId}]");
|
||||
var authorize = await _web.GetJson<TwitchBotAuth>("https://hermes.goblincaves.com/api/account/reauthorize");
|
||||
if (authorize != null && broadcasterId == authorize.BroadcasterId)
|
||||
{
|
||||
@ -71,7 +72,10 @@ public class TwitchApiClient
|
||||
_token.RefreshToken = authorize.RefreshToken;
|
||||
_token.UserId = authorize.UserId;
|
||||
_token.BroadcasterId = authorize.BroadcasterId;
|
||||
_token.ExpiresIn = authorize.ExpiresIn;
|
||||
_token.UpdatedAt = DateTime.Now;
|
||||
_logger.Information("Updated Twitch API tokens.");
|
||||
_logger.Debug($"Twitch API Auth data [user id: {_token.UserId}][id: {_token.BroadcasterId}][expires in: {_token.ExpiresIn}][expires at: {_token.ExpiresAt.ToShortTimeString()}]");
|
||||
}
|
||||
else if (authorize != null)
|
||||
{
|
||||
@ -79,6 +83,7 @@ public class TwitchApiClient
|
||||
return false;
|
||||
}
|
||||
_broadcasterId = broadcasterId;
|
||||
_logger.Debug($"Authorized Twitch API [id: {broadcasterId}]");
|
||||
return true;
|
||||
}
|
||||
catch (HttpResponseException e)
|
||||
@ -90,6 +95,7 @@ public class TwitchApiClient
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
_logger.Debug($"Failed to Authorize Twitch API due to JSON error [id: {broadcasterId}]");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -191,7 +197,7 @@ public class TwitchApiClient
|
||||
foreach (var action in actions)
|
||||
try
|
||||
{
|
||||
await _redemptionManager.Execute(action, e.RewardRedeemed.Redemption.User.DisplayName);
|
||||
await _redemptionManager.Execute(action, e.RewardRedeemed.Redemption.User.DisplayName, long.Parse(e.RewardRedeemed.Redemption.User.Id));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
3
User.cs
3
User.cs
@ -12,11 +12,12 @@ namespace TwitchChatTTS
|
||||
public long TwitchUserId { get; set; }
|
||||
public string TwitchUsername { get; set; }
|
||||
public string SevenEmoteSetId { get; set; }
|
||||
public long? OwnerId { get; set; }
|
||||
|
||||
public string DefaultTTSVoice { get; set; }
|
||||
// voice id -> voice name
|
||||
public IDictionary<string, string> VoicesAvailable { get => _voicesAvailable; set { _voicesAvailable = value; WordFilterRegex = GenerateEnabledVoicesRegex(); } }
|
||||
// chatter/twitch id -> voice name
|
||||
// chatter/twitch id -> voice id
|
||||
public IDictionary<long, string> VoicesSelected { get; set; }
|
||||
// voice names
|
||||
public HashSet<string> VoicesEnabled { get => _voicesEnabled; set { _voicesEnabled = value; WordFilterRegex = GenerateEnabledVoicesRegex(); } }
|
||||
|
Loading…
Reference in New Issue
Block a user