From 3a48f07702a342b18d3f142f09e04a32ef2b6047 Mon Sep 17 00:00:00 2001 From: Qstick Date: Thu, 6 Jan 2022 21:42:32 -0600 Subject: [PATCH] Fixed: Twitter connect not sending messages after http rework (#6901) [common] --- .../Extensions/StringExtensions.cs | 21 ++ .../Notifications/Twitter/TwitterProxy.cs | 110 ++++++++++ .../Notifications/Twitter/TwitterService.cs | 48 +---- src/NzbDrone.Core/TinyTwitter.cs | 190 ------------------ 4 files changed, 139 insertions(+), 230 deletions(-) create mode 100644 src/NzbDrone.Core/Notifications/Twitter/TwitterProxy.cs delete mode 100644 src/NzbDrone.Core/TinyTwitter.cs diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs index 246dc0561..0f842a3fa 100644 --- a/src/NzbDrone.Common/Extensions/StringExtensions.cs +++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs @@ -171,5 +171,26 @@ public static bool ContainsIgnoreCase(this IEnumerable source, string va { return source.Contains(value, StringComparer.InvariantCultureIgnoreCase); } + + public static string EncodeRFC3986(this string value) + { + // From Twitterizer http://www.twitterizer.net/ + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + var encoded = Uri.EscapeDataString(value); + + return Regex + .Replace(encoded, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper()) + .Replace("(", "%28") + .Replace(")", "%29") + .Replace("$", "%24") + .Replace("!", "%21") + .Replace("*", "%2A") + .Replace("'", "%27") + .Replace("%7E", "~"); + } } } diff --git a/src/NzbDrone.Core/Notifications/Twitter/TwitterProxy.cs b/src/NzbDrone.Core/Notifications/Twitter/TwitterProxy.cs new file mode 100644 index 000000000..3330e4a07 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Twitter/TwitterProxy.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Web; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Common.OAuth; + +namespace NzbDrone.Core.Notifications.Twitter +{ + public interface ITwitterProxy + { + NameValueCollection GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier); + string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl); + void UpdateStatus(string message, TwitterSettings settings); + void DirectMessage(string message, TwitterSettings settings); + } + + public class TwitterProxy : ITwitterProxy + { + private readonly IHttpClient _httpClient; + + public TwitterProxy(IHttpClient httpClient) + { + _httpClient = httpClient; + } + + public string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl) + { + // Creating a new instance with a helper method + var oAuthRequest = OAuthRequest.ForRequestToken(consumerKey, consumerSecret, callbackUrl); + oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/request_token"; + var qscoll = HttpUtility.ParseQueryString(ExecuteRequest(GetRequest(oAuthRequest, new Dictionary())).Content); + + return string.Format("https://api.twitter.com/oauth/authorize?oauth_token={0}", qscoll["oauth_token"]); + } + + public NameValueCollection GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier) + { + // Creating a new instance with a helper method + var oAuthRequest = OAuthRequest.ForAccessToken(consumerKey, consumerSecret, oauthToken, "", oauthVerifier); + oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/access_token"; + + return HttpUtility.ParseQueryString(ExecuteRequest(GetRequest(oAuthRequest, new Dictionary())).Content); + } + + public void UpdateStatus(string message, TwitterSettings settings) + { + var oAuthRequest = OAuthRequest.ForProtectedResource("POST", settings.ConsumerKey, settings.ConsumerSecret, settings.AccessToken, settings.AccessTokenSecret); + + oAuthRequest.RequestUrl = "https://api.twitter.com/1.1/statuses/update.json"; + + var customParams = new Dictionary + { + { "status", message.EncodeRFC3986() } + }; + + var request = GetRequest(oAuthRequest, customParams); + + request.Headers.ContentType = "application/x-www-form-urlencoded"; + request.SetContent(Encoding.ASCII.GetBytes(GetCustomParametersString(customParams))); + + ExecuteRequest(request); + } + + public void DirectMessage(string message, TwitterSettings settings) + { + var oAuthRequest = OAuthRequest.ForProtectedResource("POST", settings.ConsumerKey, settings.ConsumerSecret, settings.AccessToken, settings.AccessTokenSecret); + + oAuthRequest.RequestUrl = "https://api.twitter.com/1.1/direct_messages/new.json"; + + var customParams = new Dictionary + { + { "text", message.EncodeRFC3986() }, + { "screenname", settings.Mention.EncodeRFC3986() } + }; + + var request = GetRequest(oAuthRequest, customParams); + + request.Headers.ContentType = "application/x-www-form-urlencoded"; + request.SetContent(Encoding.ASCII.GetBytes(GetCustomParametersString(customParams))); + + ExecuteRequest(request); + } + + private string GetCustomParametersString(Dictionary customParams) + { + return customParams.Select(x => string.Format("{0}={1}", x.Key, x.Value)).Join("&"); + } + + private HttpRequest GetRequest(OAuthRequest oAuthRequest, Dictionary customParams) + { + var auth = oAuthRequest.GetAuthorizationHeader(customParams); + var request = new HttpRequest(oAuthRequest.RequestUrl); + + request.Headers.Add("Authorization", auth); + + request.Method = oAuthRequest.Method == "POST" ? HttpMethod.Post : HttpMethod.Get; + + return request; + } + + private HttpResponse ExecuteRequest(HttpRequest request) + { + return _httpClient.Execute(request); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Twitter/TwitterService.cs b/src/NzbDrone.Core/Notifications/Twitter/TwitterService.cs index 39af507f3..29ffa78bf 100644 --- a/src/NzbDrone.Core/Notifications/Twitter/TwitterService.cs +++ b/src/NzbDrone.Core/Notifications/Twitter/TwitterService.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Specialized; +using System; using System.IO; using System.Net; -using System.Web; using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Common.OAuth; namespace NzbDrone.Core.Notifications.Twitter { @@ -21,31 +17,18 @@ public interface ITwitterService public class TwitterService : ITwitterService { - private readonly IHttpClient _httpClient; + private readonly ITwitterProxy _twitterProxy; private readonly Logger _logger; - public TwitterService(IHttpClient httpClient, Logger logger) + public TwitterService(ITwitterProxy twitterProxy, Logger logger) { - _httpClient = httpClient; + _twitterProxy = twitterProxy; _logger = logger; } - private NameValueCollection OAuthQuery(OAuthRequest oAuthRequest) - { - var auth = oAuthRequest.GetAuthorizationHeader(); - var request = new Common.Http.HttpRequest(oAuthRequest.RequestUrl); - request.Headers.Add("Authorization", auth); - var response = _httpClient.Get(request); - - return HttpUtility.ParseQueryString(response.Content); - } - public OAuthToken GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier) { - // Creating a new instance with a helper method - var oAuthRequest = OAuthRequest.ForAccessToken(consumerKey, consumerSecret, oauthToken, "", oauthVerifier); - oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/access_token"; - var qscoll = OAuthQuery(oAuthRequest); + var qscoll = _twitterProxy.GetOAuthToken(consumerKey, consumerSecret, oauthToken, oauthVerifier); return new OAuthToken { @@ -56,31 +39,16 @@ public OAuthToken GetOAuthToken(string consumerKey, string consumerSecret, strin public string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl) { - // Creating a new instance with a helper method - var oAuthRequest = OAuthRequest.ForRequestToken(consumerKey, consumerSecret, callbackUrl); - oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/request_token"; - var qscoll = OAuthQuery(oAuthRequest); - - return string.Format("https://api.twitter.com/oauth/authorize?oauth_token={0}", qscoll["oauth_token"]); + return _twitterProxy.GetOAuthRedirect(consumerKey, consumerSecret, callbackUrl); } public void SendNotification(string message, TwitterSettings settings) { try { - var oAuth = new TinyTwitter.OAuthInfo - { - ConsumerKey = settings.ConsumerKey, - ConsumerSecret = settings.ConsumerSecret, - AccessToken = settings.AccessToken, - AccessSecret = settings.AccessTokenSecret - }; - - var twitter = new TinyTwitter.TinyTwitter(oAuth); - if (settings.DirectMessage) { - twitter.DirectMessage(message, settings.Mention); + _twitterProxy.DirectMessage(message, settings); } else { @@ -89,7 +57,7 @@ public void SendNotification(string message, TwitterSettings settings) message += string.Format(" @{0}", settings.Mention); } - twitter.UpdateStatus(message); + _twitterProxy.UpdateStatus(message, settings); } } catch (WebException ex) diff --git a/src/NzbDrone.Core/TinyTwitter.cs b/src/NzbDrone.Core/TinyTwitter.cs deleted file mode 100644 index acd47fb0c..000000000 --- a/src/NzbDrone.Core/TinyTwitter.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Security.Cryptography; -using System.Text; -using System.Text.RegularExpressions; - -namespace TinyTwitter -{ - public class OAuthInfo - { - public string ConsumerKey { get; set; } - public string ConsumerSecret { get; set; } - public string AccessToken { get; set; } - public string AccessSecret { get; set; } - } - - public class Tweet - { - public long Id { get; set; } - public DateTime CreatedAt { get; set; } - public string UserName { get; set; } - public string ScreenName { get; set; } - public string Text { get; set; } - } - - public class TinyTwitter - { - private readonly OAuthInfo _oauth; - - public TinyTwitter(OAuthInfo oauth) - { - _oauth = oauth; - } - - public void UpdateStatus(string message) - { - new RequestBuilder(_oauth, HttpMethod.Post, "https://api.twitter.com/1.1/statuses/update.json") - .AddParameter("status", message) - .Execute(); - } - - /** - * - * As of June 26th 2015 Direct Messaging is not part of TinyTwitter. - * I have added it to Sonarr's copy to make our implementation easier - * and added this banner so it's not blindly updated. - * - **/ - public void DirectMessage(string message, string screenName) - { - new RequestBuilder(_oauth, HttpMethod.Post, "https://api.twitter.com/1.1/direct_messages/new.json") - .AddParameter("text", message) - .AddParameter("screen_name", screenName) - .Execute(); - } - - public class RequestBuilder - { - private const string VERSION = "1.0"; - private const string SIGNATURE_METHOD = "HMAC-SHA1"; - - private readonly OAuthInfo _oauth; - private readonly HttpMethod _method; - private readonly IDictionary _customParameters; - private readonly string _url; - private readonly HttpClient _httpClient; - - public RequestBuilder(OAuthInfo oauth, HttpMethod method, string url) - { - _oauth = oauth; - _method = method; - _url = url; - _customParameters = new Dictionary(); - _httpClient = new (); - } - - public RequestBuilder AddParameter(string name, string value) - { - _customParameters.Add(name, value.EncodeRFC3986()); - return this; - } - - public string Execute() - { - var timespan = GetTimestamp(); - var nonce = CreateNonce(); - - var parameters = new Dictionary(_customParameters); - AddOAuthParameters(parameters, timespan, nonce); - - var signature = GenerateSignature(parameters); - var headerValue = GenerateAuthorizationHeaderValue(parameters, signature); - - var request = new HttpRequestMessage(_method, _url); - request.Content = new FormUrlEncodedContent(_customParameters); - - request.Headers.Add("Authorization", headerValue); - - var response = _httpClient.Send(request); - return response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - } - - private string GenerateAuthorizationHeaderValue(IEnumerable> parameters, string signature) - { - return new StringBuilder("OAuth ") - .Append(parameters.Concat(new KeyValuePair("oauth_signature", signature)) - .Where(x => x.Key.StartsWith("oauth_")) - .Select(x => string.Format("{0}=\"{1}\"", x.Key, x.Value.EncodeRFC3986())) - .Join(",")) - .ToString(); - } - - private string GenerateSignature(IEnumerable> parameters) - { - var dataToSign = new StringBuilder() - .Append(_method).Append('&') - .Append(_url.EncodeRFC3986()).Append('&') - .Append(parameters - .OrderBy(x => x.Key) - .Select(x => string.Format("{0}={1}", x.Key, x.Value)) - .Join("&") - .EncodeRFC3986()); - - var signatureKey = string.Format("{0}&{1}", _oauth.ConsumerSecret.EncodeRFC3986(), _oauth.AccessSecret.EncodeRFC3986()); - var sha1 = new HMACSHA1(Encoding.ASCII.GetBytes(signatureKey)); - - var signatureBytes = sha1.ComputeHash(Encoding.ASCII.GetBytes(dataToSign.ToString())); - return Convert.ToBase64String(signatureBytes); - } - - private void AddOAuthParameters(IDictionary parameters, string timestamp, string nonce) - { - parameters.Add("oauth_version", VERSION); - parameters.Add("oauth_consumer_key", _oauth.ConsumerKey); - parameters.Add("oauth_nonce", nonce); - parameters.Add("oauth_signature_method", SIGNATURE_METHOD); - parameters.Add("oauth_timestamp", timestamp); - parameters.Add("oauth_token", _oauth.AccessToken); - } - - private static string GetTimestamp() - { - return ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds).ToString(); - } - - private static string CreateNonce() - { - return new Random().Next(0x0000000, 0x7fffffff).ToString("X8"); - } - } - } - - public static class TinyTwitterHelperExtensions - { - public static string Join(this IEnumerable items, string separator) - { - return string.Join(separator, items.ToArray()); - } - - public static IEnumerable Concat(this IEnumerable items, T value) - { - return items.Concat(new[] { value }); - } - - public static string EncodeRFC3986(this string value) - { - // From Twitterizer http://www.twitterizer.net/ - if (string.IsNullOrEmpty(value)) - { - return string.Empty; - } - - var encoded = Uri.EscapeDataString(value); - - return Regex - .Replace(encoded, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper()) - .Replace("(", "%28") - .Replace(")", "%29") - .Replace("$", "%24") - .Replace("!", "%21") - .Replace("*", "%2A") - .Replace("'", "%27") - .Replace("%7E", "~"); - } - } -}