Twitter Image Attachment Support Added (#536)

This commit is contained in:
Chris Caron 2022-03-07 17:52:22 -05:00 committed by GitHub
parent e73025863b
commit 2a81899e6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 719 additions and 42 deletions

View File

@ -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'] += \

View File

@ -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

Binary file not shown.