diff --git a/README.md b/README.md
index 1a19a000..fa2f5e01 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,7 @@ The table below identifies the services this tool supports and some example serv
| [Ryver](https://github.com/caronc/apprise/wiki/Notify_ryver) | ryver:// | (TCP) 443 | ryver://Organization/Token
ryver://botname@Organization/Token
| [Slack](https://github.com/caronc/apprise/wiki/Notify_slack) | slack:// | (TCP) 443 | slack://TokenA/TokenB/TokenC/
slack://TokenA/TokenB/TokenC/Channel
slack://botname@TokenA/TokenB/TokenC/Channel
slack://user@TokenA/TokenB/TokenC/Channel1/Channel2/ChannelN
| [Telegram](https://github.com/caronc/apprise/wiki/Notify_telegram) | tgram:// | (TCP) 443 | tgram://bottoken/ChatID
tgram://bottoken/ChatID1/ChatID2/ChatIDN
-| [Twitter](https://github.com/caronc/apprise/wiki/Notify_twitter) | tweet:// | (TCP) 443 | tweet://user@CKey/CSecret/AKey/ASecret
+| [Twitter](https://github.com/caronc/apprise/wiki/Notify_twitter) | twitter:// | (TCP) 443 | twitter://CKey/CSecret/AKey/ASecret
twitter://user@CKey/CSecret/AKey/ASecret
twitter://CKey/CSecret/AKey/ASecret/User1/User2/User2
twitter://CKey/CSecret/AKey/ASecret?mode=tweet
| [XBMC](https://github.com/caronc/apprise/wiki/Notify_xbmc) | xbmc:// or xbmcs:// | (TCP) 8080 or 443 | xbmc://hostname
xbmc://user@hostname
xbmc://user:password@hostname:port
| [XMPP](https://github.com/caronc/apprise/wiki/Notify_xmpp) | xmpp:// or xmpps:// | (TCP) 5222 or 5223 | xmpp://password@hostname
xmpp://user:password@hostname
xmpps://user:password@hostname:port?jid=user@hostname/resource
xmpps://password@hostname/target@myhost, target2@myhost/resource
| [Windows Notification](https://github.com/caronc/apprise/wiki/Notify_windows) | windows:// | n/a | windows://
diff --git a/apprise/plugins/NotifyTwitter.py b/apprise/plugins/NotifyTwitter.py
new file mode 100644
index 00000000..2ecd6133
--- /dev/null
+++ b/apprise/plugins/NotifyTwitter.py
@@ -0,0 +1,654 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2019 Chris Caron
+# All rights reserved.
+#
+# This code is licensed under the MIT License.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files(the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions :
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# See https://developer.twitter.com/en/docs/direct-messages/\
+# sending-and-receiving/api-reference/new-event.html
+import re
+import six
+import requests
+from datetime import datetime
+from requests_oauthlib import OAuth1
+from json import dumps
+from json import loads
+from .NotifyBase import NotifyBase
+from ..common import NotifyType
+from ..utils import parse_list
+from ..utils import parse_bool
+from ..AppriseLocale import gettext_lazy as _
+
+IS_USER = re.compile(r'^\s*@?(?P[A-Z0-9_]+)$', re.I)
+
+
+class TwitterMessageMode(object):
+ """
+ Twitter Message Mode
+ """
+ # DM (a Direct Message)
+ DM = 'dm'
+
+ # A Public Tweet
+ TWEET = 'tweet'
+
+
+# Define the types in a list for validation purposes
+TWITTER_MESSAGE_MODES = (
+ TwitterMessageMode.DM,
+ TwitterMessageMode.TWEET,
+)
+
+
+class NotifyTwitter(NotifyBase):
+ """
+ A wrapper to Twitter Notifications
+
+ """
+
+ # The default descriptive name associated with the Notification
+ service_name = 'Twitter'
+
+ # The services URL
+ service_url = 'https://twitter.com/'
+
+ # The default secure protocol is twitter. 'tweet' is left behind
+ # for backwards compatibility of older apprise usage
+ secure_protocol = ('twitter', 'tweet')
+
+ # A URL that takes you to the setup/help of the specific protocol
+ setup_url = 'https://github.com/caronc/apprise/wiki/Notify_twitter'
+
+ # Do not set body_maxlen as it is set in a property value below
+ # since the length varies depending if we are doing a direct message
+ # or a tweet
+ # body_maxlen = see below @propery defined
+
+ # 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'
+
+ # Twitter API Reference To Acquire Current Users Information
+ twitter_whoami = \
+ 'https://api.twitter.com/1.1/account/verify_credentials.json'
+
+ # Twitter API Reference To Send A Private DM
+ twitter_dm = 'https://api.twitter.com/1.1/direct_messages/events/new.json'
+
+ # Twitter API Reference To Send A Public Tweet
+ twitter_tweet = 'https://api.twitter.com/1.1/statuses/update.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
+ # rate-limit to be reset.
+ # X-Rate-Limit-Remaining: an integer identifying how many requests we're
+ # still allow to make.
+ request_rate_per_sec = 0
+
+ # For Tracking Purposes
+ ratelimit_reset = datetime.utcnow()
+
+ # Default to 1000; users can send up to 1000 DM's and 2400 tweets a day
+ # This value only get's adjusted if the server sets it that way
+ ratelimit_remaining = 1
+
+ templates = (
+ '{schema}://{ckey}/{csecret}/{akey}/{asecret}/{targets}',
+ )
+
+ # Define our template tokens
+ template_tokens = dict(NotifyBase.template_tokens, **{
+ 'ckey': {
+ 'name': _('Consumer Key'),
+ 'type': 'string',
+ 'private': True,
+ 'required': True,
+ },
+ 'csecret': {
+ 'name': _('Consumer Secret'),
+ 'type': 'string',
+ 'private': True,
+ 'required': True,
+ },
+ 'akey': {
+ 'name': _('Access Key'),
+ 'type': 'string',
+ 'private': True,
+ 'required': True,
+ },
+ 'asecret': {
+ 'name': _('Access Secret'),
+ 'type': 'string',
+ 'private': True,
+ 'required': True,
+ },
+ 'target_user': {
+ 'name': _('Target User'),
+ 'type': 'string',
+ 'prefix': '@',
+ 'map_to': 'targets',
+ },
+ 'targets': {
+ 'name': _('Targets'),
+ 'type': 'list:string',
+ },
+ })
+
+ # Define our template arguments
+ template_args = dict(NotifyBase.template_args, **{
+ 'mode': {
+ 'name': _('Message Mode'),
+ 'type': 'choice:string',
+ 'values': TWITTER_MESSAGE_MODES,
+ 'default': TwitterMessageMode.DM,
+ },
+ 'cache': {
+ 'name': _('Cache Results'),
+ 'type': 'bool',
+ 'default': True,
+ },
+ 'to': {
+ 'alias_of': 'targets',
+ },
+ })
+
+ def __init__(self, ckey, csecret, akey, asecret, targets=None,
+ mode=TwitterMessageMode.DM, cache=True, **kwargs):
+ """
+ Initialize Twitter Object
+
+ """
+ super(NotifyTwitter, self).__init__(**kwargs)
+
+ if not ckey:
+ msg = 'An invalid Consumer API Key was specified.'
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ if not csecret:
+ msg = 'An invalid Consumer Secret API Key was specified.'
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ if not akey:
+ msg = 'An invalid Access Token API Key was specified.'
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ if not asecret:
+ msg = 'An invalid Access Token Secret API Key was specified.'
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ # Store our webhook mode
+ self.mode = None \
+ if not isinstance(mode, six.string_types) else mode.lower()
+
+ # Set Cache Flag
+ self.cache = cache
+
+ if self.mode not in TWITTER_MESSAGE_MODES:
+ msg = 'The Twitter message mode specified ({}) is invalid.' \
+ .format(mode)
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ # Identify our targets
+ self.targets = []
+ for target in parse_list(targets):
+ match = IS_USER.match(target)
+ if match and match.group('user'):
+ self.targets.append(match.group('user'))
+ continue
+
+ self.logger.warning(
+ 'Dropped invalid user ({}) specified.'.format(target),
+ )
+
+ # Store our data
+ self.ckey = ckey
+ self.csecret = csecret
+ self.akey = akey
+ self.asecret = asecret
+
+ return
+
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
+ """
+ Perform Twitter Notification
+ """
+
+ # Call the _send_ function applicable to whatever mode we're in
+ # - 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)
+
+ def _send_tweet(self, body, title='', notify_type=NotifyType.INFO,
+ **kwargs):
+ """
+ Twitter Public Tweet
+ """
+
+ payload = {
+ 'status': body,
+ }
+
+ # Send Tweet
+ postokay, response = self._fetch(
+ self.twitter_tweet,
+ payload=payload,
+ json=False,
+ )
+
+ if postokay:
+ self.logger.info(
+ 'Sent Twitter notification as public tweet.')
+
+ return postokay
+
+ def _send_dm(self, body, title='', notify_type=NotifyType.INFO,
+ **kwargs):
+ """
+ Twitter Direct Message
+ """
+
+ # Error Tracking
+ has_error = False
+
+ payload = {
+ 'event': {
+ 'type': 'message_create',
+ 'message_create': {
+ 'target': {
+ # This gets assigned
+ 'recipient_id': None,
+ },
+ 'message_data': {
+ 'text': body,
+ }
+ }
+ }
+ }
+
+ # Lookup our users
+ targets = self._whoami(lazy=self.cache) if not len(self.targets) \
+ else self._user_lookup(self.targets, lazy=self.cache)
+
+ if not targets:
+ # We failed to lookup any users
+ self.logger.warning(
+ '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
+
+ # 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 Twitter DM notification to @{}.'.format(screen_name))
+
+ return not has_error
+
+ def _whoami(self, lazy=True):
+ """
+ Looks details of current authenticated user
+
+ """
+
+ # Prepare a whoami key; this is to prevent conflict with other
+ # NotifyTwitter declarations that may or may not use a different
+ # set of authentication keys
+ whoami_key = '{}{}{}{}'.format(
+ self.ckey, self.csecret, self.akey, self.asecret)
+
+ if lazy and hasattr(NotifyTwitter, '_whoami_cache') \
+ and whoami_key in getattr(NotifyTwitter, '_whoami_cache'):
+ # Use cached response
+ return getattr(NotifyTwitter, '_whoami_cache')[whoami_key]
+
+ # Contains a mapping of screen_name to id
+ results = {}
+
+ # Send Twitter DM
+ postokay, response = self._fetch(
+ self.twitter_whoami,
+ method='GET',
+ json=False,
+ )
+
+ if postokay:
+ try:
+ results[response['screen_name']] = response['id']
+
+ if lazy:
+ # Cache our response for future references
+ if not hasattr(NotifyTwitter, '_whoami_cache'):
+ setattr(
+ NotifyTwitter, '_whoami_cache',
+ {whoami_key: results})
+ else:
+ getattr(NotifyTwitter, '_whoami_cache')\
+ .update({whoami_key: results})
+
+ # Update our user cache as well
+ if not hasattr(NotifyTwitter, '_user_cache'):
+ setattr(NotifyTwitter, '_user_cache', results)
+ else:
+ getattr(NotifyTwitter, '_user_cache').update(results)
+
+ except (TypeError, KeyError):
+ pass
+
+ return results
+
+ def _user_lookup(self, screen_name, lazy=True):
+ """
+ Looks up a screen name and returns the user id
+
+ the screen_name can be a list/set/tuple as well
+ """
+
+ # Contains a mapping of screen_name to id
+ results = {}
+
+ # Build a unique set of names
+ names = parse_list(screen_name)
+
+ if lazy and hasattr(NotifyTwitter, '_user_cache'):
+ # Use cached response
+ results = {k: v for k, v in getattr(
+ NotifyTwitter, '_user_cache').items() if k in names}
+
+ # limit our names if they already exist in our cache
+ names = [name for name in names if name not in results]
+
+ if not len(names):
+ # They're is nothing further to do
+ return results
+
+ # Twitters API documents that it can lookup to 100
+ # results at a time.
+ # https://developer.twitter.com/en/docs/accounts-and-users/\
+ # follow-search-get-users/api-reference/get-users-lookup
+ for i in range(0, len(names), 100):
+ # Send Twitter DM
+ postokay, response = self._fetch(
+ self.twitter_lookup,
+ payload={
+ 'screen_name': names[i:i + 100],
+ },
+ json=False,
+ )
+
+ if not postokay or not isinstance(response, list):
+ # Track our error
+ continue
+
+ # Update our user index
+ for entry in response:
+ try:
+ results[entry['screen_name']] = entry['id']
+
+ except (TypeError, KeyError):
+ pass
+
+ # Cache our response for future use; this saves on un-nessisary extra
+ # hits against the Twitter API when we already know the answer
+ if lazy:
+ if not hasattr(NotifyTwitter, '_user_cache'):
+ setattr(NotifyTwitter, '_user_cache', results)
+ else:
+ getattr(NotifyTwitter, '_user_cache').update(results)
+
+ return results
+
+ def _fetch(self, url, payload=None, method='POST', json=True):
+ """
+ Wrapper to Twitter API requests object
+ """
+
+ headers = {
+ 'Host': self.twitter_api,
+ 'User-Agent': self.app_id,
+ }
+
+ if json:
+ headers['Content-Type'] = 'application/json'
+ payload = dumps(payload)
+
+ auth = OAuth1(
+ self.ckey,
+ client_secret=self.csecret,
+ resource_owner_key=self.akey,
+ resource_owner_secret=self.asecret,
+ )
+
+ # Some Debug Logging
+ self.logger.debug('Twitter {} URL: {} (cert_verify={})'.format(
+ method, url, self.verify_certificate))
+ self.logger.debug('Twitter Payload: %s' % str(payload))
+
+ # By default set wait to None
+ wait = None
+
+ if self.ratelimit_remaining == 0:
+ # Determine how long we should wait for or if we should wait at
+ # all. This isn't fool-proof because we can't be sure the client
+ # time (calling this script) is completely synced up with the
+ # Gitter server. One would hope we're on NTP and our clocks are
+ # the same allowing this to role smoothly:
+
+ now = datetime.utcnow()
+ if now < self.ratelimit_reset:
+ # We need to throttle for the difference in seconds
+ # We add 0.5 seconds to the end just to allow a grace
+ # period.
+ wait = (self.ratelimit_reset - now).total_seconds() + 0.5
+
+ # Default content response object
+ content = {}
+
+ # Always call throttle before any remote server i/o is made;
+ self.throttle(wait=wait)
+
+ # acquire our request mode
+ fn = requests.post if method == 'POST' else requests.get
+ try:
+ r = fn(
+ url,
+ data=payload,
+ headers=headers,
+ auth=auth,
+ verify=self.verify_certificate)
+
+ if r.status_code != requests.codes.ok:
+ # We had a problem
+ status_str = \
+ NotifyTwitter.http_response_code_lookup(r.status_code)
+
+ self.logger.warning(
+ 'Failed to send Twitter {} to {}: '
+ '{}error={}.'.format(
+ method,
+ url,
+ ', ' if status_str else '',
+ r.status_code))
+
+ self.logger.debug(
+ 'Response Details:\r\n{}'.format(r.content))
+
+ # Mark our failure
+ return (False, content)
+
+ try:
+ content = loads(r.content)
+
+ except (TypeError, ValueError):
+ # ValueError = r.content is Unparsable
+ # TypeError = r.content is None
+ content = {}
+
+ try:
+ # Capture rate limiting if possible
+ self.ratelimit_remaining = \
+ int(r.headers.get('x-rate-limit-remaining'))
+ self.ratelimit_reset = datetime.utcfromtimestamp(
+ int(r.headers.get('x-rate-limit-reset')))
+
+ except (TypeError, ValueError):
+ # This is returned if we could not retrieve this information
+ # gracefully accept this state and move on
+ pass
+
+ except requests.RequestException as e:
+ self.logger.warning(
+ 'Exception received when sending Twitter {} to {}: '.
+ format(method, url))
+ self.logger.debug('Socket Exception: %s' % str(e))
+
+ # Mark our failure
+ return (False, content)
+
+ return (True, content)
+
+ @property
+ def body_maxlen(self):
+ """
+ The maximum allowable characters allowed in the body per message
+ This is used during a Private DM Message Size (not Public Tweets
+ which are limited to 280 characters)
+ """
+ return 10000 if self.mode == TwitterMessageMode.DM else 280
+
+ def url(self):
+ """
+ Returns the URL built dynamically based on specified arguments.
+ """
+
+ # Define any arguments set
+ args = {
+ 'format': self.notify_format,
+ 'overflow': self.overflow_mode,
+ 'mode': self.mode,
+ 'verify': 'yes' if self.verify_certificate else 'no',
+ }
+
+ if len(self.targets) > 0:
+ args['to'] = ','.join([NotifyTwitter.quote(x, safe='')
+ for x in self.targets])
+
+ return '{schema}://{ckey}/{csecret}/{akey}/{asecret}' \
+ '/{targets}/?{args}'.format(
+ schema=self.secure_protocol[0],
+ ckey=NotifyTwitter.quote(self.ckey, safe=''),
+ asecret=NotifyTwitter.quote(self.csecret, safe=''),
+ akey=NotifyTwitter.quote(self.akey, safe=''),
+ csecret=NotifyTwitter.quote(self.asecret, safe=''),
+ targets='/'.join(
+ [NotifyTwitter.quote('@{}'.format(target), safe='')
+ for target in self.targets]),
+ args=NotifyTwitter.urlencode(args))
+
+ @staticmethod
+ def parse_url(url):
+ """
+ Parses the URL and returns enough arguments that can allow
+ us to substantiate this object.
+
+ """
+ results = NotifyBase.parse_url(url, verify_host=False)
+
+ if not results:
+ # We're done early as we couldn't load the results
+ return results
+
+ # The first token is stored in the hostname
+ consumer_key = NotifyTwitter.unquote(results['host'])
+
+ # Acquire remaining tokens
+ tokens = NotifyTwitter.split_path(results['fullpath'])
+
+ # Now fetch the remaining tokens
+ try:
+ consumer_secret, access_token_key, access_token_secret = \
+ tokens[0:3]
+
+ except (ValueError, AttributeError, IndexError):
+ # Force some bad values that will get caught
+ # in parsing later
+ consumer_secret = None
+ access_token_key = None
+ access_token_secret = None
+
+ results['ckey'] = consumer_key
+ results['csecret'] = consumer_secret
+ results['akey'] = access_token_key
+ results['asecret'] = access_token_secret
+
+ # The defined twitter mode
+ if 'mode' in results['qsd'] and len(results['qsd']['mode']):
+ results['mode'] = \
+ NotifyTwitter.unquote(results['qsd']['mode'])
+
+ results['targets'] = []
+
+ # if a user has been defined, add it to the list of targets
+ if results.get('user'):
+ results['targets'].append(results.get('user'))
+
+ # Store any remaining items as potential targets
+ results['targets'].extend(tokens[3:])
+
+ if 'cache' in results['qsd'] and len(results['qsd']['cache']):
+ results['cache'] = \
+ parse_bool(results['qsd']['cache'], True)
+
+ # The 'to' makes it easier to use yaml configuration
+ if 'to' in results['qsd'] and len(results['qsd']['to']):
+ results['targets'] += \
+ NotifyTwitter.parse_list(results['qsd']['to'])
+
+ if results.get('schema', 'twitter').lower() == 'tweet':
+ # Deprication Notice issued for v0.7.9
+ NotifyTwitter.logger.deprecate(
+ 'tweet:// has been replaced by twitter://')
+
+ return results
diff --git a/apprise/plugins/NotifyTwitter/__init__.py b/apprise/plugins/NotifyTwitter/__init__.py
deleted file mode 100644
index 5b4411db..00000000
--- a/apprise/plugins/NotifyTwitter/__init__.py
+++ /dev/null
@@ -1,272 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2019 Chris Caron
-# All rights reserved.
-#
-# This code is licensed under the MIT License.
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files(the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions :
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-from . import tweepy
-from ..NotifyBase import NotifyBase
-from ...common import NotifyType
-from ...utils import parse_list
-from ...AppriseLocale import gettext_lazy as _
-
-
-class NotifyTwitter(NotifyBase):
- """
- A wrapper to Twitter Notifications
-
- """
-
- # The default descriptive name associated with the Notification
- service_name = 'Twitter'
-
- # The services URL
- service_url = 'https://twitter.com/'
-
- # The default secure protocol
- secure_protocol = 'tweet'
-
- # A URL that takes you to the setup/help of the specific protocol
- setup_url = 'https://github.com/caronc/apprise/wiki/Notify_twitter'
-
- # The maximum allowable characters allowed in the body per message
- # This is used during a Private DM Message Size (not Public Tweets
- # which are limited to 240 characters)
- body_maxlen = 4096
-
- # Twitter does have titles when creating a message
- title_maxlen = 0
-
- templates = (
- '{schema}://{user}@{ckey}{csecret}/{akey}/{asecret}',
- )
-
- # Define our template tokens
- template_tokens = dict(NotifyBase.template_tokens, **{
- 'ckey': {
- 'name': _('Consumer Key'),
- 'type': 'string',
- 'private': True,
- 'required': True,
- },
- 'csecret': {
- 'name': _('Consumer Secret'),
- 'type': 'string',
- 'private': True,
- 'required': True,
- },
- 'akey': {
- 'name': _('Access Key'),
- 'type': 'string',
- 'private': True,
- 'required': True,
- },
- 'asecret': {
- 'name': _('Access Secret'),
- 'type': 'string',
- 'private': True,
- 'required': True,
- },
- 'user': {
- 'name': _('User'),
- 'type': 'string',
- 'map_to': 'targets',
- },
- 'targets': {
- 'name': _('Targets'),
- 'type': 'list:string',
- },
- })
-
- # Define our template arguments
- template_args = dict(NotifyBase.template_args, **{
- 'to': {
- 'alias_of': 'targets',
- },
- })
-
- def __init__(self, ckey, csecret, akey, asecret, targets=None, **kwargs):
- """
- Initialize Twitter Object
-
- """
- super(NotifyTwitter, self).__init__(**kwargs)
-
- if not ckey:
- msg = 'An invalid Consumer API Key was specified.'
- self.logger.warning(msg)
- raise TypeError(msg)
-
- if not csecret:
- msg = 'An invalid Consumer Secret API Key was specified.'
- self.logger.warning(msg)
- raise TypeError(msg)
-
- if not akey:
- msg = 'An invalid Access Token API Key was specified.'
- self.logger.warning(msg)
- raise TypeError(msg)
-
- if not asecret:
- msg = 'An invalid Access Token Secret API Key was specified.'
- self.logger.warning(msg)
- raise TypeError(msg)
-
- # Identify our targets
- self.targets = parse_list(targets)
-
- if len(self.targets) == 0 and not self.user:
- msg = 'No user(s) were specified.'
- self.logger.warning(msg)
- raise TypeError(msg)
-
- # Store our data
- self.ckey = ckey
- self.csecret = csecret
- self.akey = akey
- self.asecret = asecret
-
- return
-
- def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
- """
- Perform Twitter Notification
- """
-
- try:
- # Attempt to Establish a connection to Twitter
- self.auth = tweepy.OAuthHandler(self.ckey, self.csecret)
-
- # Apply our Access Tokens
- self.auth.set_access_token(self.akey, self.asecret)
-
- except Exception:
- self.logger.warning(
- 'Twitter authentication failed; '
- 'please verify your configuration.'
- )
- return False
-
- # Get ourselves a list of targets
- users = list(self.targets)
- if not users:
- # notify ourselves
- users.append(self.user)
-
- # Error Tracking
- has_error = False
-
- while len(users) > 0:
- # Get our user
- user = users.pop(0)
-
- # Always call throttle before any remote server i/o is made to
- # avoid thrashing the remote server and risk being blocked.
- self.throttle()
-
- try:
- # Get our API
- api = tweepy.API(self.auth)
-
- # Send our Direct Message
- api.send_direct_message(user, text=body)
- self.logger.info(
- 'Sent Twitter DM notification to {}.'.format(user))
-
- except Exception as e:
- self.logger.warning(
- 'A Connection error occured sending Twitter '
- 'direct message to %s.' % user)
- self.logger.debug('Twitter Exception: %s' % str(e))
-
- # Track our error
- has_error = True
-
- return not has_error
-
- def url(self):
- """
- Returns the URL built dynamically based on specified arguments.
- """
-
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
-
- if len(self.targets) > 0:
- args['to'] = ','.join([NotifyTwitter.quote(x, safe='')
- for x in self.targets])
-
- return '{schema}://{auth}{ckey}/{csecret}/{akey}/{asecret}' \
- '/?{args}'.format(
- auth='' if not self.user else '{user}@'.format(
- user=NotifyTwitter.quote(self.user, safe='')),
- schema=self.secure_protocol,
- ckey=NotifyTwitter.quote(self.ckey, safe=''),
- asecret=NotifyTwitter.quote(self.csecret, safe=''),
- akey=NotifyTwitter.quote(self.akey, safe=''),
- csecret=NotifyTwitter.quote(self.asecret, safe=''),
- args=NotifyTwitter.urlencode(args))
-
- @staticmethod
- def parse_url(url):
- """
- Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
-
- """
- results = NotifyBase.parse_url(url)
-
- if not results:
- # We're done early as we couldn't load the results
- return results
-
- # Apply our settings now
-
- # The first token is stored in the hostname
- consumer_key = NotifyTwitter.unquote(results['host'])
-
- # Now fetch the remaining tokens
- try:
- consumer_secret, access_token_key, access_token_secret = \
- NotifyTwitter.split_path(results['fullpath'])[0:3]
-
- except (ValueError, AttributeError, IndexError):
- # Force some bad values that will get caught
- # in parsing later
- consumer_secret = None
- access_token_key = None
- access_token_secret = None
-
- results['ckey'] = consumer_key
- results['csecret'] = consumer_secret
- results['akey'] = access_token_key
- results['asecret'] = access_token_secret
-
- # Support the to= allowing one to identify more then one user to tweet
- # too
- results['targets'] = NotifyTwitter.parse_list(results['qsd'].get('to'))
-
- return results
diff --git a/apprise/plugins/NotifyTwitter/tweepy/__init__.py b/apprise/plugins/NotifyTwitter/tweepy/__init__.py
deleted file mode 100644
index 3466d0bc..00000000
--- a/apprise/plugins/NotifyTwitter/tweepy/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Tweepy
-# Copyright 2009-2010 Joshua Roesslein
-# See LICENSE for details.
-
-"""
-Tweepy Twitter API library
-"""
-__version__ = '3.6.0'
-__author__ = 'Joshua Roesslein'
-__license__ = 'MIT'
-
-from .models import Status, User, DirectMessage, Friendship, SavedSearch, SearchResults, ModelFactory, Category
-from .error import TweepError, RateLimitError
-from .api import API
-from .cache import Cache, MemoryCache, FileCache
-from .auth import OAuthHandler, AppAuthHandler
-from .streaming import Stream, StreamListener
-from .cursor import Cursor
-
-# Global, unauthenticated instance of API
-api = API()
-
-def debug(enable=True, level=1):
- from six.moves.http_client import HTTPConnection
- HTTPConnection.debuglevel = level
diff --git a/apprise/plugins/NotifyTwitter/tweepy/api.py b/apprise/plugins/NotifyTwitter/tweepy/api.py
deleted file mode 100644
index 22696b70..00000000
--- a/apprise/plugins/NotifyTwitter/tweepy/api.py
+++ /dev/null
@@ -1,1386 +0,0 @@
-# Tweepy
-# Copyright 2009-2010 Joshua Roesslein
-# See LICENSE for details.
-
-from __future__ import print_function
-
-import os
-import mimetypes
-
-import six
-
-from .binder import bind_api
-from .error import TweepError
-from .parsers import ModelParser, Parser
-from .utils import list_to_csv
-
-
-class API(object):
- """Twitter API"""
-
- def __init__(self, auth_handler=None,
- host='api.twitter.com', search_host='search.twitter.com',
- upload_host='upload.twitter.com', cache=None, api_root='/1.1',
- search_root='', upload_root='/1.1', retry_count=0,
- retry_delay=0, retry_errors=None, timeout=60, parser=None,
- compression=False, wait_on_rate_limit=False,
- wait_on_rate_limit_notify=False, proxy=''):
- """ Api instance Constructor
-
- :param auth_handler:
- :param host: url of the server of the rest api, default:'api.twitter.com'
- :param search_host: url of the search server, default:'search.twitter.com'
- :param upload_host: url of the upload server, default:'upload.twitter.com'
- :param cache: Cache to query if a GET method is used, default:None
- :param api_root: suffix of the api version, default:'/1.1'
- :param search_root: suffix of the search version, default:''
- :param upload_root: suffix of the upload version, default:'/1.1'
- :param retry_count: number of allowed retries, default:0
- :param retry_delay: delay in second between retries, default:0
- :param retry_errors: default:None
- :param timeout: delay before to consider the request as timed out in seconds, default:60
- :param parser: ModelParser instance to parse the responses, default:None
- :param compression: If the response is compressed, default:False
- :param wait_on_rate_limit: If the api wait when it hits the rate limit, default:False
- :param wait_on_rate_limit_notify: If the api print a notification when the rate limit is hit, default:False
- :param proxy: Url to use as proxy during the HTTP request, default:''
-
- :raise TypeError: If the given parser is not a ModelParser instance.
- """
- self.auth = auth_handler
- self.host = host
- self.search_host = search_host
- self.upload_host = upload_host
- self.api_root = api_root
- self.search_root = search_root
- self.upload_root = upload_root
- self.cache = cache
- self.compression = compression
- self.retry_count = retry_count
- self.retry_delay = retry_delay
- self.retry_errors = retry_errors
- self.timeout = timeout
- self.wait_on_rate_limit = wait_on_rate_limit
- self.wait_on_rate_limit_notify = wait_on_rate_limit_notify
- self.parser = parser or ModelParser()
- self.proxy = {}
- if proxy:
- self.proxy['https'] = proxy
-
- # Attempt to explain more clearly the parser argument requirements
- # https://github.com/tweepy/tweepy/issues/421
- #
- parser_type = Parser
- if not isinstance(self.parser, parser_type):
- raise TypeError(
- '"parser" argument has to be an instance of "{required}".'
- ' It is currently a {actual}.'.format(
- required=parser_type.__name__,
- actual=type(self.parser)
- )
- )
-
- @property
- def home_timeline(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/statuses/home_timeline
- :allowed_param:'since_id', 'max_id', 'count'
- """
- return bind_api(
- api=self,
- path='/statuses/home_timeline.json',
- payload_type='status', payload_list=True,
- allowed_param=['since_id', 'max_id', 'count'],
- require_auth=True
- )
-
- def statuses_lookup(self, id_, include_entities=None,
- trim_user=None, map_=None, tweet_mode=None):
- return self._statuses_lookup(list_to_csv(id_), include_entities,
- trim_user, map_, tweet_mode)
-
- @property
- def _statuses_lookup(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/statuses/lookup
- :allowed_param:'id', 'include_entities', 'trim_user', 'map', 'tweet_mode'
- """
- return bind_api(
- api=self,
- path='/statuses/lookup.json',
- payload_type='status', payload_list=True,
- allowed_param=['id', 'include_entities', 'trim_user', 'map', 'tweet_mode'],
- require_auth=True
- )
-
- @property
- def user_timeline(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/statuses/user_timeline
- :allowed_param:'id', 'user_id', 'screen_name', 'since_id', 'max_id', 'count', 'include_rts', 'trim_user', 'exclude_replies'
- """
- return bind_api(
- api=self,
- path='/statuses/user_timeline.json',
- payload_type='status', payload_list=True,
- allowed_param=['id', 'user_id', 'screen_name', 'since_id',
- 'max_id', 'count', 'include_rts', 'trim_user',
- 'exclude_replies']
- )
-
- @property
- def mentions_timeline(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/statuses/mentions_timeline
- :allowed_param:'since_id', 'max_id', 'count'
- """
- return bind_api(
- api=self,
- path='/statuses/mentions_timeline.json',
- payload_type='status', payload_list=True,
- allowed_param=['since_id', 'max_id', 'count'],
- require_auth=True
- )
-
- @property
- def related_results(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/get/related_results/show/%3id.format
- :allowed_param:'id'
- """
- return bind_api(
- api=self,
- path='/related_results/show/{id}.json',
- payload_type='relation', payload_list=True,
- allowed_param=['id'],
- require_auth=False
- )
-
- @property
- def retweets_of_me(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/statuses/retweets_of_me
- :allowed_param:'since_id', 'max_id', 'count'
- """
- return bind_api(
- api=self,
- path='/statuses/retweets_of_me.json',
- payload_type='status', payload_list=True,
- allowed_param=['since_id', 'max_id', 'count'],
- require_auth=True
- )
-
- @property
- def get_status(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/statuses/show/%3Aid
- :allowed_param:'id'
- """
- return bind_api(
- api=self,
- path='/statuses/show.json',
- payload_type='status',
- allowed_param=['id']
- )
-
- def update_status(self, *args, **kwargs):
- """ :reference: https://dev.twitter.com/rest/reference/post/statuses/update
- :allowed_param:'status', 'in_reply_to_status_id', 'in_reply_to_status_id_str', 'auto_populate_reply_metadata', 'lat', 'long', 'source', 'place_id', 'display_coordinates', 'media_ids'
- """
- post_data = {}
- media_ids = kwargs.pop("media_ids", None)
- if media_ids is not None:
- post_data["media_ids"] = list_to_csv(media_ids)
-
- return bind_api(
- api=self,
- path='/statuses/update.json',
- method='POST',
- payload_type='status',
- allowed_param=['status', 'in_reply_to_status_id', 'in_reply_to_status_id_str', 'auto_populate_reply_metadata', 'lat', 'long', 'source', 'place_id', 'display_coordinates'],
- require_auth=True
- )(post_data=post_data, *args, **kwargs)
-
- def media_upload(self, filename, *args, **kwargs):
- """ :reference: https://dev.twitter.com/rest/reference/post/media/upload
- :allowed_param:
- """
- f = kwargs.pop('file', None)
- headers, post_data = API._pack_image(filename, 4883, form_field='media', f=f)
- kwargs.update({'headers': headers, 'post_data': post_data})
-
- return bind_api(
- api=self,
- path='/media/upload.json',
- method='POST',
- payload_type='media',
- allowed_param=[],
- require_auth=True,
- upload_api=True
- )(*args, **kwargs)
-
- def update_with_media(self, filename, *args, **kwargs):
- """ :reference: https://dev.twitter.com/rest/reference/post/statuses/update_with_media
- :allowed_param:'status', 'possibly_sensitive', 'in_reply_to_status_id', 'in_reply_to_status_id_str', 'auto_populate_reply_metadata', 'lat', 'long', 'place_id', 'display_coordinates'
- """
- f = kwargs.pop('file', None)
- headers, post_data = API._pack_image(filename, 3072, form_field='media[]', f=f)
- kwargs.update({'headers': headers, 'post_data': post_data})
-
- return bind_api(
- api=self,
- path='/statuses/update_with_media.json',
- method='POST',
- payload_type='status',
- allowed_param=[
- 'status', 'possibly_sensitive', 'in_reply_to_status_id', 'in_reply_to_status_id_str',
- 'auto_populate_reply_metadata', 'lat', 'long', 'place_id', 'display_coordinates'
- ],
- require_auth=True
- )(*args, **kwargs)
-
- @property
- def destroy_status(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/statuses/destroy/%3Aid
- :allowed_param:'id'
- """
- return bind_api(
- api=self,
- path='/statuses/destroy/{id}.json',
- method='POST',
- payload_type='status',
- allowed_param=['id'],
- require_auth=True
- )
-
- @property
- def retweet(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/statuses/retweet/%3Aid
- :allowed_param:'id'
- """
- return bind_api(
- api=self,
- path='/statuses/retweet/{id}.json',
- method='POST',
- payload_type='status',
- allowed_param=['id'],
- require_auth=True
- )
-
- @property
- def unretweet(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/statuses/unretweet/%3Aid
- :allowed_param:'id'
- """
- return bind_api(
- api=self,
- path='/statuses/unretweet/{id}.json',
- method='POST',
- payload_type='status',
- allowed_param=['id'],
- require_auth=True
- )
-
- @property
- def retweets(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/statuses/retweets/%3Aid
- :allowed_param:'id', 'count'
- """
- return bind_api(
- api=self,
- path='/statuses/retweets/{id}.json',
- payload_type='status', payload_list=True,
- allowed_param=['id', 'count'],
- require_auth=True
- )
-
- @property
- def retweeters(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/statuses/retweeters/ids
- :allowed_param:'id', 'cursor', 'stringify_ids
- """
- return bind_api(
- api=self,
- path='/statuses/retweeters/ids.json',
- payload_type='ids',
- allowed_param=['id', 'cursor', 'stringify_ids']
- )
-
- @property
- def get_user(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/users/show
- :allowed_param:'id', 'user_id', 'screen_name'
- """
- return bind_api(
- api=self,
- path='/users/show.json',
- payload_type='user',
- allowed_param=['id', 'user_id', 'screen_name']
- )
-
- @property
- def get_oembed(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/statuses/oembed
- :allowed_param:'id', 'url', 'maxwidth', 'hide_media', 'omit_script', 'align', 'related', 'lang'
- """
- return bind_api(
- api=self,
- path='/statuses/oembed.json',
- payload_type='json',
- allowed_param=['id', 'url', 'maxwidth', 'hide_media', 'omit_script', 'align', 'related', 'lang']
- )
-
- def lookup_users(self, user_ids=None, screen_names=None, include_entities=None):
- """ Perform bulk look up of users from user ID or screenname """
- post_data = {}
- if include_entities is not None:
- include_entities = 'true' if include_entities else 'false'
- post_data['include_entities'] = include_entities
- if user_ids:
- post_data['user_id'] = list_to_csv(user_ids)
- if screen_names:
- post_data['screen_name'] = list_to_csv(screen_names)
-
- return self._lookup_users(post_data=post_data)
-
- @property
- def _lookup_users(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/users/lookup
- allowed_param='user_id', 'screen_name', 'include_entities'
- """
- return bind_api(
- api=self,
- path='/users/lookup.json',
- payload_type='user', payload_list=True,
- method='POST',
- allowed_param=['user_id', 'screen_name', 'include_entities']
- )
-
- def me(self):
- """ Get the authenticated user """
- return self.get_user(screen_name=self.auth.get_username())
-
- @property
- def search_users(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/users/search
- :allowed_param:'q', 'count', 'page'
- """
- return bind_api(
- api=self,
- path='/users/search.json',
- payload_type='user', payload_list=True,
- require_auth=True,
- allowed_param=['q', 'count', 'page']
- )
-
- @property
- def suggested_users(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/users/suggestions/%3Aslug
- :allowed_param:'slug', 'lang'
- """
- return bind_api(
- api=self,
- path='/users/suggestions/{slug}.json',
- payload_type='user', payload_list=True,
- require_auth=True,
- allowed_param=['slug', 'lang']
- )
-
- @property
- def suggested_categories(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/users/suggestions
- :allowed_param:'lang'
- """
- return bind_api(
- api=self,
- path='/users/suggestions.json',
- payload_type='category', payload_list=True,
- allowed_param=['lang'],
- require_auth=True
- )
-
- @property
- def suggested_users_tweets(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/users/suggestions/%3Aslug/members
- :allowed_param:'slug'
- """
- return bind_api(
- api=self,
- path='/users/suggestions/{slug}/members.json',
- payload_type='status', payload_list=True,
- allowed_param=['slug'],
- require_auth=True
- )
-
- @property
- def direct_messages(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/direct_messages
- :allowed_param:'since_id', 'max_id', 'count', 'full_text'
- """
- return bind_api(
- api=self,
- path='/direct_messages.json',
- payload_type='direct_message', payload_list=True,
- allowed_param=['since_id', 'max_id', 'count', 'full_text'],
- require_auth=True
- )
-
- @property
- def get_direct_message(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/direct_messages/show
- :allowed_param:'id', 'full_text'
- """
- return bind_api(
- api=self,
- path='/direct_messages/show/{id}.json',
- payload_type='direct_message',
- allowed_param=['id', 'full_text'],
- require_auth=True
- )
-
- @property
- def sent_direct_messages(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/direct_messages/sent
- :allowed_param:'since_id', 'max_id', 'count', 'page', 'full_text'
- """
- return bind_api(
- api=self,
- path='/direct_messages/sent.json',
- payload_type='direct_message', payload_list=True,
- allowed_param=['since_id', 'max_id', 'count', 'page', 'full_text'],
- require_auth=True
- )
-
- @property
- def send_direct_message(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/direct_messages/new
- :allowed_param:'user', 'screen_name', 'user_id', 'text'
- """
- return bind_api(
- api=self,
- path='/direct_messages/new.json',
- method='POST',
- payload_type='direct_message',
- allowed_param=['user', 'screen_name', 'user_id', 'text'],
- require_auth=True
- )
-
- @property
- def destroy_direct_message(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/direct_messages/destroy
- :allowed_param:'id'
- """
- return bind_api(
- api=self,
- path='/direct_messages/destroy.json',
- method='POST',
- payload_type='direct_message',
- allowed_param=['id'],
- require_auth=True
- )
-
- @property
- def create_friendship(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/friendships/create
- :allowed_param:'id', 'user_id', 'screen_name', 'follow'
- """
- return bind_api(
- api=self,
- path='/friendships/create.json',
- method='POST',
- payload_type='user',
- allowed_param=['id', 'user_id', 'screen_name', 'follow'],
- require_auth=True
- )
-
- @property
- def destroy_friendship(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/friendships/destroy
- :allowed_param:'id', 'user_id', 'screen_name'
- """
- return bind_api(
- api=self,
- path='/friendships/destroy.json',
- method='POST',
- payload_type='user',
- allowed_param=['id', 'user_id', 'screen_name'],
- require_auth=True
- )
-
- @property
- def show_friendship(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/friendships/show
- :allowed_param:'source_id', 'source_screen_name', 'target_id', 'target_screen_name'
- """
- return bind_api(
- api=self,
- path='/friendships/show.json',
- payload_type='friendship',
- allowed_param=['source_id', 'source_screen_name',
- 'target_id', 'target_screen_name']
- )
-
- def lookup_friendships(self, user_ids=None, screen_names=None):
- """ Perform bulk look up of friendships from user ID or screenname """
- return self._lookup_friendships(list_to_csv(user_ids), list_to_csv(screen_names))
-
- @property
- def _lookup_friendships(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/friendships/lookup
- :allowed_param:'user_id', 'screen_name'
- """
- return bind_api(
- api=self,
- path='/friendships/lookup.json',
- payload_type='relationship', payload_list=True,
- allowed_param=['user_id', 'screen_name'],
- require_auth=True
- )
-
- @property
- def friends_ids(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/friends/ids
- :allowed_param:'id', 'user_id', 'screen_name', 'cursor'
- """
- return bind_api(
- api=self,
- path='/friends/ids.json',
- payload_type='ids',
- allowed_param=['id', 'user_id', 'screen_name', 'cursor']
- )
-
- @property
- def friends(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/friends/list
- :allowed_param:'id', 'user_id', 'screen_name', 'cursor', 'skip_status', 'include_user_entities'
- """
- return bind_api(
- api=self,
- path='/friends/list.json',
- payload_type='user', payload_list=True,
- allowed_param=['id', 'user_id', 'screen_name', 'cursor', 'skip_status', 'include_user_entities']
- )
-
- @property
- def friendships_incoming(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/friendships/incoming
- :allowed_param:'cursor'
- """
- return bind_api(
- api=self,
- path='/friendships/incoming.json',
- payload_type='ids',
- allowed_param=['cursor']
- )
-
- @property
- def friendships_outgoing(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/friendships/outgoing
- :allowed_param:'cursor'
- """
- return bind_api(
- api=self,
- path='/friendships/outgoing.json',
- payload_type='ids',
- allowed_param=['cursor']
- )
-
- @property
- def followers_ids(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/followers/ids
- :allowed_param:'id', 'user_id', 'screen_name', 'cursor', 'count'
- """
- return bind_api(
- api=self,
- path='/followers/ids.json',
- payload_type='ids',
- allowed_param=['id', 'user_id', 'screen_name', 'cursor', 'count']
- )
-
- @property
- def followers(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/followers/list
- :allowed_param:'id', 'user_id', 'screen_name', 'cursor', 'count', 'skip_status', 'include_user_entities'
- """
- return bind_api(
- api=self,
- path='/followers/list.json',
- payload_type='user', payload_list=True,
- allowed_param=['id', 'user_id', 'screen_name', 'cursor', 'count',
- 'skip_status', 'include_user_entities']
- )
-
- @property
- def get_settings(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/account/settings
- """
- return bind_api(
- api=self,
- path='/account/settings.json',
- payload_type='json',
- use_cache=False
- )
-
- @property
- def set_settings(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/account/settings
- :allowed_param:'sleep_time_enabled', 'start_sleep_time',
- 'end_sleep_time', 'time_zone', 'trend_location_woeid',
- 'allow_contributor_request', 'lang'
- """
- return bind_api(
- api=self,
- path='/account/settings.json',
- method='POST',
- payload_type='json',
- allowed_param=['sleep_time_enabled', 'start_sleep_time',
- 'end_sleep_time', 'time_zone',
- 'trend_location_woeid', 'allow_contributor_request',
- 'lang'],
- use_cache=False
- )
-
- def verify_credentials(self, **kargs):
- """ :reference: https://dev.twitter.com/rest/reference/get/account/verify_credentials
- :allowed_param:'include_entities', 'skip_status', 'include_email'
- """
- try:
- return bind_api(
- api=self,
- path='/account/verify_credentials.json',
- payload_type='user',
- require_auth=True,
- allowed_param=['include_entities', 'skip_status', 'include_email'],
- )(**kargs)
- except TweepError as e:
- if e.response and e.response.status == 401:
- return False
- raise
-
- @property
- def rate_limit_status(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/application/rate_limit_status
- :allowed_param:'resources'
- """
- return bind_api(
- api=self,
- path='/application/rate_limit_status.json',
- payload_type='json',
- allowed_param=['resources'],
- use_cache=False
- )
-
- @property
- def set_delivery_device(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/account/update_delivery_device
- :allowed_param:'device'
- """
- return bind_api(
- api=self,
- path='/account/update_delivery_device.json',
- method='POST',
- allowed_param=['device'],
- payload_type='user',
- require_auth=True
- )
-
- def update_profile_image(self, filename, file_=None):
- """ :reference: https://dev.twitter.com/rest/reference/post/account/update_profile_image
- :allowed_param:'include_entities', 'skip_status'
- """
- headers, post_data = API._pack_image(filename, 700, f=file_)
- return bind_api(
- api=self,
- path='/account/update_profile_image.json',
- method='POST',
- payload_type='user',
- allowed_param=['include_entities', 'skip_status'],
- require_auth=True
- )(self, post_data=post_data, headers=headers)
-
- def update_profile_background_image(self, filename, **kargs):
- """ :reference: https://dev.twitter.com/rest/reference/post/account/update_profile_background_image
- :allowed_param:'tile', 'include_entities', 'skip_status', 'use'
- """
- f = kargs.pop('file', None)
- headers, post_data = API._pack_image(filename, 800, f=f)
- bind_api(
- api=self,
- path='/account/update_profile_background_image.json',
- method='POST',
- payload_type='user',
- allowed_param=['tile', 'include_entities', 'skip_status', 'use'],
- require_auth=True
- )(post_data=post_data, headers=headers)
-
- def update_profile_banner(self, filename, **kargs):
- """ :reference: https://dev.twitter.com/rest/reference/post/account/update_profile_banner
- :allowed_param:'width', 'height', 'offset_left', 'offset_right'
- """
- f = kargs.pop('file', None)
- headers, post_data = API._pack_image(filename, 700, form_field="banner", f=f)
- bind_api(
- api=self,
- path='/account/update_profile_banner.json',
- method='POST',
- allowed_param=['width', 'height', 'offset_left', 'offset_right'],
- require_auth=True
- )(post_data=post_data, headers=headers)
-
- @property
- def update_profile(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/account/update_profile
- :allowed_param:'name', 'url', 'location', 'description', 'profile_link_color'
- """
- return bind_api(
- api=self,
- path='/account/update_profile.json',
- method='POST',
- payload_type='user',
- allowed_param=['name', 'url', 'location', 'description', 'profile_link_color'],
- require_auth=True
- )
-
- @property
- def favorites(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/favorites/list
- :allowed_param:'screen_name', 'user_id', 'max_id', 'count', 'since_id', 'max_id'
- """
- return bind_api(
- api=self,
- path='/favorites/list.json',
- payload_type='status', payload_list=True,
- allowed_param=['screen_name', 'user_id', 'max_id', 'count', 'since_id', 'max_id']
- )
-
- @property
- def create_favorite(self):
- """ :reference:https://dev.twitter.com/rest/reference/post/favorites/create
- :allowed_param:'id'
- """
- return bind_api(
- api=self,
- path='/favorites/create.json',
- method='POST',
- payload_type='status',
- allowed_param=['id'],
- require_auth=True
- )
-
- @property
- def destroy_favorite(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/favorites/destroy
- :allowed_param:'id'
- """
- return bind_api(
- api=self,
- path='/favorites/destroy.json',
- method='POST',
- payload_type='status',
- allowed_param=['id'],
- require_auth=True
- )
-
- @property
- def create_block(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/blocks/create
- :allowed_param:'id', 'user_id', 'screen_name'
- """
- return bind_api(
- api=self,
- path='/blocks/create.json',
- method='POST',
- payload_type='user',
- allowed_param=['id', 'user_id', 'screen_name'],
- require_auth=True
- )
-
- @property
- def destroy_block(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/blocks/destroy
- :allowed_param:'id', 'user_id', 'screen_name'
- """
- return bind_api(
- api=self,
- path='/blocks/destroy.json',
- method='POST',
- payload_type='user',
- allowed_param=['id', 'user_id', 'screen_name'],
- require_auth=True
- )
-
- @property
- def mutes_ids(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/mutes/users/ids """
- return bind_api(
- api=self,
- path='/mutes/users/ids.json',
- payload_type='json',
- require_auth=True
- )
-
- @property
- def create_mute(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/mutes/users/create
- :allowed_param:'id', 'user_id', 'screen_name'
- """
- return bind_api(
- api=self,
- path='/mutes/users/create.json',
- method='POST',
- payload_type='user',
- allowed_param=['id', 'user_id', 'screen_name'],
- require_auth=True
- )
-
- @property
- def destroy_mute(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/mutes/users/destroy
- :allowed_param:'id', 'user_id', 'screen_name'
- """
- return bind_api(
- api=self,
- path='/mutes/users/destroy.json',
- method='POST',
- payload_type='user',
- allowed_param=['id', 'user_id', 'screen_name'],
- require_auth=True
- )
-
-
-
- @property
- def blocks(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/blocks/list
- :allowed_param:'cursor'
- """
- return bind_api(
- api=self,
- path='/blocks/list.json',
- payload_type='user', payload_list=True,
- allowed_param=['cursor'],
- require_auth=True
- )
-
- @property
- def blocks_ids(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/blocks/ids """
- return bind_api(
- api=self,
- path='/blocks/ids.json',
- payload_type='json',
- require_auth=True
- )
-
- @property
- def report_spam(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/users/report_spam
- :allowed_param:'user_id', 'screen_name'
- """
- return bind_api(
- api=self,
- path='/users/report_spam.json',
- method='POST',
- payload_type='user',
- allowed_param=['user_id', 'screen_name'],
- require_auth=True
- )
-
- @property
- def saved_searches(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/saved_searches/show/%3Aid """
- return bind_api(
- api=self,
- path='/saved_searches/list.json',
- payload_type='saved_search', payload_list=True,
- require_auth=True
- )
-
- @property
- def get_saved_search(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/saved_searches/show/%3Aid
- :allowed_param:'id'
- """
- return bind_api(
- api=self,
- path='/saved_searches/show/{id}.json',
- payload_type='saved_search',
- allowed_param=['id'],
- require_auth=True
- )
-
- @property
- def create_saved_search(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/saved_searches/create
- :allowed_param:'query'
- """
- return bind_api(
- api=self,
- path='/saved_searches/create.json',
- method='POST',
- payload_type='saved_search',
- allowed_param=['query'],
- require_auth=True
- )
-
- @property
- def destroy_saved_search(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/saved_searches/destroy/%3Aid
- :allowed_param:'id'
- """
- return bind_api(
- api=self,
- path='/saved_searches/destroy/{id}.json',
- method='POST',
- payload_type='saved_search',
- allowed_param=['id'],
- require_auth=True
- )
-
- @property
- def create_list(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/lists/create
- :allowed_param:'name', 'mode', 'description'
- """
- return bind_api(
- api=self,
- path='/lists/create.json',
- method='POST',
- payload_type='list',
- allowed_param=['name', 'mode', 'description'],
- require_auth=True
- )
-
- @property
- def destroy_list(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/lists/destroy
- :allowed_param:'owner_screen_name', 'owner_id', 'list_id', 'slug'
- """
- return bind_api(
- api=self,
- path='/lists/destroy.json',
- method='POST',
- payload_type='list',
- allowed_param=['owner_screen_name', 'owner_id', 'list_id', 'slug'],
- require_auth=True
- )
-
- @property
- def update_list(self):
- """ :reference: https://dev.twitter.com/rest/reference/post/lists/update
- :allowed_param: list_id', 'slug', 'name', 'mode', 'description', 'owner_screen_name', 'owner_id'
- """
- return bind_api(
- api=self,
- path='/lists/update.json',
- method='POST',
- payload_type='list',
- allowed_param=['list_id', 'slug', 'name', 'mode', 'description', 'owner_screen_name', 'owner_id'],
- require_auth=True
- )
-
- @property
- def lists_all(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/lists/list
- :allowed_param:'screen_name', 'user_id'
- """
- return bind_api(
- api=self,
- path='/lists/list.json',
- payload_type='list', payload_list=True,
- allowed_param=['screen_name', 'user_id'],
- require_auth=True
- )
-
- @property
- def lists_memberships(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/lists/memberships
- :allowed_param:'screen_name', 'user_id', 'filter_to_owned_lists', 'cursor'
- """
- return bind_api(
- api=self,
- path='/lists/memberships.json',
- payload_type='list', payload_list=True,
- allowed_param=['screen_name', 'user_id', 'filter_to_owned_lists', 'cursor'],
- require_auth=True
- )
-
- @property
- def lists_subscriptions(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/lists/subscriptions
- :allowed_param:'screen_name', 'user_id', 'cursor'
- """
- return bind_api(
- api=self,
- path='/lists/subscriptions.json',
- payload_type='list', payload_list=True,
- allowed_param=['screen_name', 'user_id', 'cursor'],
- require_auth=True
- )
-
- @property
- def list_timeline(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/statuses
- :allowed_param:'owner_screen_name', 'slug', 'owner_id', 'list_id',
- 'since_id', 'max_id', 'count', 'include_rts
- """
- return bind_api(
- api=self,
- path='/lists/statuses.json',
- payload_type='status', payload_list=True,
- allowed_param=['owner_screen_name', 'slug', 'owner_id',
- 'list_id', 'since_id', 'max_id', 'count',
- 'include_rts']
- )
-
- @property
- def get_list(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/lists/show
- :allowed_param:'owner_screen_name', 'owner_id', 'slug', 'list_id'
- """
- return bind_api(
- api=self,
- path='/lists/show.json',
- payload_type='list',
- allowed_param=['owner_screen_name', 'owner_id', 'slug', 'list_id']
- )
-
- @property
- def add_list_member(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/members/create
- :allowed_param:'screen_name', 'user_id', 'owner_screen_name',
- 'owner_id', 'slug', 'list_id'
- """
- return bind_api(
- api=self,
- path='/lists/members/create.json',
- method='POST',
- payload_type='list',
- allowed_param=['screen_name', 'user_id', 'owner_screen_name',
- 'owner_id', 'slug', 'list_id'],
- require_auth=True
- )
-
- @property
- def remove_list_member(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy
- :allowed_param:'screen_name', 'user_id', 'owner_screen_name',
- 'owner_id', 'slug', 'list_id'
- """
- return bind_api(
- api=self,
- path='/lists/members/destroy.json',
- method='POST',
- payload_type='list',
- allowed_param=['screen_name', 'user_id', 'owner_screen_name',
- 'owner_id', 'slug', 'list_id'],
- require_auth=True
- )
-
- def add_list_members(self, screen_name=None, user_id=None, slug=None,
- list_id=None, owner_id=None, owner_screen_name=None):
- """ Perform bulk add of list members from user ID or screenname """
- return self._add_list_members(list_to_csv(screen_name),
- list_to_csv(user_id),
- slug, list_id, owner_id,
- owner_screen_name)
-
- @property
- def _add_list_members(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/members/create_all
- :allowed_param:'screen_name', 'user_id', 'slug', 'list_id',
- 'owner_id', 'owner_screen_name'
-
- """
- return bind_api(
- api=self,
- path='/lists/members/create_all.json',
- method='POST',
- payload_type='list',
- allowed_param=['screen_name', 'user_id', 'slug', 'list_id',
- 'owner_id', 'owner_screen_name'],
- require_auth=True
- )
-
- def remove_list_members(self, screen_name=None, user_id=None, slug=None,
- list_id=None, owner_id=None, owner_screen_name=None):
- """ Perform bulk remove of list members from user ID or screenname """
- return self._remove_list_members(list_to_csv(screen_name),
- list_to_csv(user_id),
- slug, list_id, owner_id,
- owner_screen_name)
-
- @property
- def _remove_list_members(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy_all
- :allowed_param:'screen_name', 'user_id', 'slug', 'list_id',
- 'owner_id', 'owner_screen_name'
-
- """
- return bind_api(
- api=self,
- path='/lists/members/destroy_all.json',
- method='POST',
- payload_type='list',
- allowed_param=['screen_name', 'user_id', 'slug', 'list_id',
- 'owner_id', 'owner_screen_name'],
- require_auth=True
- )
-
- @property
- def list_members(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/members
- :allowed_param:'owner_screen_name', 'slug', 'list_id',
- 'owner_id', 'cursor
- """
- return bind_api(
- api=self,
- path='/lists/members.json',
- payload_type='user', payload_list=True,
- allowed_param=['owner_screen_name', 'slug', 'list_id',
- 'owner_id', 'cursor']
- )
-
- @property
- def show_list_member(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/members/show
- :allowed_param:'list_id', 'slug', 'user_id', 'screen_name',
- 'owner_screen_name', 'owner_id
- """
- return bind_api(
- api=self,
- path='/lists/members/show.json',
- payload_type='user',
- allowed_param=['list_id', 'slug', 'user_id', 'screen_name',
- 'owner_screen_name', 'owner_id']
- )
-
- @property
- def subscribe_list(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/create
- :allowed_param:'owner_screen_name', 'slug', 'owner_id',
- 'list_id'
- """
- return bind_api(
- api=self,
- path='/lists/subscribers/create.json',
- method='POST',
- payload_type='list',
- allowed_param=['owner_screen_name', 'slug', 'owner_id',
- 'list_id'],
- require_auth=True
- )
-
- @property
- def unsubscribe_list(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/destroy
- :allowed_param:'owner_screen_name', 'slug', 'owner_id',
- 'list_id'
- """
- return bind_api(
- api=self,
- path='/lists/subscribers/destroy.json',
- method='POST',
- payload_type='list',
- allowed_param=['owner_screen_name', 'slug', 'owner_id',
- 'list_id'],
- require_auth=True
- )
-
- @property
- def list_subscribers(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/subscribers
- :allowed_param:'owner_screen_name', 'slug', 'owner_id',
- 'list_id', 'cursor
- """
- return bind_api(
- api=self,
- path='/lists/subscribers.json',
- payload_type='user', payload_list=True,
- allowed_param=['owner_screen_name', 'slug', 'owner_id',
- 'list_id', 'cursor']
- )
-
- @property
- def show_list_subscriber(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/subscribers/show
- :allowed_param:'owner_screen_name', 'slug', 'screen_name',
- 'owner_id', 'list_id', 'user_id
- """
- return bind_api(
- api=self,
- path='/lists/subscribers/show.json',
- payload_type='user',
- allowed_param=['owner_screen_name', 'slug', 'screen_name',
- 'owner_id', 'list_id', 'user_id']
- )
-
- @property
- def trends_available(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/trends/available """
- return bind_api(
- api=self,
- path='/trends/available.json',
- payload_type='json'
- )
-
- @property
- def trends_place(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/trends/place
- :allowed_param:'id', 'exclude'
- """
- return bind_api(
- api=self,
- path='/trends/place.json',
- payload_type='json',
- allowed_param=['id', 'exclude']
- )
-
- @property
- def trends_closest(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/trends/closest
- :allowed_param:'lat', 'long'
- """
- return bind_api(
- api=self,
- path='/trends/closest.json',
- payload_type='json',
- allowed_param=['lat', 'long']
- )
-
- @property
- def search(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/search/tweets
- :allowed_param:'q', 'lang', 'locale', 'since_id', 'geocode',
- 'max_id', 'since', 'until', 'result_type', 'count',
- 'include_entities', 'from', 'to', 'source'
- """
- return bind_api(
- api=self,
- path='/search/tweets.json',
- payload_type='search_results',
- allowed_param=['q', 'lang', 'locale', 'since_id', 'geocode',
- 'max_id', 'since', 'until', 'result_type',
- 'count', 'include_entities', 'from',
- 'to', 'source']
- )
-
- @property
- def reverse_geocode(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/geo/reverse_geocode
- :allowed_param:'lat', 'long', 'accuracy', 'granularity', 'max_results'
- """
- return bind_api(
- api=self,
- path='/geo/reverse_geocode.json',
- payload_type='place', payload_list=True,
- allowed_param=['lat', 'long', 'accuracy', 'granularity',
- 'max_results']
- )
-
- @property
- def geo_id(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/geo/id/%3Aplace_id
- :allowed_param:'id'
- """
- return bind_api(
- api=self,
- path='/geo/id/{id}.json',
- payload_type='place',
- allowed_param=['id']
- )
-
- @property
- def geo_search(self):
- """ :reference: https://dev.twitter.com/docs/api/1.1/get/geo/search
- :allowed_param:'lat', 'long', 'query', 'ip', 'granularity',
- 'accuracy', 'max_results', 'contained_within
-
- """
- return bind_api(
- api=self,
- path='/geo/search.json',
- payload_type='place', payload_list=True,
- allowed_param=['lat', 'long', 'query', 'ip', 'granularity',
- 'accuracy', 'max_results', 'contained_within']
- )
-
- @property
- def geo_similar_places(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/geo/similar_places
- :allowed_param:'lat', 'long', 'name', 'contained_within'
- """
- return bind_api(
- api=self,
- path='/geo/similar_places.json',
- payload_type='place', payload_list=True,
- allowed_param=['lat', 'long', 'name', 'contained_within']
- )
-
- @property
- def supported_languages(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/help/languages """
- return bind_api(
- api=self,
- path='/help/languages.json',
- payload_type='json',
- require_auth=True
- )
-
- @property
- def configuration(self):
- """ :reference: https://dev.twitter.com/rest/reference/get/help/configuration """
- return bind_api(
- api=self,
- path='/help/configuration.json',
- payload_type='json',
- require_auth=True
- )
-
- """ Internal use only """
-
- @staticmethod
- def _pack_image(filename, max_size, form_field="image", f=None):
- """Pack image from file into multipart-formdata post body"""
- # image must be less than 700kb in size
- if f is None:
- try:
- if os.path.getsize(filename) > (max_size * 1024):
- raise TweepError('File is too big, must be less than %skb.' % max_size)
- except os.error as e:
- raise TweepError('Unable to access file: %s' % e.strerror)
-
- # build the mulitpart-formdata body
- fp = open(filename, 'rb')
- else:
- f.seek(0, 2) # Seek to end of file
- if f.tell() > (max_size * 1024):
- raise TweepError('File is too big, must be less than %skb.' % max_size)
- f.seek(0) # Reset to beginning of file
- fp = f
-
- # image must be gif, jpeg, or png
- file_type = mimetypes.guess_type(filename)
- if file_type is None:
- raise TweepError('Could not determine file type')
- file_type = file_type[0]
- if file_type not in ['image/gif', 'image/jpeg', 'image/png']:
- raise TweepError('Invalid file type for image: %s' % file_type)
-
- if isinstance(filename, six.text_type):
- filename = filename.encode("utf-8")
-
- BOUNDARY = b'Tw3ePy'
- body = []
- body.append(b'--' + BOUNDARY)
- body.append('Content-Disposition: form-data; name="{0}";'
- ' filename="{1}"'.format(form_field, filename)
- .encode('utf-8'))
- body.append('Content-Type: {0}'.format(file_type).encode('utf-8'))
- body.append(b'')
- body.append(fp.read())
- body.append(b'--' + BOUNDARY + b'--')
- body.append(b'')
- fp.close()
- body = b'\r\n'.join(body)
-
- # build headers
- headers = {
- 'Content-Type': 'multipart/form-data; boundary=Tw3ePy',
- 'Content-Length': str(len(body))
- }
-
- return headers, body
diff --git a/apprise/plugins/NotifyTwitter/tweepy/auth.py b/apprise/plugins/NotifyTwitter/tweepy/auth.py
deleted file mode 100644
index b450e310..00000000
--- a/apprise/plugins/NotifyTwitter/tweepy/auth.py
+++ /dev/null
@@ -1,178 +0,0 @@
-from __future__ import print_function
-
-import six
-import logging
-
-from .error import TweepError
-from .api import API
-import requests
-from requests_oauthlib import OAuth1Session, OAuth1
-from requests.auth import AuthBase
-from six.moves.urllib.parse import parse_qs
-
-WARNING_MESSAGE = """Warning! Due to a Twitter API bug, signin_with_twitter
-and access_type don't always play nice together. Details
-https://dev.twitter.com/discussions/21281"""
-
-
-class AuthHandler(object):
-
- def apply_auth(self, url, method, headers, parameters):
- """Apply authentication headers to request"""
- raise NotImplementedError
-
- def get_username(self):
- """Return the username of the authenticated user"""
- raise NotImplementedError
-
-
-class OAuthHandler(AuthHandler):
- """OAuth authentication handler"""
- OAUTH_HOST = 'api.twitter.com'
- OAUTH_ROOT = '/oauth/'
-
- def __init__(self, consumer_key, consumer_secret, callback=None):
- if type(consumer_key) == six.text_type:
- consumer_key = consumer_key.encode('ascii')
-
- if type(consumer_secret) == six.text_type:
- consumer_secret = consumer_secret.encode('ascii')
-
- self.consumer_key = consumer_key
- self.consumer_secret = consumer_secret
- self.access_token = None
- self.access_token_secret = None
- self.callback = callback
- self.username = None
- self.oauth = OAuth1Session(consumer_key,
- client_secret=consumer_secret,
- callback_uri=self.callback)
-
- def _get_oauth_url(self, endpoint):
- return 'https://' + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
-
- def apply_auth(self):
- return OAuth1(self.consumer_key,
- client_secret=self.consumer_secret,
- resource_owner_key=self.access_token,
- resource_owner_secret=self.access_token_secret,
- decoding=None)
-
- def _get_request_token(self, access_type=None):
- try:
- url = self._get_oauth_url('request_token')
- if access_type:
- url += '?x_auth_access_type=%s' % access_type
- return self.oauth.fetch_request_token(url)
- except Exception as e:
- raise TweepError(e)
-
- def set_access_token(self, key, secret):
- self.access_token = key
- self.access_token_secret = secret
-
- def get_authorization_url(self,
- signin_with_twitter=False,
- access_type=None):
- """Get the authorization URL to redirect the user"""
- try:
- if signin_with_twitter:
- url = self._get_oauth_url('authenticate')
- if access_type:
- logging.warning(WARNING_MESSAGE)
- else:
- url = self._get_oauth_url('authorize')
- self.request_token = self._get_request_token(access_type=access_type)
- return self.oauth.authorization_url(url)
- except Exception as e:
- raise TweepError(e)
-
- def get_access_token(self, verifier=None):
- """
- After user has authorized the request token, get access token
- with user supplied verifier.
- """
- try:
- url = self._get_oauth_url('access_token')
- self.oauth = OAuth1Session(self.consumer_key,
- client_secret=self.consumer_secret,
- resource_owner_key=self.request_token['oauth_token'],
- resource_owner_secret=self.request_token['oauth_token_secret'],
- verifier=verifier, callback_uri=self.callback)
- resp = self.oauth.fetch_access_token(url)
- self.access_token = resp['oauth_token']
- self.access_token_secret = resp['oauth_token_secret']
- return self.access_token, self.access_token_secret
- except Exception as e:
- raise TweepError(e)
-
- def get_xauth_access_token(self, username, password):
- """
- Get an access token from an username and password combination.
- In order to get this working you need to create an app at
- http://twitter.com/apps, after that send a mail to api@twitter.com
- and request activation of xAuth for it.
- """
- try:
- url = self._get_oauth_url('access_token')
- oauth = OAuth1(self.consumer_key,
- client_secret=self.consumer_secret)
- r = requests.post(url=url,
- auth=oauth,
- headers={'x_auth_mode': 'client_auth',
- 'x_auth_username': username,
- 'x_auth_password': password})
-
- credentials = parse_qs(r.content)
- return credentials.get('oauth_token')[0], credentials.get('oauth_token_secret')[0]
- except Exception as e:
- raise TweepError(e)
-
- def get_username(self):
- if self.username is None:
- api = API(self)
- user = api.verify_credentials()
- if user:
- self.username = user.screen_name
- else:
- raise TweepError('Unable to get username,'
- ' invalid oauth token!')
- return self.username
-
-
-class OAuth2Bearer(AuthBase):
- def __init__(self, bearer_token):
- self.bearer_token = bearer_token
-
- def __call__(self, request):
- request.headers['Authorization'] = 'Bearer ' + self.bearer_token
- return request
-
-
-class AppAuthHandler(AuthHandler):
- """Application-only authentication handler"""
-
- OAUTH_HOST = 'api.twitter.com'
- OAUTH_ROOT = '/oauth2/'
-
- def __init__(self, consumer_key, consumer_secret):
- self.consumer_key = consumer_key
- self.consumer_secret = consumer_secret
- self._bearer_token = ''
-
- resp = requests.post(self._get_oauth_url('token'),
- auth=(self.consumer_key,
- self.consumer_secret),
- data={'grant_type': 'client_credentials'})
- data = resp.json()
- if data.get('token_type') != 'bearer':
- raise TweepError('Expected token_type to equal "bearer", '
- 'but got %s instead' % data.get('token_type'))
-
- self._bearer_token = data['access_token']
-
- def _get_oauth_url(self, endpoint):
- return 'https://' + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
-
- def apply_auth(self):
- return OAuth2Bearer(self._bearer_token)
diff --git a/apprise/plugins/NotifyTwitter/tweepy/binder.py b/apprise/plugins/NotifyTwitter/tweepy/binder.py
deleted file mode 100644
index ff807313..00000000
--- a/apprise/plugins/NotifyTwitter/tweepy/binder.py
+++ /dev/null
@@ -1,261 +0,0 @@
-# Tweepy
-# Copyright 2009-2010 Joshua Roesslein
-# See LICENSE for details.
-
-from __future__ import print_function
-
-import time
-import re
-
-from six.moves.urllib.parse import quote, urlencode
-import requests
-
-import logging
-
-from .error import TweepError, RateLimitError, is_rate_limit_error_message
-from .utils import convert_to_utf8_str
-from .models import Model
-import six
-import sys
-
-
-re_path_template = re.compile(r'{\w+}')
-
-log = logging.getLogger('tweepy.binder')
-
-def bind_api(**config):
-
- class APIMethod(object):
-
- api = config['api']
- path = config['path']
- payload_type = config.get('payload_type', None)
- payload_list = config.get('payload_list', False)
- allowed_param = config.get('allowed_param', [])
- method = config.get('method', 'GET')
- require_auth = config.get('require_auth', False)
- search_api = config.get('search_api', False)
- upload_api = config.get('upload_api', False)
- use_cache = config.get('use_cache', True)
- session = requests.Session()
-
- def __init__(self, args, kwargs):
- api = self.api
- # If authentication is required and no credentials
- # are provided, throw an error.
- if self.require_auth and not api.auth:
- raise TweepError('Authentication required!')
-
- self.post_data = kwargs.pop('post_data', None)
- self.retry_count = kwargs.pop('retry_count',
- api.retry_count)
- self.retry_delay = kwargs.pop('retry_delay',
- api.retry_delay)
- self.retry_errors = kwargs.pop('retry_errors',
- api.retry_errors)
- self.wait_on_rate_limit = kwargs.pop('wait_on_rate_limit',
- api.wait_on_rate_limit)
- self.wait_on_rate_limit_notify = kwargs.pop('wait_on_rate_limit_notify',
- api.wait_on_rate_limit_notify)
- self.parser = kwargs.pop('parser', api.parser)
- self.session.headers = kwargs.pop('headers', {})
- self.build_parameters(args, kwargs)
-
- # Pick correct URL root to use
- if self.search_api:
- self.api_root = api.search_root
- elif self.upload_api:
- self.api_root = api.upload_root
- else:
- self.api_root = api.api_root
-
- # Perform any path variable substitution
- self.build_path()
-
- if self.search_api:
- self.host = api.search_host
- elif self.upload_api:
- self.host = api.upload_host
- else:
- self.host = api.host
-
- # Manually set Host header to fix an issue in python 2.5
- # or older where Host is set including the 443 port.
- # This causes Twitter to issue 301 redirect.
- # See Issue https://github.com/tweepy/tweepy/issues/12
- self.session.headers['Host'] = self.host
- # Monitoring rate limits
- self._remaining_calls = None
- self._reset_time = None
-
- def build_parameters(self, args, kwargs):
- self.session.params = {}
- for idx, arg in enumerate(args):
- if arg is None:
- continue
- try:
- self.session.params[self.allowed_param[idx]] = convert_to_utf8_str(arg)
- except IndexError:
- raise TweepError('Too many parameters supplied!')
-
- for k, arg in kwargs.items():
- if arg is None:
- continue
- if k in self.session.params:
- raise TweepError('Multiple values for parameter %s supplied!' % k)
-
- self.session.params[k] = convert_to_utf8_str(arg)
-
- log.debug("PARAMS: %r", self.session.params)
-
- def build_path(self):
- for variable in re_path_template.findall(self.path):
- name = variable.strip('{}')
-
- if name == 'user' and 'user' not in self.session.params and self.api.auth:
- # No 'user' parameter provided, fetch it from Auth instead.
- value = self.api.auth.get_username()
- else:
- try:
- value = quote(self.session.params[name])
- except KeyError:
- raise TweepError('No parameter value found for path variable: %s' % name)
- del self.session.params[name]
-
- self.path = self.path.replace(variable, value)
-
- def execute(self):
- self.api.cached_result = False
-
- # Build the request URL
- url = self.api_root + self.path
- full_url = 'https://' + self.host + url
-
- # Query the cache if one is available
- # and this request uses a GET method.
- if self.use_cache and self.api.cache and self.method == 'GET':
- cache_result = self.api.cache.get('%s?%s' % (url, urlencode(self.session.params)))
- # if cache result found and not expired, return it
- if cache_result:
- # must restore api reference
- if isinstance(cache_result, list):
- for result in cache_result:
- if isinstance(result, Model):
- result._api = self.api
- else:
- if isinstance(cache_result, Model):
- cache_result._api = self.api
- self.api.cached_result = True
- return cache_result
-
- # Continue attempting request until successful
- # or maximum number of retries is reached.
- retries_performed = 0
- while retries_performed < self.retry_count + 1:
- # handle running out of api calls
- if self.wait_on_rate_limit:
- if self._reset_time is not None:
- if self._remaining_calls is not None:
- if self._remaining_calls < 1:
- sleep_time = self._reset_time - int(time.time())
- if sleep_time > 0:
- if self.wait_on_rate_limit_notify:
- log.warning("Rate limit reached. Sleeping for: %d" % sleep_time)
- time.sleep(sleep_time + 5) # sleep for few extra sec
-
- # if self.wait_on_rate_limit and self._reset_time is not None and \
- # self._remaining_calls is not None and self._remaining_calls < 1:
- # sleep_time = self._reset_time - int(time.time())
- # if sleep_time > 0:
- # if self.wait_on_rate_limit_notify:
- # log.warning("Rate limit reached. Sleeping for: %d" % sleep_time)
- # time.sleep(sleep_time + 5) # sleep for few extra sec
-
- # Apply authentication
- auth = None
- if self.api.auth:
- auth = self.api.auth.apply_auth()
-
- # Request compression if configured
- if self.api.compression:
- self.session.headers['Accept-encoding'] = 'gzip'
-
- # Execute request
- try:
- resp = self.session.request(self.method,
- full_url,
- data=self.post_data,
- timeout=self.api.timeout,
- auth=auth,
- proxies=self.api.proxy)
- except Exception as e:
- six.reraise(TweepError, TweepError('Failed to send request: %s' % e), sys.exc_info()[2])
-
- rem_calls = resp.headers.get('x-rate-limit-remaining')
-
- if rem_calls is not None:
- self._remaining_calls = int(rem_calls)
- elif isinstance(self._remaining_calls, int):
- self._remaining_calls -= 1
- reset_time = resp.headers.get('x-rate-limit-reset')
- if reset_time is not None:
- self._reset_time = int(reset_time)
- if self.wait_on_rate_limit and self._remaining_calls == 0 and (
- # if ran out of calls before waiting switching retry last call
- resp.status_code == 429 or resp.status_code == 420):
- continue
- retry_delay = self.retry_delay
- # Exit request loop if non-retry error code
- if resp.status_code == 200:
- break
- elif (resp.status_code == 429 or resp.status_code == 420) and self.wait_on_rate_limit:
- if 'retry-after' in resp.headers:
- retry_delay = float(resp.headers['retry-after'])
- elif self.retry_errors and resp.status_code not in self.retry_errors:
- break
-
- # Sleep before retrying request again
- time.sleep(retry_delay)
- retries_performed += 1
-
- # If an error was returned, throw an exception
- self.api.last_response = resp
- if resp.status_code and not 200 <= resp.status_code < 300:
- try:
- error_msg, api_error_code = \
- self.parser.parse_error(resp.text)
- except Exception:
- error_msg = "Twitter error response: status code = %s" % resp.status_code
- api_error_code = None
-
- if is_rate_limit_error_message(error_msg):
- raise RateLimitError(error_msg, resp)
- else:
- raise TweepError(error_msg, resp, api_code=api_error_code)
-
- # Parse the response payload
- result = self.parser.parse(self, resp.text)
-
- # Store result into cache if one is available.
- if self.use_cache and self.api.cache and self.method == 'GET' and result:
- self.api.cache.store('%s?%s' % (url, urlencode(self.session.params)), result)
-
- return result
-
- def _call(*args, **kwargs):
- method = APIMethod(args, kwargs)
- if kwargs.get('create'):
- return method
- else:
- return method.execute()
-
- # Set pagination mode
- if 'cursor' in APIMethod.allowed_param:
- _call.pagination_mode = 'cursor'
- elif 'max_id' in APIMethod.allowed_param:
- if 'since_id' in APIMethod.allowed_param:
- _call.pagination_mode = 'id'
- elif 'page' in APIMethod.allowed_param:
- _call.pagination_mode = 'page'
-
- return _call
diff --git a/apprise/plugins/NotifyTwitter/tweepy/cache.py b/apprise/plugins/NotifyTwitter/tweepy/cache.py
deleted file mode 100644
index 8c287816..00000000
--- a/apprise/plugins/NotifyTwitter/tweepy/cache.py
+++ /dev/null
@@ -1,432 +0,0 @@
-# Tweepy
-# Copyright 2009-2010 Joshua Roesslein
-# See LICENSE for details.
-
-from __future__ import print_function
-
-import time
-import datetime
-import hashlib
-import threading
-import os
-import logging
-
-try:
- import cPickle as pickle
-except ImportError:
- import pickle
-
-try:
- import fcntl
-except ImportError:
- # Probably on a windows system
- # TODO: use win32file
- pass
-
-log = logging.getLogger('tweepy.cache')
-
-class Cache(object):
- """Cache interface"""
-
- def __init__(self, timeout=60):
- """Initialize the cache
- timeout: number of seconds to keep a cached entry
- """
- self.timeout = timeout
-
- def store(self, key, value):
- """Add new record to cache
- key: entry key
- value: data of entry
- """
- raise NotImplementedError
-
- def get(self, key, timeout=None):
- """Get cached entry if exists and not expired
- key: which entry to get
- timeout: override timeout with this value [optional]
- """
- raise NotImplementedError
-
- def count(self):
- """Get count of entries currently stored in cache"""
- raise NotImplementedError
-
- def cleanup(self):
- """Delete any expired entries in cache."""
- raise NotImplementedError
-
- def flush(self):
- """Delete all cached entries"""
- raise NotImplementedError
-
-
-class MemoryCache(Cache):
- """In-memory cache"""
-
- def __init__(self, timeout=60):
- Cache.__init__(self, timeout)
- self._entries = {}
- self.lock = threading.Lock()
-
- def __getstate__(self):
- # pickle
- return {'entries': self._entries, 'timeout': self.timeout}
-
- def __setstate__(self, state):
- # unpickle
- self.lock = threading.Lock()
- self._entries = state['entries']
- self.timeout = state['timeout']
-
- def _is_expired(self, entry, timeout):
- return timeout > 0 and (time.time() - entry[0]) >= timeout
-
- def store(self, key, value):
- self.lock.acquire()
- self._entries[key] = (time.time(), value)
- self.lock.release()
-
- def get(self, key, timeout=None):
- self.lock.acquire()
- try:
- # check to see if we have this key
- entry = self._entries.get(key)
- if not entry:
- # no hit, return nothing
- return None
-
- # use provided timeout in arguments if provided
- # otherwise use the one provided during init.
- if timeout is None:
- timeout = self.timeout
-
- # make sure entry is not expired
- if self._is_expired(entry, timeout):
- # entry expired, delete and return nothing
- del self._entries[key]
- return None
-
- # entry found and not expired, return it
- return entry[1]
- finally:
- self.lock.release()
-
- def count(self):
- return len(self._entries)
-
- def cleanup(self):
- self.lock.acquire()
- try:
- for k, v in dict(self._entries).items():
- if self._is_expired(v, self.timeout):
- del self._entries[k]
- finally:
- self.lock.release()
-
- def flush(self):
- self.lock.acquire()
- self._entries.clear()
- self.lock.release()
-
-
-class FileCache(Cache):
- """File-based cache"""
-
- # locks used to make cache thread-safe
- cache_locks = {}
-
- def __init__(self, cache_dir, timeout=60):
- Cache.__init__(self, timeout)
- if os.path.exists(cache_dir) is False:
- os.mkdir(cache_dir)
- self.cache_dir = cache_dir
- if cache_dir in FileCache.cache_locks:
- self.lock = FileCache.cache_locks[cache_dir]
- else:
- self.lock = threading.Lock()
- FileCache.cache_locks[cache_dir] = self.lock
-
- if os.name == 'posix':
- self._lock_file = self._lock_file_posix
- self._unlock_file = self._unlock_file_posix
- elif os.name == 'nt':
- self._lock_file = self._lock_file_win32
- self._unlock_file = self._unlock_file_win32
- else:
- log.warning('FileCache locking not supported on this system!')
- self._lock_file = self._lock_file_dummy
- self._unlock_file = self._unlock_file_dummy
-
- def _get_path(self, key):
- md5 = hashlib.md5()
- md5.update(key.encode('utf-8'))
- return os.path.join(self.cache_dir, md5.hexdigest())
-
- def _lock_file_dummy(self, path, exclusive=True):
- return None
-
- def _unlock_file_dummy(self, lock):
- return
-
- def _lock_file_posix(self, path, exclusive=True):
- lock_path = path + '.lock'
- if exclusive is True:
- f_lock = open(lock_path, 'w')
- fcntl.lockf(f_lock, fcntl.LOCK_EX)
- else:
- f_lock = open(lock_path, 'r')
- fcntl.lockf(f_lock, fcntl.LOCK_SH)
- if os.path.exists(lock_path) is False:
- f_lock.close()
- return None
- return f_lock
-
- def _unlock_file_posix(self, lock):
- lock.close()
-
- def _lock_file_win32(self, path, exclusive=True):
- # TODO: implement
- return None
-
- def _unlock_file_win32(self, lock):
- # TODO: implement
- return
-
- def _delete_file(self, path):
- os.remove(path)
- if os.path.exists(path + '.lock'):
- os.remove(path + '.lock')
-
- def store(self, key, value):
- path = self._get_path(key)
- self.lock.acquire()
- try:
- # acquire lock and open file
- f_lock = self._lock_file(path)
- datafile = open(path, 'wb')
-
- # write data
- pickle.dump((time.time(), value), datafile)
-
- # close and unlock file
- datafile.close()
- self._unlock_file(f_lock)
- finally:
- self.lock.release()
-
- def get(self, key, timeout=None):
- return self._get(self._get_path(key), timeout)
-
- def _get(self, path, timeout):
- if os.path.exists(path) is False:
- # no record
- return None
- self.lock.acquire()
- try:
- # acquire lock and open
- f_lock = self._lock_file(path, False)
- datafile = open(path, 'rb')
-
- # read pickled object
- created_time, value = pickle.load(datafile)
- datafile.close()
-
- # check if value is expired
- if timeout is None:
- timeout = self.timeout
- if timeout > 0:
- if (time.time() - created_time) >= timeout:
- # expired! delete from cache
- value = None
- self._delete_file(path)
-
- # unlock and return result
- self._unlock_file(f_lock)
- return value
- finally:
- self.lock.release()
-
- def count(self):
- c = 0
- for entry in os.listdir(self.cache_dir):
- if entry.endswith('.lock'):
- continue
- c += 1
- return c
-
- def cleanup(self):
- for entry in os.listdir(self.cache_dir):
- if entry.endswith('.lock'):
- continue
- self._get(os.path.join(self.cache_dir, entry), None)
-
- def flush(self):
- for entry in os.listdir(self.cache_dir):
- if entry.endswith('.lock'):
- continue
- self._delete_file(os.path.join(self.cache_dir, entry))
-
-
-class MemCacheCache(Cache):
- """Cache interface"""
-
- def __init__(self, client, timeout=60):
- """Initialize the cache
- client: The memcache client
- timeout: number of seconds to keep a cached entry
- """
- self.client = client
- self.timeout = timeout
-
- def store(self, key, value):
- """Add new record to cache
- key: entry key
- value: data of entry
- """
- self.client.set(key, value, time=self.timeout)
-
- def get(self, key, timeout=None):
- """Get cached entry if exists and not expired
- key: which entry to get
- timeout: override timeout with this value [optional].
- DOES NOT WORK HERE
- """
- return self.client.get(key)
-
- def count(self):
- """Get count of entries currently stored in cache. RETURN 0"""
- raise NotImplementedError
-
- def cleanup(self):
- """Delete any expired entries in cache. NO-OP"""
- raise NotImplementedError
-
- def flush(self):
- """Delete all cached entries. NO-OP"""
- raise NotImplementedError
-
-
-class RedisCache(Cache):
- """Cache running in a redis server"""
-
- def __init__(self, client,
- timeout=60,
- keys_container='tweepy:keys',
- pre_identifier='tweepy:'):
- Cache.__init__(self, timeout)
- self.client = client
- self.keys_container = keys_container
- self.pre_identifier = pre_identifier
-
- def _is_expired(self, entry, timeout):
- # Returns true if the entry has expired
- return timeout > 0 and (time.time() - entry[0]) >= timeout
-
- def store(self, key, value):
- """Store the key, value pair in our redis server"""
- # Prepend tweepy to our key,
- # this makes it easier to identify tweepy keys in our redis server
- key = self.pre_identifier + key
- # Get a pipe (to execute several redis commands in one step)
- pipe = self.client.pipeline()
- # Set our values in a redis hash (similar to python dict)
- pipe.set(key, pickle.dumps((time.time(), value)))
- # Set the expiration
- pipe.expire(key, self.timeout)
- # Add the key to a set containing all the keys
- pipe.sadd(self.keys_container, key)
- # Execute the instructions in the redis server
- pipe.execute()
-
- def get(self, key, timeout=None):
- """Given a key, returns an element from the redis table"""
- key = self.pre_identifier + key
- # Check to see if we have this key
- unpickled_entry = self.client.get(key)
- if not unpickled_entry:
- # No hit, return nothing
- return None
-
- entry = pickle.loads(unpickled_entry)
- # Use provided timeout in arguments if provided
- # otherwise use the one provided during init.
- if timeout is None:
- timeout = self.timeout
-
- # Make sure entry is not expired
- if self._is_expired(entry, timeout):
- # entry expired, delete and return nothing
- self.delete_entry(key)
- return None
- # entry found and not expired, return it
- return entry[1]
-
- def count(self):
- """Note: This is not very efficient,
- since it retreives all the keys from the redis
- server to know how many keys we have"""
- return len(self.client.smembers(self.keys_container))
-
- def delete_entry(self, key):
- """Delete an object from the redis table"""
- pipe = self.client.pipeline()
- pipe.srem(self.keys_container, key)
- pipe.delete(key)
- pipe.execute()
-
- def cleanup(self):
- """Cleanup all the expired keys"""
- keys = self.client.smembers(self.keys_container)
- for key in keys:
- entry = self.client.get(key)
- if entry:
- entry = pickle.loads(entry)
- if self._is_expired(entry, self.timeout):
- self.delete_entry(key)
-
- def flush(self):
- """Delete all entries from the cache"""
- keys = self.client.smembers(self.keys_container)
- for key in keys:
- self.delete_entry(key)
-
-
-class MongodbCache(Cache):
- """A simple pickle-based MongoDB cache sytem."""
-
- def __init__(self, db, timeout=3600, collection='tweepy_cache'):
- """Should receive a "database" cursor from pymongo."""
- Cache.__init__(self, timeout)
- self.timeout = timeout
- self.col = db[collection]
- self.col.create_index('created', expireAfterSeconds=timeout)
-
- def store(self, key, value):
- from bson.binary import Binary
-
- now = datetime.datetime.utcnow()
- blob = Binary(pickle.dumps(value))
-
- self.col.insert({'created': now, '_id': key, 'value': blob})
-
- def get(self, key, timeout=None):
- if timeout:
- raise NotImplementedError
- obj = self.col.find_one({'_id': key})
- if obj:
- return pickle.loads(obj['value'])
-
- def count(self):
- return self.col.find({}).count()
-
- def delete_entry(self, key):
- return self.col.remove({'_id': key})
-
- def cleanup(self):
- """MongoDB will automatically clear expired keys."""
- pass
-
- def flush(self):
- self.col.drop()
- self.col.create_index('created', expireAfterSeconds=self.timeout)
diff --git a/apprise/plugins/NotifyTwitter/tweepy/cursor.py b/apprise/plugins/NotifyTwitter/tweepy/cursor.py
deleted file mode 100644
index 3ab28c28..00000000
--- a/apprise/plugins/NotifyTwitter/tweepy/cursor.py
+++ /dev/null
@@ -1,214 +0,0 @@
-# Tweepy
-# Copyright 2009-2010 Joshua Roesslein
-# See LICENSE for details.
-
-from __future__ import print_function
-
-from .error import TweepError
-from .parsers import ModelParser, RawParser
-
-
-class Cursor(object):
- """Pagination helper class"""
-
- def __init__(self, method, *args, **kargs):
- if hasattr(method, 'pagination_mode'):
- if method.pagination_mode == 'cursor':
- self.iterator = CursorIterator(method, args, kargs)
- elif method.pagination_mode == 'id':
- self.iterator = IdIterator(method, args, kargs)
- elif method.pagination_mode == 'page':
- self.iterator = PageIterator(method, args, kargs)
- else:
- raise TweepError('Invalid pagination mode.')
- else:
- raise TweepError('This method does not perform pagination')
-
- def pages(self, limit=0):
- """Return iterator for pages"""
- if limit > 0:
- self.iterator.limit = limit
- return self.iterator
-
- def items(self, limit=0):
- """Return iterator for items in each page"""
- i = ItemIterator(self.iterator)
- i.limit = limit
- return i
-
-
-class BaseIterator(object):
-
- def __init__(self, method, args, kargs):
- self.method = method
- self.args = args
- self.kargs = kargs
- self.limit = 0
-
- def __next__(self):
- return self.next()
-
- def next(self):
- raise NotImplementedError
-
- def prev(self):
- raise NotImplementedError
-
- def __iter__(self):
- return self
-
-
-class CursorIterator(BaseIterator):
-
- def __init__(self, method, args, kargs):
- BaseIterator.__init__(self, method, args, kargs)
- start_cursor = kargs.pop('cursor', None)
- self.next_cursor = start_cursor or -1
- self.prev_cursor = start_cursor or 0
- self.num_tweets = 0
-
- def next(self):
- if self.next_cursor == 0 or (self.limit and self.num_tweets == self.limit):
- raise StopIteration
- data, cursors = self.method(cursor=self.next_cursor,
- *self.args,
- **self.kargs)
- self.prev_cursor, self.next_cursor = cursors
- if len(data) == 0:
- raise StopIteration
- self.num_tweets += 1
- return data
-
- def prev(self):
- if self.prev_cursor == 0:
- raise TweepError('Can not page back more, at first page')
- data, self.next_cursor, self.prev_cursor = self.method(cursor=self.prev_cursor,
- *self.args,
- **self.kargs)
- self.num_tweets -= 1
- return data
-
-
-class IdIterator(BaseIterator):
-
- def __init__(self, method, args, kargs):
- BaseIterator.__init__(self, method, args, kargs)
- self.max_id = kargs.pop('max_id', None)
- self.num_tweets = 0
- self.results = []
- self.model_results = []
- self.index = 0
-
- def next(self):
- """Fetch a set of items with IDs less than current set."""
- if self.limit and self.limit == self.num_tweets:
- raise StopIteration
-
- if self.index >= len(self.results) - 1:
- data = self.method(max_id=self.max_id, parser=RawParser(), *self.args, **self.kargs)
-
- if hasattr(self.method, '__self__'):
- old_parser = self.method.__self__.parser
- # Hack for models which expect ModelParser to be set
- self.method.__self__.parser = ModelParser()
-
- # This is a special invocation that returns the underlying
- # APIMethod class
- model = ModelParser().parse(self.method(create=True), data)
- if hasattr(self.method, '__self__'):
- self.method.__self__.parser = old_parser
- result = self.method.__self__.parser.parse(self.method(create=True), data)
- else:
- result = model
-
- if len(self.results) != 0:
- self.index += 1
- self.results.append(result)
- self.model_results.append(model)
- else:
- self.index += 1
- result = self.results[self.index]
- model = self.model_results[self.index]
-
- if len(result) == 0:
- raise StopIteration
- # TODO: Make this not dependant on the parser making max_id and
- # since_id available
- self.max_id = model.max_id
- self.num_tweets += 1
- return result
-
- def prev(self):
- """Fetch a set of items with IDs greater than current set."""
- if self.limit and self.limit == self.num_tweets:
- raise StopIteration
-
- self.index -= 1
- if self.index < 0:
- # There's no way to fetch a set of tweets directly 'above' the
- # current set
- raise StopIteration
-
- data = self.results[self.index]
- self.max_id = self.model_results[self.index].max_id
- self.num_tweets += 1
- return data
-
-
-class PageIterator(BaseIterator):
-
- def __init__(self, method, args, kargs):
- BaseIterator.__init__(self, method, args, kargs)
- self.current_page = 0
-
- def next(self):
- if self.limit > 0:
- if self.current_page > self.limit:
- raise StopIteration
-
- items = self.method(page=self.current_page, *self.args, **self.kargs)
- if len(items) == 0:
- raise StopIteration
- self.current_page += 1
- return items
-
- def prev(self):
- if self.current_page == 1:
- raise TweepError('Can not page back more, at first page')
- self.current_page -= 1
- return self.method(page=self.current_page, *self.args, **self.kargs)
-
-
-class ItemIterator(BaseIterator):
-
- def __init__(self, page_iterator):
- self.page_iterator = page_iterator
- self.limit = 0
- self.current_page = None
- self.page_index = -1
- self.num_tweets = 0
-
- def next(self):
- if self.limit > 0:
- if self.num_tweets == self.limit:
- raise StopIteration
- if self.current_page is None or self.page_index == len(self.current_page) - 1:
- # Reached end of current page, get the next page...
- self.current_page = self.page_iterator.next()
- self.page_index = -1
- self.page_index += 1
- self.num_tweets += 1
- return self.current_page[self.page_index]
-
- def prev(self):
- if self.current_page is None:
- raise TweepError('Can not go back more, at first page')
- if self.page_index == 0:
- # At the beginning of the current page, move to next...
- self.current_page = self.page_iterator.prev()
- self.page_index = len(self.current_page)
- if self.page_index == 0:
- raise TweepError('No more items')
- self.page_index -= 1
- self.num_tweets -= 1
- return self.current_page[self.page_index]
diff --git a/apprise/plugins/NotifyTwitter/tweepy/error.py b/apprise/plugins/NotifyTwitter/tweepy/error.py
deleted file mode 100644
index f7d58944..00000000
--- a/apprise/plugins/NotifyTwitter/tweepy/error.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Tweepy
-# Copyright 2009-2010 Joshua Roesslein
-# See LICENSE for details.
-
-from __future__ import print_function
-
-import six
-
-class TweepError(Exception):
- """Tweepy exception"""
-
- def __init__(self, reason, response=None, api_code=None):
- self.reason = six.text_type(reason)
- self.response = response
- self.api_code = api_code
- Exception.__init__(self, reason)
-
- def __str__(self):
- return self.reason
-
-
-def is_rate_limit_error_message(message):
- """Check if the supplied error message belongs to a rate limit error."""
- return isinstance(message, list) \
- and len(message) > 0 \
- and 'code' in message[0] \
- and message[0]['code'] == 88
-
-
-class RateLimitError(TweepError):
- """Exception for Tweepy hitting the rate limit."""
- # RateLimitError has the exact same properties and inner workings
- # as TweepError for backwards compatibility reasons.
- pass
diff --git a/apprise/plugins/NotifyTwitter/tweepy/models.py b/apprise/plugins/NotifyTwitter/tweepy/models.py
deleted file mode 100644
index 4b2a66c5..00000000
--- a/apprise/plugins/NotifyTwitter/tweepy/models.py
+++ /dev/null
@@ -1,495 +0,0 @@
-# Tweepy
-# Copyright 2009-2010 Joshua Roesslein
-# See LICENSE for details.
-
-from __future__ import absolute_import, print_function
-
-from .utils import parse_datetime, parse_html_value, parse_a_href
-
-
-class ResultSet(list):
- """A list like object that holds results from a Twitter API query."""
- def __init__(self, max_id=None, since_id=None):
- super(ResultSet, self).__init__()
- self._max_id = max_id
- self._since_id = since_id
-
- @property
- def max_id(self):
- if self._max_id:
- return self._max_id
- ids = self.ids()
- # Max_id is always set to the *smallest* id, minus one, in the set
- return (min(ids) - 1) if ids else None
-
- @property
- def since_id(self):
- if self._since_id:
- return self._since_id
- ids = self.ids()
- # Since_id is always set to the *greatest* id in the set
- return max(ids) if ids else None
-
- def ids(self):
- return [item.id for item in self if hasattr(item, 'id')]
-
-
-class Model(object):
-
- def __init__(self, api=None):
- self._api = api
-
- def __getstate__(self):
- # pickle
- pickle = dict(self.__dict__)
- try:
- del pickle['_api'] # do not pickle the API reference
- except KeyError:
- pass
- return pickle
-
- @classmethod
- def parse(cls, api, json):
- """Parse a JSON object into a model instance."""
- raise NotImplementedError
-
- @classmethod
- def parse_list(cls, api, json_list):
- """
- Parse a list of JSON objects into
- a result set of model instances.
- """
- results = ResultSet()
- for obj in json_list:
- if obj:
- results.append(cls.parse(api, obj))
- return results
-
- def __repr__(self):
- state = ['%s=%s' % (k, repr(v)) for (k, v) in vars(self).items()]
- return '%s(%s)' % (self.__class__.__name__, ', '.join(state))
-
-
-class Status(Model):
-
- @classmethod
- def parse(cls, api, json):
- status = cls(api)
- setattr(status, '_json', json)
- for k, v in json.items():
- if k == 'user':
- user_model = getattr(api.parser.model_factory, 'user') if api else User
- user = user_model.parse(api, v)
- setattr(status, 'author', user)
- setattr(status, 'user', user) # DEPRECIATED
- elif k == 'created_at':
- setattr(status, k, parse_datetime(v))
- elif k == 'source':
- if '<' in v:
- setattr(status, k, parse_html_value(v))
- setattr(status, 'source_url', parse_a_href(v))
- else:
- setattr(status, k, v)
- setattr(status, 'source_url', None)
- elif k == 'retweeted_status':
- setattr(status, k, Status.parse(api, v))
- elif k == 'quoted_status':
- setattr(status, k, Status.parse(api, v))
- elif k == 'place':
- if v is not None:
- setattr(status, k, Place.parse(api, v))
- else:
- setattr(status, k, None)
- else:
- setattr(status, k, v)
- return status
-
- def destroy(self):
- return self._api.destroy_status(self.id)
-
- def retweet(self):
- return self._api.retweet(self.id)
-
- def retweets(self):
- return self._api.retweets(self.id)
-
- def favorite(self):
- return self._api.create_favorite(self.id)
-
- def __eq__(self, other):
- if isinstance(other, Status):
- return self.id == other.id
-
- return NotImplemented
-
- def __ne__(self, other):
- result = self == other
-
- if result is NotImplemented:
- return result
-
- return not result
-
-
-class User(Model):
-
- @classmethod
- def parse(cls, api, json):
- user = cls(api)
- setattr(user, '_json', json)
- for k, v in json.items():
- if k == 'created_at':
- setattr(user, k, parse_datetime(v))
- elif k == 'status':
- setattr(user, k, Status.parse(api, v))
- elif k == 'following':
- # twitter sets this to null if it is false
- if v is True:
- setattr(user, k, True)
- else:
- setattr(user, k, False)
- else:
- setattr(user, k, v)
- return user
-
- @classmethod
- def parse_list(cls, api, json_list):
- if isinstance(json_list, list):
- item_list = json_list
- else:
- item_list = json_list['users']
-
- results = ResultSet()
- for obj in item_list:
- results.append(cls.parse(api, obj))
- return results
-
- def timeline(self, **kargs):
- return self._api.user_timeline(user_id=self.id, **kargs)
-
- def friends(self, **kargs):
- return self._api.friends(user_id=self.id, **kargs)
-
- def followers(self, **kargs):
- return self._api.followers(user_id=self.id, **kargs)
-
- def follow(self):
- self._api.create_friendship(user_id=self.id)
- self.following = True
-
- def unfollow(self):
- self._api.destroy_friendship(user_id=self.id)
- self.following = False
-
- def lists_memberships(self, *args, **kargs):
- return self._api.lists_memberships(user=self.screen_name,
- *args,
- **kargs)
-
- def lists_subscriptions(self, *args, **kargs):
- return self._api.lists_subscriptions(user=self.screen_name,
- *args,
- **kargs)
-
- def lists(self, *args, **kargs):
- return self._api.lists_all(user=self.screen_name,
- *args,
- **kargs)
-
- def followers_ids(self, *args, **kargs):
- return self._api.followers_ids(user_id=self.id,
- *args,
- **kargs)
-
-
-class DirectMessage(Model):
-
- @classmethod
- def parse(cls, api, json):
- dm = cls(api)
- for k, v in json.items():
- if k == 'sender' or k == 'recipient':
- setattr(dm, k, User.parse(api, v))
- elif k == 'created_at':
- setattr(dm, k, parse_datetime(v))
- else:
- setattr(dm, k, v)
- return dm
-
- def destroy(self):
- return self._api.destroy_direct_message(self.id)
-
-
-class Friendship(Model):
-
- @classmethod
- def parse(cls, api, json):
- relationship = json['relationship']
-
- # parse source
- source = cls(api)
- for k, v in relationship['source'].items():
- setattr(source, k, v)
-
- # parse target
- target = cls(api)
- for k, v in relationship['target'].items():
- setattr(target, k, v)
-
- return source, target
-
-
-class Category(Model):
-
- @classmethod
- def parse(cls, api, json):
- category = cls(api)
- for k, v in json.items():
- setattr(category, k, v)
- return category
-
-
-class SavedSearch(Model):
-
- @classmethod
- def parse(cls, api, json):
- ss = cls(api)
- for k, v in json.items():
- if k == 'created_at':
- setattr(ss, k, parse_datetime(v))
- else:
- setattr(ss, k, v)
- return ss
-
- def destroy(self):
- return self._api.destroy_saved_search(self.id)
-
-
-class SearchResults(ResultSet):
-
- @classmethod
- def parse(cls, api, json):
- metadata = json['search_metadata']
- results = SearchResults()
- results.refresh_url = metadata.get('refresh_url')
- results.completed_in = metadata.get('completed_in')
- results.query = metadata.get('query')
- results.count = metadata.get('count')
- results.next_results = metadata.get('next_results')
-
- status_model = getattr(api.parser.model_factory, 'status') if api else Status
-
- for status in json['statuses']:
- results.append(status_model.parse(api, status))
- return results
-
-
-class List(Model):
-
- @classmethod
- def parse(cls, api, json):
- lst = List(api)
- for k, v in json.items():
- if k == 'user':
- setattr(lst, k, User.parse(api, v))
- elif k == 'created_at':
- setattr(lst, k, parse_datetime(v))
- else:
- setattr(lst, k, v)
- return lst
-
- @classmethod
- def parse_list(cls, api, json_list, result_set=None):
- results = ResultSet()
- if isinstance(json_list, dict):
- json_list = json_list['lists']
- for obj in json_list:
- results.append(cls.parse(api, obj))
- return results
-
- def update(self, **kargs):
- return self._api.update_list(self.slug, **kargs)
-
- def destroy(self):
- return self._api.destroy_list(self.slug)
-
- def timeline(self, **kargs):
- return self._api.list_timeline(self.user.screen_name,
- self.slug,
- **kargs)
-
- def add_member(self, id):
- return self._api.add_list_member(self.slug, id)
-
- def remove_member(self, id):
- return self._api.remove_list_member(self.slug, id)
-
- def members(self, **kargs):
- return self._api.list_members(self.user.screen_name,
- self.slug,
- **kargs)
-
- def is_member(self, id):
- return self._api.is_list_member(self.user.screen_name,
- self.slug,
- id)
-
- def subscribe(self):
- return self._api.subscribe_list(self.user.screen_name, self.slug)
-
- def unsubscribe(self):
- return self._api.unsubscribe_list(self.user.screen_name, self.slug)
-
- def subscribers(self, **kargs):
- return self._api.list_subscribers(self.user.screen_name,
- self.slug,
- **kargs)
-
- def is_subscribed(self, id):
- return self._api.is_subscribed_list(self.user.screen_name,
- self.slug,
- id)
-
-
-class Relation(Model):
- @classmethod
- def parse(cls, api, json):
- result = cls(api)
- for k, v in json.items():
- if k == 'value' and json['kind'] in ['Tweet', 'LookedupStatus']:
- setattr(result, k, Status.parse(api, v))
- elif k == 'results':
- setattr(result, k, Relation.parse_list(api, v))
- else:
- setattr(result, k, v)
- return result
-
-
-class Relationship(Model):
- @classmethod
- def parse(cls, api, json):
- result = cls(api)
- for k, v in json.items():
- if k == 'connections':
- setattr(result, 'is_following', 'following' in v)
- setattr(result, 'is_followed_by', 'followed_by' in v)
- else:
- setattr(result, k, v)
- return result
-
-
-class JSONModel(Model):
-
- @classmethod
- def parse(cls, api, json):
- return json
-
-
-class IDModel(Model):
-
- @classmethod
- def parse(cls, api, json):
- if isinstance(json, list):
- return json
- else:
- return json['ids']
-
-
-class BoundingBox(Model):
-
- @classmethod
- def parse(cls, api, json):
- result = cls(api)
- if json is not None:
- for k, v in json.items():
- setattr(result, k, v)
- return result
-
- def origin(self):
- """
- Return longitude, latitude of southwest (bottom, left) corner of
- bounding box, as a tuple.
-
- This assumes that bounding box is always a rectangle, which
- appears to be the case at present.
- """
- return tuple(self.coordinates[0][0])
-
- def corner(self):
- """
- Return longitude, latitude of northeast (top, right) corner of
- bounding box, as a tuple.
-
- This assumes that bounding box is always a rectangle, which
- appears to be the case at present.
- """
- return tuple(self.coordinates[0][2])
-
-
-class Place(Model):
-
- @classmethod
- def parse(cls, api, json):
- place = cls(api)
- for k, v in json.items():
- if k == 'bounding_box':
- # bounding_box value may be null (None.)
- # Example: "United States" (id=96683cc9126741d1)
- if v is not None:
- t = BoundingBox.parse(api, v)
- else:
- t = v
- setattr(place, k, t)
- elif k == 'contained_within':
- # contained_within is a list of Places.
- setattr(place, k, Place.parse_list(api, v))
- else:
- setattr(place, k, v)
- return place
-
- @classmethod
- def parse_list(cls, api, json_list):
- if isinstance(json_list, list):
- item_list = json_list
- else:
- item_list = json_list['result']['places']
-
- results = ResultSet()
- for obj in item_list:
- results.append(cls.parse(api, obj))
- return results
-
-
-class Media(Model):
-
- @classmethod
- def parse(cls, api, json):
- media = cls(api)
- for k, v in json.items():
- setattr(media, k, v)
- return media
-
-
-class ModelFactory(object):
- """
- Used by parsers for creating instances
- of models. You may subclass this factory
- to add your own extended models.
- """
-
- status = Status
- user = User
- direct_message = DirectMessage
- friendship = Friendship
- saved_search = SavedSearch
- search_results = SearchResults
- category = Category
- list = List
- relation = Relation
- relationship = Relationship
- media = Media
-
- json = JSONModel
- ids = IDModel
- place = Place
- bounding_box = BoundingBox
diff --git a/apprise/plugins/NotifyTwitter/tweepy/parsers.py b/apprise/plugins/NotifyTwitter/tweepy/parsers.py
deleted file mode 100644
index 371ad5f9..00000000
--- a/apprise/plugins/NotifyTwitter/tweepy/parsers.py
+++ /dev/null
@@ -1,109 +0,0 @@
-# Tweepy
-# Copyright 2009-2010 Joshua Roesslein
-# See LICENSE for details.
-
-from __future__ import print_function
-
-from .models import ModelFactory
-from .utils import import_simplejson
-from .error import TweepError
-
-
-class Parser(object):
-
- def parse(self, method, payload):
- """
- Parse the response payload and return the result.
- Returns a tuple that contains the result data and the cursors
- (or None if not present).
- """
- raise NotImplementedError
-
- def parse_error(self, payload):
- """
- Parse the error message and api error code from payload.
- Return them as an (error_msg, error_code) tuple. If unable to parse the
- message, throw an exception and default error message will be used.
- """
- raise NotImplementedError
-
-
-class RawParser(Parser):
-
- def __init__(self):
- pass
-
- def parse(self, method, payload):
- return payload
-
- def parse_error(self, payload):
- return payload
-
-
-class JSONParser(Parser):
-
- payload_format = 'json'
-
- def __init__(self):
- self.json_lib = import_simplejson()
-
- def parse(self, method, payload):
- try:
- json = self.json_lib.loads(payload)
- except Exception as e:
- raise TweepError('Failed to parse JSON payload: %s' % e)
-
- needs_cursors = 'cursor' in method.session.params
- if needs_cursors and isinstance(json, dict) \
- and 'previous_cursor' in json \
- and 'next_cursor' in json:
- cursors = json['previous_cursor'], json['next_cursor']
- return json, cursors
- else:
- return json
-
- def parse_error(self, payload):
- error_object = self.json_lib.loads(payload)
-
- if 'error' in error_object:
- reason = error_object['error']
- api_code = error_object.get('code')
- else:
- reason = error_object['errors']
- api_code = [error.get('code') for error in
- reason if error.get('code')]
- api_code = api_code[0] if len(api_code) == 1 else api_code
-
- return reason, api_code
-
-
-class ModelParser(JSONParser):
-
- def __init__(self, model_factory=None):
- JSONParser.__init__(self)
- self.model_factory = model_factory or ModelFactory
-
- def parse(self, method, payload):
- try:
- if method.payload_type is None:
- return
- model = getattr(self.model_factory, method.payload_type)
- except AttributeError:
- raise TweepError('No model for this payload type: '
- '%s' % method.payload_type)
-
- json = JSONParser.parse(self, method, payload)
- if isinstance(json, tuple):
- json, cursors = json
- else:
- cursors = None
-
- if method.payload_list:
- result = model.parse_list(method.api, json)
- else:
- result = model.parse(method.api, json)
-
- if cursors:
- return result, cursors
- else:
- return result
diff --git a/apprise/plugins/NotifyTwitter/tweepy/streaming.py b/apprise/plugins/NotifyTwitter/tweepy/streaming.py
deleted file mode 100644
index c0961467..00000000
--- a/apprise/plugins/NotifyTwitter/tweepy/streaming.py
+++ /dev/null
@@ -1,476 +0,0 @@
-# Tweepy
-# Copyright 2009-2010 Joshua Roesslein
-# See LICENSE for details.
-
-# Appengine users: https://developers.google.com/appengine/docs/python/sockets/#making_httplib_use_sockets
-
-from __future__ import absolute_import, print_function
-
-import logging
-import re
-import requests
-import sys
-from requests.exceptions import Timeout
-from threading import Thread
-from time import sleep
-
-import six
-
-import ssl
-
-from .models import Status
-from .api import API
-from .error import TweepError
-
-from .utils import import_simplejson
-json = import_simplejson()
-
-STREAM_VERSION = '1.1'
-
-
-class StreamListener(object):
-
- def __init__(self, api=None):
- self.api = api or API()
-
- def on_connect(self):
- """Called once connected to streaming server.
-
- This will be invoked once a successful response
- is received from the server. Allows the listener
- to perform some work prior to entering the read loop.
- """
- pass
-
- def on_data(self, raw_data):
- """Called when raw data is received from connection.
-
- Override this method if you wish to manually handle
- the stream data. Return False to stop stream and close connection.
- """
- data = json.loads(raw_data)
-
- if 'in_reply_to_status_id' in data:
- status = Status.parse(self.api, data)
- if self.on_status(status) is False:
- return False
- elif 'delete' in data:
- delete = data['delete']['status']
- if self.on_delete(delete['id'], delete['user_id']) is False:
- return False
- elif 'event' in data:
- status = Status.parse(self.api, data)
- if self.on_event(status) is False:
- return False
- elif 'direct_message' in data:
- status = Status.parse(self.api, data)
- if self.on_direct_message(status) is False:
- return False
- elif 'friends' in data:
- if self.on_friends(data['friends']) is False:
- return False
- elif 'limit' in data:
- if self.on_limit(data['limit']['track']) is False:
- return False
- elif 'disconnect' in data:
- if self.on_disconnect(data['disconnect']) is False:
- return False
- elif 'warning' in data:
- if self.on_warning(data['warning']) is False:
- return False
- else:
- logging.error("Unknown message type: " + str(raw_data))
-
- def keep_alive(self):
- """Called when a keep-alive arrived"""
- return
-
- def on_status(self, status):
- """Called when a new status arrives"""
- return
-
- def on_exception(self, exception):
- """Called when an unhandled exception occurs."""
- return
-
- def on_delete(self, status_id, user_id):
- """Called when a delete notice arrives for a status"""
- return
-
- def on_event(self, status):
- """Called when a new event arrives"""
- return
-
- def on_direct_message(self, status):
- """Called when a new direct message arrives"""
- return
-
- def on_friends(self, friends):
- """Called when a friends list arrives.
-
- friends is a list that contains user_id
- """
- return
-
- def on_limit(self, track):
- """Called when a limitation notice arrives"""
- return
-
- def on_error(self, status_code):
- """Called when a non-200 status code is returned"""
- return False
-
- def on_timeout(self):
- """Called when stream connection times out"""
- return
-
- def on_disconnect(self, notice):
- """Called when twitter sends a disconnect notice
-
- Disconnect codes are listed here:
- https://dev.twitter.com/docs/streaming-apis/messages#Disconnect_messages_disconnect
- """
- return
-
- def on_warning(self, notice):
- """Called when a disconnection warning message arrives"""
- return
-
-
-class ReadBuffer(object):
- """Buffer data from the response in a smarter way than httplib/requests can.
-
- Tweets are roughly in the 2-12kb range, averaging around 3kb.
- Requests/urllib3/httplib/socket all use socket.read, which blocks
- until enough data is returned. On some systems (eg google appengine), socket
- reads are quite slow. To combat this latency we can read big chunks,
- but the blocking part means we won't get results until enough tweets
- have arrived. That may not be a big deal for high throughput systems.
- For low throughput systems we don't want to sacrafice latency, so we
- use small chunks so it can read the length and the tweet in 2 read calls.
- """
-
- def __init__(self, stream, chunk_size, encoding='utf-8'):
- self._stream = stream
- self._buffer = six.b('')
- self._chunk_size = chunk_size
- self._encoding = encoding
-
- def read_len(self, length):
- while not self._stream.closed:
- if len(self._buffer) >= length:
- return self._pop(length)
- read_len = max(self._chunk_size, length - len(self._buffer))
- self._buffer += self._stream.read(read_len)
- return six.b('')
-
- def read_line(self, sep=six.b('\n')):
- """Read the data stream until a given separator is found (default \n)
-
- :param sep: Separator to read until. Must by of the bytes type (str in python 2,
- bytes in python 3)
- :return: The str of the data read until sep
- """
- start = 0
- while not self._stream.closed:
- loc = self._buffer.find(sep, start)
- if loc >= 0:
- return self._pop(loc + len(sep))
- else:
- start = len(self._buffer)
- self._buffer += self._stream.read(self._chunk_size)
- return six.b('')
-
- def _pop(self, length):
- r = self._buffer[:length]
- self._buffer = self._buffer[length:]
- return r.decode(self._encoding)
-
-
-class Stream(object):
-
- host = 'stream.twitter.com'
-
- def __init__(self, auth, listener, **options):
- self.auth = auth
- self.listener = listener
- self.running = False
- self.timeout = options.get("timeout", 300.0)
- self.retry_count = options.get("retry_count")
- # values according to
- # https://dev.twitter.com/docs/streaming-apis/connecting#Reconnecting
- self.retry_time_start = options.get("retry_time", 5.0)
- self.retry_420_start = options.get("retry_420", 60.0)
- self.retry_time_cap = options.get("retry_time_cap", 320.0)
- self.snooze_time_step = options.get("snooze_time", 0.25)
- self.snooze_time_cap = options.get("snooze_time_cap", 16)
-
- # The default socket.read size. Default to less than half the size of
- # a tweet so that it reads tweets with the minimal latency of 2 reads
- # per tweet. Values higher than ~1kb will increase latency by waiting
- # for more data to arrive but may also increase throughput by doing
- # fewer socket read calls.
- self.chunk_size = options.get("chunk_size", 512)
-
- self.verify = options.get("verify", True)
-
- self.api = API()
- self.headers = options.get("headers") or {}
- self.new_session()
- self.body = None
- self.retry_time = self.retry_time_start
- self.snooze_time = self.snooze_time_step
-
- # Example: proxies = {'http': 'http://localhost:1080', 'https': 'http://localhost:1080'}
- self.proxies = options.get("proxies")
-
- def new_session(self):
- self.session = requests.Session()
- self.session.headers = self.headers
- self.session.params = None
-
- def _run(self):
- # Authenticate
- url = "https://%s%s" % (self.host, self.url)
-
- # Connect and process the stream
- error_counter = 0
- resp = None
- exc_info = None
- while self.running:
- if self.retry_count is not None:
- if error_counter > self.retry_count:
- # quit if error count greater than retry count
- break
- try:
- auth = self.auth.apply_auth()
- resp = self.session.request('POST',
- url,
- data=self.body,
- timeout=self.timeout,
- stream=True,
- auth=auth,
- verify=self.verify,
- proxies = self.proxies)
- if resp.status_code != 200:
- if self.listener.on_error(resp.status_code) is False:
- break
- error_counter += 1
- if resp.status_code == 420:
- self.retry_time = max(self.retry_420_start,
- self.retry_time)
- sleep(self.retry_time)
- self.retry_time = min(self.retry_time * 2,
- self.retry_time_cap)
- else:
- error_counter = 0
- self.retry_time = self.retry_time_start
- self.snooze_time = self.snooze_time_step
- self.listener.on_connect()
- self._read_loop(resp)
- except (Timeout, ssl.SSLError) as exc:
- # This is still necessary, as a SSLError can actually be
- # thrown when using Requests
- # If it's not time out treat it like any other exception
- if isinstance(exc, ssl.SSLError):
- if not (exc.args and 'timed out' in str(exc.args[0])):
- exc_info = sys.exc_info()
- break
- if self.listener.on_timeout() is False:
- break
- if self.running is False:
- break
- sleep(self.snooze_time)
- self.snooze_time = min(self.snooze_time + self.snooze_time_step,
- self.snooze_time_cap)
- except Exception as exc:
- exc_info = sys.exc_info()
- # any other exception is fatal, so kill loop
- break
-
- # cleanup
- self.running = False
- if resp:
- resp.close()
-
- self.new_session()
-
- if exc_info:
- # call a handler first so that the exception can be logged.
- self.listener.on_exception(exc_info[1])
- six.reraise(*exc_info)
-
- def _data(self, data):
- if self.listener.on_data(data) is False:
- self.running = False
-
- def _read_loop(self, resp):
- charset = resp.headers.get('content-type', default='')
- enc_search = re.search(r'charset=(?P\S*)', charset)
- if enc_search is not None:
- encoding = enc_search.group('enc')
- else:
- encoding = 'utf-8'
-
- buf = ReadBuffer(resp.raw, self.chunk_size, encoding=encoding)
-
- while self.running and not resp.raw.closed:
- length = 0
- while not resp.raw.closed:
- line = buf.read_line()
- stripped_line = line.strip() if line else line # line is sometimes None so we need to check here
- if not stripped_line:
- self.listener.keep_alive() # keep-alive new lines are expected
- elif stripped_line.isdigit():
- length = int(stripped_line)
- break
- else:
- raise TweepError('Expecting length, unexpected value found')
-
- next_status_obj = buf.read_len(length)
- if self.running and next_status_obj:
- self._data(next_status_obj)
-
- # # Note: keep-alive newlines might be inserted before each length value.
- # # read until we get a digit...
- # c = b'\n'
- # for c in resp.iter_content(decode_unicode=True):
- # if c == b'\n':
- # continue
- # break
- #
- # delimited_string = c
- #
- # # read rest of delimiter length..
- # d = b''
- # for d in resp.iter_content(decode_unicode=True):
- # if d != b'\n':
- # delimited_string += d
- # continue
- # break
- #
- # # read the next twitter status object
- # if delimited_string.decode('utf-8').strip().isdigit():
- # status_id = int(delimited_string)
- # next_status_obj = resp.raw.read(status_id)
- # if self.running:
- # self._data(next_status_obj.decode('utf-8'))
-
-
- if resp.raw.closed:
- self.on_closed(resp)
-
- def _start(self, is_async):
- self.running = True
- if is_async:
- self._thread = Thread(target=self._run)
- self._thread.start()
- else:
- self._run()
-
- def on_closed(self, resp):
- """ Called when the response has been closed by Twitter """
- pass
-
- def userstream(self,
- stall_warnings=False,
- _with=None,
- replies=None,
- track=None,
- locations=None,
- is_async=False,
- encoding='utf8'):
- self.session.params = {'delimited': 'length'}
- if self.running:
- raise TweepError('Stream object already connected!')
- self.url = '/%s/user.json' % STREAM_VERSION
- self.host = 'userstream.twitter.com'
- if stall_warnings:
- self.session.params['stall_warnings'] = stall_warnings
- if _with:
- self.session.params['with'] = _with
- if replies:
- self.session.params['replies'] = replies
- if locations and len(locations) > 0:
- if len(locations) % 4 != 0:
- raise TweepError("Wrong number of locations points, "
- "it has to be a multiple of 4")
- self.session.params['locations'] = ','.join(['%.2f' % l for l in locations])
- if track:
- self.session.params['track'] = u','.join(track).encode(encoding)
-
- self._start(is_async)
-
- def firehose(self, count=None, is_async=False):
- self.session.params = {'delimited': 'length'}
- if self.running:
- raise TweepError('Stream object already connected!')
- self.url = '/%s/statuses/firehose.json' % STREAM_VERSION
- if count:
- self.url += '&count=%s' % count
- self._start(is_async)
-
- def retweet(self, is_async=False):
- self.session.params = {'delimited': 'length'}
- if self.running:
- raise TweepError('Stream object already connected!')
- self.url = '/%s/statuses/retweet.json' % STREAM_VERSION
- self._start(is_async)
-
- def sample(self, is_async=False, languages=None, stall_warnings=False):
- self.session.params = {'delimited': 'length'}
- if self.running:
- raise TweepError('Stream object already connected!')
- self.url = '/%s/statuses/sample.json' % STREAM_VERSION
- if languages:
- self.session.params['language'] = ','.join(map(str, languages))
- if stall_warnings:
- self.session.params['stall_warnings'] = 'true'
- self._start(is_async)
-
- def filter(self, follow=None, track=None, is_async=False, locations=None,
- stall_warnings=False, languages=None, encoding='utf8', filter_level=None):
- self.body = {}
- self.session.headers['Content-type'] = "application/x-www-form-urlencoded"
- if self.running:
- raise TweepError('Stream object already connected!')
- self.url = '/%s/statuses/filter.json' % STREAM_VERSION
- if follow:
- self.body['follow'] = u','.join(follow).encode(encoding)
- if track:
- self.body['track'] = u','.join(track).encode(encoding)
- if locations and len(locations) > 0:
- if len(locations) % 4 != 0:
- raise TweepError("Wrong number of locations points, "
- "it has to be a multiple of 4")
- self.body['locations'] = u','.join(['%.4f' % l for l in locations])
- if stall_warnings:
- self.body['stall_warnings'] = stall_warnings
- if languages:
- self.body['language'] = u','.join(map(str, languages))
- if filter_level:
- self.body['filter_level'] = filter_level.encode(encoding)
- self.session.params = {'delimited': 'length'}
- self.host = 'stream.twitter.com'
- self._start(is_async)
-
- def sitestream(self, follow, stall_warnings=False,
- with_='user', replies=False, is_async=False):
- self.body = {}
- if self.running:
- raise TweepError('Stream object already connected!')
- self.url = '/%s/site.json' % STREAM_VERSION
- self.body['follow'] = u','.join(map(six.text_type, follow))
- self.body['delimited'] = 'length'
- if stall_warnings:
- self.body['stall_warnings'] = stall_warnings
- if with_:
- self.body['with'] = with_
- if replies:
- self.body['replies'] = replies
- self._start(is_async)
-
- def disconnect(self):
- if self.running is False:
- return
- self.running = False
diff --git a/apprise/plugins/NotifyTwitter/tweepy/utils.py b/apprise/plugins/NotifyTwitter/tweepy/utils.py
deleted file mode 100644
index e3843a73..00000000
--- a/apprise/plugins/NotifyTwitter/tweepy/utils.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# Tweepy
-# Copyright 2010 Joshua Roesslein
-# See LICENSE for details.
-
-from __future__ import print_function
-
-from datetime import datetime
-
-import six
-
-from email.utils import parsedate
-
-
-def parse_datetime(string):
- return datetime(*(parsedate(string)[:6]))
-
-
-def parse_html_value(html):
-
- return html[html.find('>')+1:html.rfind('<')]
-
-
-def parse_a_href(atag):
-
- start = atag.find('"') + 1
- end = atag.find('"', start)
- return atag[start:end]
-
-
-def convert_to_utf8_str(arg):
- # written by Michael Norton (http://docondev.blogspot.com/)
- if isinstance(arg, six.text_type):
- arg = arg.encode('utf-8')
- elif not isinstance(arg, bytes):
- arg = six.text_type(arg).encode('utf-8')
- return arg
-
-
-def import_simplejson():
- try:
- import simplejson as json
- except ImportError:
- import json
-
- return json
-
-
-def list_to_csv(item_list):
- if item_list:
- return ','.join([str(i) for i in item_list])
diff --git a/apprise/plugins/__init__.py b/apprise/plugins/__init__.py
index d441494c..ec6780a6 100644
--- a/apprise/plugins/__init__.py
+++ b/apprise/plugins/__init__.py
@@ -37,7 +37,6 @@ from . import NotifyEmail as NotifyEmailBase
# Required until re-factored into base code
from .NotifyPushjet import pushjet
from .NotifyGrowl import gntp
-from .NotifyTwitter import tweepy
# NotifyBase object is passed in as a module not class
from . import NotifyBase
@@ -66,9 +65,6 @@ __all__ = [
# pushjet (used for NotifyPushjet Testing)
'pushjet',
-
- # tweepy (used for NotifyTwitter Testing)
- 'tweepy',
]
# we mirror our base purely for the ability to reset everything; this
diff --git a/setup.cfg b/setup.cfg
index 00032554..bf961acf 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -7,7 +7,7 @@ license_file = LICENSE
[flake8]
# We exclude packages we don't maintain
-exclude = .eggs,.tox,gntp,tweepy,pushjet
+exclude = .eggs,.tox,gntp,pushjet
ignore = E722,W503,W504
statistics = true
builtins = _
diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py
index 9789f48f..3d734012 100644
--- a/test/test_rest_plugins.py
+++ b/test/test_rest_plugins.py
@@ -2169,6 +2169,152 @@ TEST_URLS = (
'test_requests_exceptions': True,
}),
+ ##################################
+ # NotifyTwitter
+ ##################################
+ ('twitter://', {
+ 'instance': None,
+ }),
+ ('twitter://:@/', {
+ 'instance': TypeError,
+ }),
+ ('twitter://consumer_key', {
+ # Missing Keys
+ 'instance': TypeError,
+ }),
+ ('twitter://consumer_key/consumer_secret/', {
+ # Missing Keys
+ 'instance': TypeError,
+ }),
+ ('twitter://consumer_key/consumer_secret/access_token/', {
+ # Missing Access Secret
+ 'instance': TypeError,
+ }),
+ ('twitter://consumer_key/consumer_secret/access_token/access_secret', {
+ # No user mean's we message ourselves
+ 'instance': plugins.NotifyTwitter,
+ # Expected notify() response False (because we won't be able
+ # to detect our user)
+ 'notify_response': False,
+ }),
+ ('twitter://consumer_key/consumer_secret/access_token/access_secret'
+ '?cache=no', {
+ # No user mean's we message ourselves
+ 'instance': plugins.NotifyTwitter,
+ # However we'll be okay if we return a proper response
+ 'requests_response_text': {
+ 'id': 12345,
+ 'screen_name': 'test'
+ },
+ }),
+ ('twitter://consumer_key/consumer_secret/access_token/access_secret', {
+ # No user mean's we message ourselves
+ 'instance': plugins.NotifyTwitter,
+ # However we'll be okay if we return a proper response
+ 'requests_response_text': {
+ 'id': 12345,
+ 'screen_name': 'test'
+ },
+ }),
+ # A duplicate of the entry above, this will cause cache to be referenced
+ ('twitter://consumer_key/consumer_secret/access_token/access_secret', {
+ # No user mean's we message ourselves
+ 'instance': plugins.NotifyTwitter,
+ # However we'll be okay if we return a proper response
+ 'requests_response_text': {
+ 'id': 12345,
+ 'screen_name': 'test'
+ },
+ }),
+ # handle cases where the screen_name is missing from the response causing
+ # an exception during parsing
+ ('twitter://consumer_key/consumer_secret2/access_token/access_secret', {
+ # No user mean's we message ourselves
+ 'instance': plugins.NotifyTwitter,
+ # However we'll be okay if we return a proper response
+ 'requests_response_text': {
+ 'id': 12345,
+ },
+ # due to a mangled response_text we'll fail
+ 'notify_response': False,
+ }),
+ ('twitter://user@consumer_key/csecret2/access_token/access_secret/-/%/', {
+ # One Invalid User
+ 'instance': plugins.NotifyTwitter,
+ # Expected notify() response False (because we won't be able
+ # to detect our user)
+ 'notify_response': False,
+ }),
+ ('twitter://user@consumer_key/csecret/access_token/access_secret'
+ '?cache=No', {
+ # No Cache
+ 'instance': plugins.NotifyTwitter,
+ 'requests_response_text': [{
+ 'id': 12345,
+ 'screen_name': 'user'
+ }],
+ }),
+ ('twitter://user@consumer_key/csecret/access_token/access_secret', {
+ # We're good!
+ 'instance': plugins.NotifyTwitter,
+ 'requests_response_text': [{
+ 'id': 12345,
+ 'screen_name': 'user'
+ }],
+ }),
+ # A duplicate of the entry above, this will cause cache to be referenced
+ # for this reason, we don't even need to return a valid response
+ ('twitter://user@consumer_key/csecret/access_token/access_secret', {
+ # We're identifying the same user we already sent to
+ 'instance': plugins.NotifyTwitter,
+ }),
+ ('twitter://ckey/csecret/access_token/access_secret?mode=tweet', {
+ # A Public Tweet
+ 'instance': plugins.NotifyTwitter,
+ }),
+ ('tweet://consumer_key/consumer_secret/access_token/access_secret', {
+ # tweet:// is to be depricated; but we will support for purposes of
+ # generating a warning to the user; the above matches an above
+ # twitter:// reference so that it can use what was cached
+ 'instance': plugins.NotifyTwitter,
+ }),
+ ('twitter://user@ckey/csecret/access_token/access_secret?mode=invalid', {
+ # An invalid mode
+ 'instance': TypeError,
+ }),
+ ('twitter://usera@consumer_key/consumer_secret/access_token/'
+ 'access_secret/user/?to=userb', {
+ # We're good!
+ 'instance': plugins.NotifyTwitter,
+ 'requests_response_text': [{
+ 'id': 12345,
+ 'screen_name': 'usera'
+ }, {
+ 'id': 12346,
+ 'screen_name': 'userb'
+ }, {
+ # A garbage entry we can test exception handling on
+ 'id': 123,
+ }],
+ }),
+ ('twitter://ckey/csecret/access_token/access_secret', {
+ 'instance': plugins.NotifyTwitter,
+ # throw a bizzare code forcing us to fail to look it up
+ 'response': False,
+ 'requests_response_code': 999,
+ }),
+ ('twitter://ckey/csecret/access_token/access_secret', {
+ 'instance': plugins.NotifyTwitter,
+ # Throws a series of connection and transfer exceptions when this flag
+ # is set and tests that we gracfully handle them
+ 'test_requests_exceptions': True,
+ }),
+ ('twitter://ckey/csecret/access_token/access_secret?mode=tweet', {
+ 'instance': plugins.NotifyTwitter,
+ # Throws a series of connection and transfer exceptions when this flag
+ # is set and tests that we gracfully handle them
+ 'test_requests_exceptions': True,
+ }),
##################################
# NotifyNexmo
##################################
diff --git a/test/test_twitter_plugin.py b/test/test_twitter_plugin.py
index 3e5a5695..f69f84fa 100644
--- a/test/test_twitter_plugin.py
+++ b/test/test_twitter_plugin.py
@@ -25,181 +25,17 @@
import six
import mock
-from random import choice
-from string import ascii_uppercase as str_alpha
-from string import digits as str_num
-
+import requests
+from json import dumps
+from datetime import datetime
from apprise import plugins
-from apprise import NotifyType
-from apprise import Apprise
-from apprise import OverflowMode
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
-TEST_URLS = (
- ##################################
- # NotifyTwitter
- ##################################
- ('tweet://', {
- 'instance': None,
- }),
- ('tweet://:@/', {
- 'instance': None,
- }),
- ('tweet://consumer_key', {
- # Missing Keys
- 'instance': TypeError,
- }),
- ('tweet://consumer_key/consumer_key/', {
- # Missing Keys
- 'instance': TypeError,
- }),
- ('tweet://consumer_key/consumer_key/access_token/', {
- # Missing Access Secret
- 'instance': TypeError,
- }),
- ('tweet://consumer_key/consumer_key/access_token/access_secret', {
- # Missing User
- 'instance': TypeError,
- }),
- ('tweet://user@consumer_key/consumer_key/access_token/access_secret', {
- # We're good!
- 'instance': plugins.NotifyTwitter,
- }),
- ('tweet://usera@consumer_key/consumer_key/access_token/'
- 'access_secret/?to=userb', {
- # We're good!
- 'instance': plugins.NotifyTwitter,
- }),
-)
-
-
-@mock.patch('apprise.plugins.tweepy.API')
-@mock.patch('apprise.plugins.tweepy.OAuthHandler')
-def test_plugin(mock_oauth, mock_api):
- """
- API: NotifyTwitter Plugin() (pt1)
-
- """
- # Disable Throttling to speed testing
- plugins.NotifyBase.request_rate_per_sec = 0
-
- # Define how many characters exist per line
- row = 80
-
- # Some variables we use to control the data we work with
- body_len = 1024
- title_len = 1024
-
- # Create a large body and title with random data
- body = ''.join(choice(str_alpha + str_num + ' ') for _ in range(body_len))
- body = '\r\n'.join([body[i: i + row] for i in range(0, len(body), row)])
-
- # Create our title using random data
- title = ''.join(choice(str_alpha + str_num) for _ in range(title_len))
-
- # iterate over our dictionary and test it out
- for (url, meta) in TEST_URLS:
-
- # Our expected instance
- instance = meta.get('instance', None)
-
- # Our expected server objects
- self = meta.get('self', None)
-
- # Our expected Query response (True, False, or exception type)
- response = meta.get('response', True)
-
- # Allow notification type override, otherwise default to INFO
- notify_type = meta.get('notify_type', NotifyType.INFO)
-
- # Allow us to force the server response code to be something other then
- # the defaults
- response = meta.get(
- 'response', True if response else False)
-
- try:
- obj = Apprise.instantiate(url, suppress_exceptions=False)
-
- if obj is None:
- if instance is not None:
- # We're done (assuming this is what we were expecting)
- print("{} didn't instantiate itself "
- "(we expected it to)".format(url))
- assert False
- continue
-
- if instance is None:
- # Expected None but didn't get it
- print('%s instantiated %s (but expected None)' % (
- url, str(obj)))
- assert False
-
- assert isinstance(obj, instance) is True
-
- if isinstance(obj, plugins.NotifyBase):
- # We loaded okay; now lets make sure we can reverse this url
- assert isinstance(obj.url(), six.string_types) is True
-
- # Instantiate the exact same object again using the URL from
- # the one that was already created properly
- obj_cmp = Apprise.instantiate(obj.url())
-
- # Our object should be the same instance as what we had
- # originally expected above.
- if not isinstance(obj_cmp, plugins.NotifyBase):
- # Assert messages are hard to trace back with the way
- # these tests work. Just printing before throwing our
- # assertion failure makes things easier to debug later on
- print('TEST FAIL: {} regenerated as {}'.format(
- url, obj.url()))
- assert False
-
- if self:
- # Iterate over our expected entries inside of our object
- for key, val in self.items():
- # Test that our object has the desired key
- assert hasattr(key, obj) is True
- assert getattr(key, obj) == val
-
- obj.request_rate_per_sec = 0
-
- # check that we're as expected
- assert obj.notify(
- title='test', body='body',
- notify_type=NotifyType.INFO) == response
-
- # check that this doesn't change using different overflow
- # methods
- assert obj.notify(
- body=body, title=title,
- notify_type=notify_type,
- overflow=OverflowMode.UPSTREAM) == response
- assert obj.notify(
- body=body, title=title,
- notify_type=notify_type,
- overflow=OverflowMode.TRUNCATE) == response
- assert obj.notify(
- body=body, title=title,
- notify_type=notify_type,
- overflow=OverflowMode.SPLIT) == response
-
- except AssertionError:
- # Don't mess with these entries
- raise
-
- except Exception as e:
- # Handle our exception
- assert(instance is not None)
- assert(isinstance(e, instance))
-
-
-@mock.patch('apprise.plugins.tweepy.API.send_direct_message')
-@mock.patch('apprise.plugins.tweepy.OAuthHandler.set_access_token')
-def test_twitter_plugin_init(set_access_token, send_direct_message):
+def test_twitter_plugin_init():
"""
API: NotifyTwitter Plugin() (pt2)
@@ -240,15 +76,16 @@ def test_twitter_plugin_init(set_access_token, send_direct_message):
try:
plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret='value')
- assert False
- except TypeError:
- # user not set
assert True
+ except TypeError:
+ # user not set; but this is okay
+ # We should not reach here
+ assert False
try:
- obj = plugins.NotifyTwitter(
+ plugins.NotifyTwitter(
ckey='value', csecret='value', akey='value', asecret='value',
- user='l2g')
+ user='l2gnux')
# We should initialize properly
assert True
@@ -256,19 +93,171 @@ def test_twitter_plugin_init(set_access_token, send_direct_message):
# We should not reach here
assert False
- set_access_token.side_effect = TypeError('Invalid')
- assert obj.notify(
- title='test', body='body',
- notify_type=NotifyType.INFO) is False
+@mock.patch('requests.get')
+@mock.patch('requests.post')
+def test_notify_twitter_plugin_general(mock_post, mock_get):
+ """
+ API: NotifyTwitter() General Tests
- # Make it so we can pass authentication, but fail on message
- # delivery
- set_access_token.side_effect = None
- set_access_token.return_value = True
- send_direct_message.side_effect = plugins.tweepy.error.TweepError(
- 0, 'tweepy.error.TweepyError() not handled'),
+ """
+ ckey = 'ckey'
+ csecret = 'csecret'
+ akey = 'akey'
+ asecret = 'asecret'
+ screen_name = 'apprise'
- assert obj.notify(
- title='test', body='body',
- notify_type=NotifyType.INFO) is False
+ response_obj = [{
+ 'screen_name': screen_name,
+ 'id': 9876,
+ }]
+
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.request_rate_per_sec = 0
+
+ # Epoch time:
+ epoch = datetime.utcfromtimestamp(0)
+
+ request = mock.Mock()
+ request.content = dumps(response_obj)
+ request.status_code = requests.codes.ok
+ request.headers = {
+ 'x-rate-limit-reset': (datetime.utcnow() - epoch).total_seconds(),
+ 'x-rate-limit-remaining': 1,
+ }
+
+ # Prepare Mock
+ mock_get.return_value = request
+ mock_post.return_value = request
+
+ # Variation Initializations
+ obj = plugins.NotifyTwitter(
+ ckey=ckey,
+ csecret=csecret,
+ akey=akey,
+ asecret=asecret,
+ targets=screen_name)
+
+ assert isinstance(obj, plugins.NotifyTwitter) is True
+ assert isinstance(obj.url(), six.string_types) is True
+
+ # apprise room was found
+ assert obj.send(body="test") is True
+
+ # Change our status code and try again
+ request.status_code = 403
+ assert obj.send(body="test") is False
+ assert obj.ratelimit_remaining == 1
+
+ # Return the status
+ request.status_code = requests.codes.ok
+ # Force a reset
+ request.headers['x-rate-limit-remaining'] = 0
+ # behind the scenes, it should cause us to update our rate limit
+ assert obj.send(body="test") is True
+ assert obj.ratelimit_remaining == 0
+
+ # This should cause us to block
+ request.headers['x-rate-limit-remaining'] = 10
+ assert obj.send(body="test") is True
+ assert obj.ratelimit_remaining == 10
+
+ # Handle cases where we simply couldn't get this field
+ del request.headers['x-rate-limit-remaining']
+ assert obj.send(body="test") is True
+ # It remains set to the last value
+ assert obj.ratelimit_remaining == 10
+
+ # Reset our variable back to 1
+ request.headers['x-rate-limit-remaining'] = 1
+
+ # Handle cases where our epoch time is wrong
+ del request.headers['x-rate-limit-reset']
+ assert obj.send(body="test") is True
+
+ # Return our object, but place it in the future forcing us to block
+ request.headers['x-rate-limit-reset'] = \
+ (datetime.utcnow() - epoch).total_seconds() + 1
+ request.headers['x-rate-limit-remaining'] = 0
+ obj.ratelimit_remaining = 0
+ assert obj.send(body="test") is True
+
+ # Return our object, but place it in the future forcing us to block
+ request.headers['x-rate-limit-reset'] = \
+ (datetime.utcnow() - epoch).total_seconds() - 1
+ request.headers['x-rate-limit-remaining'] = 0
+ obj.ratelimit_remaining = 0
+ assert obj.send(body="test") is True
+
+ # Return our limits to always work
+ request.headers['x-rate-limit-reset'] = \
+ (datetime.utcnow() - epoch).total_seconds()
+ request.headers['x-rate-limit-remaining'] = 1
+ obj.ratelimit_remaining = 1
+
+ # Alter pending targets
+ obj.targets.append('usera')
+ request.content = dumps(response_obj)
+ response_obj = [{
+ 'screen_name': 'usera',
+ 'id': 1234,
+ }]
+
+ assert obj.send(body="test") is True
+
+ # Flush our cache forcing it's re-creating
+ del plugins.NotifyTwitter._user_cache
+ assert obj.send(body="test") is True
+
+ # Cause content response to be None
+ request.content = None
+ assert obj.send(body="test") is True
+
+ # Invalid JSON
+ request.content = '{'
+ assert obj.send(body="test") is True
+
+ # Return it to a parseable string
+ request.content = '{}'
+
+ results = plugins.NotifyTwitter.parse_url(
+ 'twitter://{}/{}/{}/{}?to={}'.format(
+ ckey, csecret, akey, asecret, screen_name))
+ assert isinstance(results, dict) is True
+ assert screen_name in results['targets']
+
+ # cause a json parsing issue now
+ response_obj = None
+ assert obj.send(body="test") is True
+
+ response_obj = '{'
+ assert obj.send(body="test") is True
+
+ # Set ourselves up to handle whoami calls
+
+ # Flush out our cache
+ del plugins.NotifyTwitter._user_cache
+
+ response_obj = {
+ 'screen_name': screen_name,
+ 'id': 9876,
+ }
+ request.content = dumps(response_obj)
+
+ obj = plugins.NotifyTwitter(
+ ckey=ckey,
+ csecret=csecret,
+ akey=akey,
+ asecret=asecret)
+
+ assert obj.send(body="test") is True
+
+ # Alter the key forcing us to look up a new value of ourselves again
+ del plugins.NotifyTwitter._user_cache
+ del plugins.NotifyTwitter._whoami_cache
+ obj.ckey = 'different.then.it.was'
+ assert obj.send(body="test") is True
+
+ del plugins.NotifyTwitter._whoami_cache
+ obj.ckey = 'different.again'
+ assert obj.send(body="test") is True