Fixed Twitter notifications

New: Twitter notifications now require a Twitter (see settings for details)

Closes #1049
This commit is contained in:
Mark McDowall 2016-01-06 22:32:12 -08:00
parent 7ca67fe57a
commit a96718f7b3
8 changed files with 65 additions and 32 deletions

View File

@ -42,7 +42,7 @@ namespace NzbDrone.Core.Notifications.Twitter
{ {
nextStep = "step2", nextStep = "step2",
action = "openWindow", action = "openWindow",
url = _twitterService.GetOAuthRedirect(query["callbackUrl"].ToString()) url = _twitterService.GetOAuthRedirect(query["consumerKey"].ToString(), query["consumerSecret"].ToString(), query["callbackUrl"].ToString())
}; };
} }
else if (stage == "step2") else if (stage == "step2")
@ -50,7 +50,7 @@ namespace NzbDrone.Core.Notifications.Twitter
return new return new
{ {
action = "updateFields", action = "updateFields",
fields = _twitterService.GetOAuthToken(query["oauth_token"].ToString(), query["oauth_verifier"].ToString()) fields = _twitterService.GetOAuthToken(query["consumerKey"].ToString(), query["consumerSecret"].ToString(), query["oauth_token"].ToString(), query["oauth_verifier"].ToString())
}; };
} }
return new {}; return new {};

View File

@ -15,8 +15,8 @@ namespace NzbDrone.Core.Notifications.Twitter
{ {
void SendNotification(string message, TwitterSettings settings); void SendNotification(string message, TwitterSettings settings);
ValidationFailure Test(TwitterSettings settings); ValidationFailure Test(TwitterSettings settings);
string GetOAuthRedirect(string callbackUrl); string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl);
object GetOAuthToken(string oauthToken, string oauthVerifier); object GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier);
} }
public class TwitterService : ITwitterService public class TwitterService : ITwitterService
@ -24,8 +24,8 @@ namespace NzbDrone.Core.Notifications.Twitter
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly Logger _logger; private readonly Logger _logger;
private static string _consumerKey = "5jSR8a3cp0ToOqSMLMv5GtMQD"; // private static string _consumerKey = "5jSR8a3cp0ToOqSMLMv5GtMQD";
private static string _consumerSecret = "dxoZjyMq4BLsC8KxyhSOrIndhCzJ0Dik2hrLzqyJcqoGk4Pfsp"; // private static string _consumerSecret = "dxoZjyMq4BLsC8KxyhSOrIndhCzJ0Dik2hrLzqyJcqoGk4Pfsp";
public TwitterService(IHttpClient httpClient, Logger logger) public TwitterService(IHttpClient httpClient, Logger logger)
{ {
@ -43,10 +43,10 @@ namespace NzbDrone.Core.Notifications.Twitter
return HttpUtility.ParseQueryString(response.Content); return HttpUtility.ParseQueryString(response.Content);
} }
public object GetOAuthToken(string oauthToken, string oauthVerifier) public object GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier)
{ {
// Creating a new instance with a helper method // Creating a new instance with a helper method
var oAuthRequest = OAuthRequest.ForAccessToken(_consumerKey, _consumerSecret, oauthToken, "", oauthVerifier); var oAuthRequest = OAuthRequest.ForAccessToken(consumerKey, consumerSecret, oauthToken, "", oauthVerifier);
oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/access_token"; oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/access_token";
var qscoll = OAuthQuery(oAuthRequest); var qscoll = OAuthQuery(oAuthRequest);
@ -57,10 +57,10 @@ namespace NzbDrone.Core.Notifications.Twitter
}; };
} }
public string GetOAuthRedirect(string callbackUrl) public string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl)
{ {
// Creating a new instance with a helper method // Creating a new instance with a helper method
var oAuthRequest = OAuthRequest.ForRequestToken(_consumerKey, _consumerSecret, callbackUrl); var oAuthRequest = OAuthRequest.ForRequestToken(consumerKey, consumerSecret, callbackUrl);
oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/request_token"; oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/request_token";
var qscoll = OAuthQuery(oAuthRequest); var qscoll = OAuthQuery(oAuthRequest);
@ -73,10 +73,10 @@ namespace NzbDrone.Core.Notifications.Twitter
{ {
var oAuth = new TinyTwitter.OAuthInfo var oAuth = new TinyTwitter.OAuthInfo
{ {
ConsumerKey = settings.ConsumerKey,
ConsumerSecret = settings.ConsumerSecret,
AccessToken = settings.AccessToken, AccessToken = settings.AccessToken,
AccessSecret = settings.AccessTokenSecret, AccessSecret = settings.AccessTokenSecret
ConsumerKey = _consumerKey,
ConsumerSecret = _consumerSecret
}; };
var twitter = new TinyTwitter.TinyTwitter(oAuth); var twitter = new TinyTwitter.TinyTwitter(oAuth);
@ -96,9 +96,9 @@ namespace NzbDrone.Core.Notifications.Twitter
twitter.UpdateStatus(message); twitter.UpdateStatus(message);
} }
} }
catch (WebException e) catch (WebException ex)
{ {
using (var response = e.Response) using (var response = ex.Response)
{ {
var httpResponse = (HttpWebResponse)response; var httpResponse = (HttpWebResponse)response;
@ -107,14 +107,14 @@ namespace NzbDrone.Core.Notifications.Twitter
if (responseStream == null) if (responseStream == null)
{ {
_logger.Trace("Status Code: {0}", httpResponse.StatusCode); _logger.Trace("Status Code: {0}", httpResponse.StatusCode);
throw new TwitterException("Error received from Twitter: " + httpResponse.StatusCode, _logger , e); throw new TwitterException("Error received from Twitter: " + httpResponse.StatusCode, ex);
} }
using (var reader = new StreamReader(responseStream)) using (var reader = new StreamReader(responseStream))
{ {
var responseBody = reader.ReadToEnd(); var responseBody = reader.ReadToEnd();
_logger.Trace("Reponse: {0} Status Code: {1}", responseBody, httpResponse.StatusCode); _logger.Trace("Reponse: {0} Status Code: {1}", responseBody, httpResponse.StatusCode);
throw new TwitterException("Error received from Twitter: " + responseBody, _logger, e); throw new TwitterException("Error received from Twitter: " + responseBody, ex);
} }
} }
} }

