This commit is contained in:
Sonny Bakker 2021-04-27 12:42:02 +02:00
parent 8498303006
commit 510f7187a8
6 changed files with 108 additions and 47 deletions

View file

@ -1,5 +1,9 @@
# Changelog # Changelog
## 0.3.13.7
- Check for Twitter error codes in response
## 0.3.13.6 ## 0.3.13.6
- Try to load sentry by default for all environments - Try to load sentry by default for all environments

View file

@ -1,6 +1,6 @@
{ {
"name": "newsreader", "name": "newsreader",
"version": "0.3.13.6", "version": "0.3.13.7",
"description": "Application for viewing RSS feeds", "description": "Application for viewing RSS feeds",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "newsreader" name = "newsreader"
version = "0.3.13.6" version = "0.3.13.7"
description = "Webapplication for reading RSS feeds" description = "Webapplication for reading RSS feeds"
authors = ["Sonny <sonnyba871@gmail.com>"] authors = ["Sonny <sonnyba871@gmail.com>"]
license = "GPL-3.0" license = "GPL-3.0"

View file

@ -74,19 +74,24 @@ class TwitterClientTestCase(TestCase):
self.mocked_read.assert_called() self.mocked_read.assert_called()
def test_client_catches_stream_denied_exception(self): def test_client_catches_stream_denied_exception(self):
"""
Twitter also returns these responses for accounts which have been shutdown.
Therefore the error codes should be checked inside the response body.
See https://stackoverflow.com/questions/8357568/do-twitter-access-token-expire
"""
user = UserFactory( user = UserFactory(
twitter_oauth_token=str(uuid4()), twitter_oauth_token_secret=str(uuid4()) twitter_oauth_token=str(uuid4()), twitter_oauth_token_secret=str(uuid4())
) )
timeline = TwitterTimelineFactory(user=user) timeline = TwitterTimelineFactory(user=user)
self.mocked_read.side_effect = StreamDeniedException(message="Token expired") self.mocked_read.side_effect = StreamDeniedException(message="Not authorized")
with TwitterClient([timeline]) as client: with TwitterClient([timeline]) as client:
for data, stream in client: for data, stream in client:
with self.subTest(data=data, stream=stream): with self.subTest(data=data, stream=stream):
self.assertIsNone(data) self.assertIsNone(data)
self.assertIsNone(stream) self.assertIsNone(stream)
self.assertEquals(stream.rule.error, "Token expired") self.assertEquals(stream.rule.error, "Authorization required")
self.assertEquals(stream.rule.succeeded, False) self.assertEquals(stream.rule.succeeded, False)
self.mocked_read.assert_called() self.mocked_read.assert_called()
@ -94,8 +99,8 @@ class TwitterClientTestCase(TestCase):
user.refresh_from_db() user.refresh_from_db()
timeline.refresh_from_db() timeline.refresh_from_db()
self.assertIsNone(user.twitter_oauth_token) self.assertIsNotNone(user.twitter_oauth_token)
self.assertIsNone(user.twitter_oauth_token_secret) self.assertIsNotNone(user.twitter_oauth_token_secret)
def test_client_catches_stream_timed_out_exception(self): def test_client_catches_stream_timed_out_exception(self):
timeline = TwitterTimelineFactory() timeline = TwitterTimelineFactory()
@ -160,3 +165,31 @@ class TwitterClientTestCase(TestCase):
self.assertEquals(stream.rule.succeeded, False) self.assertEquals(stream.rule.succeeded, False)
self.mocked_read.assert_called() self.mocked_read.assert_called()
def test_client_catches_token_expired(self):
user = UserFactory(
twitter_oauth_token=str(uuid4()), twitter_oauth_token_secret=str(uuid4())
)
timeline = TwitterTimelineFactory(user=user)
response = Mock(json=lambda: {"errors": [{"code": 89}]})
self.mocked_read.side_effect = StreamDeniedException(
message="Not authorized", response=response
)
with TwitterClient([timeline]) as client:
for data, stream in client:
with self.subTest(data=data, stream=stream):
self.assertIsNone(data)
self.assertIsNone(stream)
self.assertEquals(stream.rule.error, "Authorization required")
self.assertEquals(stream.rule.succeeded, False)
self.mocked_read.assert_called()
user.refresh_from_db()
timeline.refresh_from_db()
self.assertIsNone(user.twitter_oauth_token)
self.assertIsNone(user.twitter_oauth_token_secret)

View file

@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from unittest.mock import patch from unittest.mock import Mock, patch
from uuid import uuid4 from uuid import uuid4
from django.test import TestCase from django.test import TestCase
@ -152,8 +152,8 @@ class TwitterCollectorTestCase(TestCase):
user = timeline.user user = timeline.user
self.assertIsNone(user.twitter_oauth_token) self.assertIsNotNone(user.twitter_oauth_token)
self.assertIsNone(user.twitter_oauth_token_secret) self.assertIsNotNone(user.twitter_oauth_token_secret)
def test_forbidden(self): def test_forbidden(self):
self.mocked_fetch.side_effect = StreamForbiddenException self.mocked_fetch.side_effect = StreamForbiddenException
@ -178,3 +178,25 @@ class TwitterCollectorTestCase(TestCase):
self.assertEquals(Post.objects.count(), 0) self.assertEquals(Post.objects.count(), 0)
self.assertEquals(timeline.succeeded, False) self.assertEquals(timeline.succeeded, False)
self.assertEquals(timeline.error, "Stream timed out") self.assertEquals(timeline.error, "Stream timed out")
def test_token_expired(self):
response = Mock(json=lambda: {"errors": [{"code": 89}]})
self.mocked_fetch.side_effect = StreamDeniedException(response=response)
timeline = TwitterTimelineFactory(
user__twitter_oauth_token=str(uuid4()),
user__twitter_oauth_token_secret=str(uuid4()),
)
collector = TwitterCollector()
collector.collect(rules=[timeline])
self.assertEquals(Post.objects.count(), 0)
self.assertEquals(timeline.succeeded, False)
self.assertEquals(timeline.error, "Stream does not have sufficient permissions")
user = timeline.user
self.assertIsNone(user.twitter_oauth_token)
self.assertIsNone(user.twitter_oauth_token_secret)

View file

@ -232,44 +232,6 @@ class TwitterClient(PostClient):
self.set_rule_error(stream.rule, e) self.set_rule_error(stream.rule, e)
break
except StreamDeniedException as e:
logger.warning(
f"Access token expired for user {stream.rule.user.pk}"
)
try:
import sentry_sdk
with sentry_sdk.push_scope() as scope:
scope.set_extra(
"content", e.response.content if e.response else None
)
sentry_sdk.capture_message(
"Twitter authentication credentials reset"
)
except ImportError:
pass
stream.rule.user.twitter_oauth_token = None
stream.rule.user.twitter_oauth_token_secret = None
stream.rule.user.save()
message = _(
"Your Twitter account credentials have expired. Re-authenticate in"
" the settings page to keep retrieving Twitter specific information"
" from your account."
)
send_mail(
"Twitter account needs re-authentication",
message,
None,
[stream.rule.user.email],
)
self.set_rule_error(stream.rule, e)
break break
except (StreamNotFoundException, StreamTimeOutException) as e: except (StreamNotFoundException, StreamTimeOutException) as e:
logger.warning(f"Request failed for {stream.rule.screen_name}") logger.warning(f"Request failed for {stream.rule.screen_name}")
@ -282,6 +244,46 @@ class TwitterClient(PostClient):
self.set_rule_error(stream.rule, e) self.set_rule_error(stream.rule, e)
if not e.response:
continue
try:
response_data = e.response.json()
except JSONDecodeError:
continue
if "errors" in response_data:
errors = response_data["errors"]
token_expired = any(error["code"] == 89 for error in errors)
try:
import sentry_sdk
with sentry_sdk.push_scope() as scope:
scope.set_extra("content", response_data)
sentry_sdk.capture_message(
"Twitter authentication credentials reset"
)
except ImportError:
pass
stream.rule.user.twitter_oauth_token = None
stream.rule.user.twitter_oauth_token_secret = None
stream.rule.user.save()
message = _(
"Your Twitter account credentials have expired. Re-authenticate in"
" the settings page to keep retrieving Twitter specific information"
" from your account."
)
send_mail(
"Twitter account needs re-authentication",
message,
None,
[stream.rule.user.email],
)
continue continue
finally: finally:
stream.rule.last_run = timezone.now() stream.rule.last_run = timezone.now()