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",
action = "openWindow",
url = _twitterService.GetOAuthRedirect(query["callbackUrl"].ToString())
url = _twitterService.GetOAuthRedirect(query["consumerKey"].ToString(), query["consumerSecret"].ToString(), query["callbackUrl"].ToString())
};
}
else if (stage == "step2")
@ -50,7 +50,7 @@ namespace NzbDrone.Core.Notifications.Twitter
return new
{
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 {};

View File

@ -15,8 +15,8 @@ namespace NzbDrone.Core.Notifications.Twitter
{
void SendNotification(string message, TwitterSettings settings);
ValidationFailure Test(TwitterSettings settings);
string GetOAuthRedirect(string callbackUrl);
object GetOAuthToken(string oauthToken, string oauthVerifier);
string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl);
object GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier);
}
public class TwitterService : ITwitterService
@ -24,8 +24,8 @@ namespace NzbDrone.Core.Notifications.Twitter
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
private static string _consumerKey = "5jSR8a3cp0ToOqSMLMv5GtMQD";
private static string _consumerSecret = "dxoZjyMq4BLsC8KxyhSOrIndhCzJ0Dik2hrLzqyJcqoGk4Pfsp";
// private static string _consumerKey = "5jSR8a3cp0ToOqSMLMv5GtMQD";
// private static string _consumerSecret = "dxoZjyMq4BLsC8KxyhSOrIndhCzJ0Dik2hrLzqyJcqoGk4Pfsp";
public TwitterService(IHttpClient httpClient, Logger logger)
{
@ -43,10 +43,10 @@ namespace NzbDrone.Core.Notifications.Twitter
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
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";
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
var oAuthRequest = OAuthRequest.ForRequestToken(_consumerKey, _consumerSecret, callbackUrl);
var oAuthRequest = OAuthRequest.ForRequestToken(consumerKey, consumerSecret, callbackUrl);
oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/request_token";
var qscoll = OAuthQuery(oAuthRequest);
@ -73,10 +73,10 @@ namespace NzbDrone.Core.Notifications.Twitter
{
var oAuth = new TinyTwitter.OAuthInfo
{
ConsumerKey = settings.ConsumerKey,
ConsumerSecret = settings.ConsumerSecret,
AccessToken = settings.AccessToken,
AccessSecret = settings.AccessTokenSecret,
ConsumerKey = _consumerKey,
ConsumerSecret = _consumerSecret
AccessSecret = settings.AccessTokenSecret
};
var twitter = new TinyTwitter.TinyTwitter(oAuth);
@ -96,9 +96,9 @@ namespace NzbDrone.Core.Notifications.Twitter
twitter.UpdateStatus(message);
}
}
catch (WebException e)
catch (WebException ex)
{
using (var response = e.Response)
using (var response = ex.Response)
{
var httpResponse = (HttpWebResponse)response;
@ -107,14 +107,14 @@ namespace NzbDrone.Core.Notifications.Twitter
if (responseStream == null)
{
_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))
{
var responseBody = reader.ReadToEnd();
_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()
{
RuleFor(c => c.ConsumerKey).NotEmpty();
RuleFor(c => c.ConsumerSecret).NotEmpty();
RuleFor(c => c.AccessToken).NotEmpty();
RuleFor(c => c.AccessTokenSecret).NotEmpty();
//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";
}
[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; }
[FieldDefinition(1, Label = "Access Token Secret", Advanced = true)]
[FieldDefinition(3, Label = "Access Token Secret", Advanced = true)]
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; }
[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; }
[FieldDefinition(4, Label = "Connect to twitter", Type = FieldType.Action)]
[FieldDefinition(6, Label = "Connect to twitter", Type = FieldType.Action)]
public string AuthorizeNotification { get; set; }
public NzbDroneValidationResult Validate()

View File

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

View File

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

View File

@ -1,3 +1,4 @@
var _ = require('underscore');
var vent = require('vent');
var Marionette = require('marionette');
var DeleteView = require('../Delete/NotificationDeleteView');
@ -86,10 +87,20 @@ var view = Marionette.ItemView.extend({
},
_onAuthorizeNotification : function() {
this.ui.indicator.show();
var self = this;
var callbackUrl = window.location.origin + '/oauth.html';
this.ui.indicator.show();
var promise = this.model.connectData(this.ui.authorizedNotificationButton.data('value') + '?callbackUrl=' + callbackUrl);
var fields = this.model.get('fields');
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() {
self.ui.indicator.hide();

View File

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

View File

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