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:
parent
e4a11382ef
commit
6eb927ce5f
@ -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)
|
||||
|
@ -105,7 +105,7 @@ namespace TwitchChatTTS.Seven.Socket
|
||||
|
||||
if (!Connected)
|
||||
{
|
||||
await Task.Delay(30000);
|
||||
await Task.Delay(TimeSpan.FromSeconds(30));
|
||||
await Connect();
|
||||
}
|
||||
}
|
||||
|
@ -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
14
TTS.cs
@ -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)
|
||||
|
@ -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(async () => await EndOfRaidSpamProtection(date));
|
||||
}
|
||||
|
||||
private async Task EndOfRaidSpamProtection(DateTime date)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(30));
|
||||
|
||||
lock (_lock)
|
||||
|
@ -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));
|
||||
|
@ -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]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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."));
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user