From 52d8c14abe92a6d40d86ada91f6912eaefc0abdf Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Thu, 24 Sep 2020 21:14:08 +0200 Subject: [PATCH] Add initial twitter authorization views --- .../templates/accounts/views/twitter.html | 20 +++ src/newsreader/accounts/views/integrations.py | 122 ++++++++++++++++++ src/newsreader/news/collection/twitter.py | 3 + 3 files changed, 145 insertions(+) create mode 100644 src/newsreader/accounts/templates/accounts/views/twitter.html diff --git a/src/newsreader/accounts/templates/accounts/views/twitter.html b/src/newsreader/accounts/templates/accounts/views/twitter.html new file mode 100644 index 0000000..e2c51aa --- /dev/null +++ b/src/newsreader/accounts/templates/accounts/views/twitter.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+ {% if error %} +

{% trans "Twitter authorization failed" %}

+

{{ error }}

+ {% elif authorized %} +

{% trans "Twitter account is linked" %}

+

{% trans "Your Twitter account was successfully linked." %}

+ {% endif %} + +

+ {% trans "Return to integrations page" %} +

+
+
+{% endblock %} diff --git a/src/newsreader/accounts/views/integrations.py b/src/newsreader/accounts/views/integrations.py index fa343a2..3b30ba3 100644 --- a/src/newsreader/accounts/views/integrations.py +++ b/src/newsreader/accounts/views/integrations.py @@ -1,11 +1,17 @@ import logging +from urllib.parse import parse_qs, urlencode + +from django.conf import settings from django.contrib import messages from django.core.cache import cache +from django.shortcuts import redirect from django.urls import reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import RedirectView, TemplateView +from requests_oauthlib import OAuth1 as OAuth + from newsreader.news.collection.exceptions import StreamException from newsreader.news.collection.reddit import ( get_reddit_access_token, @@ -13,6 +19,12 @@ from newsreader.news.collection.reddit import ( revoke_reddit_token, ) from newsreader.news.collection.tasks import RedditTokenTask +from newsreader.news.collection.twitter import ( + TWITTER_ACCESS_TOKEN_URL, + TWITTER_AUTH_URL, + TWITTER_REQUEST_TOKEN_URL, +) +from newsreader.news.collection.utils import post logger = logging.getLogger(__name__) @@ -148,3 +160,113 @@ class RedditRevokeRedirectView(RedirectView): messages.success(request, _("Reddit account deathorized")) return response + + +# TODO hookup url to urlconf +# TODO hookup url to integrations button +class TwitterAuthRedirectView(RedirectView): + url = reverse_lazy("accounts:integrations") + + def get(self, request, *args, **kwargs): + oauth = OAuth( + settings.TWITTER_CONSUMER_ID, + client_secret=settings.TWITTER_CONSUMER_SECRET, + callback_uri=settings.TWITTER_REDIRECT_URL, + ) + + try: + response = post(TWITTER_REQUEST_TOKEN_URL, auth=oauth) + except StreamException: + logger.exception("Failed requesting Twitter authentication token") + + messages.error(request, _("Unable to retrieve initial Twitter token")) + return super().get(request, *args, **kwargs) + + params = parse_qs(response.content) + + request_oauth_token = params.get("oauth_token")[0] + request_oauth_secret = params.get("oauth_token_secret")[0] + + cache.set_many( + { + f"twitter-{request.user.email}-token": request_oauth_token, + f"twitter-{request.user.email}-secret": request_oauth_secret, + } + ) + + request_params = urlencode({"oauth_token": request_oauth_secret}) + return redirect(f"{TWITTER_AUTH_URL}/?{request_params}") + + +# TODO hookup url +class TwitterTemplateView(TemplateView): + template_name = "accounts/views/twitter.html" + + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + + denied = request.GET.get("denied", True) + oauth_token = request.GET.get("oauth_token") + request.GET.get("oauth_verifier") + + if denied: + return self.render_to_response( + { + **context, + "error": "Twitter authorization failed", + "authorized": False, + } + ) + + cached_token = cache.get(f"twitter-{request.user.email}-token") + + if oauth_token != cached_token: + return self.render_to_response( + { + **context, + "error": "OAuth tokens failed to match", + "authorized": False, + } + ) + + cached_secret = cache.get(f"twitter-{request.user.email}-secret") + + if not cached_token or not cached_token_secret: + return self.render_to_response( + { + **context, + "error": "No matching tokens found for this user", + "authorized": False, + } + ) + + oauth = OAuth( + settings.TWITTER_CONSUMER_ID, + client_secret=settings.TWITTER_CONSUMER_SECRET, + resource_owner_key=cached_token, + resource_owner_secret=cached_secret, + verifier=oauth_token_secret, + ) + + try: + response = post(TWITTER_ACCESS_TOKEN_URL, auth=oauth) + except StreamException: + logger.exception("Failed requesting Twitter access token") + + return self.render_to_response( + { + **context, + "error": "Failed requesting access token", + "authorized": False, + } + ) + + params = parse_qs(response.content) + oauth_token = params.get("oauth_token")[0] + oauth_secret = params.get("oauth_token_secret")[0] + + request.user.twitter_oauth_token = oauth_token + request.user.twitter_oauth_token_secret = oauth_secret + request.user.save() + + return self.render_to_response({**context, "error": None, "authorized": True}) diff --git a/src/newsreader/news/collection/twitter.py b/src/newsreader/news/collection/twitter.py index c2d693f..7f075d7 100644 --- a/src/newsreader/news/collection/twitter.py +++ b/src/newsreader/news/collection/twitter.py @@ -31,6 +31,9 @@ logger = logging.getLogger(__name__) TWITTER_URL = "https://twitter.com" TWITTER_API_URL = "https://api.twitter.com/1.1" +TWITTER_REQUEST_TOKEN_URL = "https://api.twitter.com/oauth/request_token" +TWITTER_AUTH_URL = "https://api.twitter.com/oauth/authorize" +TWITTER_ACCESS_TOKEN_URL = "https://api.twitter.com/oauth/access_token" class TwitterBuilder(PostBuilder):