mirror of
https://github.com/caronc/apprise.git
synced 2024-11-08 09:14:53 +01:00
Twitter Image Attachment Support Added (#536)
This commit is contained in:
parent
e73025863b
commit
2a81899e6e
@ -28,6 +28,7 @@
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from requests_oauthlib import OAuth1
|
||||
from json import dumps
|
||||
@ -39,6 +40,7 @@ from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
from ..attachment.AttachBase import AttachBase
|
||||
|
||||
IS_USER = re.compile(r'^\s*@?(?P<user>[A-Z0-9_]+)$', re.I)
|
||||
|
||||
@ -87,9 +89,6 @@ class NotifyTwitter(NotifyBase):
|
||||
# Twitter does have titles when creating a message
|
||||
title_maxlen = 0
|
||||
|
||||
# Twitter API
|
||||
twitter_api = 'api.twitter.com'
|
||||
|
||||
# Twitter API Reference To Acquire Someone's Twitter ID
|
||||
twitter_lookup = 'https://api.twitter.com/1.1/users/lookup.json'
|
||||
|
||||
@ -103,6 +102,13 @@ class NotifyTwitter(NotifyBase):
|
||||
# Twitter API Reference To Send A Public Tweet
|
||||
twitter_tweet = 'https://api.twitter.com/1.1/statuses/update.json'
|
||||
|
||||
# it is documented on the site that the maximum images per tweet
|
||||
# is 4 (unless it's a GIF, then it's only 1)
|
||||
__tweet_non_gif_images_batch = 4
|
||||
|
||||
# Twitter Media (Attachment) Upload Location
|
||||
twitter_media = 'https://upload.twitter.com/1.1/media/upload.json'
|
||||
|
||||
# Twitter is kind enough to return how many more requests we're allowed to
|
||||
# continue to make within it's header response as:
|
||||
# X-Rate-Limit-Reset: The epoc time (in seconds) we can expect our
|
||||
@ -176,10 +182,15 @@ class NotifyTwitter(NotifyBase):
|
||||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
'batch': {
|
||||
'name': _('Batch Mode'),
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, ckey, csecret, akey, asecret, targets=None,
|
||||
mode=TwitterMessageMode.DM, cache=True, **kwargs):
|
||||
mode=TwitterMessageMode.DM, cache=True, batch=True, **kwargs):
|
||||
"""
|
||||
Initialize Twitter Object
|
||||
|
||||
@ -217,6 +228,9 @@ class NotifyTwitter(NotifyBase):
|
||||
# Set Cache Flag
|
||||
self.cache = cache
|
||||
|
||||
# Prepare Image Batch Mode Flag
|
||||
self.batch = batch
|
||||
|
||||
if self.mode not in TWITTER_MESSAGE_MODES:
|
||||
msg = 'The Twitter message mode specified ({}) is invalid.' \
|
||||
.format(mode)
|
||||
@ -250,42 +264,171 @@ class NotifyTwitter(NotifyBase):
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Perform Twitter Notification
|
||||
"""
|
||||
|
||||
# Call the _send_ function applicable to whatever mode we're in
|
||||
# Build a list of our attachments
|
||||
attachments = []
|
||||
|
||||
if attach:
|
||||
# We need to upload our payload first so that we can source it
|
||||
# in remaining messages
|
||||
for attachment in attach:
|
||||
|
||||
# Perform some simple error checking
|
||||
if not attachment:
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access attachment {}.'.format(
|
||||
attachment.url(privacy=True)))
|
||||
return False
|
||||
|
||||
if not re.match(r'^image/.*', attachment.mimetype, re.I):
|
||||
# Only support images at this time
|
||||
self.logger.warning(
|
||||
'Ignoring unsupported Twitter attachment {}.'.format(
|
||||
attachment.url(privacy=True)))
|
||||
continue
|
||||
|
||||
self.logger.debug(
|
||||
'Preparing Twiter attachment {}'.format(
|
||||
attachment.url(privacy=True)))
|
||||
|
||||
# Upload our image and get our id associated with it
|
||||
# see: https://developer.twitter.com/en/docs/twitter-api/v1/\
|
||||
# media/upload-media/api-reference/post-media-upload
|
||||
postokay, response = self._fetch(
|
||||
self.twitter_media,
|
||||
payload=attachment,
|
||||
)
|
||||
|
||||
if not postokay:
|
||||
# We can't post our attachment
|
||||
return False
|
||||
|
||||
if not (isinstance(response, dict)
|
||||
and response.get('media_id')):
|
||||
self.logger.debug(
|
||||
'Could not attach the file to Twitter: %s (mime=%s)',
|
||||
attachment.name, attachment.mimetype)
|
||||
continue
|
||||
|
||||
# If we get here, our output will look something like this:
|
||||
# {
|
||||
# "media_id": 710511363345354753,
|
||||
# "media_id_string": "710511363345354753",
|
||||
# "media_key": "3_710511363345354753",
|
||||
# "size": 11065,
|
||||
# "expires_after_secs": 86400,
|
||||
# "image": {
|
||||
# "image_type": "image/jpeg",
|
||||
# "w": 800,
|
||||
# "h": 320
|
||||
# }
|
||||
# }
|
||||
|
||||
response.update({
|
||||
# Update our response to additionally include the
|
||||
# attachment details
|
||||
'file_name': attachment.name,
|
||||
'file_mime': attachment.mimetype,
|
||||
'file_path': attachment.path,
|
||||
})
|
||||
|
||||
# Save our pre-prepared payload for attachment posting
|
||||
attachments.append(response)
|
||||
|
||||
# - calls _send_tweet if the mode is set so
|
||||
# - calls _send_dm (direct message) otherwise
|
||||
return getattr(self, '_send_{}'.format(self.mode))(
|
||||
body=body, title=title, notify_type=notify_type, **kwargs)
|
||||
body=body, title=title, notify_type=notify_type,
|
||||
attachments=attachments, **kwargs)
|
||||
|
||||
def _send_tweet(self, body, title='', notify_type=NotifyType.INFO,
|
||||
**kwargs):
|
||||
attachments=None, **kwargs):
|
||||
"""
|
||||
Twitter Public Tweet
|
||||
"""
|
||||
|
||||
# Error Tracking
|
||||
has_error = False
|
||||
|
||||
payload = {
|
||||
'status': body,
|
||||
}
|
||||
|
||||
# Send Tweet
|
||||
postokay, response = self._fetch(
|
||||
self.twitter_tweet,
|
||||
payload=payload,
|
||||
json=False,
|
||||
)
|
||||
payloads = []
|
||||
if not attachments:
|
||||
payloads.append(payload)
|
||||
|
||||
else:
|
||||
# Group our images if batch is set to do so
|
||||
batch_size = 1 if not self.batch \
|
||||
else self.__tweet_non_gif_images_batch
|
||||
|
||||
# Track our batch control in our message generation
|
||||
batches = []
|
||||
batch = []
|
||||
for attachment in attachments:
|
||||
batch.append(str(attachment['media_id']))
|
||||
|
||||
# Twitter supports batching images together. This allows
|
||||
# the batching of multiple images together. Twitter also
|
||||
# makes it clear that you can't batch `gif` files; they need
|
||||
# to be separate. So the below preserves the ordering that
|
||||
# a user passed their attachments in. if 4-non-gif images
|
||||
# are passed, they are all part of a single message.
|
||||
#
|
||||
# however, if they pass in image, gif, image, gif. The
|
||||
# gif's inbetween break apart the batches so this would
|
||||
# produce 4 separate tweets.
|
||||
#
|
||||
# If you passed in, image, image, gif, image. <- This would
|
||||
# produce 3 images (as the first 2 images could be lumped
|
||||
# together as a batch)
|
||||
if not re.match(
|
||||
r'^image/(png|jpe?g)', attachment['file_mime'], re.I) \
|
||||
or len(batch) >= batch_size:
|
||||
batches.append(','.join(batch))
|
||||
batch = []
|
||||
|
||||
if batch:
|
||||
batches.append(','.join(batch))
|
||||
|
||||
for no, media_ids in enumerate(batches):
|
||||
_payload = deepcopy(payload)
|
||||
_payload['media_ids'] = media_ids
|
||||
|
||||
if no:
|
||||
# strip text and replace it with the image representation
|
||||
_payload['status'] = \
|
||||
'{:02d}/{:02d}'.format(no + 1, len(batches))
|
||||
payloads.append(_payload)
|
||||
|
||||
for no, payload in enumerate(payloads, start=1):
|
||||
# Send Tweet
|
||||
postokay, response = self._fetch(
|
||||
self.twitter_tweet,
|
||||
payload=payload,
|
||||
json=False,
|
||||
)
|
||||
|
||||
if not postokay:
|
||||
# Track our error
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
if postokay:
|
||||
self.logger.info(
|
||||
'Sent Twitter notification as public tweet.')
|
||||
'Sent [{:02d}/{:02d}] Twitter notification as public tweet.'
|
||||
.format(no, len(payloads)))
|
||||
|
||||
return postokay
|
||||
return not has_error
|
||||
|
||||
def _send_dm(self, body, title='', notify_type=NotifyType.INFO,
|
||||
**kwargs):
|
||||
attachments=None, **kwargs):
|
||||
"""
|
||||
Twitter Direct Message
|
||||
"""
|
||||
@ -318,24 +461,48 @@ class NotifyTwitter(NotifyBase):
|
||||
'Failed to acquire user(s) to Direct Message via Twitter')
|
||||
return False
|
||||
|
||||
for screen_name, user_id in targets.items():
|
||||
# Assign our user
|
||||
payload['event']['message_create']['target']['recipient_id'] = \
|
||||
user_id
|
||||
payloads = []
|
||||
if not attachments:
|
||||
payloads.append(payload)
|
||||
|
||||
# Send Twitter DM
|
||||
postokay, response = self._fetch(
|
||||
self.twitter_dm,
|
||||
payload=payload,
|
||||
)
|
||||
else:
|
||||
for no, attachment in enumerate(attachments):
|
||||
_payload = deepcopy(payload)
|
||||
_data = _payload['event']['message_create']['message_data']
|
||||
_data['attachment'] = {
|
||||
'type': 'media',
|
||||
'media': {
|
||||
'id': attachment['media_id']
|
||||
},
|
||||
'additional_owners':
|
||||
','.join([str(x) for x in targets.values()])
|
||||
}
|
||||
if no:
|
||||
# strip text and replace it with the image representation
|
||||
_data['text'] = \
|
||||
'{:02d}/{:02d}'.format(no + 1, len(attachments))
|
||||
payloads.append(_payload)
|
||||
|
||||
if not postokay:
|
||||
# Track our error
|
||||
has_error = True
|
||||
continue
|
||||
for no, payload in enumerate(payloads, start=1):
|
||||
for screen_name, user_id in targets.items():
|
||||
# Assign our user
|
||||
target = payload['event']['message_create']['target']
|
||||
target['recipient_id'] = user_id
|
||||
|
||||
self.logger.info(
|
||||
'Sent Twitter DM notification to @{}.'.format(screen_name))
|
||||
# Send Twitter DM
|
||||
postokay, response = self._fetch(
|
||||
self.twitter_dm,
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
if not postokay:
|
||||
# Track our error
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
self.logger.info(
|
||||
'Sent [{:02d}/{:02d}] Twitter DM notification to @{}.'
|
||||
.format(no, len(payloads), screen_name))
|
||||
|
||||
return not has_error
|
||||
|
||||
@ -458,13 +625,23 @@ class NotifyTwitter(NotifyBase):
|
||||
"""
|
||||
|
||||
headers = {
|
||||
'Host': self.twitter_api,
|
||||
'User-Agent': self.app_id,
|
||||
}
|
||||
|
||||
if json:
|
||||
data = None
|
||||
files = None
|
||||
|
||||
# Open our attachment path if required:
|
||||
if isinstance(payload, AttachBase):
|
||||
# prepare payload
|
||||
files = {'media': (payload.name, open(payload.path, 'rb'))}
|
||||
|
||||
elif json:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
payload = dumps(payload)
|
||||
data = dumps(payload)
|
||||
|
||||
else:
|
||||
data = payload
|
||||
|
||||
auth = OAuth1(
|
||||
self.ckey,
|
||||
@ -506,7 +683,8 @@ class NotifyTwitter(NotifyBase):
|
||||
try:
|
||||
r = fn(
|
||||
url,
|
||||
data=payload,
|
||||
data=data,
|
||||
files=files,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
@ -562,6 +740,20 @@ class NotifyTwitter(NotifyBase):
|
||||
# Mark our failure
|
||||
return (False, content)
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occurred while handling {}.'.format(
|
||||
payload.name if isinstance(payload, AttachBase)
|
||||
else payload))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return (False, content)
|
||||
|
||||
finally:
|
||||
# Close our file (if it's open) stored in the second element
|
||||
# of our files tuple (index 1)
|
||||
if files:
|
||||
files['media'][1].close()
|
||||
|
||||
return (True, content)
|
||||
|
||||
@property
|
||||
@ -581,6 +773,8 @@ class NotifyTwitter(NotifyBase):
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'mode': self.mode,
|
||||
'batch': 'yes' if self.batch else 'no',
|
||||
'cache': 'yes' if self.cache else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
@ -653,10 +847,16 @@ class NotifyTwitter(NotifyBase):
|
||||
# Store any remaining items as potential targets
|
||||
results['targets'].extend(tokens[3:])
|
||||
|
||||
# Get Cache Flag (reduces lookup hits)
|
||||
if 'cache' in results['qsd'] and len(results['qsd']['cache']):
|
||||
results['cache'] = \
|
||||
parse_bool(results['qsd']['cache'], True)
|
||||
|
||||
# Get Batch Mode Flag
|
||||
results['batch'] = \
|
||||
parse_bool(results['qsd'].get(
|
||||
'batch', NotifyTwitter.template_args['batch']['default']))
|
||||
|
||||
# The 'to' makes it easier to use yaml configuration
|
||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||
results['targets'] += \
|
||||
|
@ -23,19 +23,26 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import os
|
||||
import six
|
||||
import mock
|
||||
import pytest
|
||||
import requests
|
||||
from json import dumps
|
||||
from datetime import datetime
|
||||
from apprise import Apprise
|
||||
from apprise import plugins
|
||||
from apprise import NotifyType
|
||||
from apprise import AppriseAttachment
|
||||
from helpers import AppriseURLTester
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
import logging
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
# Attachment Directory
|
||||
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
|
||||
|
||||
# Our Testing URLs
|
||||
apprise_url_tests = (
|
||||
##################################
|
||||
@ -77,7 +84,9 @@ apprise_url_tests = (
|
||||
# However we'll be okay if we return a proper response
|
||||
'requests_response_text': {
|
||||
'id': 12345,
|
||||
'screen_name': 'test'
|
||||
'screen_name': 'test',
|
||||
# For attachment handling
|
||||
'media_id': 123,
|
||||
},
|
||||
}),
|
||||
('twitter://consumer_key/consumer_secret/access_token/access_secret', {
|
||||
@ -86,7 +95,9 @@ apprise_url_tests = (
|
||||
# However we'll be okay if we return a proper response
|
||||
'requests_response_text': {
|
||||
'id': 12345,
|
||||
'screen_name': 'test'
|
||||
'screen_name': 'test',
|
||||
# For attachment handling
|
||||
'media_id': 123,
|
||||
},
|
||||
}),
|
||||
# A duplicate of the entry above, this will cause cache to be referenced
|
||||
@ -96,7 +107,9 @@ apprise_url_tests = (
|
||||
# However we'll be okay if we return a proper response
|
||||
'requests_response_text': {
|
||||
'id': 12345,
|
||||
'screen_name': 'test'
|
||||
'screen_name': 'test',
|
||||
# For attachment handling
|
||||
'media_id': 123,
|
||||
},
|
||||
}),
|
||||
# handle cases where the screen_name is missing from the response causing
|
||||
@ -107,6 +120,8 @@ apprise_url_tests = (
|
||||
# However we'll be okay if we return a proper response
|
||||
'requests_response_text': {
|
||||
'id': 12345,
|
||||
# For attachment handling
|
||||
'media_id': 123,
|
||||
},
|
||||
# due to a mangled response_text we'll fail
|
||||
'notify_response': False,
|
||||
@ -119,8 +134,8 @@ apprise_url_tests = (
|
||||
'notify_response': False,
|
||||
}),
|
||||
('twitter://user@consumer_key/csecret/access_token/access_secret'
|
||||
'?cache=No', {
|
||||
# No Cache
|
||||
'?cache=No&batch=No', {
|
||||
# No Cache & No Batch
|
||||
'instance': plugins.NotifyTwitter,
|
||||
'requests_response_text': [{
|
||||
'id': 12345,
|
||||
@ -404,3 +419,465 @@ def test_plugin_twitter_edge_cases():
|
||||
plugins.NotifyTwitter(
|
||||
ckey='value', csecret='value', akey='value', asecret='value',
|
||||
targets='%G@rB@g3')
|
||||
|
||||
|
||||
@mock.patch('requests.post')
|
||||
@mock.patch('requests.get')
|
||||
def test_plugin_twitter_dm_attachments(mock_get, mock_post):
|
||||
"""
|
||||
NotifyTwitter() DM Attachment Checks
|
||||
|
||||
"""
|
||||
ckey = 'ckey'
|
||||
csecret = 'csecret'
|
||||
akey = 'akey'
|
||||
asecret = 'asecret'
|
||||
screen_name = 'apprise'
|
||||
|
||||
good_dm_response_obj = {
|
||||
'screen_name': screen_name,
|
||||
'id': 9876,
|
||||
}
|
||||
|
||||
# Disable Throttling to speed testing
|
||||
plugins.NotifyBase.request_rate_per_sec = 0
|
||||
|
||||
# Prepare a good DM response
|
||||
good_dm_response = mock.Mock()
|
||||
good_dm_response.content = dumps(good_dm_response_obj)
|
||||
good_dm_response.status_code = requests.codes.ok
|
||||
|
||||
# Prepare bad response
|
||||
bad_response = mock.Mock()
|
||||
bad_response.content = dumps({})
|
||||
bad_response.status_code = requests.codes.internal_server_error
|
||||
|
||||
# Prepare a good media response
|
||||
good_media_response = mock.Mock()
|
||||
good_media_response.content = dumps({
|
||||
"media_id": 710511363345354753,
|
||||
"media_id_string": "710511363345354753",
|
||||
"media_key": "3_710511363345354753",
|
||||
"size": 11065,
|
||||
"expires_after_secs": 86400,
|
||||
"image": {
|
||||
"image_type": "image/jpeg",
|
||||
"w": 800,
|
||||
"h": 320
|
||||
}
|
||||
})
|
||||
good_media_response.status_code = requests.codes.ok
|
||||
|
||||
# Prepare a bad media response
|
||||
bad_media_response = mock.Mock()
|
||||
bad_media_response.content = dumps({
|
||||
"errors": [
|
||||
{
|
||||
"code": 93,
|
||||
"message": "This application is not allowed to access or "
|
||||
"delete your direct messages.",
|
||||
}]})
|
||||
bad_media_response.status_code = requests.codes.internal_server_error
|
||||
|
||||
mock_post.side_effect = [good_media_response, good_dm_response]
|
||||
mock_get.return_value = good_dm_response
|
||||
|
||||
twitter_url = 'twitter://{}/{}/{}/{}'.format(ckey, csecret, akey, asecret)
|
||||
|
||||
# attach our content
|
||||
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
|
||||
|
||||
# instantiate our object
|
||||
obj = Apprise.instantiate(twitter_url)
|
||||
|
||||
# Send our notification
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is True
|
||||
|
||||
# Test our call count
|
||||
assert mock_get.call_count == 1
|
||||
assert mock_get.call_args_list[0][0][0] == \
|
||||
'https://api.twitter.com/1.1/account/verify_credentials.json'
|
||||
assert mock_post.call_count == 2
|
||||
assert mock_post.call_args_list[0][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[1][0][0] == \
|
||||
'https://api.twitter.com/1.1/direct_messages/events/new.json'
|
||||
|
||||
mock_get.reset_mock()
|
||||
mock_post.reset_mock()
|
||||
|
||||
# Test case where upload fails
|
||||
mock_get.return_value = good_dm_response
|
||||
mock_post.side_effect = [bad_media_response, good_dm_response]
|
||||
|
||||
# instantiate our object
|
||||
obj = Apprise.instantiate(twitter_url)
|
||||
|
||||
# Send our notification; it will fail because of the media response
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is False
|
||||
|
||||
# Test our call count
|
||||
assert mock_get.call_count == 0
|
||||
# No get request as cached response is used
|
||||
assert mock_post.call_count == 1
|
||||
assert mock_post.call_args_list[0][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
|
||||
mock_get.reset_mock()
|
||||
mock_post.reset_mock()
|
||||
|
||||
# Test case where upload fails
|
||||
mock_get.return_value = good_dm_response
|
||||
mock_post.side_effect = [good_media_response, bad_response]
|
||||
|
||||
# instantiate our object
|
||||
obj = Apprise.instantiate(twitter_url)
|
||||
|
||||
# Send our notification; it will fail because of the media response
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is False
|
||||
|
||||
assert mock_get.call_count == 0
|
||||
# No get request as cached response is used
|
||||
assert mock_post.call_count == 2
|
||||
assert mock_post.call_args_list[0][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[1][0][0] == \
|
||||
'https://api.twitter.com/1.1/direct_messages/events/new.json'
|
||||
|
||||
mock_get.reset_mock()
|
||||
mock_post.reset_mock()
|
||||
|
||||
mock_post.side_effect = [good_media_response, good_dm_response]
|
||||
mock_get.return_value = good_dm_response
|
||||
|
||||
# An invalid attachment will cause a failure
|
||||
path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=path) is False
|
||||
|
||||
# No get request as cached response is used
|
||||
assert mock_get.call_count == 0
|
||||
|
||||
# No post request as attachment is no good anyway
|
||||
assert mock_post.call_count == 0
|
||||
|
||||
mock_get.reset_mock()
|
||||
mock_post.reset_mock()
|
||||
|
||||
mock_post.side_effect = [
|
||||
good_media_response, good_media_response, good_media_response,
|
||||
good_media_response, good_dm_response, good_dm_response,
|
||||
good_dm_response, good_dm_response]
|
||||
mock_get.return_value = good_dm_response
|
||||
|
||||
# 4 images are produced
|
||||
attach = [
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.jpeg'),
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.png'),
|
||||
# This one is not supported, so it's ignored gracefully
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.mp4'),
|
||||
]
|
||||
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is True
|
||||
|
||||
assert mock_get.call_count == 0
|
||||
# No get request as cached response is used
|
||||
assert mock_post.call_count == 8
|
||||
assert mock_post.call_args_list[0][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[1][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[2][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[3][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[4][0][0] == \
|
||||
'https://api.twitter.com/1.1/direct_messages/events/new.json'
|
||||
assert mock_post.call_args_list[5][0][0] == \
|
||||
'https://api.twitter.com/1.1/direct_messages/events/new.json'
|
||||
assert mock_post.call_args_list[6][0][0] == \
|
||||
'https://api.twitter.com/1.1/direct_messages/events/new.json'
|
||||
assert mock_post.call_args_list[7][0][0] == \
|
||||
'https://api.twitter.com/1.1/direct_messages/events/new.json'
|
||||
|
||||
mock_get.reset_mock()
|
||||
mock_post.reset_mock()
|
||||
|
||||
# We have an OSError thrown in the middle of our preparation
|
||||
mock_post.side_effect = [good_media_response, OSError()]
|
||||
mock_get.return_value = good_dm_response
|
||||
|
||||
# 2 images are produced
|
||||
attach = [
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.png'),
|
||||
# This one is not supported, so it's ignored gracefully
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.mp4'),
|
||||
]
|
||||
|
||||
# We'll fail to send this time
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is False
|
||||
|
||||
assert mock_get.call_count == 0
|
||||
# No get request as cached response is used
|
||||
assert mock_post.call_count == 2
|
||||
assert mock_post.call_args_list[0][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[1][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
|
||||
|
||||
@mock.patch('requests.post')
|
||||
@mock.patch('requests.get')
|
||||
def test_plugin_twitter_tweet_attachments(mock_get, mock_post):
|
||||
"""
|
||||
NotifyTwitter() Tweet Attachment Checks
|
||||
|
||||
"""
|
||||
ckey = 'ckey'
|
||||
csecret = 'csecret'
|
||||
akey = 'akey'
|
||||
asecret = 'asecret'
|
||||
screen_name = 'apprise'
|
||||
|
||||
good_tweet_response_obj = {
|
||||
'screen_name': screen_name,
|
||||
'id': 9876,
|
||||
}
|
||||
|
||||
# Disable Throttling to speed testing
|
||||
plugins.NotifyBase.request_rate_per_sec = 0
|
||||
|
||||
# Prepare a good DM response
|
||||
good_tweet_response = mock.Mock()
|
||||
good_tweet_response.content = dumps(good_tweet_response_obj)
|
||||
good_tweet_response.status_code = requests.codes.ok
|
||||
|
||||
# Prepare bad response
|
||||
bad_response = mock.Mock()
|
||||
bad_response.content = dumps({})
|
||||
bad_response.status_code = requests.codes.internal_server_error
|
||||
|
||||
# Prepare a good media response
|
||||
good_media_response = mock.Mock()
|
||||
good_media_response.content = dumps({
|
||||
"media_id": 710511363345354753,
|
||||
"media_id_string": "710511363345354753",
|
||||
"media_key": "3_710511363345354753",
|
||||
"size": 11065,
|
||||
"expires_after_secs": 86400,
|
||||
"image": {
|
||||
"image_type": "image/jpeg",
|
||||
"w": 800,
|
||||
"h": 320
|
||||
}
|
||||
})
|
||||
good_media_response.status_code = requests.codes.ok
|
||||
|
||||
# Prepare a bad media response
|
||||
bad_media_response = mock.Mock()
|
||||
bad_media_response.content = dumps({
|
||||
"errors": [
|
||||
{
|
||||
"code": 93,
|
||||
"message": "This application is not allowed to access or "
|
||||
"delete your direct messages.",
|
||||
}]})
|
||||
bad_media_response.status_code = requests.codes.internal_server_error
|
||||
|
||||
mock_post.side_effect = [good_media_response, good_tweet_response]
|
||||
mock_get.return_value = good_tweet_response
|
||||
|
||||
twitter_url = 'twitter://{}/{}/{}/{}?mode=tweet'.format(
|
||||
ckey, csecret, akey, asecret)
|
||||
|
||||
# attach our content
|
||||
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
|
||||
|
||||
# instantiate our object
|
||||
obj = Apprise.instantiate(twitter_url)
|
||||
|
||||
# Send our notification
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is True
|
||||
|
||||
# Test our call count
|
||||
assert mock_get.call_count == 0
|
||||
assert mock_post.call_count == 2
|
||||
assert mock_post.call_args_list[0][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[1][0][0] == \
|
||||
'https://api.twitter.com/1.1/statuses/update.json'
|
||||
|
||||
mock_get.reset_mock()
|
||||
mock_post.reset_mock()
|
||||
|
||||
# Test case where upload fails
|
||||
mock_get.return_value = good_tweet_response
|
||||
mock_post.side_effect = [good_media_response, bad_response]
|
||||
|
||||
# instantiate our object
|
||||
obj = Apprise.instantiate(twitter_url)
|
||||
|
||||
# Send our notification; it will fail because of the media response
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is False
|
||||
|
||||
assert mock_get.call_count == 0
|
||||
assert mock_post.call_count == 2
|
||||
assert mock_post.call_args_list[0][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[1][0][0] == \
|
||||
'https://api.twitter.com/1.1/statuses/update.json'
|
||||
|
||||
mock_get.reset_mock()
|
||||
mock_post.reset_mock()
|
||||
|
||||
mock_post.side_effect = [good_media_response, good_tweet_response]
|
||||
mock_get.return_value = good_tweet_response
|
||||
|
||||
# An invalid attachment will cause a failure
|
||||
path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=path) is False
|
||||
|
||||
# No get request as cached response is used
|
||||
assert mock_get.call_count == 0
|
||||
|
||||
# No post request as attachment is no good anyway
|
||||
assert mock_post.call_count == 0
|
||||
|
||||
mock_get.reset_mock()
|
||||
mock_post.reset_mock()
|
||||
|
||||
mock_post.side_effect = [
|
||||
good_media_response, good_media_response, good_media_response,
|
||||
good_media_response, good_tweet_response, good_tweet_response,
|
||||
good_tweet_response, good_tweet_response]
|
||||
mock_get.return_value = good_tweet_response
|
||||
|
||||
# instantiate our object (without a batch mode)
|
||||
obj = Apprise.instantiate(twitter_url + "&batch=no")
|
||||
|
||||
# 4 images are produced
|
||||
attach = [
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.jpeg'),
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.png'),
|
||||
# This one is not supported, so it's ignored gracefully
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.mp4'),
|
||||
]
|
||||
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is True
|
||||
|
||||
assert mock_get.call_count == 0
|
||||
# No get request as cached response is used
|
||||
assert mock_post.call_count == 8
|
||||
assert mock_post.call_args_list[0][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[1][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[2][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[3][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[4][0][0] == \
|
||||
'https://api.twitter.com/1.1/statuses/update.json'
|
||||
assert mock_post.call_args_list[5][0][0] == \
|
||||
'https://api.twitter.com/1.1/statuses/update.json'
|
||||
assert mock_post.call_args_list[6][0][0] == \
|
||||
'https://api.twitter.com/1.1/statuses/update.json'
|
||||
assert mock_post.call_args_list[7][0][0] == \
|
||||
'https://api.twitter.com/1.1/statuses/update.json'
|
||||
|
||||
mock_get.reset_mock()
|
||||
mock_post.reset_mock()
|
||||
|
||||
mock_post.side_effect = [
|
||||
good_media_response, good_media_response, good_media_response,
|
||||
good_media_response, good_tweet_response, good_tweet_response,
|
||||
good_tweet_response, good_tweet_response]
|
||||
mock_get.return_value = good_tweet_response
|
||||
|
||||
# instantiate our object
|
||||
obj = Apprise.instantiate(twitter_url)
|
||||
|
||||
# 4 images are produced
|
||||
attach = [
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.jpeg'),
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.png'),
|
||||
# This one is not supported, so it's ignored gracefully
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.mp4'),
|
||||
]
|
||||
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is True
|
||||
|
||||
assert mock_get.call_count == 0
|
||||
# No get request as cached response is used
|
||||
assert mock_post.call_count == 7
|
||||
assert mock_post.call_args_list[0][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[1][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[2][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[3][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[4][0][0] == \
|
||||
'https://api.twitter.com/1.1/statuses/update.json'
|
||||
assert mock_post.call_args_list[5][0][0] == \
|
||||
'https://api.twitter.com/1.1/statuses/update.json'
|
||||
# The 2 images are grouped together (batch mode)
|
||||
assert mock_post.call_args_list[6][0][0] == \
|
||||
'https://api.twitter.com/1.1/statuses/update.json'
|
||||
|
||||
mock_get.reset_mock()
|
||||
mock_post.reset_mock()
|
||||
|
||||
# We have an OSError thrown in the middle of our preparation
|
||||
mock_post.side_effect = [good_media_response, OSError()]
|
||||
mock_get.return_value = good_tweet_response
|
||||
|
||||
# 2 images are produced
|
||||
attach = [
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.png'),
|
||||
# This one is not supported, so it's ignored gracefully
|
||||
os.path.join(TEST_VAR_DIR, 'apprise-test.mp4'),
|
||||
]
|
||||
|
||||
# We'll fail to send this time
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
attach=attach) is False
|
||||
|
||||
assert mock_get.call_count == 0
|
||||
# No get request as cached response is used
|
||||
assert mock_post.call_count == 2
|
||||
assert mock_post.call_args_list[0][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
assert mock_post.call_args_list[1][0][0] == \
|
||||
'https://upload.twitter.com/1.1/media/upload.json'
|
||||
|
BIN
test/var/apprise-test.mp4
Normal file
BIN
test/var/apprise-test.mp4
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user