Fixed raid spam prevention. Gave a proper error message when connecting to Twitch websockets without linking Twitch account to Twitch.

This commit is contained in:
Tom 2024-08-07 22:01:04 +00:00
parent e4a11382ef
commit 1761f1eaf6
10 changed files with 162 additions and 84 deletions

View File

@ -6,13 +6,13 @@ namespace TwitchChatTTS.Helpers
public class WebClientWrap
{
private readonly HttpClient _client;
private readonly JsonSerializerOptions _options;
public JsonSerializerOptions Options { get; }
public WebClientWrap(JsonSerializerOptions options)
{
_client = new HttpClient();
_options = options;
Options = options;
}
@ -26,7 +26,7 @@ namespace TwitchChatTTS.Helpers
public async Task<T?> GetJson<T>(string uri, JsonSerializerOptions? options = null)
{
var response = await _client.GetAsync(uri);
return JsonSerializer.Deserialize<T>(await response.Content.ReadAsStreamAsync(), options ?? _options);
return JsonSerializer.Deserialize<T>(await response.Content.ReadAsStreamAsync(), options ?? Options);
}
public async Task<HttpResponseMessage> Get(string uri)
@ -36,17 +36,17 @@ namespace TwitchChatTTS.Helpers
public async Task<HttpResponseMessage> Post<T>(string uri, T data)
{
return await _client.PostAsJsonAsync(uri, data, _options);
return await _client.PostAsJsonAsync(uri, data, Options);
}
public async Task<HttpResponseMessage> Post(string uri)
{
return await _client.PostAsJsonAsync(uri, new object(), _options);
return await _client.PostAsJsonAsync(uri, new object(), Options);
}
public async Task<T?> Delete<T>(string uri)
{
return await _client.DeleteFromJsonAsync<T>(uri, _options);
return await _client.DeleteFromJsonAsync<T>(uri, Options);
}
public async Task<HttpResponseMessage> Delete(string uri)

View File

@ -105,7 +105,7 @@ namespace TwitchChatTTS.Seven.Socket
if (!Connected)
{
await Task.Delay(30000);
await Task.Delay(TimeSpan.FromSeconds(30));
await Connect();
}
}

View File

@ -127,11 +127,12 @@ 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, ChannelChatClearUserHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatDeleteMessageHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelCustomRedemptionHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelFollowHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelRaidHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelResubscriptionHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionGiftHandler>("twitch-notifications");

14
TTS.cs
View File

@ -43,7 +43,6 @@ namespace TwitchChatTTS
User user,
HermesApiClient hermesApiClient,
SevenApiClient sevenApiClient,
TwitchApiClient twitchApiClient,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
[FromKeyedServices("7tv")] SocketClient<WebSocketMessage> seven,
@ -60,7 +59,6 @@ namespace TwitchChatTTS
_user = user;
_hermesApiClient = hermesApiClient;
_sevenApiClient = sevenApiClient;
_twitchApiClient = twitchApiClient;
_hermes = (hermes as HermesSocketClient)!;
_obs = (obs as OBSSocketClient)!;
_seven = (seven as SevenSocketClient)!;
@ -127,7 +125,17 @@ namespace TwitchChatTTS
await Task.Delay(TimeSpan.FromSeconds(30));
return;
}
await _twitch.Connect();
try
{
await _twitch.Connect();
}
catch (Exception e)
{
_logger.Error(e, "Failed to connect to Twitch websocket server.");
await Task.Delay(TimeSpan.FromSeconds(30));
return;
}
var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId.ToString());
if (emoteSet != null)

View File