View File

@ -9,6 +9,8 @@ namespace NzbDrone.Core.Notifications.Twitter
{ {
public TwitterSettingsValidator() public TwitterSettingsValidator()
{ {
RuleFor(c => c.ConsumerKey).NotEmpty();
RuleFor(c => c.ConsumerSecret).NotEmpty();
RuleFor(c => c.AccessToken).NotEmpty(); RuleFor(c => c.AccessToken).NotEmpty();
RuleFor(c => c.AccessTokenSecret).NotEmpty(); RuleFor(c => c.AccessTokenSecret).NotEmpty();
//TODO: Validate that it is a valid username (numbers, letters and underscores - I think) //TODO: Validate that it is a valid username (numbers, letters and underscores - I think)
@ -30,19 +32,25 @@ namespace NzbDrone.Core.Notifications.Twitter
AuthorizeNotification = "step1"; AuthorizeNotification = "step1";
} }
[FieldDefinition(0, Label = "Access Token", Advanced = true)] [FieldDefinition(0, Label = "Consumer Key", HelpText = "Consumer key from a Twitter application", HelpLink = "https://github.com/Sonarr/Sonarr/wiki/Twitter-Notifications")]
public string ConsumerKey { get; set; }
[FieldDefinition(1, Label = "Consumer Secret", HelpText = "Consumer secret from a Twitter application", HelpLink = "https://github.com/Sonarr/Sonarr/wiki/Twitter-Notifications")]
public string ConsumerSecret { get; set; }
[FieldDefinition(2, Label = "Access Token", Advanced = true)]
public string AccessToken { get; set; } public string AccessToken { get; set; }
[FieldDefinition(1, Label = "Access Token Secret", Advanced = true)] [FieldDefinition(3, Label = "Access Token Secret", Advanced = true)]
public string AccessTokenSecret { get; set; } public string AccessTokenSecret { get; set; }
[FieldDefinition(2, Label = "Mention", HelpText = "Mention this user in sent tweets")] [FieldDefinition(4, Label = "Mention", HelpText = "Mention this user in sent tweets")]
public string Mention { get; set; } public string Mention { get; set; }
[FieldDefinition(3, Label = "Direct Message", Type = FieldType.Checkbox, HelpText = "Send a direct message instead of a public message")] [FieldDefinition(5, Label = "Direct Message", Type = FieldType.Checkbox, HelpText = "Send a direct message instead of a public message")]
public bool DirectMessage { get; set; } public bool DirectMessage { get; set; }
[FieldDefinition(4, Label = "Connect to twitter", Type = FieldType.Action)] [FieldDefinition(6, Label = "Connect to twitter", Type = FieldType.Action)]
public string AuthorizeNotification { get; set; } public string AuthorizeNotification { get; set; }
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()

View File

@ -2,5 +2,6 @@
<project version="4"> <project version="4">
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false"> <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false">
<file url="file://$PROJECT_DIR$/System/Logs/Files/LogFileModel.js" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/System/Logs/Files/LogFileModel.js" charset="UTF-8" />
<file url="PROJECT" charset="UTF-8" />
</component> </component>
</project> </project>

View File

@ -4,5 +4,4 @@
<option name="state" value="git@github.com:NzbDrone/NzbDrone.git" /> <option name="state" value="git@github.com:NzbDrone/NzbDrone.git" />
</component> </component>
<component name="ProjectRootManager" version="2" /> <component name="ProjectRootManager" version="2" />
</project> </project>

View File

@ -1,3 +1,4 @@
var _ = require('underscore');
var vent = require('vent'); var vent = require('vent');
var Marionette = require('marionette'); var Marionette = require('marionette');
var DeleteView = require('../Delete/NotificationDeleteView'); var DeleteView = require('../Delete/NotificationDeleteView');
@ -86,10 +87,20 @@ var view = Marionette.ItemView.extend({
}, },
_onAuthorizeNotification : function() { _onAuthorizeNotification : function() {
this.ui.indicator.show();
var self = this; var self = this;
var callbackUrl = window.location.origin + '/oauth.html'; var callbackUrl = window.location.origin + '/oauth.html';
this.ui.indicator.show(); var fields = this.model.get('fields');
var promise = this.model.connectData(this.ui.authorizedNotificationButton.data('value') + '?callbackUrl=' + callbackUrl); var consumerKeyObj = _.findWhere(fields, { name: 'ConsumerKey' });
var consumerSecretObj = _.findWhere(fields, { name: 'ConsumerSecret' });
var queryParams = {
callbackUrl: callbackUrl,
consumerKey: (consumerKeyObj ? consumerKeyObj.value : ''),
consumerSecret: (consumerSecretObj ? consumerSecretObj.value : '')
};
var promise = this.model.connectData(this.ui.authorizedNotificationButton.data('value'), queryParams);
promise.always(function() { promise.always(function() {
self.ui.indicator.hide(); self.ui.indicator.hide();

View File

@ -4,14 +4,19 @@ var DeepModel = require('backbone.deepmodel');
var Messenger = require('../Shared/Messenger'); var Messenger = require('../Shared/Messenger');
module.exports = DeepModel.extend({ module.exports = DeepModel.extend({
connectData : function(action, initialQueryString) { connectData : function(action, initialQueryParams) {
var self = this; var self = this;
this.trigger('connect:sync'); this.trigger('connect:sync');
var promise = $.Deferred(); var promise = $.Deferred();
var callAction = function(action) { var callAction = function(action, queryParams) {
if (queryParams) {
action = action + '?' + $.param(queryParams, true);
}
var params = { var params = {
url : self.collection.url + '/connectData/' + action, url : self.collection.url + '/connectData/' + action,
contentType : 'application/json', contentType : 'application/json',
@ -30,11 +35,20 @@ module.exports = DeepModel.extend({
{ {
window.open(response.url); window.open(response.url);
var selfWindow = window; var selfWindow = window;
selfWindow.onCompleteOauth = function(query, callback) { selfWindow.onCompleteOauth = function(query, callback) {
delete selfWindow.onCompleteOauth; delete selfWindow.onCompleteOauth;
if (response.nextStep) { if (response.nextStep) {
callAction(response.nextStep + query); var queryParams = {};
var splitQuery = query.substring(1).split('&');
_.each(splitQuery, function (param) {
var paramSplit = param.split('=');
queryParams[paramSplit[0]] = paramSplit[1];
});
callAction(response.nextStep, _.extend(initialQueryParams, queryParams));
} }
else { else {
promise.resolve(response); promise.resolve(response);
@ -59,7 +73,7 @@ module.exports = DeepModel.extend({
} }
} }
if (response.nextStep) { if (response.nextStep) {
callAction(response.nextStep); callAction(response.nextStep, initialQueryParams);
} }
else { else {
promise.resolve(response); promise.resolve(response);
@ -67,7 +81,7 @@ module.exports = DeepModel.extend({
}); });
}; };
callAction(action, initialQueryString); callAction(action, initialQueryParams);
Messenger.monitor({ Messenger.monitor({
promise : promise, promise : promise,

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>oauth landing page</title> <title>oauth landing page</title>
<script><!-- <script><!--
window.opener.onCompleteOauth(window.location.search, function() { window.close(); }); window.opener.onCompleteOauth(window.location.search, function() { window.close(); });
--></script> --></script>
</head> </head>