diff --git a/TwitchLib.PubSub/Extensions/LoggingExtesions.cs b/TwitchLib.PubSub/Extensions/LoggingExtesions.cs new file mode 100644 index 0000000..2c8116e --- /dev/null +++ b/TwitchLib.PubSub/Extensions/LoggingExtesions.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Logging; +using System; + +namespace TwitchLib.PubSub.Extensions +{ + internal static partial class LoggingExtesions + { + [LoggerMessage(LogLevel.Error, "OnError in PubSub Websocket connection occured! Exception: {ex}")] + public static partial void LogOnError(this ILogger logger, Exception ex); + + [LoggerMessage(LogLevel.Debug, "Received Websocket OnMessage: {message}")] + public static partial void LogReceivednMessage(this ILogger logger, string message); + + [LoggerMessage(LogLevel.Warning, "PubSub Websocket connection closed")] + public static partial void LogConnectionClosed(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "PubSub Websocket connection established")] + public static partial void LogConnectionEstablished(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "[TwitchPubSub] {message}")] + public static partial void LogUnaccountedFor(this ILogger logger, string message); + } +} diff --git a/TwitchLib.PubSub/Models/Request.cs b/TwitchLib.PubSub/Models/Request.cs new file mode 100644 index 0000000..73c1a00 --- /dev/null +++ b/TwitchLib.PubSub/Models/Request.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace TwitchLib.PubSub.Models +{ + internal class Request + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("nonce")] + public string Nonce { get; set; } + + [JsonProperty("data")] + public RequestData Data { get; set; } + } + + internal class RequestData + { + [JsonProperty("topics")] + public List Topics { get; set; } + + [JsonProperty("auth_token", NullValueHandling = NullValueHandling.Ignore)] + public string AuthToken { get; set; } + } +} diff --git a/TwitchLib.PubSub/Models/Responses/Message.cs b/TwitchLib.PubSub/Models/Responses/Message.cs index 3dcbf40..2e9c9c1 100644 --- a/TwitchLib.PubSub/Models/Responses/Message.cs +++ b/TwitchLib.PubSub/Models/Responses/Message.cs @@ -24,9 +24,17 @@ public class Message /// PubSub Message model constructor. /// /// The json string. - public Message(string jsonStr) + public Message(string jsonStr) : this(JObject.Parse(jsonStr)) { - var json = JObject.Parse(jsonStr).SelectToken("data"); + } + + /// + /// PubSub Message model constructor. + /// + /// The json. + internal Message(JObject jsonObject) + { + var json = jsonObject.SelectToken("data"); Topic = json.SelectToken("topic")?.ToString(); var encodedJsonMessage = json.SelectToken("message").ToString(); switch (Topic?.Split('.')[0]) @@ -71,7 +79,7 @@ public Message(string jsonStr) break; case "raid": MessageData = new RaidEvents(encodedJsonMessage); - break; + break; case "predictions-channel-v1": MessageData = new PredictionEvents(encodedJsonMessage); break; diff --git a/TwitchLib.PubSub/Models/Responses/Response.cs b/TwitchLib.PubSub/Models/Responses/Response.cs index 7bbea55..38f6748 100644 --- a/TwitchLib.PubSub/Models/Responses/Response.cs +++ b/TwitchLib.PubSub/Models/Responses/Response.cs @@ -29,10 +29,18 @@ public class Response /// Response model constructor. /// /// The json. - public Response(string json) + public Response(string json) : this(JObject.Parse(json)) { - Error = JObject.Parse(json).SelectToken("error")?.ToString(); - Nonce = JObject.Parse(json).SelectToken("nonce")?.ToString(); + } + + /// + /// Response model constructor. + /// + /// The json. + internal Response(JObject json) + { + Error = json.SelectToken("error")?.ToString(); + Nonce = json.SelectToken("nonce")?.ToString(); if (string.IsNullOrWhiteSpace(Error)) Successful = true; } diff --git a/TwitchLib.PubSub/TwitchLib.PubSub.csproj b/TwitchLib.PubSub/TwitchLib.PubSub.csproj index cb821cf..0539fe8 100644 --- a/TwitchLib.PubSub/TwitchLib.PubSub.csproj +++ b/TwitchLib.PubSub/TwitchLib.PubSub.csproj @@ -2,6 +2,7 @@ netstandard2.0 + latest TwitchLib.PubSub 4.0.0 $(VersionSuffix) @@ -23,8 +24,8 @@ True - - + + diff --git a/TwitchLib.PubSub/TwitchPubSub.cs b/TwitchLib.PubSub/TwitchPubSub.cs index 8136f83..ae5b2d1 100644 --- a/TwitchLib.PubSub/TwitchPubSub.cs +++ b/TwitchLib.PubSub/TwitchPubSub.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Timers; @@ -12,6 +12,7 @@ using TwitchLib.Communication.Models; using TwitchLib.PubSub.Enums; using TwitchLib.PubSub.Events; +using TwitchLib.PubSub.Extensions; using TwitchLib.PubSub.Interfaces; using TwitchLib.PubSub.Models; using TwitchLib.PubSub.Models.Responses.Messages; @@ -35,7 +36,7 @@ public class TwitchPubSub : ITwitchPubSub /// The random /// private static readonly Random Random = new Random(); - + /// /// The socket /// @@ -265,7 +266,7 @@ public class TwitchPubSub : ITwitchPubSub /// /// Fires when PubSub receives notice when a channel cancels the raid /// - public event EventHandler OnRaidCancel; + public event EventHandler OnRaidCancel; /// /// /// Fires when PubSub receives any data from Twitch @@ -320,7 +321,7 @@ public TwitchPubSub(ILogger logger = null) /// The instance containing the event data. private Task OnErrorAsync(object sender, OnErrorEventArgs e) { - _logger?.LogError($"OnError in PubSub Websocket connection occured! Exception: {e.Exception}"); + _logger?.LogOnError(e.Exception); OnPubSubServiceError?.Invoke(this, new OnPubSubServiceErrorArgs { Exception = e.Exception }); return Task.CompletedTask; @@ -333,7 +334,7 @@ private Task OnErrorAsync(object sender, OnErrorEventArgs e) /// The instance containing the event data. private Task OnMessageAsync(object sender, OnMessageEventArgs e) { - _logger?.LogDebug($"Received Websocket OnMessage: {e.Message}"); + _logger?.LogReceivednMessage(e.Message); OnLog?.Invoke(this, new OnLogArgs { Data = e.Message }); return ParseMessageAsync(e.Message); } @@ -345,7 +346,7 @@ private Task OnMessageAsync(object sender, OnMessageEventArgs e) /// The instance containing the event data. private Task Socket_OnDisconnectedAsync(object sender, EventArgs e) { - _logger?.LogWarning("PubSub Websocket connection closed"); + _logger?.LogConnectionClosed(); _pingTimer.Stop(); _pongTimer.Stop(); OnPubSubServiceClosed?.Invoke(this, null); @@ -360,7 +361,7 @@ private Task Socket_OnDisconnectedAsync(object sender, EventArgs e) /// The instance containing the event data. private Task Socket_OnConnectedAsync(object sender, EventArgs e) { - _logger?.LogInformation("PubSub Websocket connection established"); + _logger?.LogConnectionEstablished(); _pingTimer.Interval = 180000; _pingTimer.Elapsed += PingTimerTickAsync; _pingTimer.Start(); @@ -368,7 +369,7 @@ private Task Socket_OnConnectedAsync(object sender, EventArgs e) return Task.CompletedTask; } - + /// /// Pings the timer tick. /// @@ -414,12 +415,13 @@ private async void PongTimerTickAsync(object sender, ElapsedEventArgs e) /// The message. private async Task ParseMessageAsync(string message) { - var type = JObject.Parse(message).SelectToken("type")?.ToString(); + var parsedJson = JObject.Parse(message); + var type = parsedJson.SelectToken("type")?.ToString(); switch (type?.ToLower()) { case "response": - var resp = new Models.Responses.Response(message); + var resp = new Models.Responses.Response(parsedJson); if (_previousRequests.Count != 0) { bool handled = false; @@ -451,7 +453,7 @@ private async Task ParseMessageAsync(string message) } break; case "message": - var msg = new Models.Responses.Message(message); + var msg = new Models.Responses.Message(parsedJson); _topicToChannelId.TryGetValue(msg.Topic, out var channelId); channelId = channelId ?? ""; switch (msg.Topic.Split('.')[0]) @@ -687,21 +689,27 @@ private async Task ParseMessageAsync(string message) _pongReceived = true; return; case "reconnect": - await _socket.CloseAsync(); + await _socket.CloseAsync(); break; } - + UnaccountedFor(message); } - + /// /// Generates the nonce. /// /// System.String. private static string GenerateNonce() { - return new string(Enumerable.Repeat("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 8) - .Select(s => s[Random.Next(s.Length)]).ToArray()); + const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + Span nonce = stackalloc char[8]; + foreach (ref var c in nonce) + { + var index = Random.Next(chars.Length); + c = chars[index]; + } + return nonce.ToString(); } /// @@ -730,7 +738,7 @@ public void SendTopics(string oauth = null, bool unlisten = false) { SendTopicsAsync(oauth, unlisten).GetAwaiter().GetResult(); } - + /// /// /// Sends the topics. @@ -746,14 +754,14 @@ public async Task SendTopicsAsync(string oauth = null, bool unlisten = false) var nonce = GenerateNonce(); - var topics = new JArray(); + var topics = new List(_topicList.Count); _previousRequestsSemaphore.WaitOne(); try { foreach (var val in _topicList) { _previousRequests.Add(new PreviousRequest(nonce, PubSubRequestType.ListenToTopic, val)); - topics.Add(new JValue(val)); + topics.Add(val); } } finally @@ -761,21 +769,19 @@ public async Task SendTopicsAsync(string oauth = null, bool unlisten = false) _previousRequestsSemaphore.Release(); } - var jsonData = new JObject( - new JProperty("type", !unlisten ? "LISTEN" : "UNLISTEN"), - new JProperty("nonce", nonce), - new JProperty("data", - new JObject( - new JProperty("topics", topics) - ) - ) - ); - if (oauth != null) + var request = new Request() { - ((JObject)jsonData.SelectToken("data"))?.Add(new JProperty("auth_token", oauth)); - } + Type = unlisten ? "UNLISTEN" : "LISTEN", + Nonce = nonce, + Data = new() + { + Topics = topics, + AuthToken = oauth, + } + }; - await _socket.SendAsync(jsonData.ToString()); + var json = JsonConvert.SerializeObject(request); + await _socket.SendAsync(json); _topicList.Clear(); } @@ -786,7 +792,7 @@ public async Task SendTopicsAsync(string oauth = null, bool unlisten = false) /// The message. private void UnaccountedFor(string message) { - _logger?.LogInformation($"[TwitchPubSub] {message}"); + _logger?.LogUnaccountedFor(message); } #region Listeners @@ -970,7 +976,7 @@ public void ListenToPredictions(string channelTwitchId) _topicToChannelId[topic] = channelTwitchId; ListenToTopic(topic); } - + /// /// /// Message sent when a user earns a new Bits badge in a particular channel, and chooses to share the notification with chat. @@ -982,7 +988,7 @@ public void ListenToChannelBitsBadgeUnlocks(string channelTwitchId) _topicToChannelId[topic] = channelTwitchId; ListenToTopic(topic); } - + /// /// /// The broadcaster or a moderator updates the low trust status of a user, or a new message has been sent in chat by a potential ban evader or a bans shared user. @@ -1005,7 +1011,7 @@ public void Connect() { ConnectAsync().GetAwaiter().GetResult(); } - + /// public async Task ConnectAsync() { @@ -1017,7 +1023,7 @@ public void Disconnect() { DisconnectAsync().GetAwaiter().GetResult(); } - + /// public async Task DisconnectAsync() {