@ -25,7 +25,8 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
if (data is not ChannelRaidMessage message)
return;
var chatters = await _api.GetChatters(message.ToBroadcasterUserId, message.ToBroadcasterUserLogin);
_logger.Information($"A raid has started. Starting raid spam prevention. [from: {message.FromBroadcasterUserLogin}][from id: {message.FromBroadcasterUserId}].");
var chatters = await _api.GetChatters(_user.TwitchUserId.ToString(), _user.TwitchUserId.ToString());
if (chatters?.Data == null)
{
_logger.Error("Could not fetch the list of chatters in chat.");
@ -43,6 +44,11 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
}
}
Task.Run(EndOfRaidSpamProtection);
}
private async Task EndOfRaidSpamProtection()
{
await Task.Delay(TimeSpan.FromSeconds(30));
lock (_lock)

View File

@ -33,10 +33,11 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_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.clear_user_messages", typeof(ChannelChatClearUserMessage));
_messageTypes.Add("channel.chat.message_delete", typeof(ChannelChatDeleteMessage));
_messageTypes.Add("channel.channel_points_custom_reward_redemption.add", typeof(ChannelCustomRedemptionMessage));
_messageTypes.Add("channel.raid", typeof(ChannelRaidMessage));
_messageTypes.Add("channel.follow", typeof(ChannelFollowMessage));
_messageTypes.Add("channel.subscribe", typeof(ChannelSubscriptionMessage));
_messageTypes.Add("channel.subscription.message", typeof(ChannelResubscriptionMessage));

View File

@ -31,9 +31,17 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
return;
}
await _hermes.AuthorizeTwitch();
var token = await _hermes.FetchTwitchBotToken();
_api.Initialize(token);
try
{
await _hermes.AuthorizeTwitch();
var token = await _hermes.FetchTwitchBotToken();
_api.Initialize(token);
}
catch (Exception)
{
_logger.Error("Ensure you have your Twitch account linked on TTS. Restart application once you do.");
return;
}
string broadcasterId = _user.TwitchUserId.ToString();
string[] subscriptionsv1 = [
@ -46,8 +54,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
"channel.subscription.message",
"channel.ad_break.begin",
"channel.ban",
"channel.channel_points_custom_reward_redemption.add",
"channel.raid"
"channel.channel_points_custom_reward_redemption.add"
];
string[] subscriptionsv2 = [
"channel.follow",
@ -77,6 +84,8 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
await Subscribe(sender, subscription, message.Session.Id, broadcasterId, "1");
foreach (var subscription in subscriptionsv2)
await Subscribe(sender, subscription, message.Session.Id, broadcasterId, "2");
await Subscribe(sender, "channel.raid", broadcasterId, async () => await _api.CreateChannelRaidEventSubscription("1", message.Session.Id, to: broadcasterId));
sender.Identify(message.Session.Id);
}
@ -111,5 +120,36 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger.Error(ex, $"Failed to create an event subscription [subscription type: {subscriptionName}][reason: exception]");
}
}
private async Task Subscribe(TwitchWebsocketClient sender, string subscriptionName, string broadcasterId, Func<Task<EventResponse<NotificationInfo>?>> subscribe)
{
try
{
var response = await subscribe();
if (response == null)
{
return;
}
if (response.Data == null)
{
_logger.Error($"Failed to create an event subscription [subscription type: {subscriptionName}][reason: data is null]");
return;
}
if (!response.Data.Any())
{
_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)
{
_logger.Error(ex, $"Failed to create an event subscription [subscription type: {subscriptionName}][reason: exception]");
}
}
}
}

View File

@ -64,69 +64,73 @@ namespace TwitchChatTTS.Twitch.Socket
client.Initialize();
_backup = client;
client.OnIdentified += async (s, e) =>
{
bool clientDisconnect = false;
lock (_lock)
{
if (_identified == null || _identified.ReceivedReconnecting)
{
if (_backup != null && _backup.UID == client.UID)
{
_logger.Information($"Twitch client has been identified [client: {client.UID}][main: {_identified?.UID}][backup: {_backup.UID}]");
_identified = _backup;
_backup = null;
}
else
_logger.Warning($"Twitch client identified from unknown sources [client: {client.UID}][main: {_identified?.UID}][backup: {_backup?.UID}]");
}
else if (_identified.UID == client.UID)
{
_logger.Warning($"Twitch client has been re-identified [client: {client.UID}][main: {_identified.UID}][backup: {_backup?.UID}]");
}
else if (_backup == null || _backup.UID != client.UID)
{
_logger.Warning($"Twitch client has been identified, but isn't main or backup [client: {client.UID}][main: {_identified.UID}][backup: {_backup?.UID}]");
clientDisconnect = true;
}
}
if (clientDisconnect)
await client.DisconnectAsync(new SocketDisconnectionEventArgs("Closed", "No need for a tertiary client."));
};
client.OnDisconnected += async (s, e) =>
{
bool reconnecting = false;
lock (_lock)
{
if (_identified?.UID == client.UID)
{
_logger.Warning($"Identified Twitch client has disconnected [client: {client.UID}][main: {_identified.UID}][backup: {_backup?.UID}]");
_identified = null;
reconnecting = true;
}
else if (_backup?.UID == client.UID)
{
_logger.Warning($"Backup Twitch client has disconnected [client: {client.UID}][main: {_identified?.UID}][backup: {_backup.UID}]");
_backup = null;
}
else if (client.ReceivedReconnecting)
{
_logger.Debug($"Twitch client disconnected due to reconnection [client: {client.UID}][main: {_identified?.UID}][backup: {_backup?.UID}]");
}
else
_logger.Error($"Twitch client disconnected from unknown source [client: {client.UID}][main: {_identified?.UID}][backup: {_backup?.UID}]");
}
if (reconnecting)
{
var client = GetWorkingClient();
await client.Connect();
}
};
client.OnIdentified += async (_, _) => await OnIdentified(client);
client.OnDisconnected += async (_, _) => await OnDisconnection(client);
_logger.Debug("Created a Twitch websocket client.");
return client;
}
private async Task OnDisconnection(TwitchWebsocketClient client)
{
bool reconnecting = false;
lock (_lock)
{
if (_identified?.UID == client.UID)
{
_logger.Warning($"Identified Twitch client has disconnected [client: {client.UID}][main: {_identified.UID}][backup: {_backup?.UID}]");
_identified = null;
reconnecting = true;
}
else if (_backup?.UID == client.UID)
{
_logger.Warning($"Backup Twitch client has disconnected [client: {client.UID}][main: {_identified?.UID}][backup: {_backup.UID}]");
_backup = null;
}
else if (client.ReceivedReconnecting)
{
_logger.Debug($"Twitch client disconnected due to reconnection [client: {client.UID}][main: {_identified?.UID}][backup: {_backup?.UID}]");
}
else
_logger.Error($"Twitch client disconnected from unknown source [client: {client.UID}][main: {_identified?.UID}][backup: {_backup?.UID}]");
}
if (reconnecting)
{
var newClient = GetWorkingClient();
await newClient.Connect();
}
}
private async Task OnIdentified(TwitchWebsocketClient client)
{
bool clientDisconnect = false;
lock (_lock)
{
if (_identified == null || _identified.ReceivedReconnecting)
{
if (_backup != null && _backup.UID == client.UID)
{
_logger.Information($"Twitch client has been identified [client: {client.UID}][main: {_identified?.UID}][backup: {_backup.UID}]");
_identified = _backup;
_backup = null;
}
else
_logger.Warning($"Twitch client identified from unknown sources [client: {client.UID}][main: {_identified?.UID}][backup: {_backup?.UID}]");
}
else if (_identified.UID == client.UID)
{
_logger.Warning($"Twitch client has been re-identified [client: {client.UID}][main: {_identified.UID}][backup: {_backup?.UID}]");
}
else if (_backup == null || _backup.UID != client.UID)
{
_logger.Warning($"Twitch client has been identified, but isn't main or backup [client: {client.UID}][main: {_identified.UID}][backup: {_backup?.UID}]");
clientDisconnect = true;
}
}
if (clientDisconnect)
await client.DisconnectAsync(new SocketDisconnectionEventArgs("Closed", "No need for a tertiary client."));
}
}
}

View File

@ -176,7 +176,7 @@ namespace TwitchChatTTS.Twitch.Socket
_logger.Warning("Twitch websocket message payload is null.");
return;
}
await handler.Execute(this, data);
await Task.Run(async () => await handler.Execute(this, data));
}
public async Task Send<T>(string type, T data)

View File

@ -28,9 +28,8 @@ public class TwitchApiClient
});
}
public async Task<EventResponse<NotificationInfo>?> CreateEventSubscription(string type, string version, string sessionId, string userId, string? broadcasterId = null)
public async Task<EventResponse<NotificationInfo>?> CreateEventSubscription(string type, string version, string sessionId, IDictionary<string, string> conditions)
{
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 base_url = _configuration.Environment == "PROD" || string.IsNullOrWhiteSpace(_configuration.Twitch?.ApiUrl)
? "https://api.twitch.tv/helix" : _configuration.Twitch.ApiUrl;
@ -38,12 +37,31 @@ public class TwitchApiClient
if (response.StatusCode == HttpStatusCode.Accepted)
{
_logger.Debug($"Twitch API call [type: create event subscription][subscription type: {type}][response: {await response.Content.ReadAsStringAsync()}]");
return await response.Content.ReadFromJsonAsync(typeof(EventResponse<NotificationInfo>)) as EventResponse<NotificationInfo>;
return await response.Content.ReadFromJsonAsync(typeof(EventResponse<NotificationInfo>), _web.Options) as EventResponse<NotificationInfo>;
}
_logger.Error($"Twitch API call failed [type: create event subscription][subscription type: {type}][response: {await response.Content.ReadAsStringAsync()}]");
return null;
}
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", broadcasterId ?? userId }, { "moderator_user_id", broadcasterId ?? userId } };
return await CreateEventSubscription(type, version, sessionId, conditions);
}
public async Task<EventResponse<NotificationInfo>?> CreateChannelRaidEventSubscription(string version, string sessionId, string? from = null, string? to = null)
{
var conditions = new Dictionary<string, string>();
if (from == null && to == null)
throw new InvalidOperationException("Either or both from and to values must be non-null.");
if (from != null)
conditions.Add("from_broadcaster_user_id", from);
if (to != null)
conditions.Add("to_broadcaster_user_id", to);
return await CreateEventSubscription("channel.raid", version, sessionId, conditions);
}
public async Task DeleteEventSubscription(string subscriptionId)
{
var base_url = _configuration.Environment == "PROD" || string.IsNullOrWhiteSpace(_configuration.Twitch?.ApiUrl)
@ -55,10 +73,10 @@ public class TwitchApiClient
{
moderatorId ??= broadcasterId;
var response = await _web.Get($"https://api.twitch.tv/helix/chat/chatters?broadcaster_id={broadcasterId}&moderator_id={moderatorId}");
if (response.StatusCode == HttpStatusCode.Accepted)
if (response.StatusCode == HttpStatusCode.OK)
{
_logger.Debug($"Twitch API call [type: get chatters][response: {await response.Content.ReadAsStringAsync()}]");
return await response.Content.ReadFromJsonAsync(typeof(EventResponse<ChatterMessage>)) as EventResponse<ChatterMessage>;
return await response.Content.ReadFromJsonAsync(typeof(EventResponse<ChatterMessage>), _web.Options) as EventResponse<ChatterMessage>;
}
_logger.Error($"Twitch API call failed [type: get chatters][response: {await response.Content.ReadAsStringAsync()}]");
return null;