From 2b437dbf358dcb57428927e07e8d527dc6d8a17b Mon Sep 17 00:00:00 2001 From: hepeng Date: Wed, 12 Jul 2017 08:38:59 +0800 Subject: [PATCH 01/83] add api.Replyto for convenience --- twitter/api.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/twitter/api.py b/twitter/api.py index 1385fce2..c67d40e0 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -4693,6 +4693,30 @@ def VerifyCredentials(self, include_entities=None, skip_status=None, include_ema return User.NewFromJsonDict(data) + def RelayTo(self, status, in_reply_to_status_id, **kwargs): + """Relay to a status. Automatically add @username before the status for convenience. + + Args: + status (str): + The message text to be replyed.Must be less than or equal to 140 characters. + in_reply_to_status_id (int): + The ID of an existing status that the status to be posted is in reply to. + **kwargs: + The other args api.PostUpadtes need. + + Returns: + (twitter.Status) A twitter.Status instance representing the message replied. + """ + reply_status = self.GetStatus(in_reply_to_status_id) + u_status = "@%s " % reply_status.user.screen_name + if isinstance(status, str) or self._input_encoding is None: + u_status = u_status + status + else: + u_status = u_status + str(u_status, self._input_encoding) + + return self.PostUpdate(u_status, in_reply_to_status_id=in_reply_to_status_id, **kwargs) + + def SetCache(self, cache): """Override the default cache. Set to None to prevent caching. From 02049603275c57bd32066dc9508ad333bc27fab2 Mon Sep 17 00:00:00 2001 From: hepeng Date: Wed, 12 Jul 2017 20:04:54 +0800 Subject: [PATCH 02/83] Fix spell error and update the docstring of api.ReplyTo --- twitter/api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index c67d40e0..29bce607 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -4693,8 +4693,10 @@ def VerifyCredentials(self, include_entities=None, skip_status=None, include_ema return User.NewFromJsonDict(data) - def RelayTo(self, status, in_reply_to_status_id, **kwargs): - """Relay to a status. Automatically add @username before the status for convenience. + def ReplyTo(self, status, in_reply_to_status_id, **kwargs): + """Relay to a status. Automatically add @username before the status for + convenience.This method calls api.GetStatus to get username. + Args: status (str): From c1fdec198001a806c6dff3101d9014feef282e15 Mon Sep 17 00:00:00 2001 From: Amnisia Date: Tue, 10 Oct 2017 15:42:42 +0500 Subject: [PATCH 03/83] merge in master for uses in my projects --- twitter/api.py | 2 +- twitter/twitter_utils.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index 58917c5c..f0082d34 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -1197,7 +1197,7 @@ def _UploadMediaChunkedInit(self, """ url = '%s/media/upload.json' % self.upload_url - media_fp, filename, file_size, media_type = parse_media_file(media) + media_fp, filename, file_size, media_type = parse_media_file(media, async_upload=True) if not all([media_fp, filename, file_size, media_type]): raise TwitterError({'message': 'Could not process media file'}) diff --git a/twitter/twitter_utils.py b/twitter/twitter_utils.py index 66ce8b24..743cf65e 100644 --- a/twitter/twitter_utils.py +++ b/twitter/twitter_utils.py @@ -190,12 +190,13 @@ def http_to_file(http): return data_file -def parse_media_file(passed_media): +def parse_media_file(passed_media, async_upload=False): """ Parses a media file and attempts to return a file-like object and information about the media file. Args: passed_media: media file which to parse. + async_upload: flag, for validation media file attributes. Returns: file-like object, the filename of the media file, the file size, and @@ -240,8 +241,10 @@ def parse_media_file(passed_media): if media_type is not None: if media_type in img_formats and file_size > 5 * 1048576: raise TwitterError({'message': 'Images must be less than 5MB.'}) - elif media_type in video_formats and file_size > 15 * 1048576: + elif media_type in video_formats and not async_upload and file_size > 15 * 1048576: raise TwitterError({'message': 'Videos must be less than 15MB.'}) + elif media_type in video_formats and async_upload and file_size > 512 * 1048576: + raise TwitterError({'message': 'Videos must be less than 512MB.'}) elif media_type not in img_formats and media_type not in video_formats: raise TwitterError({'message': 'Media type could not be determined.'}) From e31de5ce66a9eca15a6c8046932d4a791ac94c87 Mon Sep 17 00:00:00 2001 From: Timofey Date: Tue, 26 Dec 2017 18:28:50 +0500 Subject: [PATCH 04/83] CHANGE 140 to 280 --- twitter/api.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index f0082d34..7ba58833 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -72,7 +72,7 @@ if sys.version_info > (3,): long = int -CHARACTER_LIMIT = 140 +CHARACTER_LIMIT = 280 # A singleton representing a lazily instantiated FileCache. DEFAULT_CACHE = object() @@ -976,7 +976,7 @@ def PostUpdate(self, Args: status (str): - The message text to be posted. Must be less than or equal to 140 + The message text to be posted. Must be less than or equal to 280 characters. media (int, str, fp, optional): A URL, a local file, or a file-like object (something with a @@ -1029,7 +1029,7 @@ def PostUpdate(self, otherwise the payload will contain the full user data item. verify_status_length (bool, optional): If True, api throws a hard error that the status is over - 140 characters. If False, Api will attempt to post the + 280 characters. If False, Api will attempt to post the status. Returns: (twitter.Status) A twitter.Status instance representing the @@ -1042,8 +1042,8 @@ def PostUpdate(self, else: u_status = str(status, self._input_encoding) - if verify_status_length and calc_expected_status_length(u_status) > 140: - raise TwitterError("Text must be less than or equal to 140 characters.") + if verify_status_length and calc_expected_status_length(u_status) > 280: + raise TwitterError("Text must be less than or equal to 280 characters.") if auto_populate_reply_metadata and not in_reply_to_status_id: raise TwitterError("If auto_populate_reply_metadata is True, you must set in_reply_to_status_id") @@ -1514,7 +1514,7 @@ def PostMultipleMedia(self, status, media, possibly_sensitive=None, def _TweetTextWrap(self, status, - char_lim=140): + char_lim=280): if not self._config: self.GetHelpConfiguration() @@ -1525,7 +1525,7 @@ def _TweetTextWrap(self, words = re.split(r'\s', status) if len(words) == 1 and not is_url(words[0]): - if len(words[0]) > 140: + if len(words[0]) > 280: raise TwitterError({"message": "Unable to split status into tweetable parts. Word was: {0}/{1}".format(len(words[0]), char_lim)}) else: tweets.append(words[0]) @@ -1541,7 +1541,7 @@ def _TweetTextWrap(self, else: new_len += len(word) + 1 - if new_len > 140: + if new_len > 280: tweets.append(' '.join(line)) line = [word] line_length = new_len - line_length @@ -1559,12 +1559,12 @@ def PostUpdates(self, """Post one or more twitter status messages from the authenticated user. Unlike api.PostUpdate, this method will post multiple status updates - if the message is longer than 140 characters. + if the message is longer than 280 characters. Args: status: The message text to be posted. - May be longer than 140 characters. + May be longer than 280 characters. continuation: The character string, if any, to be appended to all but the last message. Note that Twitter strips trailing '...' strings @@ -2978,7 +2978,7 @@ def GetDirectMessages(self, objects. [Optional] full_text: When set to True full message will be included in the returned message - object if message length is bigger than 140 characters. [Optional] + object if message length is bigger than 280 characters. [Optional] page: If you want more than 200 messages, you can use this and get 20 messages each time. You must recall it and increment the page value until it From b5fd51db80f11e984c1b12811a7c762aff9dae64 Mon Sep 17 00:00:00 2001 From: Amnisia Date: Tue, 23 Jan 2018 16:24:07 +0500 Subject: [PATCH 05/83] Revert "CHANGE 140 to 280" This reverts commit e31de5ce66a9eca15a6c8046932d4a791ac94c87. --- twitter/api.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index 7ba58833..f0082d34 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -72,7 +72,7 @@ if sys.version_info > (3,): long = int -CHARACTER_LIMIT = 280 +CHARACTER_LIMIT = 140 # A singleton representing a lazily instantiated FileCache. DEFAULT_CACHE = object() @@ -976,7 +976,7 @@ def PostUpdate(self, Args: status (str): - The message text to be posted. Must be less than or equal to 280 + The message text to be posted. Must be less than or equal to 140 characters. media (int, str, fp, optional): A URL, a local file, or a file-like object (something with a @@ -1029,7 +1029,7 @@ def PostUpdate(self, otherwise the payload will contain the full user data item. verify_status_length (bool, optional): If True, api throws a hard error that the status is over - 280 characters. If False, Api will attempt to post the + 140 characters. If False, Api will attempt to post the status. Returns: (twitter.Status) A twitter.Status instance representing the @@ -1042,8 +1042,8 @@ def PostUpdate(self, else: u_status = str(status, self._input_encoding) - if verify_status_length and calc_expected_status_length(u_status) > 280: - raise TwitterError("Text must be less than or equal to 280 characters.") + if verify_status_length and calc_expected_status_length(u_status) > 140: + raise TwitterError("Text must be less than or equal to 140 characters.") if auto_populate_reply_metadata and not in_reply_to_status_id: raise TwitterError("If auto_populate_reply_metadata is True, you must set in_reply_to_status_id") @@ -1514,7 +1514,7 @@ def PostMultipleMedia(self, status, media, possibly_sensitive=None, def _TweetTextWrap(self, status, - char_lim=280): + char_lim=140): if not self._config: self.GetHelpConfiguration() @@ -1525,7 +1525,7 @@ def _TweetTextWrap(self, words = re.split(r'\s', status) if len(words) == 1 and not is_url(words[0]): - if len(words[0]) > 280: + if len(words[0]) > 140: raise TwitterError({"message": "Unable to split status into tweetable parts. Word was: {0}/{1}".format(len(words[0]), char_lim)}) else: tweets.append(words[0]) @@ -1541,7 +1541,7 @@ def _TweetTextWrap(self, else: new_len += len(word) + 1 - if new_len > 280: + if new_len > 140: tweets.append(' '.join(line)) line = [word] line_length = new_len - line_length @@ -1559,12 +1559,12 @@ def PostUpdates(self, """Post one or more twitter status messages from the authenticated user. Unlike api.PostUpdate, this method will post multiple status updates - if the message is longer than 280 characters. + if the message is longer than 140 characters. Args: status: The message text to be posted. - May be longer than 280 characters. + May be longer than 140 characters. continuation: The character string, if any, to be appended to all but the last message. Note that Twitter strips trailing '...' strings @@ -2978,7 +2978,7 @@ def GetDirectMessages(self, objects. [Optional] full_text: When set to True full message will be included in the returned message - object if message length is bigger than 280 characters. [Optional] + object if message length is bigger than 140 characters. [Optional] page: If you want more than 200 messages, you can use this and get 20 messages each time. You must recall it and increment the page value until it From 2f896ecb28f0e15835f9c46ed8fc877f5b2bcf76 Mon Sep 17 00:00:00 2001 From: Amnisia Date: Thu, 1 Feb 2018 14:00:17 +0500 Subject: [PATCH 06/83] change for GIF sizers --- twitter/twitter_utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/twitter/twitter_utils.py b/twitter/twitter_utils.py index 743cf65e..d05d6b9d 100644 --- a/twitter/twitter_utils.py +++ b/twitter/twitter_utils.py @@ -204,9 +204,11 @@ def parse_media_file(passed_media, async_upload=False): """ img_formats = ['image/jpeg', 'image/png', - 'image/gif', 'image/bmp', 'image/webp'] + long_img_formats = [ + 'image/gif' + ] video_formats = ['video/mp4', 'video/quicktime'] @@ -241,11 +243,13 @@ def parse_media_file(passed_media, async_upload=False): if media_type is not None: if media_type in img_formats and file_size > 5 * 1048576: raise TwitterError({'message': 'Images must be less than 5MB.'}) + elif media_type in long_img_formats and file_size > 15 * 1048576: + raise TwitterError({'message': 'GIF Image must be less than 15MB.'}) elif media_type in video_formats and not async_upload and file_size > 15 * 1048576: raise TwitterError({'message': 'Videos must be less than 15MB.'}) elif media_type in video_formats and async_upload and file_size > 512 * 1048576: raise TwitterError({'message': 'Videos must be less than 512MB.'}) - elif media_type not in img_formats and media_type not in video_formats: + elif media_type not in img_formats and media_type not in video_formats and media_type not in long_img_formats: raise TwitterError({'message': 'Media type could not be determined.'}) return data_file, filename, file_size, media_type From 438be010724e3e1fa47f1533092a9d03bb55f288 Mon Sep 17 00:00:00 2001 From: Amnisia Date: Mon, 5 Feb 2018 15:52:23 +0500 Subject: [PATCH 07/83] mini fix media_category to postUpdate and simple upload file --- twitter/api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index 71d719b7..2bfde8dc 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -1098,9 +1098,13 @@ def PostUpdate(self, else: _, _, file_size, media_type = parse_media_file(media) if file_size > self.chunk_size or media_type in chunked_types: - media_ids.append(self.UploadMediaChunked(media, media_additional_owners)) + media_ids.append(self.UploadMediaChunked( + media, media_additional_owners, media_category=media_category + )) else: - media_ids.append(self.UploadMediaSimple(media, media_additional_owners)) + media_ids.append(self.UploadMediaSimple( + media, media_additional_owners, media_category=media_category + )) parameters['media_ids'] = ','.join([str(mid) for mid in media_ids]) if latitude is not None and longitude is not None: From 177a67e48110c6507e422aabe68ab3f7f06fb80a Mon Sep 17 00:00:00 2001 From: Rikhav Mamania Date: Mon, 9 Apr 2018 19:36:12 +0530 Subject: [PATCH 08/83] api - ssl certfication verification Added verify_ssl and cert_ssl parameters to the twitter.Api - these mirror the verify and cert parameters from requests.request --- twitter/api.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index f0541b85..bb4091df 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -162,7 +162,9 @@ def __init__(self, timeout=None, sleep_on_rate_limit=False, tweet_mode='compat', - proxies=None): + proxies=None, + verify_ssl=None, + cert_ssl=None): """Instantiate a new twitter.Api object. Args: @@ -216,6 +218,13 @@ def __init__(self, proxies (dict, optional): A dictionary of proxies for the request to pass through, if not specified allows requests lib to use environmental variables for proxy if any. + verify_ssl (optional): + Either a boolean, in which case it controls whether we verify + the server's TLS certificate, or a string, in which case it must be a path + to a CA bundle to use. Defaults to ``True``. + cert_ssl (optional): + If String, path to ssl client cert file (.pem). + If Tuple, ('cert', 'key') pair. """ # check to see if the library is running on a Google App Engine instance @@ -247,6 +256,8 @@ def __init__(self, self.sleep_on_rate_limit = sleep_on_rate_limit self.tweet_mode = tweet_mode self.proxies = proxies + self.verify_ssl = verify_ssl + self.cert_ssl = cert_ssl if base_url is None: self.base_url = 'https://api.twitter.com/1.1' @@ -4913,7 +4924,9 @@ def _RequestChunkedUpload(self, url, headers, data): data=data, auth=self.__auth, timeout=self._timeout, - proxies=self.proxies + proxies=self.proxies, + verify=self.verify_ssl, + cert=self.cert_ssl ) except requests.RequestException as e: raise TwitterError(str(e)) @@ -4954,20 +4967,20 @@ def _RequestUrl(self, url, verb, data=None, json=None, enforce_auth=True): if data: if 'media_ids' in data: url = self._BuildUrl(url, extra_params={'media_ids': data['media_ids']}) - resp = requests.post(url, data=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) + resp = requests.post(url, data=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies, verify=self.verify_ssl, cert=self.cert_ssl) elif 'media' in data: - resp = requests.post(url, files=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) + resp = requests.post(url, files=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies, verify=self.verify_ssl, cert=self.cert_ssl) else: - resp = requests.post(url, data=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) + resp = requests.post(url, data=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies, verify=self.verify_ssl, cert=self.cert_ssl) elif json: - resp = requests.post(url, json=json, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) + resp = requests.post(url, json=json, auth=self.__auth, timeout=self._timeout, proxies=self.proxies, verify=self.verify_ssl, cert=self.cert_ssl) else: resp = 0 # POST request, but without data or json elif verb == 'GET': data['tweet_mode'] = self.tweet_mode url = self._BuildUrl(url, extra_params=data) - resp = requests.get(url, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) + resp = requests.get(url, auth=self.__auth, timeout=self._timeout, proxies=self.proxies, verify=self.verify_ssl, cert=self.cert_ssl) else: resp = 0 # if not a POST or GET request @@ -5002,14 +5015,17 @@ def _RequestStream(self, url, verb, data=None, session=None): return session.post(url, data=data, stream=True, auth=self.__auth, timeout=self._timeout, - proxies=self.proxies) + proxies=self.proxies, + verify=self.verify_ssl, + cert=self.cert_ssl) except requests.RequestException as e: raise TwitterError(str(e)) if verb == 'GET': url = self._BuildUrl(url, extra_params=data) try: return session.get(url, stream=True, auth=self.__auth, - timeout=self._timeout, proxies=self.proxies) + timeout=self._timeout, proxies=self.proxies, + verify=self.verify_ssl, cert=self.cert_ssl) except requests.RequestException as e: raise TwitterError(str(e)) return 0 # if not a POST or GET request From 88fff545da6064f974a9b4b663ea28d0ba32fda1 Mon Sep 17 00:00:00 2001 From: Rikhav Mamania Date: Mon, 9 Apr 2018 19:39:52 +0530 Subject: [PATCH 09/83] api - ssl certificate verification included parameters in twitter.Api to mirror requests.request verify and cert parameters --- twitter/api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index bb4091df..861cda6b 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -163,8 +163,8 @@ def __init__(self, sleep_on_rate_limit=False, tweet_mode='compat', proxies=None, - verify_ssl=None, - cert_ssl=None): + verify_ssl=None, + cert_ssl=None): """Instantiate a new twitter.Api object. Args: @@ -218,8 +218,8 @@ def __init__(self, proxies (dict, optional): A dictionary of proxies for the request to pass through, if not specified allows requests lib to use environmental variables for proxy if any. - verify_ssl (optional): - Either a boolean, in which case it controls whether we verify + verify_ssl (optional): + Either a boolean, in which case it controls whether we verify the server's TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to ``True``. cert_ssl (optional): @@ -4926,7 +4926,7 @@ def _RequestChunkedUpload(self, url, headers, data): timeout=self._timeout, proxies=self.proxies, verify=self.verify_ssl, - cert=self.cert_ssl + cert=self.cert_ssl ) except requests.RequestException as e: raise TwitterError(str(e)) From eed985f1cb5cd3dadfbd42732bb02c319a4f4653 Mon Sep 17 00:00:00 2001 From: Thorsten Schwander Date: Tue, 8 May 2018 18:21:11 -0600 Subject: [PATCH 10/83] fix typo in dict passed to TwitterError Signed-off-by: Thorsten Schwander --- twitter/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index f0541b85..dfabb046 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -2,7 +2,7 @@ # # -# Copyright 2007-2016 The Python-Twitter Developers +# Copyright 2007-2016, 2018 The Python-Twitter Developers # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -4882,7 +4882,7 @@ def _ParseAndCheckTwitter(self, json_data): raise TwitterError({'message': "Exceeded connection limit for user"}) if "Error 401 Unauthorized" in json_data: raise TwitterError({'message': "Unauthorized"}) - raise TwitterError({'Unknown error: {0}'.format(json_data)}) + raise TwitterError({'Unknown error': '{0}'.format(json_data)}) self._CheckForTwitterError(data) return data From 6163a2115fc2a0c266a8167df57fad960a566514 Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Fri, 18 May 2018 15:33:46 -0400 Subject: [PATCH 11/83] bump supported versions: 2.7.15 3.6.5 pypy-5.7.1 pypy3.5-6.0.0 --- Makefile | 12 ++++++------ setup.cfg | 2 +- twitter/parse_tweet.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index d9a47f53..6b9ce69d 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ +SUPPORTED_VERSIONS = 2.7.15 3.6.5 pypy-5.7.1 pypy3.5-6.0.0 help: @echo " env install all production dependencies" @@ -12,13 +13,12 @@ env: pip install -Ur requirements.txt pyenv: - pyenv install -s 2.7.11 - pyenv install -s 3.6.1 - pyenv install -s pypy-5.3.1 - # pyenv install -s pypy3-2.4.0 - pyenv local 2.7.11 3.6.1 pypy-5.3.1 # pypy3-2.4.0 + for version in $(SUPPORTED_VERSIONS) ; do \ + pyenv install -s $$version; \ + done + pyenv local $(SUPPORTED_VERSIONS) -dev: env pyenv +dev: pyenv env pip install -Ur requirements.testing.txt info: diff --git a/setup.cfg b/setup.cfg index 1949d5c6..ffcde717 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,6 +13,6 @@ ignore = [flake8] ignore = E111,E124,E126,E221,E501 -[pep8] +[pycodestyle] ignore = E111,E124,E126,E221,E501 max-line-length = 100 diff --git a/twitter/parse_tweet.py b/twitter/parse_tweet.py index c662016e..70e1c7ef 100644 --- a/twitter/parse_tweet.py +++ b/twitter/parse_tweet.py @@ -96,5 +96,5 @@ def getHashtags(tweet): @staticmethod def getURLs(tweet): - """ URL : [http://]?[\w\.?/]+""" + r""" URL : [http://]?[\w\.?/]+""" return re.findall(ParseTweet.regexp["URL"], tweet) From 5cf614358351f1a7178dd308658310dece6616f4 Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Fri, 18 May 2018 15:45:24 -0400 Subject: [PATCH 12/83] update makefile to update circleci to latest definitions --- Makefile | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 6b9ce69d..b91397cd 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ env: pip install -Ur requirements.txt pyenv: + pyenv update for version in $(SUPPORTED_VERSIONS) ; do \ pyenv install -s $$version; \ done @@ -50,7 +51,10 @@ coverage: clean coverage html coverage report -ci: pyenv tox +update-pyenv: + cd /opt/circleci/.pyenv/plugins/python-build/../.. && git pull && cd - + +ci: update-pyenv pyenv tox CODECOV_TOKEN=`cat .codecov-token` codecov build: clean @@ -59,9 +63,9 @@ build: clean python setup.py bdist_wheel upload: clean - pyenv 2.7.11 + pyenv 2.7.15 python setup.py sdist upload python setup.py bdist_wheel upload - pyenv 3.6.1 + pyenv 3.6.5 python setup.py bdist_wheel upload - pyenv local 2.7.11 3.6.1 pypy-5.3.1 + pyenv local $(SUPPORTED_VERSIONS) From 0e03ada846b63adf54313687cc5cffabaaff4fb8 Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Fri, 18 May 2018 15:48:33 -0400 Subject: [PATCH 13/83] fix pyenv error in CI --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index b91397cd..1fcdaec5 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,6 @@ env: pip install -Ur requirements.txt pyenv: - pyenv update for version in $(SUPPORTED_VERSIONS) ; do \ pyenv install -s $$version; \ done From 9863990cc7412bb9908c6a00b222393f6b2eec5f Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Fri, 18 May 2018 15:52:48 -0400 Subject: [PATCH 14/83] update CI dependency integration --- Makefile | 2 +- circle.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 1fcdaec5..40b8677b 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ coverage: clean update-pyenv: cd /opt/circleci/.pyenv/plugins/python-build/../.. && git pull && cd - -ci: update-pyenv pyenv tox +ci: update-pyenv pyenv dev tox CODECOV_TOKEN=`cat .codecov-token` codecov build: clean diff --git a/circle.yml b/circle.yml index 3c318ad5..e45f625b 100644 --- a/circle.yml +++ b/circle.yml @@ -1,7 +1,6 @@ dependencies: override: - pip install -U pip - - make dev test: pre: From b10baa882635f2c86d444eaa9a7528de47f55c74 Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Thu, 7 Jun 2018 13:29:39 -0400 Subject: [PATCH 15/83] bump version --- doc/changelog.rst | 7 +++++++ doc/conf.py | 4 ++-- twitter/__init__.py | 9 ++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index e3aec468..18050c6e 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,13 @@ Changelog --------- +Version 3.4.2 +============= + +Bugfixes: + +* Allow upload of GIFs with size up to 15mb. See `#538 `_ + Version 3.4.1 ============= diff --git a/doc/conf.py b/doc/conf.py index 41a6cae8..2121447d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -57,9 +57,9 @@ # built documents. # # The short X.Y version. -version = '3.4.1' +version = '3.4' # The full version, including alpha/beta/rc tags. -release = '3.4.1' +release = '3.4.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/twitter/__init__.py b/twitter/__init__.py index 24cec746..c6e433f8 100644 --- a/twitter/__init__.py +++ b/twitter/__init__.py @@ -1,8 +1,7 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # -# vim: sw=2 ts=2 sts=2 -# -# Copyright 2007 The Python-Twitter Developers +# Copyright 2007-2018 The Python-Twitter Developers # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,14 +15,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""A library that provides a Python interface to the Twitter API""" +"""A library that provides a Python interface to the Twitter API.""" from __future__ import absolute_import __author__ = 'The Python-Twitter Developers' __email__ = 'python-twitter@googlegroups.com' __copyright__ = 'Copyright (c) 2007-2016 The Python-Twitter Developers' __license__ = 'Apache License 2.0' -__version__ = '3.4.1' +__version__ = '3.4.2' __url__ = 'https://github.com/bear/python-twitter' __download_url__ = 'https://pypi.python.org/pypi/python-twitter' __description__ = 'A Python wrapper around the Twitter API' From e17af0e67b7270ae448908ad44235d03562509eb Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Sat, 9 Jun 2018 07:51:34 -0400 Subject: [PATCH 16/83] add ensure_ascii param to AsJsonString method for models close issue #527 --- twitter/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twitter/models.py b/twitter/models.py index a79515df..82dc3ada 100644 --- a/twitter/models.py +++ b/twitter/models.py @@ -35,10 +35,10 @@ def __hash__(self): raise TypeError('unhashable type: {} (no id attribute)' .format(type(self))) - def AsJsonString(self): + def AsJsonString(self, ensure_ascii=True): """ Returns the TwitterModel as a JSON string based on key/value pairs returned from the AsDict() method. """ - return json.dumps(self.AsDict(), sort_keys=True) + return json.dumps(self.AsDict(), ensure_ascii=ensure_ascii, sort_keys=True) def AsDict(self): """ Create a dictionary representation of the object. Please see inline From b2af803a8354cadcc646fe6507fb744d828ff693 Mon Sep 17 00:00:00 2001 From: Manuel Lamelas Date: Tue, 19 Jun 2018 16:26:03 +0200 Subject: [PATCH 17/83] Deleted uncomfortable print This line print the parameters when you call UsersLookup. It shouldn't be there --- twitter/api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index b287fb03..94bbbbc1 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -2823,8 +2823,6 @@ def UsersLookup(self, if len(uids) > 100: raise TwitterError("No more than 100 users may be requested per request.") - print(parameters) - resp = self._RequestUrl(url, 'GET', data=parameters) data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) From 2e6c6a034dbb58140a3b219b12c39909e50aa038 Mon Sep 17 00:00:00 2001 From: Louis Sautier Date: Tue, 19 Jun 2018 21:48:57 +0200 Subject: [PATCH 18/83] Include doc, examples and tests in source distributions --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index a04a897d..ca78efe0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,3 +5,4 @@ include NOTICE include *.rst include requirements.txt prune .DS_Store +graft doc examples testdata tests From f4d9161adb44b1fab7a6d88de00b4ddcf196fd55 Mon Sep 17 00:00:00 2001 From: Louis Sautier Date: Tue, 19 Jun 2018 22:18:15 +0200 Subject: [PATCH 19/83] Remove unnecessary pytest-runner from install_requires --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 99635fee..dece197e 100755 --- a/setup.py +++ b/setup.py @@ -57,7 +57,6 @@ def extract_metaitem(meta): packages=find_packages(exclude=('tests', 'docs')), platforms=['Any'], install_requires=['future', 'requests', 'requests-oauthlib'], - setup_requires=['pytest-runner'], tests_require=['pytest'], keywords='twitter api', classifiers=[ From 15d703f3ce8a987399cdbe781dcc47c244605b19 Mon Sep 17 00:00:00 2001 From: tuftedocelot Date: Wed, 4 Jul 2018 17:05:43 -0500 Subject: [PATCH 20/83] Add support for the Geo API and Place models --- tests/test_place.py | 141 ++++++++++++++++++++++++++++++++++++++++++++ twitter/__init__.py | 1 + twitter/api.py | 7 +++ twitter/models.py | 32 ++++++++++ 4 files changed, 181 insertions(+) create mode 100644 tests/test_place.py diff --git a/tests/test_place.py b/tests/test_place.py new file mode 100644 index 00000000..fb343517 --- /dev/null +++ b/tests/test_place.py @@ -0,0 +1,141 @@ +import twitter +import json +import sys +import unittest + + +class PlaceTest(unittest.TestCase): + SAMPLE_JSON = '''{"id": "df51dec6f4ee2b2c", "url": "https://api.twitter.com/1.1/geo/id/df51dec6f4ee2b2c.json", "place_type": "neighborhood", "name": "Presidio", "full_name": "Presidio, San Francisco", "country_code": "US", "country": "United States", "contained_within": [{"id": "5a110d312052166f", "url": "https://api.twitter.com/1.1/geo/id/5a110d312052166f.json", "place_type": "city", "name": "San Francisco", "full_name": "San Francisco, CA", "country_code": "US", "country": "United States", "centroid": [-122.4461400159226, 37.759828999999996], "bounding_box": {"type": "Polygon", "coordinates": [[[-122.514926, 37.708075], [-122.514926, 37.833238], [-122.357031, 37.833238], [-122.357031, 37.708075], [-122.514926, 37.708075]]]}, "attributes": {}}], "geometry": null, "polylines": [], "centroid": [-122.46598425785236, 37.79989625], "bounding_box": {"type": "Polygon", "coordinates": [[[-122.4891333, 37.786925], [-122.4891333, 37.8128675], [-122.446306, 37.8128675], [-122.446306, 37.786925], [-122.4891333, 37.786925]]]}, "attributes": {"geotagCount": "6", "162834:id": "2202"}}''' + + def _GetSampleContainedPlace(self): + return twitter.Place(id='5a110d312052166f', + url='https://api.twitter.com/1.1/geo/id/5a110d312052166f.json', + place_type='city', + name='San Franciso', + full_name='San Francisco, CA', + country_code='US', + country='United States', + centroid=[-122.4461400159226, + 37.759828999999996], + bounding_box=dict( + type='Polygon', + coordinates=[ + [ + [-122.514926, 37.708075], + [-122.514926, 37.833238], + [-122.357031, 37.833238], + [-122.357031, 37.708075], + [-122.514926, 37.708075] + ] + ], + attributes=dict() + ) + ) + + def _GetSamplePlace(self): + return twitter.Place(id='df51dec6f4ee2b2c', + url='https://api.twitter.com/1.1/geo/id/df51dec6f4ee2b2c.json', + place_type='neighborhood', + name='Presidio', + full_name='Presidio, San Francisco', + country_code='US', + country='United States', + contained_within=[ + self._GetSampleContainedPlace() + ], + geometry='null', + polylines=[], + centroid=[-122.46598425785236, 37.79989625], + bounding_box=dict( + type='Polygon', + coordinates=[ + [ + [-122.4891333, 37.786925], + [-122.4891333, 37.8128675], + [-122.446306, 37.8128675], + [-122.446306, 37.786925], + [-122.4891333, 37.786925] + ] + ] + ), + attributes={ + 'geotagCount': '6', + '162834:id': '2202' + } + ) + + def testProperties(self): + '''Test all of the twitter.Place properties''' + place = twitter.Place() + place.id = 'df51dec6f4ee2b2c' + self.assertEqual('df51dec6f4ee2b2c', place.id) + place.name = 'Presidio' + self.assertEqual('Presidio', place.name) + place.full_name = 'Presidio, San Francisco' + self.assertEqual('Presidio, San Francisco', place.full_name) + place.country_code = 'US' + self.assertEqual('US', place.country_code) + place.country = 'United States' + self.assertEqual('United States', place.country) + place.url = 'https://api.twitter.com/1.1/geo/id/df51dec6f4ee2b2c.json' + self.assertEqual('https://api.twitter.com/1.1/geo/id/df51dec6f4ee2b2c.json', place.url) + place.contained_within = [self._GetSampleContainedPlace()] + self.assertEqual('5a110d312052166f', place.contained_within[0].id) + + @unittest.skipIf(sys.version_info.major >= 3, "skipped until fix found for v3 python") + def testAsJsonString(self): + '''Test the twitter.Place AsJsonString method''' + self.assertEqual(PlaceTest.SAMPLE_JSON, + self._GetSamplePlace().AsJsonString()) + + def testAsDict(self): + '''Test the twitter.Place AsDict method''' + place = self._GetSamplePlace() + data = place.AsDict() + self.assertEqual('df51dec6f4ee2b2c', data['id']) + self.assertEqual('Presidio', data['name']) + self.assertEqual('Presidio, San Francisco', data['full_name']) + self.assertEqual('US', data['country_code']) + self.assertEqual('United States', data['country']) + self.assertEqual('https://api.twitter.com/1.1/geo/id/df51dec6f4ee2b2c.json', data['url']) + + def testEq(self): + '''Test the twitter.Place __eq__ method''' + place = twitter.Place() + place.id = 'df51dec6f4ee2b2c' + place.name = 'Presidio' + place.full_name = 'Presidio, San Francisco' + place.country_code = 'US' + place.country = 'United States' + place.url = 'https://api.twitter.com/1.1/geo/id/df51dec6f4ee2b2c.json' + place.place_type = 'neighborhood' + place.centroid = [-122.4461400159226, 37.759828999999996] + place.geometry = 'null' + place.polylines = [] + place.bounding_box = dict(type='Polygon', + coordinates=[ + [ + [-122.4891333, 37.786925], + [-122.4891333, 37.8128675], + [-122.446306, 37.8128675], + [-122.446306, 37.786925], + [-122.4891333, 37.786925] + ] + ] + ) + place.attributes = { + 'geotagCount': '6', + '162834:id': '2202' + } + self.assertEqual(place, self._GetSamplePlace()) + + def testHash(self): + '''Test the twitter.Place __hash__ method''' + place = self._GetSamplePlace() + self.assertEqual(hash(place), hash(place.id)) + + def testNewFromJsonDict(self): + '''Test the twitter.Status NewFromJsonDict method''' + data = json.loads(PlaceTest.SAMPLE_JSON) + place = twitter.Place.NewFromJsonDict(data) + self.assertEqual(self._GetSamplePlace(), place) diff --git a/twitter/__init__.py b/twitter/__init__.py index c6e433f8..96b058bd 100644 --- a/twitter/__init__.py +++ b/twitter/__init__.py @@ -45,6 +45,7 @@ Hashtag, # noqa List, # noqa Media, # noqa + Place, # noga Trend, # noqa Url, # noqa User, # noqa diff --git a/twitter/api.py b/twitter/api.py index 94bbbbc1..33290ff8 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -49,6 +49,7 @@ Category, DirectMessage, List, + Place, Status, Trend, User, @@ -4648,6 +4649,12 @@ def GetUserStream(self, elif include_keepalive: yield None + def GetPlace(self, id): + url = '{}/geo/id/{}.json'.format(self.base_url, id) + resp = self._RequestUrl(url, 'GET') + data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) + return Place.NewFromJsonDict(data) + def VerifyCredentials(self, include_entities=None, skip_status=None, include_email=None): """Returns a twitter.User instance if the authenticating user is valid. diff --git a/twitter/models.py b/twitter/models.py index 82dc3ada..1dfda872 100644 --- a/twitter/models.py +++ b/twitter/models.py @@ -537,3 +537,35 @@ def NewFromJsonDict(cls, data, **kwargs): urls=urls, user=user, user_mentions=user_mentions) + + +class Place(TwitterModel): + """A class representing the Place structure used by the twitter API.""" + + def __init__(self, **kwargs): + self.param_defaults = { + 'id': None, + 'url': None, + 'place_type': None, + 'name': None, + 'full_name': None, + 'country_code': None, + 'country': None, + 'bounding_box': None, + 'attributes': None + } + + for (param, default) in self.param_defaults.items(): + setattr(self, param, kwargs.get(param, default)) + + def __repr__(self): + """ A string representation of this twitter.Place instance. + The return value is the ID of status, username and datetime. + + Returns: + string: A string representation of this twitter.Placeinstance with + the ID of status, name, and country. + """ + return "Place(ID={0}, Name={1}, Country={2})".format(self.id, + self.name, + self.country) From ccc4cb47b19e3176351e3085ff8098beed56c756 Mon Sep 17 00:00:00 2001 From: tuftedocelot Date: Thu, 5 Jul 2018 15:30:54 -0500 Subject: [PATCH 21/83] Link Place model to Status --- tests/test_status.py | 25 +++++++++++++++++++++++++ twitter/models.py | 6 +++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/tests/test_status.py b/tests/test_status.py index 475ac154..4ddfe122 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -26,6 +26,29 @@ def _GetSampleStatus(self): text=u'A légpárnás hajóm tele van angolnákkal.', user=self._GetSampleUser()) + def _GetSamplePlace(self): + return twitter.Place( + id='07d9db48bc083000', + url='https://api.twitter.com/1.1/geo/id/07d9db48bc083000.json', + place_type='poi', + name='McIntosh Lake', + full_name='McIntosh Lake', + country_code='US', + country='United States', + bounding_box=dict( + type='Polygon', + coordinates=[ + [ + [-105.14544, 40.192138], + [-105.14544, 40.192138], + [-105.14544, 40.192138], + [-105.14544, 40.192138] + ] + ] + ), + attributes={} + ) + def testInit(self): '''Test the twitter.Status constructor''' twitter.Status(created_at='Fri Jan 26 23:17:14 +0000 2007', @@ -43,7 +66,9 @@ def testProperties(self): self.assertEqual('Fri Jan 26 23:17:14 +0000 2007', status.created_at) self.assertEqual(created_at, status.created_at_in_seconds) status.user = self._GetSampleUser() + status.place = self._GetSamplePlace() self.assertEqual(718443, status.user.id) + self.assertEqual('07d9db48bc083000', status.place.id) @unittest.skipIf(sys.version_info.major >= 3, "skipped until fix found for v3 python") def testAsJsonString(self): diff --git a/twitter/models.py b/twitter/models.py index 1dfda872..88d7b01c 100644 --- a/twitter/models.py +++ b/twitter/models.py @@ -498,6 +498,7 @@ def NewFromJsonDict(cls, data, **kwargs): urls = None user = None user_mentions = None + place = None # for loading extended tweets from the streaming API. if 'extended_tweet' in data: @@ -512,6 +513,8 @@ def NewFromJsonDict(cls, data, **kwargs): current_user_retweet = data['current_user_retweet']['id'] if 'quoted_status' in data: quoted_status = Status.NewFromJsonDict(data.get('quoted_status')) + if 'place' in data and data['place'] is not None: + place = Place.NewFromJsonDict(data['place']) if 'entities' in data: if 'urls' in data['entities']: @@ -536,7 +539,8 @@ def NewFromJsonDict(cls, data, **kwargs): retweeted_status=retweeted_status, urls=urls, user=user, - user_mentions=user_mentions) + user_mentions=user_mentions, + place=place) class Place(TwitterModel): From ba6d269feea77689a2890e79a873eaa7176c8e59 Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Fri, 6 Jul 2018 11:16:48 -0400 Subject: [PATCH 22/83] update get_access_token to work with py2/py3 --- get_access_token.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/get_access_token.py b/get_access_token.py index d3010fe6..f6ec0973 100755 --- a/get_access_token.py +++ b/get_access_token.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # # Copyright 2007-2013 The Python-Twitter Developers # @@ -18,6 +19,11 @@ from requests_oauthlib import OAuth1Session import webbrowser +import sys + +if sys.version_info.major < 3: + input = raw_input + REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' @@ -29,10 +35,7 @@ def get_access_token(consumer_key, consumer_secret): print('\nRequesting temp token from Twitter...\n') - try: - resp = oauth_client.fetch_request_token(REQUEST_TOKEN_URL) - except ValueError as e: - raise 'Invalid response from Twitter requesting temp token: {0}'.format(e) + resp = oauth_client.fetch_request_token(REQUEST_TOKEN_URL) url = oauth_client.authorization_url(AUTHORIZATION_URL) From 96ccf1f7301f2ad95cbc632a95588ef81a156cd4 Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Mon, 9 Jul 2018 19:08:58 -0400 Subject: [PATCH 23/83] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a735cfab..1a6246a7 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,7 @@ Introduction This library provides a pure Python interface for the `Twitter API `_. It works with Python versions from 2.7+ and Python 3. -`Twitter `_ provides a service that allows people to connect via the web, IM, and SMS. Twitter exposes a `web services API `_ and this library is intended to make it even easier for Python programmers to use. +`Twitter `_ provides a service that allows people to connect via the web, IM, and SMS. Twitter exposes a `web services API `_ and this library is intended to make it even easier for Python programmers to use. ========== Installing From 64525804ea43044e876d767e8d2913c6df4c238a Mon Sep 17 00:00:00 2001 From: Eugene Tan Date: Thu, 16 Aug 2018 00:38:56 +0800 Subject: [PATCH 24/83] migrate api.PostDirectMessage to new twitter api --- .gitignore | 1 + testdata/post_post_direct_message.json | 23 ++++++++++++- tests/test_api_30.py | 10 ++---- tests/test_models.py | 22 ------------ twitter/api.py | 46 ++++++++++++++++---------- twitter/models.py | 10 +----- 6 files changed, 55 insertions(+), 57 deletions(-) mode change 100644 => 100755 twitter/api.py diff --git a/.gitignore b/.gitignore index cba5cd9e..7ef3b53c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ htmlcov nosetests.xml htmlcov coverage.xml +.hypothesis # PyCharm data .idea diff --git a/testdata/post_post_direct_message.json b/testdata/post_post_direct_message.json index 9735f6f9..6f32e2c1 100644 --- a/testdata/post_post_direct_message.json +++ b/testdata/post_post_direct_message.json @@ -1 +1,22 @@ -{"id":761517675243679747,"id_str":"761517675243679747","text":"test message","sender":{"id":4012966701,"id_str":"4012966701","name":"notinourselves","screen_name":"notinourselves","location":"","description":"","url":null,"entities":{"description":{"urls":[]}},"protected":true,"followers_count":1,"friends_count":2,"listed_count":1,"created_at":"Wed Oct 21 23:53:04 +0000 2015","favourites_count":1,"utc_offset":null,"time_zone":null,"geo_enabled":true,"verified":false,"statuses_count":83,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"000000","profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/736320724164448256\/LgaAQoav.jpg","profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/736320724164448256\/LgaAQoav.jpg","profile_background_tile":false,"profile_image_url":"http:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_2_normal.png","profile_image_url_https":"https:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_2_normal.png","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/4012966701\/1453123196","profile_link_color":"000000","profile_sidebar_border_color":"000000","profile_sidebar_fill_color":"000000","profile_text_color":"000000","profile_use_background_image":true,"has_extended_profile":false,"default_profile":false,"default_profile_image":true,"following":false,"follow_request_sent":false,"notifications":false},"sender_id":4012966701,"sender_id_str":"4012966701","sender_screen_name":"notinourselves","recipient":{"id":372018022,"id_str":"372018022","name":"jeremy","screen_name":"__jcbl__","location":"not a very good kingdom tbh","description":"my kingdom for a microwave that doesn't beep","url":"http:\/\/t.co\/wtg3XzqQTX","entities":{"url":{"urls":[{"url":"http:\/\/t.co\/wtg3XzqQTX","expanded_url":"http:\/\/iseverythingstilltheworst.com","display_url":"iseverythingstilltheworst.com","indices":[0,22]}]},"description":{"urls":[]}},"protected":false,"followers_count":79,"friends_count":423,"listed_count":8,"created_at":"Sun Sep 11 23:49:28 +0000 2011","favourites_count":2696,"utc_offset":-14400,"time_zone":"Eastern Time (US & Canada)","geo_enabled":false,"verified":false,"statuses_count":587,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"FFFFFF","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/755782670572027904\/L5YRsZAY_normal.jpg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/755782670572027904\/L5YRsZAY_normal.jpg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/372018022\/1469027675","profile_link_color":"EE3355","profile_sidebar_border_color":"000000","profile_sidebar_fill_color":"000000","profile_text_color":"000000","profile_use_background_image":false,"has_extended_profile":false,"default_profile":false,"default_profile_image":false,"following":true,"follow_request_sent":false,"notifications":false},"recipient_id":372018022,"recipient_id_str":"372018022","recipient_screen_name":"__jcbl__","created_at":"Fri Aug 05 11:02:16 +0000 2016","entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]}} \ No newline at end of file +{ + "event": { + "created_timestamp": "1534347829024", + "message_create": { + "message_data": { + "text": "test message", + "entities": { + "symbols": [], + "user_mentions": [], + "hashtags": [], + "urls": [] + } + }, + "sender_id": "4012966701", + "target": { + "recipient_id": "372018022" + } + }, + "type": "message_create", + "id": "761517675243679747" + } +} diff --git a/tests/test_api_30.py b/tests/test_api_30.py index c12b0662..7dfd7126 100644 --- a/tests/test_api_30.py +++ b/tests/test_api_30.py @@ -1643,16 +1643,10 @@ def testPostDirectMessage(self): status=200) resp = self.api.PostDirectMessage(text="test message", user_id=372018022) self.assertEqual(resp.text, "test message") - - resp = self.api.PostDirectMessage(text="test message", screen_name="__jcbl__") - self.assertEqual(resp.sender_id, 4012966701) - self.assertEqual(resp.recipient_id, 372018022) + self.assertEqual(resp.sender_id, "4012966701") + self.assertEqual(resp.recipient_id, "372018022") self.assertTrue(resp._json) - self.assertRaises( - twitter.TwitterError, - lambda: self.api.PostDirectMessage(text="test message")) - @responses.activate def testDestroyDirectMessage(self): with open('testdata/post_destroy_direct_message.json') as f: diff --git a/tests/test_models.py b/tests/test_models.py index fe7b6716..ed56fb47 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -58,28 +58,6 @@ def test_direct_message(self): self.assertTrue(dm.AsJsonString()) self.assertTrue(dm.AsDict()) - def test_direct_message_sender_is_user_model(self): - """Test that each Direct Message object contains a fully hydrated - twitter.models.User object for both ``dm.sender`` & ``dm.recipient``.""" - dm = twitter.DirectMessage.NewFromJsonDict(self.DIRECT_MESSAGE_SAMPLE_JSON) - - self.assertTrue(isinstance(dm.sender, twitter.models.User)) - self.assertEqual(dm.sender.id, 372018022) - - # Let's make sure this doesn't break the construction of the DM object. - self.assertEqual(dm.id, 678629245946433539) - - def test_direct_message_recipient_is_user_model(self): - """Test that each Direct Message object contains a fully hydrated - twitter.models.User object for both ``dm.sender`` & ``dm.recipient``.""" - dm = twitter.DirectMessage.NewFromJsonDict(self.DIRECT_MESSAGE_SAMPLE_JSON) - - self.assertTrue(isinstance(dm.recipient, twitter.models.User)) - self.assertEqual(dm.recipient.id, 4012966701) - - # Same as above. - self.assertEqual(dm.id, 678629245946433539) - def test_hashtag(self): """ Test twitter.Hashtag object """ ht = twitter.Hashtag.NewFromJsonDict(self.HASHTAG_SAMPLE_JSON) diff --git a/twitter/api.py b/twitter/api.py old mode 100644 new mode 100755 index 94bbbbc1..4c462e35 --- a/twitter/api.py +++ b/twitter/api.py @@ -2997,38 +2997,50 @@ def GetSentDirectMessages(self, def PostDirectMessage(self, text, - user_id=None, - screen_name=None, + user_id, return_json=False): """Post a twitter direct message from the authenticated user. Args: text: The message text to be posted. user_id: - The ID of the user who should receive the direct message. [Optional] - screen_name: - The screen name of the user who should receive the direct message. [Optional] + The ID of the user who should receive the direct message. return_json (bool, optional): - If True JSON data will be returned, instead of twitter.User + If True JSON data will be returned, instead of twitter.DirectMessage Returns: A twitter.DirectMessage instance representing the message posted """ - url = '%s/direct_messages/new.json' % self.base_url - data = {'text': text} - if user_id: - data['user_id'] = user_id - elif screen_name: - data['screen_name'] = screen_name - else: - raise TwitterError({'message': "Specify at least one of user_id or screen_name."}) + url = '%s/direct_messages/events/new.json' % self.base_url + + event = { + 'event': { + 'type': 'message_create', + 'message_create': { + 'target': { + 'recipient_id': user_id, + }, + 'message_data': { + 'text': text + } + } + } + } - resp = self._RequestUrl(url, 'POST', data=data) - data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) + resp = self._RequestUrl(url, 'POST', json=event) + data = resp.json() if return_json: return data else: - return DirectMessage.NewFromJsonDict(data) + dm = DirectMessage( + created_at=data['event']['created_timestamp'], + id=data['event']['id'], + recipient_id=data['event']['message_create']['target']['recipient_id'], + sender_id=data['event']['message_create']['sender_id'], + text=data['event']['message_create']['message_data']['text'], + ) + dm._json = data + return dm def DestroyDirectMessage(self, message_id, include_entities=True, return_json=False): """Destroys the direct message specified in the required ID parameter. diff --git a/twitter/models.py b/twitter/models.py index 82dc3ada..e8974ebe 100644 --- a/twitter/models.py +++ b/twitter/models.py @@ -185,21 +185,13 @@ def __init__(self, **kwargs): self.param_defaults = { 'created_at': None, 'id': None, - 'recipient': None, 'recipient_id': None, - 'recipient_screen_name': None, - 'sender': None, 'sender_id': None, - 'sender_screen_name': None, 'text': None, } for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) - if 'sender' in kwargs: - self.sender = User.NewFromJsonDict(kwargs.get('sender', None)) - if 'recipient' in kwargs: - self.recipient = User.NewFromJsonDict(kwargs.get('recipient', None)) def __repr__(self): if self.text and len(self.text) > 140: @@ -208,7 +200,7 @@ def __repr__(self): text = self.text return "DirectMessage(ID={dm_id}, Sender={sender}, Created={time}, Text='{text!r}')".format( dm_id=self.id, - sender=self.sender_screen_name, + sender=self.sender_id, time=self.created_at, text=text) From eb4687923e983e00858ea5828cb90aea079aee70 Mon Sep 17 00:00:00 2001 From: "bear (Mike Taylor)" Date: Sat, 29 Sep 2018 19:48:25 -0400 Subject: [PATCH 25/83] fix test assert for sender_screen_name --- tests/test_direct_messages.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_direct_messages.py b/tests/test_direct_messages.py index 4f60e0d7..348ea525 100644 --- a/tests/test_direct_messages.py +++ b/tests/test_direct_messages.py @@ -56,14 +56,13 @@ def test_get_sent_direct_messages(): assert isinstance(resp, list) assert isinstance(direct_message, twitter.DirectMessage) assert direct_message.id == 678629283007303683 - assert [dm.sender_screen_name == 'notinourselves' for dm in resp] @responses.activate def test_post_direct_message(): with open('testdata/direct_messages/post_post_direct_message.json', 'r') as f: responses.add(POST, DEFAULT_URL, body=f.read()) - resp = api.PostDirectMessage(screen_name='TheGIFingBot', + resp = api.PostDirectMessage(user_id='372018022', text='https://t.co/L4MIplKUwR') assert isinstance(resp, twitter.DirectMessage) assert resp.text == 'https://t.co/L4MIplKUwR' From 61b8c6325e272290bcdbf92d7dbb09d3478b3748 Mon Sep 17 00:00:00 2001 From: "bear (Mike Taylor)" Date: Sat, 29 Sep 2018 19:49:24 -0400 Subject: [PATCH 26/83] allow user_id to be filled if optional screen_name is included --- twitter/api.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/twitter/api.py b/twitter/api.py index 4c462e35..15a778f1 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -2997,7 +2997,8 @@ def GetSentDirectMessages(self, def PostDirectMessage(self, text, - user_id, + user_id=None, + screen_name=None, return_json=False): """Post a twitter direct message from the authenticated user. @@ -3012,6 +3013,11 @@ def PostDirectMessage(self, """ url = '%s/direct_messages/events/new.json' % self.base_url + # Hack to allow some sort of backwards compatibility with older versions + # part of the fix for Issue #587 + if user_id is None and screen_name is not None: + user_id = self.GetUser(screen_name=screen_name).id + event = { 'event': { 'type': 'message_create', From 8b65eed05a0d700a34069aa62bb9ea92684e01ee Mon Sep 17 00:00:00 2001 From: "bear (Mike Taylor)" Date: Sat, 29 Sep 2018 19:51:23 -0400 Subject: [PATCH 27/83] bump version to 2.5 --- twitter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twitter/__init__.py b/twitter/__init__.py index c6e433f8..b4836243 100644 --- a/twitter/__init__.py +++ b/twitter/__init__.py @@ -22,7 +22,7 @@ __email__ = 'python-twitter@googlegroups.com' __copyright__ = 'Copyright (c) 2007-2016 The Python-Twitter Developers' __license__ = 'Apache License 2.0' -__version__ = '3.4.2' +__version__ = '3.5' __url__ = 'https://github.com/bear/python-twitter' __download_url__ = 'https://pypi.python.org/pypi/python-twitter' __description__ = 'A Python wrapper around the Twitter API' From f7eb83d9dca3ba0ee93e629ba5322732f99a3a30 Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Sun, 30 Sep 2018 09:23:40 -0400 Subject: [PATCH 28/83] fix test for PostDirectMessage endpoint with new data --- testdata/direct_messages/post_post_direct_message.json | 2 +- tests/test_direct_messages.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testdata/direct_messages/post_post_direct_message.json b/testdata/direct_messages/post_post_direct_message.json index c4c2d59f..9e861211 100644 --- a/testdata/direct_messages/post_post_direct_message.json +++ b/testdata/direct_messages/post_post_direct_message.json @@ -1 +1 @@ -{"sender_id_str": "372018022", "entities": {"urls": [{"expanded_url": "https://twitter.com/CamilleStein/status/854322543364382720", "indices": [0, 23], "url": "https://t.co/L4MIplKUwR", "display_url": "twitter.com/CamilleStein/s\u2026"}], "symbols": [], "hashtags": [], "user_mentions": []}, "recipient_id": 3206731269, "text": "https://t.co/L4MIplKUwR", "id_str": "855194351294656515", "sender_id": 372018022, "id": 855194351294656515, "created_at": "Thu Apr 20 22:59:56 +0000 2017", "recipient_id_str": "3206731269", "recipient": {"is_translation_enabled": false, "following": true, "name": "The GIFing Bot", "profile_image_url": "http://pbs.twimg.com/profile_images/592359786659880960/IwQsKZ7b_normal.png", "profile_sidebar_border_color": "000000", "followers_count": 84, "created_at": "Sat Apr 25 16:31:07 +0000 2015", "profile_link_color": "02B400", "is_translator": false, "id": 3206731269, "profile_sidebar_fill_color": "000000", "has_extended_profile": false, "profile_background_tile": false, "profile_use_background_image": false, "default_profile_image": false, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "lang": "en", "verified": false, "favourites_count": 0, "protected": false, "notifications": false, "friends_count": 100, "listed_count": 7, "location": "", "statuses_count": 19, "entities": {"url": {"urls": [{"expanded_url": "http://iseverythingstilltheworst.com/projects/the-gifing-bot/", "indices": [0, 23], "url": "https://t.co/BTsv2OJnqv", "display_url": "iseverythingstilltheworst.com/projects/the-g\u2026"}]}, "description": {"urls": []}}, "url": "https://t.co/BTsv2OJnqv", "utc_offset": null, "time_zone": null, "profile_background_color": "000000", "id_str": "3206731269", "description": "DM me a tweet with a GIF in it and I'll make it into an actual GIF!!", "follow_request_sent": false, "screen_name": "TheGIFingBot", "profile_text_color": "000000", "translator_type": "none", "profile_image_url_https": "https://pbs.twimg.com/profile_images/592359786659880960/IwQsKZ7b_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "contributors_enabled": false, "default_profile": false, "geo_enabled": false}, "sender": {"is_translation_enabled": false, "following": false, "name": "Jeremy", "profile_image_url": "http://pbs.twimg.com/profile_images/800076020741246976/fMpwMcBJ_normal.jpg", "profile_sidebar_border_color": "000000", "followers_count": 142, "created_at": "Sun Sep 11 23:49:28 +0000 2011", "profile_link_color": "EE3355", "is_translator": false, "id": 372018022, "profile_sidebar_fill_color": "000000", "has_extended_profile": false, "profile_background_tile": false, "profile_use_background_image": false, "default_profile_image": false, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "lang": "en", "verified": false, "favourites_count": 7546, "protected": false, "notifications": false, "friends_count": 593, "listed_count": 11, "location": "philly", "statuses_count": 1785, "entities": {"url": {"urls": [{"expanded_url": "http://iseverythingstilltheworst.com", "indices": [0, 23], "url": "https://t.co/wtg3XyREnj", "display_url": "iseverythingstilltheworst.com"}]}, "description": {"urls": []}}, "url": "https://t.co/wtg3XyREnj", "utc_offset": -14400, "time_zone": "Eastern Time (US & Canada)", "profile_background_color": "FFFFFF", "id_str": "372018022", "description": "these people have addresses | #botally", "follow_request_sent": false, "screen_name": "__jcbl__", "profile_text_color": "000000", "translator_type": "none", "profile_image_url_https": "https://pbs.twimg.com/profile_images/800076020741246976/fMpwMcBJ_normal.jpg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_banner_url": "https://pbs.twimg.com/profile_banners/372018022/1475799101", "contributors_enabled": false, "default_profile": false, "geo_enabled": false}, "sender_screen_name": "__jcbl__", "recipient_screen_name": "TheGIFingBot"} \ No newline at end of file +{"event": {"type": "message_create", "id": "1046388258840748037", "created_timestamp": "1538313376518", "message_create": {"target": {"recipient_id": "372018022"}, "sender_id": "372018022", "message_data": {"text": "hello", "entities": {"hashtags": [], "symbols": [], "user_mentions": [], "urls": []}}}}} \ No newline at end of file diff --git a/tests/test_direct_messages.py b/tests/test_direct_messages.py index 348ea525..fa9108c6 100644 --- a/tests/test_direct_messages.py +++ b/tests/test_direct_messages.py @@ -63,9 +63,9 @@ def test_post_direct_message(): with open('testdata/direct_messages/post_post_direct_message.json', 'r') as f: responses.add(POST, DEFAULT_URL, body=f.read()) resp = api.PostDirectMessage(user_id='372018022', - text='https://t.co/L4MIplKUwR') + text='hello') assert isinstance(resp, twitter.DirectMessage) - assert resp.text == 'https://t.co/L4MIplKUwR' + assert resp.text == 'hello' @responses.activate From d373eb901a2bcc7e16d06c5b9eb3ad08f656c955 Mon Sep 17 00:00:00 2001 From: AnSq Date: Sun, 14 Oct 2018 07:09:41 -0700 Subject: [PATCH 29/83] use a requests.Session to speed up most network operations --- twitter/api.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index 15a778f1..a823fc38 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -292,6 +292,8 @@ def __init__(self, requests_log.setLevel(logging.DEBUG) requests_log.propagate = True + self._session = requests.Session() + @staticmethod def GetAppOnlyAuthToken(consumer_key, consumer_secret): """ @@ -4974,20 +4976,20 @@ def _RequestUrl(self, url, verb, data=None, json=None, enforce_auth=True): if data: if 'media_ids' in data: url = self._BuildUrl(url, extra_params={'media_ids': data['media_ids']}) - resp = requests.post(url, data=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) + resp = self._session.post(url, data=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) elif 'media' in data: - resp = requests.post(url, files=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) + resp = self._session.post(url, files=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) else: - resp = requests.post(url, data=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) + resp = self._session.post(url, data=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) elif json: - resp = requests.post(url, json=json, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) + resp = self._session.post(url, json=json, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) else: resp = 0 # POST request, but without data or json elif verb == 'GET': data['tweet_mode'] = self.tweet_mode url = self._BuildUrl(url, extra_params=data) - resp = requests.get(url, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) + resp = self._session.get(url, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) else: resp = 0 # if not a POST or GET request From be3f1b422d063a676a1ecc91c2ed13fe21c39927 Mon Sep 17 00:00:00 2001 From: tuftedocelot Date: Sun, 14 Oct 2018 14:50:37 -0500 Subject: [PATCH 30/83] Add initial tests of status and Place API usage For now, Place models are only accessible via a Status. Querying the API for Places is to come later --- testdata/get_status_with_place.json | 185 ++++++++++++++++++++++++++++ tests/test_status_place.py | 45 +++++++ 2 files changed, 230 insertions(+) create mode 100644 testdata/get_status_with_place.json create mode 100644 tests/test_status_place.py diff --git a/testdata/get_status_with_place.json b/testdata/get_status_with_place.json new file mode 100644 index 00000000..f4c82bc6 --- /dev/null +++ b/testdata/get_status_with_place.json @@ -0,0 +1,185 @@ +{ + "created_at": "Sat Oct 13 20:15:27 +0000 2018", + "hashtags": [], + "id": 1051204790334746624, + "id_str": "1051204790334746624", + "lang": "en", + "place": { + "attributes": {}, + "bounding_box": { + "coordinates": [ + [ + [ + -87.940033, + 41.644102 + ], + [ + -87.523993, + 41.644102 + ], + [ + -87.523993, + 42.0230669 + ], + [ + -87.940033, + 42.0230669 + ] + ] + ], + "type": "Polygon" + }, + "contained_within": [], + "country": "United States", + "country_code": "US", + "full_name": "Chicago, IL", + "id": "1d9a5370a355ab0c", + "name": "Chicago", + "place_type": "city", + "url": "https://api.twitter.com/1.1/geo/id/1d9a5370a355ab0c.json" + }, + "quoted_status": { + "created_at": "Fri Oct 12 20:38:11 +0000 2018", + "favorite_count": 24161, + "hashtags": [], + "id": 1050848124036685824, + "id_str": "1050848124036685824", + "lang": "en", + "media": [ + { + "display_url": "pic.twitter.com/7BhtdUPIQD", + "expanded_url": "https://twitter.com/fazopri/status/1049144383457677317/video/1", + "id": 1049144139172855808, + "media_url": "http://pbs.twimg.com/ext_tw_video_thumb/1049144139172855808/pu/img/WaU8axg3hOI425zK.jpg", + "media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/1049144139172855808/pu/img/WaU8axg3hOI425zK.jpg", + "sizes": { + "large": { + "h": 720, + "resize": "fit", + "w": 720 + }, + "medium": { + "h": 720, + "resize": "fit", + "w": 720 + }, + "small": { + "h": 680, + "resize": "fit", + "w": 680 + }, + "thumb": { + "h": 150, + "resize": "crop", + "w": 150 + } + }, + "type": "video", + "url": "https://t.co/7BhtdUPIQD", + "video_info": { + "aspect_ratio": [ + 1, + 1 + ], + "duration_millis": 26153, + "variants": [ + { + "content_type": "application/x-mpegURL", + "url": "https://video.twimg.com/ext_tw_video/1049144139172855808/pu/pl/TQ2FvESC2EWTKpxR.m3u8?tag=5" + }, + { + "bitrate": 256000, + "content_type": "video/mp4", + "url": "https://video.twimg.com/ext_tw_video/1049144139172855808/pu/vid/240x240/7O7a4ritrh587trx.mp4?tag=5" + }, + { + "bitrate": 832000, + "content_type": "video/mp4", + "url": "https://video.twimg.com/ext_tw_video/1049144139172855808/pu/vid/480x480/BXTtsb-hW9DvNJFR.mp4?tag=5" + }, + { + "bitrate": 1280000, + "content_type": "video/mp4", + "url": "https://video.twimg.com/ext_tw_video/1049144139172855808/pu/vid/720x720/_Xb-Q1NmNlv-hY-x.mp4?tag=5" + } + ] + } + } + ], + "possibly_sensitive": true, + "retweet_count": 9042, + "source": "Twitter for iPhone", + "text": "Swallowing when you have a sore throat then remembering how you used to swallow painlessly\n\n https://t.co/7BhtdUPIQD", + "urls": [], + "user": { + "created_at": "Wed Dec 01 19:30:08 +0000 2010", + "description": "Artist |bookings/beats: supermanage17@gmail.com", + "favourites_count": 15056, + "followers_count": 2680, + "friends_count": 1566, + "geo_enabled": true, + "id": 221843085, + "id_str": "221843085", + "lang": "en", + "listed_count": 40, + "location": "The Mountains", + "name": "O\u2019dack Black", + "profile_background_color": "C0DEED", + "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_tile": true, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/221843085/1530302259", + "profile_image_url": "http://pbs.twimg.com/profile_images/1036223132032483328/qu1sNzYk_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/1036223132032483328/qu1sNzYk_normal.jpg", + "profile_link_color": "0084B4", + "profile_sidebar_border_color": "C0DEED", + "profile_sidebar_fill_color": "DDEEF6", + "profile_text_color": "D633D6", + "profile_use_background_image": true, + "screen_name": "iamodeal", + "statuses_count": 15429, + "url": "https://t.co/RWx3Olu79R" + }, + "user_mentions": [] + }, + "quoted_status_id": 1050848124036685824, + "quoted_status_id_str": "1050848124036685824", + "source": "Twitter for iPhone", + "text": "OXJKDS ME LAST NIGHT https://t.co/yyFEAn8ZM8", + "urls": [ + { + "expanded_url": "https://twitter.com/iamodeal/status/1050848124036685824", + "url": "https://t.co/yyFEAn8ZM8" + } + ], + "user": { + "created_at": "Tue Feb 05 01:30:25 +0000 2013", + "description": "I wish everyone was a lil nicer to each other. photographer. 19 years old. business: haley.paolini@icloud.com", + "favourites_count": 86250, + "followers_count": 15571, + "friends_count": 707, + "geo_enabled": true, + "id": 1149579998, + "id_str": "1149579998", + "lang": "en", + "listed_count": 270, + "location": "Chicago, IL, USA", + "name": "haley", + "profile_background_color": "FFFFFF", + "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_tile": true, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/1149579998/1538861179", + "profile_image_url": "http://pbs.twimg.com/profile_images/1048685903886147591/LW8shQOq_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/1048685903886147591/LW8shQOq_normal.jpg", + "profile_link_color": "664479", + "profile_sidebar_border_color": "FFFFFF", + "profile_sidebar_fill_color": "DDEEF6", + "profile_text_color": "0084B4", + "profile_use_background_image": true, + "screen_name": "haleypaolini", + "statuses_count": 194551, + "url": "https://t.co/qlr16Jh9XJ" + }, + "user_mentions": [] +} \ No newline at end of file diff --git a/tests/test_status_place.py b/tests/test_status_place.py new file mode 100644 index 00000000..fd485ae1 --- /dev/null +++ b/tests/test_status_place.py @@ -0,0 +1,45 @@ +import unittest +import re +import sys +import twitter +import responses +from responses import GET + +DEFAULT_URL = re.compile(r'https?://.*\.twitter.com/1\.1/.*') + + +class ErrNull(object): + """ Suppress output of tests while writing to stdout or stderr. This just + takes in data and does nothing with it. + """ + + def write(self, data): + pass + + +class ApiPlaceTest(unittest.TestCase): + def setUp(self): + self.api = twitter.Api( + consumer_key='test', + consumer_secret='test', + access_token_key='test', + access_token_secret='test' + ) + self.base_url = 'https://api.twitter.com/1.1' + self._stderr = sys.stderr + sys.stderr = ErrNull() + + def tearDown(self): + sys.stderr = self._stderr + pass + + @responses.activate + def testGetStatusWithPlace(self): + with open('testdata/get_status_with_place.json') as f: + resp_data = f.read() + responses.add(GET, DEFAULT_URL, body=resp_data) + + resp = self.api.GetStatus(1051204790334746624) + self.assertTrue(isinstance(resp, twitter.Status)) + self.assertTrue(isinstance(resp.place, twitter.Place)) + self.assertEqual(resp.id, 1051204790334746624) From a38ac1d6601f19bc99da9c2fe0f606397f2bfe2f Mon Sep 17 00:00:00 2001 From: tuftedocelot Date: Sun, 14 Oct 2018 15:56:50 -0500 Subject: [PATCH 31/83] Add testing of using a Place when posting a new update --- testdata/post_update_with_place.json | 59 ++++++++++++++++++++++++++++ tests/test_status_place.py | 11 +++++- 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 testdata/post_update_with_place.json diff --git a/testdata/post_update_with_place.json b/testdata/post_update_with_place.json new file mode 100644 index 00000000..ee3369cd --- /dev/null +++ b/testdata/post_update_with_place.json @@ -0,0 +1,59 @@ +{ + "created_at": "Sun Oct 14 20:44:37 +0000 2018", + "hashtags": [], + "id": 1051574521818435584, + "id_str": "1051574521818435584", + "lang": "en", + "place": { + "bounding_box": { + "coordinates": [ + [ + [ + -105.14544044849526, + 40.19213775503984 + ], + [ + -105.14544044849526, + 40.19213775503984 + ], + [ + -105.14544044849526, + 40.19213775503984 + ], + [ + -105.14544044849526, + 40.19213775503984 + ] + ] + ], + "type": "Polygon" + }, + "full_name": "McIntosh Lake", + "id": "07d9db48bc083000", + "name": "McIntosh Lake", + "place_type": "poi", + "url": "https://api.twitter.com/1.1/geo/id/07d9db48bc083000.json" + }, + "urls": [], + "user": { + "created_at": "Sat May 26 17:34:57 +0000 2018", + "default_profile": true, + "default_profile_image": true, + "geo_enabled": true, + "id": 1000430098892378113, + "id_str": "1000430098892378113", + "lang": "en", + "name": "DECKSET", + "profile_background_color": "F5F8FA", + "profile_image_url": "http://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png", + "profile_image_url_https": "https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png", + "profile_link_color": "1DA1F2", + "profile_sidebar_border_color": "C0DEED", + "profile_sidebar_fill_color": "DDEEF6", + "profile_text_color": "333333", + "profile_use_background_image": true, + "screen_name": "DECKSET1", + "statuses_count": 1 + }, + "user_mentions": [] +} \ No newline at end of file diff --git a/tests/test_status_place.py b/tests/test_status_place.py index fd485ae1..628bc229 100644 --- a/tests/test_status_place.py +++ b/tests/test_status_place.py @@ -3,7 +3,7 @@ import sys import twitter import responses -from responses import GET +from responses import GET, POST DEFAULT_URL = re.compile(r'https?://.*\.twitter.com/1\.1/.*') @@ -43,3 +43,12 @@ def testGetStatusWithPlace(self): self.assertTrue(isinstance(resp, twitter.Status)) self.assertTrue(isinstance(resp.place, twitter.Place)) self.assertEqual(resp.id, 1051204790334746624) + + @responses.activate + def testPostUpdateWithPlace(self): + with open('testdata/post_update_with_place.json') as f: + resp_data = f.read() + responses.add(POST, DEFAULT_URL, body=resp_data, status=200) + + post = self.api.PostUpdate('test place', place_id='07d9db48bc083000') + self.assertEqual(post.place.id, '07d9db48bc083000') From 98ef62f68b04bfdee8cb4df2db8b81933f390ccf Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Sat, 3 Nov 2018 09:26:57 -0400 Subject: [PATCH 32/83] fix issue #584 --- twitter/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twitter/api.py b/twitter/api.py index a823fc38..fe1f6e54 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -4994,7 +4994,7 @@ def _RequestUrl(self, url, verb, data=None, json=None, enforce_auth=True): else: resp = 0 # if not a POST or GET request - if url and self.rate_limit: + if url and self.rate_limit and resp: limit = resp.headers.get('x-rate-limit-limit', 0) remaining = resp.headers.get('x-rate-limit-remaining', 0) reset = resp.headers.get('x-rate-limit-reset', 0) From a51b4c25e6b3861270a71d7b1a56a3f1f4bc69da Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Sat, 3 Nov 2018 20:25:10 -0400 Subject: [PATCH 33/83] update circleci stuff for version 2.0 --- .circleci/config.yml | 20 ++++++++++++++++++++ .circleci/images/Dockerfile | 21 +++++++++++++++++++++ circle.yml | 11 ----------- tox.ini | 2 +- 4 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 .circleci/config.yml create mode 100644 .circleci/images/Dockerfile delete mode 100644 circle.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..eb59bbc7 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,20 @@ +version: 2 +jobs: + build: + working_directory: ~/repo + docker: + - image: jeremylow/python-twitter + steps: + - checkout + - run: sudo chown -R circleci:circleci ~/repo + - run: + name: set up pyenv + command: pyenv local 2.7.15 3.7.1 pypy-5.7.1 pypy3.5-6.0.0 + - run: + name: install deps + command: pip install -r requirements.testing.txt + - run: + name: run tests + command: | + export PATH=/home/circleci/.local/bin:$PATH + make tox diff --git a/.circleci/images/Dockerfile b/.circleci/images/Dockerfile new file mode 100644 index 00000000..04d96343 --- /dev/null +++ b/.circleci/images/Dockerfile @@ -0,0 +1,21 @@ +# We could use a smaller image, but this ensures that everything CircleCI needs +# is installed already. +FROM circleci/python:3.6 +MAINTAINER Jeremy Low + +# These are the version of python currently supported. +ENV SUPPORTED_VERSIONS="2.7.15 3.7.1 pypy-5.7.1 pypy3.5-6.0.0" +ENV PYENV_ROOT /home/circleci/.pyenv +ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH + +# Get and install pyenv. +RUN curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash + +# pyenv installer doesn't set these for us. +RUN echo "export PATH=${PYENV_ROOT}/bin:$$PATH \n\ +eval '\$(pyenv init -)' \n\ +eval '\$(pyenv virtualenv-init -)'" >> ~/.bashrc +RUN pyenv update + +# Install each supported version into the container. +RUN for i in $SUPPORTED_VERSIONS; do pyenv install "$i"; done diff --git a/circle.yml b/circle.yml deleted file mode 100644 index e45f625b..00000000 --- a/circle.yml +++ /dev/null @@ -1,11 +0,0 @@ -dependencies: - override: - - pip install -U pip - -test: - pre: - - make info - - uname -a - - lsb_release -a - override: - - make ci diff --git a/tox.ini b/tox.ini index c072ade8..a98acd94 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = clean,py27,py36,pypy,pypy3,codestyle,coverage +envlist = clean,py27,py37,pypy,pypy3,codestyle,coverage skip_missing_interpreters = True [testenv] From 254e9ece107c4acff07d80712e7ec05da908cf69 Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Wed, 23 May 2018 18:01:34 -0400 Subject: [PATCH 34/83] add doc strings to get_access_token script --- get_access_token.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/get_access_token.py b/get_access_token.py index f6ec0973..84845c11 100755 --- a/get_access_token.py +++ b/get_access_token.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright 2007-2013 The Python-Twitter Developers +# Copyright 2007-2018 The Python-Twitter Developers # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,6 +14,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +"""Utility to get your access tokens.""" + from __future__ import print_function from requests_oauthlib import OAuth1Session @@ -31,6 +33,17 @@ def get_access_token(consumer_key, consumer_secret): + """Get an access token for a given consumer key and secret. + + Args: + consumer_key (str): + Your application consumer key. + consumer_secret (str): + Your application consumer secret. + + Returns: + (None) Prints to command line. + """ oauth_client = OAuth1Session(consumer_key, client_secret=consumer_secret, callback_uri='oob') print('\nRequesting temp token from Twitter...\n') @@ -71,6 +84,7 @@ def get_access_token(consumer_key, consumer_secret): def main(): + """Run script to get access token and secret for given app.""" consumer_key = input('Enter your consumer key: ') consumer_secret = input('Enter your consumer secret: ') get_access_token(consumer_key, consumer_secret) From 24dfe8f2fe66f520428430a6092a091b3e7080a3 Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Wed, 23 May 2018 18:14:10 -0400 Subject: [PATCH 35/83] add/fix inline documentation for utilites --- twitter/twitter_utils.py | 44 ++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/twitter/twitter_utils.py b/twitter/twitter_utils.py index b402ae63..3bd5d411 100644 --- a/twitter/twitter_utils.py +++ b/twitter/twitter_utils.py @@ -1,4 +1,5 @@ -# encoding: utf-8 +# -*- coding: utf-8 -*- +"""Collection of utilities for use in API calls, functions.""" from __future__ import unicode_literals import mimetypes @@ -18,7 +19,7 @@ import twitter if sys.version_info < (3,): - range = xrange + range = xrange # noqa if sys.version_info > (3,): unicode = str @@ -169,8 +170,9 @@ def calc_expected_status_length(status, short_url_length=23): - """ Calculates the length of a tweet, taking into account Twitter's - replacement of URLs with https://t.co links. + """Calculate the length of a tweet. + + Takes into account Twitter's replacement of URLs with https://t.co links. Args: status: text of the status message to be posted. @@ -178,7 +180,6 @@ def calc_expected_status_length(status, short_url_length=23): Returns: Expected length of the status message as an integer. - """ status_length = 0 if isinstance(status, bytes): @@ -197,7 +198,7 @@ def calc_expected_status_length(status, short_url_length=23): def is_url(text): - """ Checks to see if a bit of text is a URL. + """Check to see if a bit of text is a URL. Args: text: text to check. @@ -209,6 +210,14 @@ def is_url(text): def http_to_file(http): + """Turn a URL into a file-like object. + + Args: + http (str): URL of the file to download. + + Returns: + File-like object of downloaded URL. + """ data_file = NamedTemporaryFile() req = requests.get(http, stream=True) for chunk in req.iter_content(chunk_size=1024 * 1024): @@ -217,8 +226,7 @@ def http_to_file(http): def parse_media_file(passed_media, async_upload=False): - """ Parses a media file and attempts to return a file-like object and - information about the media file. + """Parse a media file and attempts to return a file-like object and information about the media file. Args: passed_media: media file which to parse. @@ -282,9 +290,11 @@ def parse_media_file(passed_media, async_upload=False): def enf_type(field, _type, val): - """ Checks to see if a given val for a field (i.e., the name of the field) - is of the proper _type. If it is not, raises a TwitterError with a brief - explanation. + """Enforce type checking on variable. + + Check to see if a given val for a field (i.e., the name + of the field) is of the proper _type. If it is not, + raise a TwitterError with a brief explanation. Args: field: @@ -307,6 +317,18 @@ def enf_type(field, _type, val): def parse_arg_list(args, attr): + """Parse a set of args for list functions. + + Convenience/DRY function. + + Args: + args (str, twitter.User, list, tuple): set of arguments to + pass to API. + attr (str): The attribute to be extracted from each arg. + + Returns: + (str) A string representing the concatenated arguments. + """ out = [] if isinstance(args, (str, unicode)): out.append(args) From 4ad779c172387fe778f565955226864d2a831b6a Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Sat, 10 Nov 2018 17:09:07 +0000 Subject: [PATCH 36/83] Enable post direct message with media attached --- testdata/media/happy.jpg | Bin 0 -> 3032 bytes tests/test_direct_messages.py | 12 ++++++++++++ twitter/api.py | 36 +++++++++++++++++++++++++++++----- 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 testdata/media/happy.jpg diff --git a/testdata/media/happy.jpg b/testdata/media/happy.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e6a80f42839f0b5ab5010a04a20c5465aa1592d8 GIT binary patch literal 3032 zcmbW3c|6oxAIE=V7PBZbmKlQ#hKNgcC0kjdLMXzu%aSDfzQi@Qk&H~GM3xYeJ?q#d z6;h_uz0BxZvrK5F=USd|?|nVb>-GHgJkRI6zUOsb=X}5CocB5B^?jYa4|_8J`i!Z$ zDF6Zi0EkL3b%IDk2DkROf4 zppkq6f&yqkZX3Nn31~kO41se4(I^Cp+xtIiuN%O?0c*es43Y+TFd#4nwAT+1xch{H z{uJ=HKs;aw6vhijAW>Y0CN#jqU7H62hC(3_u6G=F9Drb;0#d3cVS?7~ywX8JYBzEo z!HK8pdW3Dp*Jac_f|C$PEKUS3DtlN?{)mExrk3`x<2pvhCZ?y&%+J_fxM*j8$-&Xn z>$10xuiurB(6I1`$f)S#Us6b^Y3Ui1+}nB7J9qEp7nhWlJua`Pd{W=g*wozeytS>j zuYX{0=*95Mw-fK)PtrepoMJF%zx+Nozp%K(`nIvT^_|W6@sE8j5CHxQi(CH%_8(jr zE*B4kd!)SkTp*qZu7NQSsFW&9;G{LLdyt^C+6}nSshmf3JqV(@&APBh@Hi4Hqrs46 z?W6rk_TPae{awW{HoN8utgzVE0X9mthFsp)U7e_CL~eI<1-mfuMyo+1S9&<&B1IU z-ZnrdfLM3vh%KXVz!g=O+f5S>Q(Qm`q*X?kfFNQAYoPs!Y|kJfMydpr?! zR0czCxWu?CIq(!ITiq>DV`4XFw&|MXFeuvyn_r8|5jv z6y1I6B;;Z~z|dEUq&bW=zBcIHMNy7&&Tc!0hIXQdMRJrZ2cy=?EJ=!y7r#i(ocLkw zrBU@TwO0Z&9?M_k#ECdbGHLmxgK}1bWWMT?lR2XMmXL4m748#p&EanDR2CE z%uoWlte@&AI51OyNqfNq$i4F>Z%JH!Re5c;b;9xy6^UlpF4S`Jmd&3}4(&8dv-_m_ZttpSI|V$ZO< zE!0NrIIXko{QO3hX5$V+3d0&~+3+>h9)XUhRh`-@HdUPwnCCuLjMRbbR0I6B5+N%d z$7vht*U_ak!kF*VRL>O&3e`R17rNmf=J}+VZcA2?0oN2~$b(-phzwl+eG6$>bfn_Y z9W~ifPkp;QCT!zy=%Er4%jlW7d}~(f(NJh~S@5(_tdPERtRB4sf9OMf+q>#tuLg!E zO=?%BHnp8g-ssaYQFtJ(amB9?8G9|BxP5G$o#owtwRbEd2gdn4xPRjP59NSt*Zd&1EL^E%j7o4?zB_QT7}F#w{f^GgDn;OgzsfnbR_~>$ zCCGV1PHD4X5t+v|d<@~H_({Hqe=gN>qPA&d*+R@@VpUUY#ngA@>em%_zsIflrYP~C z!J>wCb$KL6<`z)9@QD$Sov}$HxnL%Aqk`G`ha1PLB`3Y3jB8}SITe3a4eHC43~BEQ zG0Ec#T)Y&^q?0igR&aP&S3N{h3DiuIbxI>%byNGfk&lSd}<{vb03VE8~uB;0((371n){waM0vHe zqy$?EXn-zwD^UO$pbR1lA*Ti2q1hscq#zOD**DqPP=Cf;=<8pJWhVZ zDH(33@4lh8{W=InG@ZEPpE+a`^6h^5Wd*hCa6I$bwqZGw)?Z}>H%gC{or$T?bPhL> zDxpoH*BYMb7k~cRe*3y{(hIL-Y5J4%AQ%6tg(-U9fh&8!;6wQMYEvNpQoZIRjZvj( zs~d5yyQ|EtG|{e#!%a`5YlKylkvoJ+cQ!&H=geS_UJZ| zp)~|EQO`1Cqk#B5og-CkHd)0|H=YReRFIk2x_2`#4u8Bm#;Hzx_yM6K`O)Un$)Wyu ziT6tR8|Ubu%k80J`InR_#^87L6jeh}4f{hiv6neUjHa9pBbHUKs9U;2;Y#$cjmChQ zC4R`wzaV>dH-~==_mKI zF-}gtRC%{_;DWIU=%V5NKMo00G79+?;t!71=xQ?Bp$!vXZ&zEd`H4_HMv3i|Z)U5n z)`--IaBq)S{RL;_i+D2rGJd|D>OJ7g>E=Bk+I0u&&&p-ew{N!4wuTcnAB%GHXD7`) W;Rl~DZEH$rM}(Ug Date: Mon, 12 Nov 2018 16:35:44 +0000 Subject: [PATCH 37/83] Updated variable names to be generic media type --- twitter/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index 02cb6891..2c6437ce 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -3031,11 +3031,11 @@ def PostDirectMessage(self, } if media_file_path is not None: try: - image = open(media_file_path, 'rb') + media = open(media_file_path, 'rb') except IOError: - raise TwitterError({'message': 'Image file could not be opened.'}) + raise TwitterError({'message': 'Media file could not be opened.'}) - response_media_id = self.UploadMediaChunked(media=image, media_category=media_type) + response_media_id = self.UploadMediaChunked(media=media, media_category=media_type) # With media message_data_value = { From b670be35c4dd9eadd16ceb686fc93b780199b866 Mon Sep 17 00:00:00 2001 From: skillyamamoto <45992480+skillyamamoto@users.noreply.github.com> Date: Wed, 19 Dec 2018 18:15:28 +0900 Subject: [PATCH 38/83] Fixed 'additional_owner' does not work properly 'TwitterApi' requires a comma-separated string, but passed an array. https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-init --- twitter/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twitter/api.py b/twitter/api.py index 2c6437ce..48f75873 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -1278,7 +1278,7 @@ def _UploadMediaChunkedInit(self, if additional_owners and len(additional_owners) > 100: raise TwitterError({'message': 'Maximum of 100 additional owners may be specified for a Media object'}) if additional_owners: - parameters['additional_owners'] = additional_owners + parameters['additional_owners'] = ','.join(map(str,additional_owners)) if media_category: parameters['media_category'] = media_category From 8645f657745ea7f4a42a9615869efec5ef016414 Mon Sep 17 00:00:00 2001 From: Bas van Gaalen Date: Tue, 25 Dec 2018 12:23:07 +0100 Subject: [PATCH 39/83] Added _ParseAndCheckTwitter to PostDirectMessage API call --- twitter/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twitter/api.py b/twitter/api.py index 48f75873..08d5337c 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -3061,7 +3061,7 @@ def PostDirectMessage(self, } resp = self._RequestUrl(url, 'POST', json=event) - data = resp.json() + data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) if return_json: return data From 188997834fba9660f53441e632cfa73239858bb0 Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Mon, 31 Dec 2018 10:06:18 -0500 Subject: [PATCH 40/83] fix error in DM tests. update pep8 section for linter --- Makefile | 3 ++- setup.cfg | 2 +- tests/test_direct_messages.py | 28 +++++++++++----------------- twitter/api.py | 2 +- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 40b8677b..76039c40 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,8 @@ lint: pycodestyle --config={toxinidir}/setup.cfg twitter tests test: lint - python setup.py test + pytest -s + #python setup.py test tox: clean tox diff --git a/setup.cfg b/setup.cfg index ffcde717..1949d5c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,6 +13,6 @@ ignore = [flake8] ignore = E111,E124,E126,E221,E501 -[pycodestyle] +[pep8] ignore = E111,E124,E126,E221,E501 max-line-length = 100 diff --git a/tests/test_direct_messages.py b/tests/test_direct_messages.py index 4e0d5721..a477a16a 100644 --- a/tests/test_direct_messages.py +++ b/tests/test_direct_messages.py @@ -3,24 +3,15 @@ from __future__ import unicode_literals, print_function -import json -import os import re -import sys -from tempfile import NamedTemporaryFile -import unittest -try: - from unittest.mock import patch -except ImportError: - from mock import patch -import warnings import twitter import responses from responses import GET, POST -DEFAULT_URL = re.compile(r'https?://.*\.twitter.com/1\.1/.*') +DEFAULT_BASE_URL = re.compile(r'https?://api\.twitter.com/1\.1/.*') +DEFAULT_UPLOAD_URL = re.compile(r'https?://upload\.twitter.com/1\.1/.*') global api api = twitter.Api('test', 'test', 'test', 'test', tweet_mode='extended') @@ -30,7 +21,7 @@ def test_get_direct_messages(): with open('testdata/direct_messages/get_direct_messages.json') as f: resp_data = f.read() - responses.add(GET, DEFAULT_URL, body=resp_data) + responses.add(GET, DEFAULT_BASE_URL, body=resp_data) resp = api.GetDirectMessages(count=1, page=1) direct_message = resp[0] @@ -49,7 +40,7 @@ def test_get_direct_messages(): def test_get_sent_direct_messages(): with open('testdata/direct_messages/get_sent_direct_messages.json') as f: resp_data = f.read() - responses.add(GET, DEFAULT_URL, body=resp_data) + responses.add(GET, DEFAULT_BASE_URL, body=resp_data) resp = api.GetSentDirectMessages(count=1, page=1) direct_message = resp[0] @@ -61,7 +52,7 @@ def test_get_sent_direct_messages(): @responses.activate def test_post_direct_message(): with open('testdata/direct_messages/post_post_direct_message.json', 'r') as f: - responses.add(POST, DEFAULT_URL, body=f.read()) + responses.add(POST, DEFAULT_BASE_URL, body=f.read()) resp = api.PostDirectMessage(user_id='372018022', text='hello') assert isinstance(resp, twitter.DirectMessage) @@ -71,10 +62,13 @@ def test_post_direct_message(): @responses.activate def test_post_direct_message_with_media(): with open('testdata/direct_messages/post_post_direct_message.json', 'r') as f: - responses.add(POST, DEFAULT_URL, body=f.read()) + responses.add(POST, DEFAULT_BASE_URL, body=f.read()) + with open('testdata/post_upload_chunked_INIT.json') as f: + responses.add(POST, DEFAULT_UPLOAD_URL, body=f.read()) + resp = api.PostDirectMessage(user_id='372018022', text='hello', - media_file_path='testdata/media/happy.png', + media_file_path='testdata/media/happy.jpg', media_type='dm_image') assert isinstance(resp, twitter.DirectMessage) assert resp.text == 'hello' @@ -83,7 +77,7 @@ def test_post_direct_message_with_media(): @responses.activate def test_destroy_direct_message(): with open('testdata/direct_messages/post_destroy_direct_message.json', 'r') as f: - responses.add(POST, DEFAULT_URL, body=f.read()) + responses.add(POST, DEFAULT_BASE_URL, body=f.read()) resp = api.DestroyDirectMessage(message_id=855194351294656515) assert isinstance(resp, twitter.DirectMessage) diff --git a/twitter/api.py b/twitter/api.py index 08d5337c..6c1348da 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -1278,7 +1278,7 @@ def _UploadMediaChunkedInit(self, if additional_owners and len(additional_owners) > 100: raise TwitterError({'message': 'Maximum of 100 additional owners may be specified for a Media object'}) if additional_owners: - parameters['additional_owners'] = ','.join(map(str,additional_owners)) + parameters['additional_owners'] = ','.join(map(str, additional_owners)) if media_category: parameters['media_category'] = media_category From 090ff41234c5629391b0615e9bce56bc9d76a8e8 Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Mon, 31 Dec 2018 14:44:25 -0500 Subject: [PATCH 41/83] add example for getting user timeline --- examples/get_all_user_tweets.py | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 examples/get_all_user_tweets.py diff --git a/examples/get_all_user_tweets.py b/examples/get_all_user_tweets.py new file mode 100644 index 00000000..96524250 --- /dev/null +++ b/examples/get_all_user_tweets.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Downloads all tweets from a given user. + +Uses twitter.Api.GetUserTimeline to retreive the last 3,200 tweets from a user. +Twitter doesn't allow retreiving more tweets than this through the API, so we get +as many as possible. + +t.py should contain the imported variables. +""" + +from __future__ import print_function + +import json +import sys + +import twitter +from t import ACCESS_TOKEN_KEY, ACCESS_TOKEN_SECRET, CONSUMER_KEY, CONSUMER_SECRET + + +def get_tweets(api=None, screen_name=None): + timeline = api.GetUserTimeline(screen_name=screen_name, count=200) + earliest_tweet = min(timeline, key=lambda x: x.id).id + print("getting tweets before:", earliest_tweet) + + while True: + tweets = api.GetUserTimeline( + screen_name=screen_name, max_id=earliest_tweet, count=200 + ) + new_earliest = min(tweets, key=lambda x: x.id).id + + if not tweets or new_earliest == earliest_tweet: + break + else: + earliest_tweet = new_earliest + print("getting tweets before:", earliest_tweet) + timeline += tweets + + return timeline + + +if __name__ == "__main__": + api = twitter.Api( + CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN_KEY, ACCESS_TOKEN_SECRET + ) + screen_name = sys.argv[1] + print(screen_name) + timeline = get_tweets(api=api, screen_name=screen_name) + + with open('examples/timeline.json', 'w+') as f: + for tweet in timeline: + f.write(json.dumps(tweet._json)) + f.write('\n') From 95c3cd5aa82986393fa36493d9a84024d8bd6ea5 Mon Sep 17 00:00:00 2001 From: Nicholas Buse Date: Thu, 3 Jan 2019 06:56:01 -0600 Subject: [PATCH 42/83] Add Report Spam endpoint to the API https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-users-report_spam --- twitter/api.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/twitter/api.py b/twitter/api.py index 6c1348da..b1268bcf 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -2073,6 +2073,41 @@ def _BlockMute(self, return User.NewFromJsonDict(data) + def ReportSpam(self, + user_id=None, + screen_name=None, + perform_block=True): + """Report a user as spam on behalf of the authenticated user. + + Args: + user_id (int, optional) + The numerical ID of the user to report. + screen_name (str, optional): + The screen name of the user to report. + perform_block (bool, optional): + Addionally perform a block of reported users. Defaults to True. + Returns: + twitter.User: twitter.User object representing the blocked/muted user. + """ + + url = '%s/users/report_spam.json' % (self.base_url) + post_data = {} + + if user_id: + post_data['user_id'] = enf_type('user_id', int, user_id) + elif screen_name: + post_data['screen_name'] = screen_name + else: + raise TwitterError("You must specify either a user_id or screen_name") + + if perform_block: + post_data['perform_block'] = enf_type('perform_block', bool, perform_block) + + resp = self._RequestUrl(url, 'POST', data=post_data) + data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) + + return User.NewFromJsonDict(data) + def CreateBlock(self, user_id=None, screen_name=None, From db1fbc6341a891ee5a2d6c97ebd577393630c796 Mon Sep 17 00:00:00 2001 From: lpmi-13 Date: Sat, 16 Feb 2019 05:49:24 +0000 Subject: [PATCH 43/83] fix simple typos --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 1a6246a7..6e7931a3 100644 --- a/README.rst +++ b/README.rst @@ -143,7 +143,7 @@ API The API is exposed via the ``twitter.Api`` class. -The python-twitter requires the use of OAuth keys for nearly all operations. As of Twitter's API v1.1, authentication is required for most, if not all, endpoints. Therefore, you will need to register an app with Twitter in order to use this library. Please see the "Getting Started" guide on https://python-twitter.readthedocs.io for a more information. +The python-twitter requires the use of OAuth keys for nearly all operations. As of Twitter's API v1.1, authentication is required for most, if not all, endpoints. Therefore, you will need to register an app with Twitter in order to use this library. Please see the "Getting Started" guide on https://python-twitter.readthedocs.io for more information. To generate an Access Token you have to pick what type of access your application requires and then do one of the following: @@ -173,7 +173,7 @@ To fetch a single user's public status messages, where ``user`` is a Twitter use >>> statuses = api.GetUserTimeline(screen_name=user) >>> print([s.text for s in statuses]) -To fetch a list a user's friends:: +To fetch a list of a user's friends:: >>> users = api.GetFriends() >>> print([u.name for u in users]) @@ -220,7 +220,7 @@ Please visit `the google group `_ Contributors ------------ -Originally two libraries by DeWitt Clinton and Mike Taylor which was then merged into python-twitter. +Originally two libraries by DeWitt Clinton and Mike Taylor which were then merged into python-twitter. Now it's a full-on open source project with many contributors over time. See AUTHORS.rst for the complete list. From 0d51325049edf8801beac79503de0db192091f10 Mon Sep 17 00:00:00 2001 From: Fitblip Date: Thu, 21 Feb 2019 16:46:48 -0500 Subject: [PATCH 44/83] Fix typing issue in `Api._ParseAndCheckTwitter` Right now whenever the twitter API returns an un-parsable response that isn't explicitly known/handled, the TwitterException sets the message parameter to a `set` type, not a `dict` type or a `str` type (which is used in some cases elsewhere). This is causing errors for me as I'm inspecting this exception for graceful error handling, and received a `set` much to my surprise! Looks to me like a copy/paste bug. I'd like to have it be a `dict`, but if you think a `str` is more appropriate that works too! Thanks for the great library :) --- twitter/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twitter/api.py b/twitter/api.py index b1268bcf..b01a6272 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -4965,7 +4965,7 @@ def _ParseAndCheckTwitter(self, json_data): raise TwitterError({'message': "Exceeded connection limit for user"}) if "Error 401 Unauthorized" in json_data: raise TwitterError({'message': "Unauthorized"}) - raise TwitterError({'Unknown error': '{0}'.format(json_data)}) + raise TwitterError({'message': 'Unknown error': '{0}'.format(json_data)}) self._CheckForTwitterError(data) return data From 47bfb54f0413b14248692d2dcab323914eab354b Mon Sep 17 00:00:00 2001 From: Anthony Munoz Date: Tue, 5 Mar 2019 12:55:05 -0500 Subject: [PATCH 45/83] fix documentation typo and search parameters problem --- twitter/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index b1268bcf..53565911 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -451,7 +451,7 @@ def GetSearch(self, >>> # or: >>> api.GetSearch(geocode=("37.781157", "-122.398720", "1mi")) count (int, optional): - Number of results to return. Default is 15 and maxmimum that + Number of results to return. Default is 15 and maximum that Twitter returns is 100 irrespective of what you type in. lang (str, optional): Language for results as ISO 639-1 code. Default is None @@ -523,7 +523,7 @@ def GetSearch(self, url = "{url}?{raw_query}".format( url=url, raw_query=raw_query) - resp = self._RequestUrl(url, 'GET') + resp = self._RequestUrl(url, 'GET', data=parameters) else: resp = self._RequestUrl(url, 'GET', data=parameters) From 4711cd47935f1b85ca074c95eb09555f0e373025 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Sat, 9 Mar 2019 14:18:17 -0500 Subject: [PATCH 46/83] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d539fa58..c0557ba5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ future requests -requests_oauthlib +requests-oauthlib From b2319f757d9335903f523c302016ced124c4d4d6 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Sat, 9 Mar 2019 14:18:38 -0500 Subject: [PATCH 47/83] Update requirements.testing.txt --- requirements.testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.testing.txt b/requirements.testing.txt index a4a493ed..a71efed5 100644 --- a/requirements.testing.txt +++ b/requirements.testing.txt @@ -1,6 +1,6 @@ future requests -requests_oauthlib +requests-oauthlib responses pytest From 41253ba3d6e6df4186531183f74d69860f8ce096 Mon Sep 17 00:00:00 2001 From: sharkykh Date: Thu, 2 May 2019 00:35:44 +0300 Subject: [PATCH 48/83] Fix deprecation warnings - UserWarning: [pep8] section is deprecated. Use [pycodestyle]. - DeprecationWarning: Please use assertTrue instead. --- setup.cfg | 2 +- tests/test_filecache.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 1949d5c6..ffcde717 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,6 +13,6 @@ ignore = [flake8] ignore = E111,E124,E126,E221,E501 -[pep8] +[pycodestyle] ignore = E111,E124,E126,E221,E501 max-line-length = 100 diff --git a/tests/test_filecache.py b/tests/test_filecache.py index 5e3d19c2..36d5d4a5 100644 --- a/tests/test_filecache.py +++ b/tests/test_filecache.py @@ -7,7 +7,7 @@ class FileCacheTest(unittest.TestCase): def testInit(self): """Test the twitter._FileCache constructor""" cache = twitter._FileCache() - self.assert_(cache is not None, 'cache is None') + self.assertTrue(cache is not None, 'cache is None') def testSet(self): """Test the twitter._FileCache.Set method""" @@ -38,6 +38,6 @@ def testGetCachedTime(self): cache.Set("foo", 'Hello World!') cached_time = cache.GetCachedTime("foo") delta = cached_time - now - self.assert_(delta <= 1, - 'Cached time differs from clock time by more than 1 second.') + self.assertTrue(delta <= 1, + 'Cached time differs from clock time by more than 1 second.') cache.Remove("foo") From 61257c0a61fedac5ea332b473efb2cbcf75976cd Mon Sep 17 00:00:00 2001 From: sharkykh Date: Thu, 2 May 2019 01:08:45 +0300 Subject: [PATCH 49/83] Remove `future` dependency --- requirements.docs.txt | 1 - requirements.testing.txt | 1 - requirements.txt | 1 - setup.py | 2 +- 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/requirements.docs.txt b/requirements.docs.txt index d721b239..b6ef910d 100644 --- a/requirements.docs.txt +++ b/requirements.docs.txt @@ -1,4 +1,3 @@ -future requests requests-oauthlib sphinx diff --git a/requirements.testing.txt b/requirements.testing.txt index a71efed5..56c660d0 100644 --- a/requirements.testing.txt +++ b/requirements.testing.txt @@ -1,4 +1,3 @@ -future requests requests-oauthlib responses diff --git a/requirements.txt b/requirements.txt index c0557ba5..cbbb9376 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -future requests requests-oauthlib diff --git a/setup.py b/setup.py index 99635fee..1a0a4120 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def extract_metaitem(meta): download_url=extract_metaitem('download_url'), packages=find_packages(exclude=('tests', 'docs')), platforms=['Any'], - install_requires=['future', 'requests', 'requests-oauthlib'], + install_requires=['requests', 'requests-oauthlib'], setup_requires=['pytest-runner'], tests_require=['pytest'], keywords='twitter api', From d06a698773f5b8e520f8be954c83b2ec2ed13c8a Mon Sep 17 00:00:00 2001 From: Jeremy Low Date: Thu, 6 Jun 2019 07:13:30 -0400 Subject: [PATCH 50/83] change default tweet mode to extended --- twitter/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twitter/api.py b/twitter/api.py index b1268bcf..f1b7fb15 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -161,7 +161,7 @@ def __init__(self, debugHTTP=False, timeout=None, sleep_on_rate_limit=False, - tweet_mode='compat', + tweet_mode='extended', proxies=None): """Instantiate a new twitter.Api object. From 0bfc7796646ca9c58f5c192155a365ac0a579196 Mon Sep 17 00:00:00 2001 From: Cristiana S Parada Date: Thu, 13 Jun 2019 11:24:27 -0300 Subject: [PATCH 51/83] new screens from twitter dev new screenshots and instructions according to current twitter dev screens. --- doc/getting_started.rst | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/getting_started.rst b/doc/getting_started.rst index 40716d4f..bcc535e8 100644 --- a/doc/getting_started.rst +++ b/doc/getting_started.rst @@ -26,15 +26,21 @@ _________ Once your app is created, you'll be directed to a new page showing you some information about it. -.. image:: python-twitter-app-creation-part2.png +.. image:: python-twitter-app-creation-part2-new.png Your Keys _________ -Click on the "Keys and Access Tokens" tab on the top there, just under the green notification in the image above. +Click on the "Keys and Access Tokens" tab on the top. -.. image:: python-twitter-app-creation-part3.png +.. image:: python-twitter-app-creation-part3-new.png + + +Under the "Access token & access token secret" option, click on the "create" button to generate a new access token and token secret. + +.. image:: python-twitter-app-creation-part3-1-new.png + At this point, you can test out your application using the keys under "Your Application Tokens". The ``twitter.Api()`` object can be created as follows:: From ceb229bf330f182799c3ce9118a18e9cf30bce1a Mon Sep 17 00:00:00 2001 From: Cristiana S Parada Date: Thu, 13 Jun 2019 11:27:11 -0300 Subject: [PATCH 52/83] new screenshots new screenshots from twitter dev pages --- doc/python-twitter-app-creation-part2-new.png | Bin 0 -> 33655 bytes doc/python-twitter-app-creation-part3-1-new.png | Bin 0 -> 18861 bytes doc/python-twitter-app-creation-part3-new.png | Bin 0 -> 16921 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/python-twitter-app-creation-part2-new.png create mode 100644 doc/python-twitter-app-creation-part3-1-new.png create mode 100644 doc/python-twitter-app-creation-part3-new.png diff --git a/doc/python-twitter-app-creation-part2-new.png b/doc/python-twitter-app-creation-part2-new.png new file mode 100644 index 0000000000000000000000000000000000000000..f88e18b76e3267d3c6f933ad85dc7218b01b54ba GIT binary patch literal 33655 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 literal 0 HcmV?d00001 diff --git a/doc/python-twitter-app-creation-part3-1-new.png b/doc/python-twitter-app-creation-part3-1-new.png new file mode 100644 index 0000000000000000000000000000000000000000..3de5bf44233d730b31dc9bdb6ab1f034adc507d8 GIT binary patch literal 18861 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 A`2YX_ literal 0 HcmV?d00001 diff --git a/doc/python-twitter-app-creation-part3-new.png b/doc/python-twitter-app-creation-part3-new.png new file mode 100644 index 0000000000000000000000000000000000000000..739d6162f6bd1e7d86c657630a0ab09dbe661610 GIT binary patch literal 16921 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 Date: Mon, 17 Jun 2019 18:33:55 +0300 Subject: [PATCH 53/83] Some small updates --- twitter/api.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index f1b7fb15..08bd818b 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -248,20 +248,9 @@ def __init__(self, self.tweet_mode = tweet_mode self.proxies = proxies - if base_url is None: - self.base_url = 'https://api.twitter.com/1.1' - else: - self.base_url = base_url - - if stream_url is None: - self.stream_url = 'https://stream.twitter.com/1.1' - else: - self.stream_url = stream_url - - if upload_url is None: - self.upload_url = 'https://upload.twitter.com/1.1' - else: - self.upload_url = upload_url + self.base_url = base_url or 'https://api.twitter.com/1.1' + self.stream_url = stream_url or 'https://stream.twitter.com/1.1' + self.upload_url = upload_url or 'https://upload.twitter.com/1.1' self.chunk_size = chunk_size From 1fff6dc213714c0accc0fb96ec5cdc1bbf87f3b2 Mon Sep 17 00:00:00 2001 From: "Mark R. Masterson" Date: Wed, 31 Jul 2019 12:24:59 -0700 Subject: [PATCH 54/83] add external urls, base64 string options to UpdateBanner --- twitter/api.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index 08bd818b..0f7141f6 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -4490,7 +4490,7 @@ def UpdateImage(self, reflected due to image processing on Twitter's side. Args: - image (str): + image (str, optional): Location of local image file to use. include_entities (bool, optional): Include the entities node in the return data. @@ -4522,14 +4522,20 @@ def UpdateImage(self, raise TwitterError({'message': "The image could not be resized or is too large."}) def UpdateBanner(self, - image, + image=False, + external_image=False, + encoded_image=False, include_entities=False, skip_status=False): """Updates the authenticated users profile banner. Args: - image: - Location of image in file system + image (str, optional): + Location of local image in file system + external_image (str, optional): + URL of image + encoded_image (str, optional): + base64 string of an image include_entities: If True, each tweet will include a node called "entities." This node offers a variety of metadata about the tweet in a @@ -4540,8 +4546,12 @@ def UpdateBanner(self, A twitter.List instance representing the list subscribed to """ url = '%s/account/update_profile_banner.json' % (self.base_url) - with open(image, 'rb') as image_file: - encoded_image = base64.b64encode(image_file.read()) + if image: + with open(image, 'rb') as image_file: + encoded_image = base64.b64encode(image_file.read()) + if external_image: + content = self._RequestUrl(external_image, 'GET').content + encoded_image = base64.b64encode(content) data = { # When updated for API v1.1 use image, not banner # https://dev.twitter.com/docs/api/1.1/post/account/update_profile_banner From 81e39e3f75f1998dfd32b08f37797f3e23e0cdb5 Mon Sep 17 00:00:00 2001 From: Charles Reid <53452777+chmreid@users.noreply.github.com> Date: Sun, 20 Oct 2019 09:14:06 -0700 Subject: [PATCH 55/83] Clarify API creation example Closes #637 --- doc/getting_started.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/getting_started.rst b/doc/getting_started.rst index bcc535e8..6dd64af6 100644 --- a/doc/getting_started.rst +++ b/doc/getting_started.rst @@ -45,10 +45,10 @@ Under the "Access token & access token secret" option, click on the "create" but At this point, you can test out your application using the keys under "Your Application Tokens". The ``twitter.Api()`` object can be created as follows:: import twitter - api = twitter.Api(consumer_key=[consumer key], - consumer_secret=[consumer secret], - access_token_key=[access token], - access_token_secret=[access token secret]) + api = twitter.Api(consumer_key=, + consumer_secret=, + access_token_key=, + access_token_secret=) Note: Make sure to enclose your keys in quotes (ie, api = twitter.Api(consumer_key='1234567', ...) and so on) or you will receive a NameError. From 27cd204e5f823fea644db3f59baf1f4f78ca6a9c Mon Sep 17 00:00:00 2001 From: Dan Pope Date: Sun, 17 Nov 2019 21:52:10 +0000 Subject: [PATCH 56/83] Fix failing tests due to updated default tweet_mode --- tests/test_api_30.py | 78 ++++++++++++++++++++-------------------- tests/test_rate_limit.py | 10 +++--- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/tests/test_api_30.py b/tests/test_api_30.py index 7dfd7126..e80ac825 100644 --- a/tests/test_api_30.py +++ b/tests/test_api_30.py @@ -133,7 +133,7 @@ def testGetHomeTimeline(self): with open('testdata/get_home_timeline.json') as f: resp_data = f.read() responses.add( - GET, 'https://api.twitter.com/1.1/statuses/home_timeline.json?tweet_mode=compat', + GET, 'https://api.twitter.com/1.1/statuses/home_timeline.json?tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -219,7 +219,7 @@ def testGetBlocks(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/blocks/list.json?cursor=-1&stringify_ids=False&include_entities=False&skip_status=False&tweet_mode=compat', + 'https://api.twitter.com/1.1/blocks/list.json?cursor=-1&stringify_ids=False&include_entities=False&skip_status=False&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -227,7 +227,7 @@ def testGetBlocks(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/blocks/list.json?cursor=1524574483549312671&stringify_ids=False&include_entities=False&skip_status=False&tweet_mode=compat', + 'https://api.twitter.com/1.1/blocks/list.json?cursor=1524574483549312671&stringify_ids=False&include_entities=False&skip_status=False&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -278,7 +278,7 @@ def testGetBlocksIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/blocks/ids.json?cursor=-1&stringify_ids=False&include_entities=True&skip_status=False&tweet_mode=compat', + 'https://api.twitter.com/1.1/blocks/ids.json?cursor=-1&stringify_ids=False&include_entities=True&skip_status=False&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -286,7 +286,7 @@ def testGetBlocksIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/blocks/ids.json?cursor=1524566179872860311&stringify_ids=False&include_entities=True&skip_status=False&tweet_mode=compat', + 'https://api.twitter.com/1.1/blocks/ids.json?cursor=1524566179872860311&stringify_ids=False&include_entities=True&skip_status=False&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -325,7 +325,7 @@ def testGetFriendIDs(self): resp_data = f.read() responses.add( GET, - 'https://api.twitter.com/1.1/friends/ids.json?count=5000&cursor=-1&stringify_ids=False&screen_name=EricHolthaus&tweet_mode=compat', + 'https://api.twitter.com/1.1/friends/ids.json?count=5000&cursor=-1&stringify_ids=False&screen_name=EricHolthaus&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -335,7 +335,7 @@ def testGetFriendIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/friends/ids.json?stringify_ids=False&count=5000&cursor=1417903878302254556&screen_name=EricHolthaus&tweet_mode=compat', + 'https://api.twitter.com/1.1/friends/ids.json?stringify_ids=False&count=5000&cursor=1417903878302254556&screen_name=EricHolthaus&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -413,7 +413,7 @@ def testGetFriends(self): for i in range(0, 5): with open('testdata/get_friends_{0}.json'.format(i)) as f: resp_data = f.read() - endpoint = 'https://api.twitter.com/1.1/friends/list.json?count=200&tweet_mode=compat&include_user_entities=True&screen_name=codebear&skip_status=False&cursor={0}'.format(cursor) + endpoint = 'https://api.twitter.com/1.1/friends/list.json?count=200&tweet_mode=extended&include_user_entities=True&screen_name=codebear&skip_status=False&cursor={0}'.format(cursor) responses.add(GET, endpoint, body=resp_data, match_querystring=True) cursor = json.loads(resp_data)['next_cursor'] @@ -446,7 +446,7 @@ def testGetFollowersIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/followers/ids.json?tweet_mode=compat&cursor=-1&stringify_ids=False&count=5000&screen_name=GirlsMakeGames', + 'https://api.twitter.com/1.1/followers/ids.json?tweet_mode=extended&cursor=-1&stringify_ids=False&count=5000&screen_name=GirlsMakeGames', body=resp_data, match_querystring=True, status=200) @@ -456,7 +456,7 @@ def testGetFollowersIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/followers/ids.json?tweet_mode=compat&count=5000&screen_name=GirlsMakeGames&cursor=1482201362283529597&stringify_ids=False', + 'https://api.twitter.com/1.1/followers/ids.json?tweet_mode=extended&count=5000&screen_name=GirlsMakeGames&cursor=1482201362283529597&stringify_ids=False', body=resp_data, match_querystring=True, status=200) @@ -478,7 +478,7 @@ def testGetFollowers(self): resp_data = f.read() responses.add( responses.GET, - '{base_url}/followers/list.json?tweet_mode=compat&include_user_entities=True&count=200&screen_name=himawari8bot&skip_status=False&cursor=-1'.format( + '{base_url}/followers/list.json?tweet_mode=extended&include_user_entities=True&count=200&screen_name=himawari8bot&skip_status=False&cursor=-1'.format( base_url=self.api.base_url), body=resp_data, match_querystring=True, @@ -489,7 +489,7 @@ def testGetFollowers(self): resp_data = f.read() responses.add( responses.GET, - '{base_url}/followers/list.json?tweet_mode=compat&include_user_entities=True&skip_status=False&count=200&screen_name=himawari8bot&cursor=1516850034842747602'.format( + '{base_url}/followers/list.json?tweet_mode=extended&include_user_entities=True&skip_status=False&count=200&screen_name=himawari8bot&cursor=1516850034842747602'.format( base_url=self.api.base_url), body=resp_data, match_querystring=True, @@ -517,7 +517,7 @@ def testGetFollowerIDsPaged(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/followers/ids.json?tweet_mode=compat&count=5000&stringify_ids=False&cursor=-1&screen_name=himawari8bot', + 'https://api.twitter.com/1.1/followers/ids.json?tweet_mode=extended&count=5000&stringify_ids=False&cursor=-1&screen_name=himawari8bot', body=resp_data, match_querystring=True, status=200) @@ -533,7 +533,7 @@ def testGetFollowerIDsPaged(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/followers/ids.json?tweet_mode=compat&count=5000&stringify_ids=True&user_id=12&cursor=-1', + 'https://api.twitter.com/1.1/followers/ids.json?tweet_mode=extended&count=5000&stringify_ids=True&user_id=12&cursor=-1', body=resp_data, match_querystring=True, status=200) @@ -738,7 +738,7 @@ def testGetListsList(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/list.json?tweet_mode=compat', + 'https://api.twitter.com/1.1/lists/list.json?tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -751,7 +751,7 @@ def testGetListsList(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/list.json?tweet_mode=compat&screen_name=inky', + 'https://api.twitter.com/1.1/lists/list.json?tweet_mode=extended&screen_name=inky', body=resp_data, match_querystring=True, status=200) @@ -764,7 +764,7 @@ def testGetListsList(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/list.json?tweet_mode=compat&user_id=13148', + 'https://api.twitter.com/1.1/lists/list.json?tweet_mode=extended&user_id=13148', body=resp_data, match_querystring=True, status=200) @@ -793,7 +793,7 @@ def testGetListMembers(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/members.json?count=100&include_entities=False&skip_status=False&list_id=93527328&cursor=-1&tweet_mode=compat', + 'https://api.twitter.com/1.1/lists/members.json?count=100&include_entities=False&skip_status=False&list_id=93527328&cursor=-1&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -802,7 +802,7 @@ def testGetListMembers(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/members.json?list_id=93527328&skip_status=False&include_entities=False&count=100&tweet_mode=compat&cursor=4611686020936348428', + 'https://api.twitter.com/1.1/lists/members.json?list_id=93527328&skip_status=False&include_entities=False&count=100&tweet_mode=extended&cursor=4611686020936348428', body=resp_data, match_querystring=True, status=200) @@ -816,7 +816,7 @@ def testGetListMembersPaged(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/members.json?count=100&include_entities=True&cursor=4611686020936348428&list_id=93527328&skip_status=False&tweet_mode=compat', + 'https://api.twitter.com/1.1/lists/members.json?count=100&include_entities=True&cursor=4611686020936348428&list_id=93527328&skip_status=False&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -827,7 +827,7 @@ def testGetListMembersPaged(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/members.json?count=100&tweet_mode=compat&cursor=4611686020936348428&list_id=93527328&skip_status=True&include_entities=False', + 'https://api.twitter.com/1.1/lists/members.json?count=100&tweet_mode=extended&cursor=4611686020936348428&list_id=93527328&skip_status=True&include_entities=False', body=resp_data, match_querystring=True, status=200) @@ -844,7 +844,7 @@ def testGetListTimeline(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/statuses.json?&list_id=229581524&tweet_mode=compat', + 'https://api.twitter.com/1.1/lists/statuses.json?&list_id=229581524&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -855,7 +855,7 @@ def testGetListTimeline(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/statuses.json?owner_screen_name=notinourselves&slug=test&max_id=692980243339071488&tweet_mode=compat&since_id=692829211019575296', + 'https://api.twitter.com/1.1/lists/statuses.json?owner_screen_name=notinourselves&slug=test&max_id=692980243339071488&tweet_mode=extended&since_id=692829211019575296', body=resp_data, match_querystring=True, status=200) @@ -880,7 +880,7 @@ def testGetListTimeline(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/statuses.json?include_rts=False&count=13&tweet_mode=compat&include_entities=False&slug=test&owner_id=4012966701', + 'https://api.twitter.com/1.1/lists/statuses.json?include_rts=False&count=13&tweet_mode=extended&include_entities=False&slug=test&owner_id=4012966701', body=resp_data, match_querystring=True, status=200) @@ -958,7 +958,7 @@ def testShowSubscription(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/subscribers/show.json?tweet_mode=compat&user_id=4040207472&list_id=189643778', + 'https://api.twitter.com/1.1/lists/subscribers/show.json?tweet_mode=extended&user_id=4040207472&list_id=189643778', body=resp_data, match_querystring=True, status=200) @@ -974,7 +974,7 @@ def testShowSubscription(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/subscribers/show.json?list_id=189643778&tweet_mode=compat&screen_name=__jcbl__', + 'https://api.twitter.com/1.1/lists/subscribers/show.json?list_id=189643778&tweet_mode=extended&screen_name=__jcbl__', body=resp_data, match_querystring=True, status=200) @@ -989,7 +989,7 @@ def testShowSubscription(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/subscribers/show.json?include_entities=True&tweet_mode=compat&list_id=18964377&skip_status=True&screen_name=__jcbl__', + 'https://api.twitter.com/1.1/lists/subscribers/show.json?include_entities=True&tweet_mode=extended&list_id=18964377&skip_status=True&screen_name=__jcbl__', body=resp_data, match_querystring=True, status=200) @@ -1025,7 +1025,7 @@ def testGetMemberships(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/memberships.json?count=20&cursor=-1&tweet_mode=compat', + 'https://api.twitter.com/1.1/lists/memberships.json?count=20&cursor=-1&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -1037,7 +1037,7 @@ def testGetMemberships(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/memberships.json?count=20&cursor=-1&screen_name=himawari8bot&tweet_mode=compat', + 'https://api.twitter.com/1.1/lists/memberships.json?count=20&cursor=-1&screen_name=himawari8bot&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -1161,26 +1161,26 @@ def testLookupFriendship(self): responses.add( responses.GET, - 'https://api.twitter.com/1.1/friendships/lookup.json?user_id=12&tweet_mode=compat', + 'https://api.twitter.com/1.1/friendships/lookup.json?user_id=12&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) responses.add( responses.GET, - 'https://api.twitter.com/1.1/friendships/lookup.json?user_id=12,6385432&tweet_mode=compat', + 'https://api.twitter.com/1.1/friendships/lookup.json?user_id=12,6385432&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) responses.add( responses.GET, - 'https://api.twitter.com/1.1/friendships/lookup.json?screen_name=jack&tweet_mode=compat', + 'https://api.twitter.com/1.1/friendships/lookup.json?screen_name=jack&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) responses.add( responses.GET, - 'https://api.twitter.com/1.1/friendships/lookup.json?screen_name=jack,dickc&tweet_mode=compat', + 'https://api.twitter.com/1.1/friendships/lookup.json?screen_name=jack,dickc&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -1330,13 +1330,13 @@ def testGetStatusOembed(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/statuses/oembed.json?tweet_mode=compat&id=397', + 'https://api.twitter.com/1.1/statuses/oembed.json?tweet_mode=extended&id=397', body=resp_data, match_querystring=True, status=200) responses.add( responses.GET, - 'https://api.twitter.com/1.1/statuses/oembed.json?tweet_mode=compat&url=https://twitter.com/jack/statuses/397', + 'https://api.twitter.com/1.1/statuses/oembed.json?tweet_mode=extended&url=https://twitter.com/jack/statuses/397', body=resp_data, match_querystring=True, status=200) @@ -1366,7 +1366,7 @@ def testGetMutes(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/mutes/users/list.json?cursor=-1&stringify_ids=False&include_entities=True&skip_status=False&tweet_mode=compat', + 'https://api.twitter.com/1.1/mutes/users/list.json?cursor=-1&stringify_ids=False&include_entities=True&skip_status=False&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -1376,7 +1376,7 @@ def testGetMutes(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/mutes/users/list.json?cursor=1535206520056388207&stringify_ids=False&include_entities=True&skip_status=False&tweet_mode=compat', + 'https://api.twitter.com/1.1/mutes/users/list.json?cursor=1535206520056388207&stringify_ids=False&include_entities=True&skip_status=False&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -1391,7 +1391,7 @@ def testGetMutesIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/mutes/users/ids.json?cursor=-1&stringify_ids=False&include_entities=True&skip_status=False&tweet_mode=compat', + 'https://api.twitter.com/1.1/mutes/users/ids.json?cursor=-1&stringify_ids=False&include_entities=True&skip_status=False&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) @@ -1401,7 +1401,7 @@ def testGetMutesIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/mutes/users/ids.json?cursor=1535206520056565155&stringify_ids=False&include_entities=True&skip_status=False&tweet_mode=compat', + 'https://api.twitter.com/1.1/mutes/users/ids.json?cursor=1535206520056565155&stringify_ids=False&include_entities=True&skip_status=False&tweet_mode=extended', body=resp_data, match_querystring=True, status=200) diff --git a/tests/test_rate_limit.py b/tests/test_rate_limit.py index 26981630..52db1ca3 100644 --- a/tests/test_rate_limit.py +++ b/tests/test_rate_limit.py @@ -97,7 +97,7 @@ def setUp(self): with open('testdata/ratelimit.json') as f: resp_data = f.read() - url = '%s/application/rate_limit_status.json?tweet_mode=compat' % self.api.base_url + url = '%s/application/rate_limit_status.json?tweet_mode=extended' % self.api.base_url responses.add( responses.GET, url, @@ -213,12 +213,12 @@ def testLimitsViaHeadersWithSleep(self): sleep_on_rate_limit=True) # Add handler for ratelimit check - url = '%s/application/rate_limit_status.json?tweet_mode=compat' % api.base_url + url = '%s/application/rate_limit_status.json?tweet_mode=extended' % api.base_url responses.add( method=responses.GET, url=url, body='{}', match_querystring=True) # Get initial rate limit data to populate api.rate_limit object - url = "https://api.twitter.com/1.1/search/tweets.json?tweet_mode=compat&q=test&count=15&result_type=mixed" + url = "https://api.twitter.com/1.1/search/tweets.json?tweet_mode=extended&q=test&count=15&result_type=mixed" responses.add( method=responses.GET, url=url, @@ -242,14 +242,14 @@ def testLimitsViaHeadersWithSleepLimitReached(self): sleep_on_rate_limit=True) # Add handler for ratelimit check - this forces the codepath which goes through the time.sleep call - url = '%s/application/rate_limit_status.json?tweet_mode=compat' % api.base_url + url = '%s/application/rate_limit_status.json?tweet_mode=extended' % api.base_url responses.add( method=responses.GET, url=url, body='{"resources": {"search": {"/search/tweets": {"limit": 1, "remaining": 0, "reset": 1}}}}', match_querystring=True) # Get initial rate limit data to populate api.rate_limit object - url = "https://api.twitter.com/1.1/search/tweets.json?tweet_mode=compat&q=test&count=15&result_type=mixed" + url = "https://api.twitter.com/1.1/search/tweets.json?tweet_mode=extended&q=test&count=15&result_type=mixed" responses.add( method=responses.GET, url=url, From bbabc8f6d6332d7d50b5fd86e999d6865e27ebd9 Mon Sep 17 00:00:00 2001 From: Dan Pope Date: Sun, 17 Nov 2019 23:10:30 +0000 Subject: [PATCH 57/83] Allow subtitles to be uploaded as media --- testdata/subs.srt | 8 ++++++++ tests/test_twitter_utils.py | 7 +++++++ twitter/twitter_utils.py | 6 +++++- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 testdata/subs.srt diff --git a/testdata/subs.srt b/testdata/subs.srt new file mode 100644 index 00000000..c246531e --- /dev/null +++ b/testdata/subs.srt @@ -0,0 +1,8 @@ +1 +00:00:00,000 --> 00:00:03,251 +Lorem ipsum dolor sit amet, +consectetur adipiscing elit. + +2 +00:00:03,251 --> 00:00:06,502 +Maecenas rutrum suscipit accumsan. diff --git a/tests/test_twitter_utils.py b/tests/test_twitter_utils.py index 9e4934d8..cd162bd5 100644 --- a/tests/test_twitter_utils.py +++ b/tests/test_twitter_utils.py @@ -75,6 +75,13 @@ def test_parse_media_file_fileobj(self): self.assertEqual(file_size, 44772) self.assertEqual(media_type, 'image/jpeg') + def test_parse_media_file_subtitle(self): + data_file, filename, file_size, media_type = parse_media_file('testdata/subs.srt') + self.assertTrue(hasattr(data_file, 'read')) + self.assertEqual(filename, 'subs.srt') + self.assertEqual(file_size, 157) + self.assertEqual(media_type, 'text/srt') + def test_utils_error_checking(self): with open('testdata/168NQ.jpg', 'r') as f: self.assertRaises( diff --git a/twitter/twitter_utils.py b/twitter/twitter_utils.py index 3bd5d411..a4a07a36 100644 --- a/twitter/twitter_utils.py +++ b/twitter/twitter_utils.py @@ -243,6 +243,7 @@ def parse_media_file(passed_media, async_upload=False): long_img_formats = [ 'image/gif' ] + subtitle_formats = ['text/srt'] video_formats = ['video/mp4', 'video/quicktime'] @@ -274,6 +275,9 @@ def parse_media_file(passed_media, async_upload=False): pass media_type = mimetypes.guess_type(os.path.basename(filename))[0] + # The .srt extension is not recognised by the mimetypes module. + if os.path.basename(filename).endswith('.srt'): + media_type = 'text/srt' if media_type is not None: if media_type in img_formats and file_size > 5 * 1048576: raise TwitterError({'message': 'Images must be less than 5MB.'}) @@ -283,7 +287,7 @@ def parse_media_file(passed_media, async_upload=False): raise TwitterError({'message': 'Videos must be less than 15MB.'}) elif media_type in video_formats and async_upload and file_size > 512 * 1048576: raise TwitterError({'message': 'Videos must be less than 512MB.'}) - elif media_type not in img_formats and media_type not in video_formats and media_type not in long_img_formats: + elif media_type not in img_formats + long_img_formats + subtitle_formats + video_formats: raise TwitterError({'message': 'Media type could not be determined.'}) return data_file, filename, file_size, media_type From 4913e4421f36b0c3a937786d993459f57c5474df Mon Sep 17 00:00:00 2001 From: Dan Pope Date: Sun, 24 Nov 2019 16:20:57 +0000 Subject: [PATCH 58/83] Add /media/subtitles/create and delete endpoints --- tests/test_api_30.py | 81 ++++++++++++++++++++++++++++++++++++++++++++ twitter/api.py | 75 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/tests/test_api_30.py b/tests/test_api_30.py index e80ac825..1cd09f49 100644 --- a/tests/test_api_30.py +++ b/tests/test_api_30.py @@ -1548,6 +1548,87 @@ def testPostUploadMediaChunkedFinalize(self): self.assertEqual(len(responses.calls), 1) self.assertTrue(resp) + @responses.activate + def testPostMediaSubtitlesCreateSuccess(self): + responses.add( + POST, + 'https://upload.twitter.com/1.1/media/subtitles/create.json', + body=b'', + status=200) + expected_body = { + 'media_id': '1234', + 'media_category': 'TweetVideo', + 'subtitle_info': { + 'subtitles': [{ + 'media_id': '5678', + 'language_code': 'en', + 'display_name': 'English' + }] + } + } + resp = self.api.PostMediaSubtitlesCreate(video_media_id=1234, + subtitle_media_id=5678, + language_code='en', + display_name='English') + + self.assertEqual(len(responses.calls), 1) + self.assertEqual(responses.calls[0].request.url, + 'https://upload.twitter.com/1.1/media/subtitles/create.json') + request_body = json.loads(responses.calls[0].request.body.decode('utf-8')) + self.assertTrue(resp) + self.assertDictEqual(expected_body, request_body) + + @responses.activate + def testPostMediaSubtitlesCreateFailure(self): + responses.add( + POST, + 'https://upload.twitter.com/1.1/media/subtitles/create.json', + body=b'{"error":"Some error happened"}', + status=400) + self.assertRaises( + twitter.TwitterError, + lambda: self.api.PostMediaSubtitlesCreate(video_media_id=1234, + subtitle_media_id=5678, + language_code='en', + display_name='English')) + + @responses.activate + def testPostMediaSubtitlesDeleteSuccess(self): + responses.add( + POST, + 'https://upload.twitter.com/1.1/media/subtitles/delete.json', + body=b'', + status=200) + expected_body = { + 'media_id': '1234', + 'media_category': 'TweetVideo', + 'subtitle_info': { + 'subtitles': [{ + 'language_code': 'en' + }] + } + } + resp = self.api.PostMediaSubtitlesDelete(video_media_id=1234, language_code='en') + + self.assertEqual(len(responses.calls), 1) + self.assertEqual(responses.calls[0].request.url, + 'https://upload.twitter.com/1.1/media/subtitles/delete.json') + request_body = json.loads(responses.calls[0].request.body.decode('utf-8')) + self.assertTrue(resp) + self.assertDictEqual(expected_body, request_body) + + @responses.activate + def testPostMediaSubtitlesDeleteFailure(self): + responses.add( + POST, + 'https://upload.twitter.com/1.1/media/subtitles/delete.json', + body=b'{"error":"Some error happened"}', + status=400) + self.assertRaises( + twitter.TwitterError, + lambda: self.api.PostMediaSubtitlesDelete(video_media_id=1234, + language_code='en')) + @responses.activate def testGetUserSuggestionCategories(self): with open('testdata/get_user_suggestion_categories.json') as f: diff --git a/twitter/api.py b/twitter/api.py index 18d5e708..62c836fd 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -1424,6 +1424,81 @@ def UploadMediaChunked(self, except KeyError: raise TwitterError('Media could not be uploaded.') + def PostMediaSubtitlesCreate(self, + video_media_id, + subtitle_media_id, + language_code, + display_name): + """Associate uploaded subtitles to an uploaded video. You can associate + subtitles to a video before or after Tweeting. + + Args: + video_media_id (int): + Media ID of the uploaded video to add the subtitles to. The + video must have been uploaded using the category 'TweetVideo'. + subtitle_media_id (int): + Media ID of the uploaded subtitle file. The subtitles myst have + been uploaded using the category 'Subtitles'. + language_code (str): + The language code that the subtitles are written in. The + language code should be a BCP47 code (e.g. 'en', 'sp') + display_name (str): + Language name (e.g. 'English', 'Spanish') + + Returns: + True if successful. Raises otherwise. + """ + url = '%s/media/subtitles/create.json' % self.upload_url + + subtitle = {} + subtitle['media_id'] = str(subtitle_media_id) + subtitle['language_code'] = language_code + subtitle['display_name'] = display_name + parameters = {} + parameters['media_id'] = str(video_media_id) + parameters['media_category'] = 'TweetVideo' + parameters['subtitle_info'] = {} + parameters['subtitle_info']['subtitles'] = [subtitle] + + resp = self._RequestUrl(url, 'POST', json=parameters) + # Response body should be blank, so only do error checking if the response is not blank. + if resp.content.decode('utf-8'): + self._ParseAndCheckTwitter(resp.content.decode('utf-8')) + + return True + + def PostMediaSubtitlesDelete(self, + video_media_id, + language_code): + """Remove subtitles from an uploaded video. + + Args: + video_media_id (int): + Media ID of the video for which the subtitles will be removed. + language_code (str): + The language code of the subtitle file that should be deleted. + The language code should be a BCP47 code (e.g. 'en', 'sp') + + Returns: + True if successful. Raises otherwise. + """ + url = '%s/media/subtitles/delete.json' % self.upload_url + + subtitle = {} + subtitle['language_code'] = language_code + parameters = {} + parameters['media_id'] = str(video_media_id) + parameters['media_category'] = 'TweetVideo' + parameters['subtitle_info'] = {} + parameters['subtitle_info']['subtitles'] = [subtitle] + + resp = self._RequestUrl(url, 'POST', json=parameters) + # Response body should be blank, so only do error checking if the response is not blank. + if resp.content.decode('utf-8'): + self._ParseAndCheckTwitter(resp.content.decode('utf-8')) + + return True + def _TweetTextWrap(self, status, char_lim=CHARACTER_LIMIT): From c94744bdd4d935da6abeb0257c60be8f8f4c9512 Mon Sep 17 00:00:00 2001 From: Dan Pope Date: Sun, 24 Nov 2019 16:32:12 +0000 Subject: [PATCH 59/83] Update documentation wording and broken link --- twitter/api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index 62c836fd..13d88799 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -1028,15 +1028,15 @@ def PostUpdate(self, attachment_url=None): """Post a twitter status message from the authenticated user. - https://dev.twitter.com/docs/api/1.1/post/statuses/update + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update Args: status (str): The message text to be posted. Must be less than or equal to CHARACTER_LIMIT characters. media (int, str, fp, optional): - A URL, a local file, or a file-like object (something with a - read() method), or a list of any combination of the above. + A media ID, URL, local file, or file-like object (something with + a read() method), or a list of any combination of the above. media_additional_owners (list, optional): A list of user ids representing Twitter users that should be able to use the uploaded media in their tweets. If you pass a list of @@ -1398,7 +1398,7 @@ def UploadMediaChunked(self, number of additional owners is capped at 100 by Twitter. media_category: Category with which to identify media upload. Only use with Ads - API & video files. + API, video files and subtitles. Returns: media_id: From 5afb1563c79e9efe912df98e00c44ae4fdfdf710 Mon Sep 17 00:00:00 2001 From: Martin Kolman Date: Tue, 14 Jan 2020 02:20:15 +0100 Subject: [PATCH 60/83] Add documented but missing count to GetLists() The GetLists() method documents the count parameter, which is actually not available in the method signature and can't be used. Due to this, the GetLists() method is basically limited to retrieving ~300 lists at once, as the internally called GetListsPaged() method will retrieve only 20 lists at once and will hit rate limits after 15 calls in a row. So add "count" to the method signature and pass it to GetListsPaged(). Also add a note about rate limits & hint to bump "count" once they start to be hit. With this change a caller that passes say 500 as "count" (tested) can retrieve 300+ lists without unnecessarily hitting rate limits. --- twitter/api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index 13d88799..40b94857 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -4470,7 +4470,8 @@ def GetListsPaged(self, def GetLists(self, user_id=None, - screen_name=None): + screen_name=None, + count=20): """Fetch the sequence of lists for a user. If no user_id or screen_name is passed, the data returned will be for the authenticated user. @@ -4482,6 +4483,8 @@ def GetLists(self, for. [Optional] count: The amount of results to return per page. + Consider bumping this up if you are getting rate limit issues + with GetLists(), generally at > 15 * 20 = 300 lists. No more than 1000 results will ever be returned in a single page. Defaults to 20. [Optional] cursor: @@ -4500,7 +4503,8 @@ def GetLists(self, next_cursor, prev_cursor, lists = self.GetListsPaged( user_id=user_id, screen_name=screen_name, - cursor=cursor) + cursor=cursor, + count=count) result += lists if next_cursor == 0 or next_cursor == prev_cursor: break From eecccff4b2886ca00c062b4139169d0db4a64cf6 Mon Sep 17 00:00:00 2001 From: Miguel Sozinho Ramalho <19508417+msramalho@users.noreply.github.com> Date: Fri, 14 Feb 2020 15:13:12 +0000 Subject: [PATCH 61/83] fixed inconsistent naming of variable GetRetweets used to have `statusid` instead of `status_id` --- twitter/api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index 13d88799..0cbd8be9 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -1676,14 +1676,14 @@ def GetReplies(self, exclude_replies=False, include_rts=False) def GetRetweets(self, - statusid, + status_id, count=None, trim_user=False): """Returns up to 100 of the first retweets of the tweet identified - by statusid + by status_id Args: - statusid (int): + status_id (int): The ID of the tweet for which retweets should be searched for count (int, optional): The number of status messages to retrieve. @@ -1692,9 +1692,9 @@ def GetRetweets(self, otherwise the payload will contain the full user data item. Returns: - A list of twitter.Status instances, which are retweets of statusid + A list of twitter.Status instances, which are retweets of status_id """ - url = '%s/statuses/retweets/%s.json' % (self.base_url, statusid) + url = '%s/statuses/retweets/%s.json' % (self.base_url, status_id) parameters = { 'trim_user': enf_type('trim_user', bool, trim_user), } From 71a33d4e07caa3bab64d0113cd86b56b6426c30f Mon Sep 17 00:00:00 2001 From: Simon Bohnen Date: Tue, 24 Mar 2020 09:36:02 +0100 Subject: [PATCH 62/83] Update documentation for GetListMembers --- twitter/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index 13d88799..a09b5851 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -4261,10 +4261,10 @@ def GetListMembers(self, user_timeline. Helpful for disambiguating when a valid screen name is also a user ID. skip_status (bool, optional): - If True the statuses will not be returned in the user items. + If True the statuses will not be returned in the user items. Defaults to False. include_entities (bool, optional): If False, the timeline will not contain additional metadata. - Defaults to True. + Defaults to False. Returns: list: A sequence of twitter.user.User instances, one for each From 6931e9a064c647319e6de12968dd5bee718fc913 Mon Sep 17 00:00:00 2001 From: Stephen Cowley Date: Sat, 11 Apr 2020 17:28:05 -0600 Subject: [PATCH 63/83] Update getting_started.rst --- doc/getting_started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/getting_started.rst b/doc/getting_started.rst index 6dd64af6..b5044a18 100644 --- a/doc/getting_started.rst +++ b/doc/getting_started.rst @@ -52,6 +52,6 @@ At this point, you can test out your application using the keys under "Your Appl Note: Make sure to enclose your keys in quotes (ie, api = twitter.Api(consumer_key='1234567', ...) and so on) or you will receive a NameError. -If you are creating an application for end users/consumers, then you will want them to authorize you application, but that is outside the scope of this document. +If you are creating an application for end users/consumers, then you will want them to authorize your application, but that is outside the scope of this document. And that should be it! If you need a little more help, check out the `examples on Github `_. If you have an open source application using python-twitter, send us a link and we'll add a link to it here. From 29647e6cb2a050e9e9c4a02c865d0231e2e497fc Mon Sep 17 00:00:00 2001 From: Robert Huselius Date: Wed, 29 Apr 2020 23:44:03 +0200 Subject: [PATCH 64/83] Always add tweet_mode to API requests --- .gitignore | 6 ++++++ twitter/api.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7ef3b53c..f5955911 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,9 @@ violations.flake8.txt # Built docs doc/_build/** + +# Mypy cache +**/.mypy_cache + +# VS Code +**/.vscode diff --git a/twitter/api.py b/twitter/api.py index 13d88799..986b35f5 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -5107,6 +5107,8 @@ def _RequestUrl(self, url, verb, data=None, json=None, enforce_auth=True): if not data: data = {} + data['tweet_mode'] = self.tweet_mode + if verb == 'POST': if data: if 'media_ids' in data: @@ -5122,7 +5124,6 @@ def _RequestUrl(self, url, verb, data=None, json=None, enforce_auth=True): resp = 0 # POST request, but without data or json elif verb == 'GET': - data['tweet_mode'] = self.tweet_mode url = self._BuildUrl(url, extra_params=data) resp = self._session.get(url, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) From 56ce285c04bf96a1beade0275bbbfadd2c3b6275 Mon Sep 17 00:00:00 2001 From: Thomas Braccia Date: Tue, 12 May 2020 13:57:19 -0600 Subject: [PATCH 65/83] Added code to check if it is a gif and over 5MB and if so notify twitter that it is a gif so that it will be uploaded. --- twitter/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/twitter/api.py b/twitter/api.py index 13d88799..868d7b5b 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -1277,6 +1277,10 @@ def _UploadMediaChunkedInit(self, parameters['media_type'] = media_type parameters['total_bytes'] = file_size + # Without this GIF files over 5MB but less than 15MB will fail uploading. + if media_type == 'image/gif' and file_size > 5242880: + parameters['media_category'] = 'tweet_gif' + resp = self._RequestUrl(url, 'POST', data=parameters) data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) From fa1b12a68151fbe6df89d8739b9d3e83e318049f Mon Sep 17 00:00:00 2001 From: Armin Samii Date: Mon, 31 Aug 2020 19:41:20 -0400 Subject: [PATCH 66/83] Remove redundant PostDirectMessage it was listed twice --- twitter/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/twitter/api.py b/twitter/api.py index 13d88799..2c9a5a48 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -120,7 +120,6 @@ class Api(object): There are many other methods, including: >>> api.PostUpdates(status) - >>> api.PostDirectMessage(user, text) >>> api.GetUser(user) >>> api.GetReplies() >>> api.GetUserTimeline(user) From 913644a0030095798cc99dfbcf801bd398c335e7 Mon Sep 17 00:00:00 2001 From: BennyThink Date: Tue, 1 Dec 2020 13:10:59 +0800 Subject: [PATCH 67/83] replace mimetypes to filetype mimetypes relies on file extension, while filetype relies on file header which is more stable. --- requirements.txt | 1 + setup.py | 2 +- twitter/twitter_utils.py | 7 ++----- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index cbbb9376..fc12ad28 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests requests-oauthlib +filetype diff --git a/setup.py b/setup.py index 1a0a4120..38062f47 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def extract_metaitem(meta): download_url=extract_metaitem('download_url'), packages=find_packages(exclude=('tests', 'docs')), platforms=['Any'], - install_requires=['requests', 'requests-oauthlib'], + install_requires=['requests', 'requests-oauthlib', 'filetype'], setup_requires=['pytest-runner'], tests_require=['pytest'], keywords='twitter api', diff --git a/twitter/twitter_utils.py b/twitter/twitter_utils.py index a4a07a36..02e1ded3 100644 --- a/twitter/twitter_utils.py +++ b/twitter/twitter_utils.py @@ -2,7 +2,7 @@ """Collection of utilities for use in API calls, functions.""" from __future__ import unicode_literals -import mimetypes +import filetype import os import re import sys @@ -274,10 +274,7 @@ def parse_media_file(passed_media, async_upload=False): except Exception as e: pass - media_type = mimetypes.guess_type(os.path.basename(filename))[0] - # The .srt extension is not recognised by the mimetypes module. - if os.path.basename(filename).endswith('.srt'): - media_type = 'text/srt' + media_type = filetype.guess_mime(data_file.name) if media_type is not None: if media_type in img_formats and file_size > 5 * 1048576: raise TwitterError({'message': 'Images must be less than 5MB.'}) From dc449cc39d63a4fdb354bf9165611b49816aa50d Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Thu, 24 Dec 2020 17:51:45 +1100 Subject: [PATCH 68/83] docs: fix simple typo, unsuccesful -> unsuccessful There is a small typo in twitter/api.py. Should read `unsuccessful` rather than `unsuccesful`. --- twitter/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twitter/api.py b/twitter/api.py index 13d88799..c6e96212 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -1403,7 +1403,7 @@ def UploadMediaChunked(self, Returns: media_id: ID of the uploaded media returned by the Twitter API. Raises if - unsuccesful. + unsuccessful. """ media_id, media_fp, filename = self._UploadMediaChunkedInit(media=media, From 2b99c95677d12ec4152696bdd7b12c341895ee81 Mon Sep 17 00:00:00 2001 From: Oliver Cardoza Date: Sun, 27 Dec 2020 02:32:12 -0500 Subject: [PATCH 69/83] fixed "ambiguous error name" error blocking tests --- tests/test_api_30.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_api_30.py b/tests/test_api_30.py index 1cd09f49..d6e349e9 100644 --- a/tests/test_api_30.py +++ b/tests/test_api_30.py @@ -1017,7 +1017,7 @@ def testGetSubscriptionsSN(self): resp = self.api.GetSubscriptions(screen_name='inky') self.assertEqual(len(resp), 20) - self.assertTrue([isinstance(l, twitter.List) for l in resp]) + self.assertTrue([isinstance(lst, twitter.List) for lst in resp]) @responses.activate def testGetMemberships(self): @@ -1289,7 +1289,7 @@ def testGetStatuses(self): rsps.add(GET, DEFAULT_URL, body=resp_data) with open('testdata/get_statuses.ids.txt') as f: - status_ids = [int(l) for l in f] + status_ids = [int(line) for line in f] resp = self.api.GetStatuses(status_ids) @@ -1312,7 +1312,7 @@ def testGetStatusesMap(self): rsps.add(GET, DEFAULT_URL, body=resp_data) with open('testdata/get_statuses.ids.txt') as f: - status_ids = [int(l) for l in f] + status_ids = [int(line) for line in f] resp = self.api.GetStatuses(status_ids, map=True) From c0160881349a17fe9ae353656f42fb599a2ac692 Mon Sep 17 00:00:00 2001 From: Oliver Cardoza Date: Sun, 27 Dec 2020 02:57:24 -0500 Subject: [PATCH 70/83] Fix issue where GetListMembers would only return the first 100 members. The Twitter API appears to return the same value for `next_cursor` and `previous_cursor` even when there is another page. I've updated the test data to reflect reality. Then I added test assertions to verify pages are concatenated. Finally applied fix to ensure test passes. --- testdata/get_list_members_0.json | 2 +- tests/test_api_30.py | 6 +++++- twitter/api.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/testdata/get_list_members_0.json b/testdata/get_list_members_0.json index aa1488dd..651cce5c 100644 --- a/testdata/get_list_members_0.json +++ b/testdata/get_list_members_0.json @@ -1 +1 @@ -{"next_cursor": 4611686020936348428, "users": [{"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 24, "profile_image_url": "http://pbs.twimg.com/profile_images/659410881806135296/PdVxDc0W_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 362, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 4048395140, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 106, "friends_count": 0, "location": "", "description": "I am a bot that simulates a series of mechanical linkages (+ noise) to draw a curve 4x/day. // by @tinysubversions, inspired by @ra & @bahrami_", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/659410881806135296/PdVxDc0W_normal.png", "geo_enabled": false, "time_zone": null, "name": "Spinny Machine", "id_str": "4048395140", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "animated_gif", "video_info": {"variants": [{"content_type": "video/mp4", "bitrate": 0, "url": "https://pbs.twimg.com/tweet_video/CZ9xAlyWQAAVsZk.mp4"}], "aspect_ratio": [1, 1]}, "id": 693397122595569664, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CZ9xAlyWQAAVsZk.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CZ9xAlyWQAAVsZk.png", "display_url": "pic.twitter.com/n6lbayOFFQ", "indices": [30, 53], "expanded_url": "http://twitter.com/spinnymachine/status/693397123023396864/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 360, "h": 360}, "small": {"resize": "fit", "w": 340, "h": 340}, "medium": {"resize": "fit", "w": 360, "h": 360}}, "id_str": "693397122595569664", "url": "https://t.co/n6lbayOFFQ"}]}, "truncated": false, "id": 693397123023396864, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "a casually scorched northeast https://t.co/n6lbayOFFQ", "id_str": "693397123023396864", "entities": {"media": [{"type": "photo", "id": 693397122595569664, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CZ9xAlyWQAAVsZk.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CZ9xAlyWQAAVsZk.png", "display_url": "pic.twitter.com/n6lbayOFFQ", "indices": [30, 53], "expanded_url": "http://twitter.com/spinnymachine/status/693397123023396864/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 360, "h": 360}, "small": {"resize": "fit", "w": 340, "h": 340}, "medium": {"resize": "fit", "w": 360, "h": 360}}, "id_str": "693397122595569664", "url": "https://t.co/n6lbayOFFQ"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Spinny Machine", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 11:35:31 +0000 2016"}, "is_translator": false, "screen_name": "spinnymachine", "created_at": "Wed Oct 28 16:43:01 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 6, "profile_image_url": "http://pbs.twimg.com/profile_images/658781950472138752/FOQCSbLg_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "https://t.co/OOS2jbeYND", "statuses_count": 135, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 4029020052, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "DBAF44", "followers_count": 23, "friends_count": 2, "location": "TV", "profile_banner_url": "https://pbs.twimg.com/profile_banners/4029020052/1445900976", "description": "I'm a bot that tweets fake Empire plots, inspired by @eveewing https://t.co/OOS2jbeYND // by @tinysubversions", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/658781950472138752/FOQCSbLg_normal.png", "geo_enabled": false, "time_zone": null, "name": "Empire Plots Bot", "id_str": "4029020052", "entities": {"description": {"urls": [{"display_url": "twitter.com/eveewing/statu\u2026", "indices": [63, 86], "expanded_url": "https://twitter.com/eveewing/status/658478802327183360", "url": "https://t.co/OOS2jbeYND"}]}, "url": {"urls": [{"display_url": "twitter.com/eveewing/statu\u2026", "indices": [0, 23], "expanded_url": "https://twitter.com/eveewing/status/658478802327183360", "url": "https://t.co/OOS2jbeYND"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "photo", "id": 671831157646876672, "media_url": "http://pbs.twimg.com/media/CVLS4NyU4AAshFm.png", "media_url_https": "https://pbs.twimg.com/media/CVLS4NyU4AAshFm.png", "display_url": "pic.twitter.com/EQ8oGhG502", "indices": [101, 124], "expanded_url": "http://twitter.com/EmpirePlots/status/671831157739118593/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 300, "h": 400}, "small": {"resize": "fit", "w": 300, "h": 400}, "large": {"resize": "fit", "w": 300, "h": 400}}, "id_str": "671831157646876672", "url": "https://t.co/EQ8oGhG502"}]}, "truncated": false, "id": 671831157739118593, "place": null, "favorite_count": 1, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Jamal is stuck in a wild forest with Kene Holliday and can't find their way out (it's just a dream). https://t.co/EQ8oGhG502", "id_str": "671831157739118593", "entities": {"media": [{"type": "photo", "id": 671831157646876672, "media_url": "http://pbs.twimg.com/media/CVLS4NyU4AAshFm.png", "media_url_https": "https://pbs.twimg.com/media/CVLS4NyU4AAshFm.png", "display_url": "pic.twitter.com/EQ8oGhG502", "indices": [101, 124], "expanded_url": "http://twitter.com/EmpirePlots/status/671831157739118593/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 300, "h": 400}, "small": {"resize": "fit", "w": 300, "h": 400}, "large": {"resize": "fit", "w": 300, "h": 400}}, "id_str": "671831157646876672", "url": "https://t.co/EQ8oGhG502"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Empire Plots Bot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Tue Dec 01 23:20:04 +0000 2015"}, "is_translator": false, "screen_name": "EmpirePlots", "created_at": "Mon Oct 26 22:49:42 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 25, "profile_image_url": "http://pbs.twimg.com/profile_images/652238580765462528/BQVTvFS9_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 346, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 3829470974, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "027F45", "followers_count": 287, "friends_count": 1, "location": "Portland, OR", "profile_banner_url": "https://pbs.twimg.com/profile_banners/3829470974/1444340849", "description": "Portland is such a weird place! We show real pics of places in Portland! ONLY IN PORTLAND as they say! // a bot by @tinysubversions, 4x daily", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/652238580765462528/BQVTvFS9_normal.png", "geo_enabled": false, "time_zone": null, "name": "Wow So Portland!", "id_str": "3829470974", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "photo", "id": 693471873166761984, "media_url": "http://pbs.twimg.com/media/CZ-0_pXUYAAGzn4.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-0_pXUYAAGzn4.jpg", "display_url": "pic.twitter.com/CF8JrGCQcY", "indices": [57, 80], "expanded_url": "http://twitter.com/wowsoportland/status/693471873246502912/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 600, "h": 300}, "small": {"resize": "fit", "w": 340, "h": 170}, "medium": {"resize": "fit", "w": 600, "h": 300}}, "id_str": "693471873166761984", "url": "https://t.co/CF8JrGCQcY"}]}, "truncated": false, "id": 693471873246502912, "place": null, "favorite_count": 1, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "those silly Portlanders are always up to something funny https://t.co/CF8JrGCQcY", "id_str": "693471873246502912", "entities": {"media": [{"type": "photo", "id": 693471873166761984, "media_url": "http://pbs.twimg.com/media/CZ-0_pXUYAAGzn4.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-0_pXUYAAGzn4.jpg", "display_url": "pic.twitter.com/CF8JrGCQcY", "indices": [57, 80], "expanded_url": "http://twitter.com/wowsoportland/status/693471873246502912/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 600, "h": 300}, "small": {"resize": "fit", "w": 340, "h": 170}, "medium": {"resize": "fit", "w": 600, "h": 300}}, "id_str": "693471873166761984", "url": "https://t.co/CF8JrGCQcY"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Wow so portland", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 16:32:33 +0000 2016"}, "is_translator": false, "screen_name": "wowsoportland", "created_at": "Thu Oct 08 21:38:27 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 28, "profile_image_url": "http://pbs.twimg.com/profile_images/650161232540909568/yyvPEOnF_normal.jpg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "https://t.co/8fYJI1TWEs", "statuses_count": 515, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 3765991992, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "ABB8C2", "followers_count": 331, "friends_count": 1, "location": "Mare Tranquilitatis", "profile_banner_url": "https://pbs.twimg.com/profile_banners/3765991992/1443845573", "description": "Tweeting pics from the Project Apollo Archive, four times a day. Not affiliated with the Project Apollo Archive. // a bot by @tinysubversions", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": -28800, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/650161232540909568/yyvPEOnF_normal.jpg", "geo_enabled": false, "time_zone": "Pacific Time (US & Canada)", "name": "Moon Shot Bot", "id_str": "3765991992", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "flickr.com/photos/project\u2026", "indices": [0, 23], "expanded_url": "https://www.flickr.com/photos/projectapolloarchive/", "url": "https://t.co/8fYJI1TWEs"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "photo", "id": 693470001316171776, "media_url": "http://pbs.twimg.com/media/CZ-zSsLXEAAhEia.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-zSsLXEAAhEia.jpg", "display_url": "pic.twitter.com/2j5ezW6i9G", "indices": [103, 126], "expanded_url": "http://twitter.com/moonshotbot/status/693470003249684481/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 1024, "h": 1070}, "small": {"resize": "fit", "w": 340, "h": 355}, "medium": {"resize": "fit", "w": 600, "h": 627}}, "id_str": "693470001316171776", "url": "https://t.co/2j5ezW6i9G"}]}, "truncated": false, "id": 693470003249684481, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "\ud83c\udf0c\ud83c\udf18\ud83c\udf1b\nApollo 15 Hasselblad image from film magazine 97/O - lunar orbit view\n\ud83d\ude80\ud83c\udf1a\ud83c\udf19\n https://t.co/n4WH1ZTyuZ https://t.co/2j5ezW6i9G", "id_str": "693470003249684481", "entities": {"media": [{"type": "photo", "id": 693470001316171776, "media_url": "http://pbs.twimg.com/media/CZ-zSsLXEAAhEia.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-zSsLXEAAhEia.jpg", "display_url": "pic.twitter.com/2j5ezW6i9G", "indices": [103, 126], "expanded_url": "http://twitter.com/moonshotbot/status/693470003249684481/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 1024, "h": 1070}, "small": {"resize": "fit", "w": 340, "h": 355}, "medium": {"resize": "fit", "w": 600, "h": 627}}, "id_str": "693470001316171776", "url": "https://t.co/2j5ezW6i9G"}], "hashtags": [], "urls": [{"display_url": "flickr.com/photos/project\u2026", "indices": [79, 102], "expanded_url": "https://www.flickr.com/photos/projectapolloarchive/21831461788", "url": "https://t.co/n4WH1ZTyuZ"}], "symbols": [], "user_mentions": []}, "source": "Moon Shot Bot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 16:25:07 +0000 2016"}, "is_translator": false, "screen_name": "moonshotbot", "created_at": "Sat Oct 03 04:03:02 +0000 2015"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 17, "profile_image_url": "http://pbs.twimg.com/profile_images/629374160993710081/q-lr9vsE_normal.jpg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/mRUwkqVO7i", "statuses_count": 598, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 3406094211, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 133, "friends_count": 0, "location": "Not affiliated with the FBI", "description": "I tweet random pages from FOIA-requested FBI records, 4x/day. I'm a bot by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/629374160993710081/q-lr9vsE_normal.jpg", "geo_enabled": false, "time_zone": null, "name": "FBI Bot", "id_str": "3406094211", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "vault.fbi.gov", "indices": [0, 22], "expanded_url": "http://vault.fbi.gov", "url": "http://t.co/mRUwkqVO7i"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "photo", "id": 693483330080210944, "media_url": "http://pbs.twimg.com/media/CZ-_ahsWAAADnwA.png", "media_url_https": "https://pbs.twimg.com/media/CZ-_ahsWAAADnwA.png", "display_url": "pic.twitter.com/Dey5JLxCvb", "indices": [63, 86], "expanded_url": "http://twitter.com/FBIbot/status/693483330218622976/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 600, "h": 776}, "small": {"resize": "fit", "w": 340, "h": 439}, "large": {"resize": "fit", "w": 1000, "h": 1294}}, "id_str": "693483330080210944", "url": "https://t.co/Dey5JLxCvb"}]}, "truncated": false, "id": 693483330218622976, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "The FBI was watching Fred G. Randaccio https://t.co/NAkQc4FYKp https://t.co/Dey5JLxCvb", "id_str": "693483330218622976", "entities": {"media": [{"type": "photo", "id": 693483330080210944, "media_url": "http://pbs.twimg.com/media/CZ-_ahsWAAADnwA.png", "media_url_https": "https://pbs.twimg.com/media/CZ-_ahsWAAADnwA.png", "display_url": "pic.twitter.com/Dey5JLxCvb", "indices": [63, 86], "expanded_url": "http://twitter.com/FBIbot/status/693483330218622976/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 600, "h": 776}, "small": {"resize": "fit", "w": 340, "h": 439}, "large": {"resize": "fit", "w": 1000, "h": 1294}}, "id_str": "693483330080210944", "url": "https://t.co/Dey5JLxCvb"}], "hashtags": [], "urls": [{"display_url": "vault.fbi.gov/frank-randaccio", "indices": [39, 62], "expanded_url": "https://vault.fbi.gov/frank-randaccio", "url": "https://t.co/NAkQc4FYKp"}], "symbols": [], "user_mentions": []}, "source": "FBIBot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 17:18:04 +0000 2016"}, "is_translator": false, "screen_name": "FBIbot", "created_at": "Thu Aug 06 19:28:10 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 23, "profile_image_url": "http://pbs.twimg.com/profile_images/631231593164607488/R4hRHjBI_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/6cpr8h0hGa", "statuses_count": 664, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 3312790286, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "4A913C", "followers_count": 239, "friends_count": 0, "location": "", "description": "Animal videos sourced from @macaulaylibrary. Turn up the sound! // A bot by @tinysubversions. Not affiliated with the Macaulay Library.", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/631231593164607488/R4hRHjBI_normal.png", "geo_enabled": false, "time_zone": null, "name": "Animal Video Bot", "id_str": "3312790286", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "macaulaylibrary.org", "indices": [0, 22], "expanded_url": "http://macaulaylibrary.org/", "url": "http://t.co/6cpr8h0hGa"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "ro", "extended_entities": {"media": [{"type": "video", "video_info": {"variants": [{"content_type": "video/webm", "bitrate": 832000, "url": "https://video.twimg.com/ext_tw_video/693439580935094273/pu/vid/640x360/hY01KGCSXl-isZzt.webm"}, {"content_type": "video/mp4", "bitrate": 320000, "url": "https://video.twimg.com/ext_tw_video/693439580935094273/pu/vid/320x180/3NEGIMyzX2tdBm5i.mp4"}, {"content_type": "video/mp4", "bitrate": 832000, "url": "https://video.twimg.com/ext_tw_video/693439580935094273/pu/vid/640x360/hY01KGCSXl-isZzt.mp4"}, {"content_type": "application/x-mpegURL", "url": "https://video.twimg.com/ext_tw_video/693439580935094273/pu/pl/G53mlN6oslnMAWd5.m3u8"}, {"content_type": "application/dash+xml", "url": "https://video.twimg.com/ext_tw_video/693439580935094273/pu/pl/G53mlN6oslnMAWd5.mpd"}], "aspect_ratio": [16, 9], "duration_millis": 20021}, "id": 693439580935094273, "media_url": "http://pbs.twimg.com/ext_tw_video_thumb/693439580935094273/pu/img/K0BdyKh0qQc3P5_N.jpg", "media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/693439580935094273/pu/img/K0BdyKh0qQc3P5_N.jpg", "display_url": "pic.twitter.com/aaGNPmPpni", "indices": [85, 108], "expanded_url": "http://twitter.com/AnimalVidBot/status/693439595946479617/video/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 640, "h": 360}, "small": {"resize": "fit", "w": 340, "h": 191}, "medium": {"resize": "fit", "w": 600, "h": 338}}, "id_str": "693439580935094273", "url": "https://t.co/aaGNPmPpni"}]}, "truncated": false, "id": 693439595946479617, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Maui Parrotbill\n\nPseudonestor xanthophrys\nBarksdale, Timothy https://t.co/c6n9M2Cjnv https://t.co/aaGNPmPpni", "id_str": "693439595946479617", "entities": {"media": [{"type": "photo", "id": 693439580935094273, "media_url": "http://pbs.twimg.com/ext_tw_video_thumb/693439580935094273/pu/img/K0BdyKh0qQc3P5_N.jpg", "media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/693439580935094273/pu/img/K0BdyKh0qQc3P5_N.jpg", "display_url": "pic.twitter.com/aaGNPmPpni", "indices": [85, 108], "expanded_url": "http://twitter.com/AnimalVidBot/status/693439595946479617/video/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 640, "h": 360}, "small": {"resize": "fit", "w": 340, "h": 191}, "medium": {"resize": "fit", "w": 600, "h": 338}}, "id_str": "693439580935094273", "url": "https://t.co/aaGNPmPpni"}], "hashtags": [], "urls": [{"display_url": "macaulaylibrary.org/video/428118", "indices": [61, 84], "expanded_url": "http://macaulaylibrary.org/video/428118", "url": "https://t.co/c6n9M2Cjnv"}], "symbols": [], "user_mentions": []}, "source": "Animal Video Bot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 14:24:17 +0000 2016"}, "is_translator": false, "screen_name": "AnimalVidBot", "created_at": "Tue Aug 11 22:25:35 +0000 2015"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 10, "profile_image_url": "http://pbs.twimg.com/profile_images/624358417818238976/CfSPOEr4_normal.jpg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/YEWmunbcFZ", "statuses_count": 4, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 3300809963, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 56, "friends_count": 0, "location": "The Most Relevant Ad Agency", "description": "The most happenin' new trends on the internet. // A bot by @tinysubversions.", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/624358417818238976/CfSPOEr4_normal.jpg", "geo_enabled": false, "time_zone": null, "name": "Trend Reportz", "id_str": "3300809963", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "slideshare.net/trendreportz", "indices": [0, 22], "expanded_url": "http://www.slideshare.net/trendreportz", "url": "http://t.co/YEWmunbcFZ"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 1, "lang": "en", "truncated": false, "id": 638389840472600576, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "NEW REPORT! Learn what steamers mean to 7-18 y/o men http://t.co/veQGWz1Lqn", "id_str": "638389840472600576", "entities": {"hashtags": [], "urls": [{"display_url": "slideshare.net/trendreportz/t\u2026", "indices": [53, 75], "expanded_url": "http://www.slideshare.net/trendreportz/trends-in-steamers", "url": "http://t.co/veQGWz1Lqn"}], "symbols": [], "user_mentions": []}, "source": "Trend Reportz", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Mon Aug 31 16:36:13 +0000 2015"}, "is_translator": false, "screen_name": "TrendReportz", "created_at": "Wed May 27 19:43:37 +0000 2015"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 14, "profile_image_url": "http://pbs.twimg.com/profile_images/585540228439498752/OPHSe1Yw_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/lrCYkDGPrm", "statuses_count": 888, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 3145355109, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 106, "friends_count": 1, "location": "The Middle East", "profile_banner_url": "https://pbs.twimg.com/profile_banners/3145355109/1428438665", "description": "Public domain photos from the Qatar Digital Library of Middle Eastern (& nearby) history. Tweets 4x/day. // bot by @tinysubversions, not affiliated with the QDL", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/585540228439498752/OPHSe1Yw_normal.png", "geo_enabled": false, "time_zone": null, "name": "Middle East History", "id_str": "3145355109", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "qdl.qa/en/search/site\u2026", "indices": [0, 22], "expanded_url": "http://www.qdl.qa/en/search/site/?f%5B0%5D=document_source%3Aarchive_source", "url": "http://t.co/lrCYkDGPrm"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "photo", "id": 693479308619300866, "media_url": "http://pbs.twimg.com/media/CZ-7wclWQAIpxlu.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-7wclWQAIpxlu.jpg", "display_url": "pic.twitter.com/xojYCSnUZu", "indices": [72, 95], "expanded_url": "http://twitter.com/MidEastHistory/status/693479308745113600/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 1024, "h": 1024}, "small": {"resize": "fit", "w": 340, "h": 340}, "medium": {"resize": "fit", "w": 600, "h": 600}}, "id_str": "693479308619300866", "url": "https://t.co/xojYCSnUZu"}]}, "truncated": false, "id": 693479308745113600, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "'Distant View of Hormuz.' Photographer: Unknown https://t.co/hkDmSbTLgT https://t.co/xojYCSnUZu", "id_str": "693479308745113600", "entities": {"media": [{"type": "photo", "id": 693479308619300866, "media_url": "http://pbs.twimg.com/media/CZ-7wclWQAIpxlu.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-7wclWQAIpxlu.jpg", "display_url": "pic.twitter.com/xojYCSnUZu", "indices": [72, 95], "expanded_url": "http://twitter.com/MidEastHistory/status/693479308745113600/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 1024, "h": 1024}, "small": {"resize": "fit", "w": 340, "h": 340}, "medium": {"resize": "fit", "w": 600, "h": 600}}, "id_str": "693479308619300866", "url": "https://t.co/xojYCSnUZu"}], "hashtags": [], "urls": [{"display_url": "qdl.qa//en/archive/81\u2026", "indices": [48, 71], "expanded_url": "http://www.qdl.qa//en/archive/81055/vdc_100024111424.0x00000f", "url": "https://t.co/hkDmSbTLgT"}], "symbols": [], "user_mentions": []}, "source": "Mid east history pics", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 17:02:06 +0000 2016"}, "is_translator": false, "screen_name": "MidEastHistory", "created_at": "Tue Apr 07 20:27:15 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 99, "profile_image_url": "http://pbs.twimg.com/profile_images/584076161325473793/gufAEGJv_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/s6OmUwb6Bn", "statuses_count": 91531, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 3131670665, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "ABB8C2", "followers_count": 46186, "friends_count": 1, "location": "Hogwarts", "description": "I'm the Sorting Hat and I'm here to say / I love sorting students in a major way // a bot by @tinysubversions, follow to get sorted!", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 1, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/584076161325473793/gufAEGJv_normal.png", "geo_enabled": false, "time_zone": null, "name": "The Sorting Hat Bot", "id_str": "3131670665", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "tinysubversions.com/notes/sorting-\u2026", "indices": [0, 22], "expanded_url": "http://tinysubversions.com/notes/sorting-bot/", "url": "http://t.co/s6OmUwb6Bn"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "truncated": false, "id": 693474779018362880, "place": null, "favorite_count": 0, "in_reply_to_status_id_str": null, "in_reply_to_user_id": 1210222826, "in_reply_to_status_id": null, "in_reply_to_screen_name": "Nyrfall", "text": "@Nyrfall The Sorting time is here at last, I know each time you send\nI've figured out that Slytherin's the right place for your blend", "id_str": "693474779018362880", "entities": {"hashtags": [], "urls": [], "symbols": [], "user_mentions": [{"indices": [0, 8], "id": 1210222826, "name": "Desi Sobrino", "screen_name": "Nyrfall", "id_str": "1210222826"}]}, "source": "The Sorting Hat Bot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": "1210222826", "retweeted": false, "created_at": "Sat Jan 30 16:44:06 +0000 2016"}, "is_translator": false, "screen_name": "SortingBot", "created_at": "Fri Apr 03 19:27:31 +0000 2015"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 34, "profile_image_url": "http://pbs.twimg.com/profile_images/580173179236196352/nWsIPbqH_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/2YPE0x0Knw", "statuses_count": 1233, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 3105672877, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 424, "friends_count": 0, "location": "NYC", "profile_banner_url": "https://pbs.twimg.com/profile_banners/3105672877/1427159206", "description": "Posting random flyers from hip hop's formative years every 6 hours. Most art by Buddy Esquire and Phase 2. Full collection at link. // a bot by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/580173179236196352/nWsIPbqH_normal.png", "geo_enabled": false, "time_zone": null, "name": "Old School Flyers", "id_str": "3105672877", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "toledohiphop.org/images/old_sch\u2026", "indices": [0, 22], "expanded_url": "http://www.toledohiphop.org/images/old_school_source_code/", "url": "http://t.co/2YPE0x0Knw"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "und", "extended_entities": {"media": [{"type": "photo", "id": 693422418480742400, "media_url": "http://pbs.twimg.com/media/CZ-IBATWQAAhkZA.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-IBATWQAAhkZA.jpg", "display_url": "pic.twitter.com/B7jv7lwB9S", "indices": [0, 23], "expanded_url": "http://twitter.com/oldschoolflyers/status/693422418929524736/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 503, "h": 646}, "small": {"resize": "fit", "w": 340, "h": 435}, "medium": {"resize": "fit", "w": 503, "h": 646}}, "id_str": "693422418480742400", "url": "https://t.co/B7jv7lwB9S"}]}, "truncated": false, "id": 693422418929524736, "place": null, "favorite_count": 1, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "https://t.co/B7jv7lwB9S", "id_str": "693422418929524736", "entities": {"media": [{"type": "photo", "id": 693422418480742400, "media_url": "http://pbs.twimg.com/media/CZ-IBATWQAAhkZA.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-IBATWQAAhkZA.jpg", "display_url": "pic.twitter.com/B7jv7lwB9S", "indices": [0, 23], "expanded_url": "http://twitter.com/oldschoolflyers/status/693422418929524736/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 503, "h": 646}, "small": {"resize": "fit", "w": 340, "h": 435}, "medium": {"resize": "fit", "w": 503, "h": 646}}, "id_str": "693422418480742400", "url": "https://t.co/B7jv7lwB9S"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "oldschoolflyers", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 13:16:02 +0000 2016"}, "is_translator": false, "screen_name": "oldschoolflyers", "created_at": "Tue Mar 24 00:55:10 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 41, "profile_image_url": "http://pbs.twimg.com/profile_images/561971159927771136/sEQ5u1zM_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 1396, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 3010688583, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "961EE4", "followers_count": 243, "friends_count": 1, "location": "", "profile_banner_url": "https://pbs.twimg.com/profile_banners/3010688583/1422819642", "description": "DAD: weird conversation joke is bae ME: ugh dad no DAD: [something unhip] // a bot by @tinysubversions", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/561971159927771136/sEQ5u1zM_normal.png", "geo_enabled": false, "time_zone": null, "name": "Weird Convo Bot", "id_str": "3010688583", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "truncated": false, "id": 693429980093620224, "place": null, "favorite_count": 0, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "DOG: Wtf are you bae'\nME: fart. Ugh.\nDOG: so sex with me is sex vape\nME: Wtf are you skeleton'", "id_str": "693429980093620224", "entities": {"hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "WeirdConvoBot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 13:46:05 +0000 2016"}, "is_translator": false, "screen_name": "WeirdConvoBot", "created_at": "Sun Feb 01 19:05:21 +0000 2015"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 110, "profile_image_url": "http://pbs.twimg.com/profile_images/556129692554493953/82ISdQxF_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/3gDtETAFpu", "statuses_count": 1510, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 2981339967, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 1308, "friends_count": 1, "location": "Frankfurt School", "profile_banner_url": "https://pbs.twimg.com/profile_banners/2981339967/1421426775", "description": "We love #innovation and are always thinking of the next #disruptive startup #idea! // a bot by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 1, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/556129692554493953/82ISdQxF_normal.png", "geo_enabled": false, "time_zone": null, "name": "Hottest Startups", "id_str": "2981339967", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "marxists.org/archive/index.\u2026", "indices": [0, 22], "expanded_url": "http://www.marxists.org/archive/index.htm", "url": "http://t.co/3gDtETAFpu"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "truncated": false, "id": 693477791883350016, "place": null, "favorite_count": 0, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Startup idea: The architect thinks of the building contractor as a layman who tells him what he needs and what he can pay.", "id_str": "693477791883350016", "entities": {"hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "hottest startups", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 16:56:04 +0000 2016"}, "is_translator": false, "screen_name": "HottestStartups", "created_at": "Fri Jan 16 16:33:45 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 40, "profile_image_url": "http://pbs.twimg.com/profile_images/549979314541039617/fZ_XDnWz_normal.jpeg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 6748, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 2951486632, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "FFCC4D", "followers_count": 3155, "friends_count": 1, "location": "(bot by @tinysubversions)", "description": "We are the Academy For Annual Recognition and each year we give out the Yearly Awards. Follow (or re-follow) for your award! Warning: sometimes it's mean.", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 2, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/549979314541039617/fZ_XDnWz_normal.jpeg", "geo_enabled": false, "time_zone": null, "name": "The Yearly Awards", "id_str": "2951486632", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "animated_gif", "video_info": {"variants": [{"content_type": "video/mp4", "bitrate": 0, "url": "https://pbs.twimg.com/tweet_video/CZ-2l2fWwAEH6MB.mp4"}], "aspect_ratio": [4, 3]}, "id": 693473629036789761, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CZ-2l2fWwAEH6MB.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CZ-2l2fWwAEH6MB.png", "display_url": "pic.twitter.com/XGDD3tMJPF", "indices": [86, 109], "expanded_url": "http://twitter.com/YearlyAwards/status/693473629766598656/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 400, "h": 300}, "small": {"resize": "fit", "w": 340, "h": 255}, "large": {"resize": "fit", "w": 400, "h": 300}}, "id_str": "693473629036789761", "url": "https://t.co/XGDD3tMJPF"}]}, "truncated": false, "id": 693473629766598656, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": 4863901738, "in_reply_to_status_id": null, "in_reply_to_screen_name": "know_fast", "text": "@know_fast It's official: you're the Least Prodigiously Despondent Confidant of 2015! https://t.co/XGDD3tMJPF", "id_str": "693473629766598656", "entities": {"media": [{"type": "photo", "id": 693473629036789761, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CZ-2l2fWwAEH6MB.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CZ-2l2fWwAEH6MB.png", "display_url": "pic.twitter.com/XGDD3tMJPF", "indices": [86, 109], "expanded_url": "http://twitter.com/YearlyAwards/status/693473629766598656/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 400, "h": 300}, "small": {"resize": "fit", "w": 340, "h": 255}, "large": {"resize": "fit", "w": 400, "h": 300}}, "id_str": "693473629036789761", "url": "https://t.co/XGDD3tMJPF"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": [{"indices": [0, 10], "id": 4863901738, "name": "Know Fast", "screen_name": "know_fast", "id_str": "4863901738"}]}, "source": "The Yearly Awards", "contributors": null, "favorited": false, "in_reply_to_user_id_str": "4863901738", "retweeted": false, "created_at": "Sat Jan 30 16:39:32 +0000 2016"}, "is_translator": false, "screen_name": "YearlyAwards", "created_at": "Tue Dec 30 16:59:34 +0000 2014"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 20, "profile_image_url": "http://pbs.twimg.com/profile_images/512673377283080194/eFnJQJSp_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 1972, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 2817629347, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "ABB8C2", "followers_count": 93, "friends_count": 1, "location": "", "profile_banner_url": "https://pbs.twimg.com/profile_banners/2817629347/1411065932", "description": "Wise sayings, four times daily. by @tinysubversions", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/512673377283080194/eFnJQJSp_normal.png", "geo_enabled": false, "time_zone": null, "name": "Received Wisdom", "id_str": "2817629347", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "truncated": false, "id": 693418402392719360, "place": null, "favorite_count": 0, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Water is thicker than blood.", "id_str": "693418402392719360", "entities": {"hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Received Wisdom", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 13:00:04 +0000 2016"}, "is_translator": false, "screen_name": "received_wisdom", "created_at": "Thu Sep 18 18:32:38 +0000 2014"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 11, "profile_image_url": "http://pbs.twimg.com/profile_images/517404591218900992/kf2iYD1f_normal.jpeg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 1767, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 2798799669, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "4A913C", "followers_count": 40, "friends_count": 0, "location": "Everywhere", "profile_banner_url": "https://pbs.twimg.com/profile_banners/2798799669/1412195008", "description": "Chronicling men doing things. // A bot by @tinysubversions, Powered By Giphy", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/517404591218900992/kf2iYD1f_normal.jpeg", "geo_enabled": false, "time_zone": null, "name": "Men Doing Things", "id_str": "2798799669", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "animated_gif", "video_info": {"variants": [{"content_type": "video/mp4", "bitrate": 0, "url": "https://pbs.twimg.com/tweet_video/CZ-WORAWQAED6It.mp4"}], "aspect_ratio": [167, 104]}, "id": 693438039465541633, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CZ-WORAWQAED6It.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CZ-WORAWQAED6It.png", "display_url": "pic.twitter.com/wsk2GyEsGh", "indices": [37, 60], "expanded_url": "http://twitter.com/MenDoing/status/693438039801073664/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 334, "h": 208}, "small": {"resize": "fit", "w": 334, "h": 208}, "large": {"resize": "fit", "w": 334, "h": 208}}, "id_str": "693438039465541633", "url": "https://t.co/wsk2GyEsGh"}]}, "truncated": false, "id": 693438039801073664, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Men Authoring Landmarks in Manhattan https://t.co/wsk2GyEsGh", "id_str": "693438039801073664", "entities": {"media": [{"type": "photo", "id": 693438039465541633, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CZ-WORAWQAED6It.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CZ-WORAWQAED6It.png", "display_url": "pic.twitter.com/wsk2GyEsGh", "indices": [37, 60], "expanded_url": "http://twitter.com/MenDoing/status/693438039801073664/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 334, "h": 208}, "small": {"resize": "fit", "w": 334, "h": 208}, "large": {"resize": "fit", "w": 334, "h": 208}}, "id_str": "693438039465541633", "url": "https://t.co/wsk2GyEsGh"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Men Doing Things", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 14:18:06 +0000 2016"}, "is_translator": false, "screen_name": "MenDoing", "created_at": "Wed Oct 01 19:59:55 +0000 2014"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 139, "profile_image_url": "http://pbs.twimg.com/profile_images/495988901790482432/le2-dKgs_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/r2HzjsqHTU", "statuses_count": 2157, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 2704554914, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 1172, "friends_count": 1, "location": "", "profile_banner_url": "https://pbs.twimg.com/profile_banners/2704554914/1407087962", "description": "A bot that picks a word and then draws randomly until an OCR library (http://t.co/XmDeI5TWoF) reads that word. 4x daily. Also on Tumblr. // by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/495988901790482432/le2-dKgs_normal.png", "geo_enabled": false, "time_zone": null, "name": "Reverse OCR", "id_str": "2704554914", "entities": {"description": {"urls": [{"display_url": "antimatter15.com/ocrad.js/demo.\u2026", "indices": [70, 92], "expanded_url": "http://antimatter15.com/ocrad.js/demo.html", "url": "http://t.co/XmDeI5TWoF"}]}, "url": {"urls": [{"display_url": "reverseocr.tumblr.com", "indices": [0, 22], "expanded_url": "http://reverseocr.tumblr.com", "url": "http://t.co/r2HzjsqHTU"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "und", "extended_entities": {"media": [{"type": "photo", "id": 693404391072776192, "media_url": "http://pbs.twimg.com/media/CZ93nq-WwAAdSAo.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ93nq-WwAAdSAo.jpg", "display_url": "pic.twitter.com/WbD9lkNarf", "indices": [8, 31], "expanded_url": "http://twitter.com/reverseocr/status/693404391160860673/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 600, "h": 150}, "small": {"resize": "fit", "w": 340, "h": 85}, "large": {"resize": "fit", "w": 800, "h": 200}}, "id_str": "693404391072776192", "url": "https://t.co/WbD9lkNarf"}]}, "truncated": false, "id": 693404391160860673, "place": null, "favorite_count": 2, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "larceny https://t.co/WbD9lkNarf", "id_str": "693404391160860673", "entities": {"media": [{"type": "photo", "id": 693404391072776192, "media_url": "http://pbs.twimg.com/media/CZ93nq-WwAAdSAo.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ93nq-WwAAdSAo.jpg", "display_url": "pic.twitter.com/WbD9lkNarf", "indices": [8, 31], "expanded_url": "http://twitter.com/reverseocr/status/693404391160860673/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 600, "h": 150}, "small": {"resize": "fit", "w": 340, "h": 85}, "large": {"resize": "fit", "w": 800, "h": 200}}, "id_str": "693404391072776192", "url": "https://t.co/WbD9lkNarf"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Reverse OCR", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 12:04:24 +0000 2016"}, "is_translator": false, "screen_name": "reverseocr", "created_at": "Sun Aug 03 17:26:28 +0000 2014"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 5, "profile_image_url": "http://pbs.twimg.com/profile_images/479836451182362624/0fAtv_AN_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 2, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 2577963498, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 14, "friends_count": 1, "location": "", "description": "Deploying GIFs every six hours. // by @tinysubvesions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/479836451182362624/0fAtv_AN_normal.png", "geo_enabled": false, "time_zone": null, "name": "GIF Deployer", "id_str": "2577963498", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "animated_gif", "video_info": {"variants": [{"content_type": "video/mp4", "bitrate": 0, "url": "https://pbs.twimg.com/tweet_video/BqkTB2fIEAA8zCs.mp4"}], "aspect_ratio": [1, 1]}, "id": 479935757818531840, "media_url": "http://pbs.twimg.com/tweet_video_thumb/BqkTB2fIEAA8zCs.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/BqkTB2fIEAA8zCs.png", "display_url": "pic.twitter.com/WEYISUSsJR", "indices": [21, 43], "expanded_url": "http://twitter.com/gifDeployer/status/479935760033153024/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 600, "h": 600}, "small": {"resize": "fit", "w": 340, "h": 340}, "large": {"resize": "fit", "w": 612, "h": 612}}, "id_str": "479935757818531840", "url": "http://t.co/WEYISUSsJR"}]}, "truncated": false, "id": 479935760033153024, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Vietnamese decadence http://t.co/WEYISUSsJR", "id_str": "479935760033153024", "entities": {"media": [{"type": "photo", "id": 479935757818531840, "media_url": "http://pbs.twimg.com/tweet_video_thumb/BqkTB2fIEAA8zCs.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/BqkTB2fIEAA8zCs.png", "display_url": "pic.twitter.com/WEYISUSsJR", "indices": [21, 43], "expanded_url": "http://twitter.com/gifDeployer/status/479935760033153024/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 600, "h": 600}, "small": {"resize": "fit", "w": 340, "h": 340}, "large": {"resize": "fit", "w": 612, "h": 612}}, "id_str": "479935757818531840", "url": "http://t.co/WEYISUSsJR"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "GIF Deployer", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Fri Jun 20 10:36:16 +0000 2014"}, "is_translator": false, "screen_name": "gifDeployer", "created_at": "Fri Jun 20 03:56:13 +0000 2014"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 48, "profile_image_url": "http://pbs.twimg.com/profile_images/479364877551538176/HN0wLHbt_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/XJeqwaDhQg", "statuses_count": 11970, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 2575445382, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 428, "friends_count": 1, "location": "", "description": "Generating new aesthetics every hour. Botpunk. // by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": -18000, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/479364877551538176/HN0wLHbt_normal.png", "geo_enabled": false, "time_zone": "Eastern Time (US & Canada)", "name": "Brand New Aesthetics", "id_str": "2575445382", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "brand-new-aesthetics.tumblr.com", "indices": [0, 22], "expanded_url": "http://brand-new-aesthetics.tumblr.com", "url": "http://t.co/XJeqwaDhQg"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "da", "extended_entities": {"media": [{"type": "animated_gif", "video_info": {"variants": [{"content_type": "video/mp4", "bitrate": 0, "url": "https://pbs.twimg.com/tweet_video/CX5BF-lWMAEyoPA.mp4"}], "aspect_ratio": [3, 2]}, "id": 684055764361687041, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CX5BF-lWMAEyoPA.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CX5BF-lWMAEyoPA.png", "display_url": "pic.twitter.com/d4ZGIYqyt9", "indices": [13, 36], "expanded_url": "http://twitter.com/neweraesthetics/status/684055764768522240/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 300, "h": 200}, "small": {"resize": "fit", "w": 300, "h": 200}, "large": {"resize": "fit", "w": 300, "h": 200}}, "id_str": "684055764361687041", "url": "https://t.co/d4ZGIYqyt9"}]}, "truncated": false, "id": 684055764768522240, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "SOLUTIONPUNK https://t.co/d4ZGIYqyt9", "id_str": "684055764768522240", "entities": {"media": [{"type": "photo", "id": 684055764361687041, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CX5BF-lWMAEyoPA.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CX5BF-lWMAEyoPA.png", "display_url": "pic.twitter.com/d4ZGIYqyt9", "indices": [13, 36], "expanded_url": "http://twitter.com/neweraesthetics/status/684055764768522240/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 300, "h": 200}, "small": {"resize": "fit", "w": 300, "h": 200}, "large": {"resize": "fit", "w": 300, "h": 200}}, "id_str": "684055764361687041", "url": "https://t.co/d4ZGIYqyt9"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Brand New Aesthetics", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Mon Jan 04 16:56:18 +0000 2016"}, "is_translator": false, "screen_name": "neweraesthetics", "created_at": "Wed Jun 18 20:39:25 +0000 2014"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 12, "profile_image_url": "http://pbs.twimg.com/profile_images/479355596076892160/p_jT5KqM_normal.jpeg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/DZYA6d8tU5", "statuses_count": 8587, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 2575407888, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 113, "friends_count": 1, "location": "Bodymore, Murdaland", "description": "This Twitter account automatically tweets GIFs of The Wire. Also a Tumblr. Updates hourly. // by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 1, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/479355596076892160/p_jT5KqM_normal.jpeg", "geo_enabled": false, "time_zone": null, "name": "Scenes from The Wire", "id_str": "2575407888", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "wirescenes.tumblr.com", "indices": [0, 22], "expanded_url": "http://wirescenes.tumblr.com/", "url": "http://t.co/DZYA6d8tU5"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "und", "extended_entities": {"media": [{"type": "animated_gif", "video_info": {"variants": [{"content_type": "video/mp4", "bitrate": 0, "url": "https://pbs.twimg.com/tweet_video/COXAcgVU8AACS4W.mp4"}], "aspect_ratio": [132, 119]}, "id": 641130117918420992, "media_url": "http://pbs.twimg.com/tweet_video_thumb/COXAcgVU8AACS4W.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/COXAcgVU8AACS4W.png", "display_url": "pic.twitter.com/XBoaB2klZq", "indices": [0, 22], "expanded_url": "http://twitter.com/wirescenes/status/641130118132314112/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 264, "h": 238}, "small": {"resize": "fit", "w": 264, "h": 238}, "medium": {"resize": "fit", "w": 264, "h": 238}}, "id_str": "641130117918420992", "url": "http://t.co/XBoaB2klZq"}]}, "truncated": false, "id": 641130118132314112, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "http://t.co/XBoaB2klZq", "id_str": "641130118132314112", "entities": {"media": [{"type": "photo", "id": 641130117918420992, "media_url": "http://pbs.twimg.com/tweet_video_thumb/COXAcgVU8AACS4W.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/COXAcgVU8AACS4W.png", "display_url": "pic.twitter.com/XBoaB2klZq", "indices": [0, 22], "expanded_url": "http://twitter.com/wirescenes/status/641130118132314112/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 264, "h": 238}, "small": {"resize": "fit", "w": 264, "h": 238}, "medium": {"resize": "fit", "w": 264, "h": 238}}, "id_str": "641130117918420992", "url": "http://t.co/XBoaB2klZq"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Scenes from The Wire", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Tue Sep 08 06:05:06 +0000 2015"}, "is_translator": false, "screen_name": "wirescenes", "created_at": "Wed Jun 18 20:08:31 +0000 2014"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 229, "profile_image_url": "http://pbs.twimg.com/profile_images/468570294253150208/DlK5sGe2_normal.jpeg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/qTVWPkaIgo", "statuses_count": 2026, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 2508960524, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 4176, "friends_count": 1, "location": "(not affiliated with the Met)", "description": "I am a bot that tweets a random high-res Open Access image from the Metropolitan Museum of Art, four times a day. // by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 3, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/468570294253150208/DlK5sGe2_normal.jpeg", "geo_enabled": false, "time_zone": null, "name": "Museum Bot", "id_str": "2508960524", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "metmuseum.org/about-the-muse\u2026", "indices": [0, 22], "expanded_url": "http://metmuseum.org/about-the-museum/press-room/news/2014/oasc-access", "url": "http://t.co/qTVWPkaIgo"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 3, "lang": "en", "extended_entities": {"media": [{"type": "photo", "id": 693407092623949824, "media_url": "http://pbs.twimg.com/media/CZ96E7CWQAAVYYz.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ96E7CWQAAVYYz.jpg", "display_url": "pic.twitter.com/mRktzdlEB1", "indices": [33, 56], "expanded_url": "http://twitter.com/MuseumBot/status/693407092863033344/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 1024, "h": 1247}, "small": {"resize": "fit", "w": 340, "h": 414}, "medium": {"resize": "fit", "w": 600, "h": 730}}, "id_str": "693407092623949824", "url": "https://t.co/mRktzdlEB1"}]}, "truncated": false, "id": 693407092863033344, "place": null, "favorite_count": 2, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Nativity https://t.co/OpNseJO3oL https://t.co/mRktzdlEB1", "id_str": "693407092863033344", "entities": {"media": [{"type": "photo", "id": 693407092623949824, "media_url": "http://pbs.twimg.com/media/CZ96E7CWQAAVYYz.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ96E7CWQAAVYYz.jpg", "display_url": "pic.twitter.com/mRktzdlEB1", "indices": [33, 56], "expanded_url": "http://twitter.com/MuseumBot/status/693407092863033344/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 1024, "h": 1247}, "small": {"resize": "fit", "w": 340, "h": 414}, "medium": {"resize": "fit", "w": 600, "h": 730}}, "id_str": "693407092623949824", "url": "https://t.co/mRktzdlEB1"}], "hashtags": [], "urls": [{"display_url": "metmuseum.org/collection/the\u2026", "indices": [9, 32], "expanded_url": "http://www.metmuseum.org/collection/the-collection-online/search/462886?rpp=30&pg=1413&rndkey=20160130&ao=on&ft=*&pos=42373", "url": "https://t.co/OpNseJO3oL"}], "symbols": [], "user_mentions": []}, "source": "Museum bot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 12:15:08 +0000 2016"}, "is_translator": false, "screen_name": "MuseumBot", "created_at": "Tue May 20 01:32:24 +0000 2014"}], "previous_cursor_str": "0", "next_cursor_str": "4611686020936348428", "previous_cursor": 0} \ No newline at end of file +{"next_cursor": 4611686020936348428, "users": [{"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 24, "profile_image_url": "http://pbs.twimg.com/profile_images/659410881806135296/PdVxDc0W_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 362, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 4048395140, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 106, "friends_count": 0, "location": "", "description": "I am a bot that simulates a series of mechanical linkages (+ noise) to draw a curve 4x/day. // by @tinysubversions, inspired by @ra & @bahrami_", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/659410881806135296/PdVxDc0W_normal.png", "geo_enabled": false, "time_zone": null, "name": "Spinny Machine", "id_str": "4048395140", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "animated_gif", "video_info": {"variants": [{"content_type": "video/mp4", "bitrate": 0, "url": "https://pbs.twimg.com/tweet_video/CZ9xAlyWQAAVsZk.mp4"}], "aspect_ratio": [1, 1]}, "id": 693397122595569664, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CZ9xAlyWQAAVsZk.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CZ9xAlyWQAAVsZk.png", "display_url": "pic.twitter.com/n6lbayOFFQ", "indices": [30, 53], "expanded_url": "http://twitter.com/spinnymachine/status/693397123023396864/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 360, "h": 360}, "small": {"resize": "fit", "w": 340, "h": 340}, "medium": {"resize": "fit", "w": 360, "h": 360}}, "id_str": "693397122595569664", "url": "https://t.co/n6lbayOFFQ"}]}, "truncated": false, "id": 693397123023396864, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "a casually scorched northeast https://t.co/n6lbayOFFQ", "id_str": "693397123023396864", "entities": {"media": [{"type": "photo", "id": 693397122595569664, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CZ9xAlyWQAAVsZk.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CZ9xAlyWQAAVsZk.png", "display_url": "pic.twitter.com/n6lbayOFFQ", "indices": [30, 53], "expanded_url": "http://twitter.com/spinnymachine/status/693397123023396864/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 360, "h": 360}, "small": {"resize": "fit", "w": 340, "h": 340}, "medium": {"resize": "fit", "w": 360, "h": 360}}, "id_str": "693397122595569664", "url": "https://t.co/n6lbayOFFQ"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Spinny Machine", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 11:35:31 +0000 2016"}, "is_translator": false, "screen_name": "spinnymachine", "created_at": "Wed Oct 28 16:43:01 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 6, "profile_image_url": "http://pbs.twimg.com/profile_images/658781950472138752/FOQCSbLg_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "https://t.co/OOS2jbeYND", "statuses_count": 135, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 4029020052, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "DBAF44", "followers_count": 23, "friends_count": 2, "location": "TV", "profile_banner_url": "https://pbs.twimg.com/profile_banners/4029020052/1445900976", "description": "I'm a bot that tweets fake Empire plots, inspired by @eveewing https://t.co/OOS2jbeYND // by @tinysubversions", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/658781950472138752/FOQCSbLg_normal.png", "geo_enabled": false, "time_zone": null, "name": "Empire Plots Bot", "id_str": "4029020052", "entities": {"description": {"urls": [{"display_url": "twitter.com/eveewing/statu\u2026", "indices": [63, 86], "expanded_url": "https://twitter.com/eveewing/status/658478802327183360", "url": "https://t.co/OOS2jbeYND"}]}, "url": {"urls": [{"display_url": "twitter.com/eveewing/statu\u2026", "indices": [0, 23], "expanded_url": "https://twitter.com/eveewing/status/658478802327183360", "url": "https://t.co/OOS2jbeYND"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "photo", "id": 671831157646876672, "media_url": "http://pbs.twimg.com/media/CVLS4NyU4AAshFm.png", "media_url_https": "https://pbs.twimg.com/media/CVLS4NyU4AAshFm.png", "display_url": "pic.twitter.com/EQ8oGhG502", "indices": [101, 124], "expanded_url": "http://twitter.com/EmpirePlots/status/671831157739118593/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 300, "h": 400}, "small": {"resize": "fit", "w": 300, "h": 400}, "large": {"resize": "fit", "w": 300, "h": 400}}, "id_str": "671831157646876672", "url": "https://t.co/EQ8oGhG502"}]}, "truncated": false, "id": 671831157739118593, "place": null, "favorite_count": 1, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Jamal is stuck in a wild forest with Kene Holliday and can't find their way out (it's just a dream). https://t.co/EQ8oGhG502", "id_str": "671831157739118593", "entities": {"media": [{"type": "photo", "id": 671831157646876672, "media_url": "http://pbs.twimg.com/media/CVLS4NyU4AAshFm.png", "media_url_https": "https://pbs.twimg.com/media/CVLS4NyU4AAshFm.png", "display_url": "pic.twitter.com/EQ8oGhG502", "indices": [101, 124], "expanded_url": "http://twitter.com/EmpirePlots/status/671831157739118593/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 300, "h": 400}, "small": {"resize": "fit", "w": 300, "h": 400}, "large": {"resize": "fit", "w": 300, "h": 400}}, "id_str": "671831157646876672", "url": "https://t.co/EQ8oGhG502"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Empire Plots Bot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Tue Dec 01 23:20:04 +0000 2015"}, "is_translator": false, "screen_name": "EmpirePlots", "created_at": "Mon Oct 26 22:49:42 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 25, "profile_image_url": "http://pbs.twimg.com/profile_images/652238580765462528/BQVTvFS9_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 346, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 3829470974, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "027F45", "followers_count": 287, "friends_count": 1, "location": "Portland, OR", "profile_banner_url": "https://pbs.twimg.com/profile_banners/3829470974/1444340849", "description": "Portland is such a weird place! We show real pics of places in Portland! ONLY IN PORTLAND as they say! // a bot by @tinysubversions, 4x daily", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/652238580765462528/BQVTvFS9_normal.png", "geo_enabled": false, "time_zone": null, "name": "Wow So Portland!", "id_str": "3829470974", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "photo", "id": 693471873166761984, "media_url": "http://pbs.twimg.com/media/CZ-0_pXUYAAGzn4.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-0_pXUYAAGzn4.jpg", "display_url": "pic.twitter.com/CF8JrGCQcY", "indices": [57, 80], "expanded_url": "http://twitter.com/wowsoportland/status/693471873246502912/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 600, "h": 300}, "small": {"resize": "fit", "w": 340, "h": 170}, "medium": {"resize": "fit", "w": 600, "h": 300}}, "id_str": "693471873166761984", "url": "https://t.co/CF8JrGCQcY"}]}, "truncated": false, "id": 693471873246502912, "place": null, "favorite_count": 1, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "those silly Portlanders are always up to something funny https://t.co/CF8JrGCQcY", "id_str": "693471873246502912", "entities": {"media": [{"type": "photo", "id": 693471873166761984, "media_url": "http://pbs.twimg.com/media/CZ-0_pXUYAAGzn4.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-0_pXUYAAGzn4.jpg", "display_url": "pic.twitter.com/CF8JrGCQcY", "indices": [57, 80], "expanded_url": "http://twitter.com/wowsoportland/status/693471873246502912/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 600, "h": 300}, "small": {"resize": "fit", "w": 340, "h": 170}, "medium": {"resize": "fit", "w": 600, "h": 300}}, "id_str": "693471873166761984", "url": "https://t.co/CF8JrGCQcY"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Wow so portland", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 16:32:33 +0000 2016"}, "is_translator": false, "screen_name": "wowsoportland", "created_at": "Thu Oct 08 21:38:27 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 28, "profile_image_url": "http://pbs.twimg.com/profile_images/650161232540909568/yyvPEOnF_normal.jpg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "https://t.co/8fYJI1TWEs", "statuses_count": 515, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 3765991992, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "ABB8C2", "followers_count": 331, "friends_count": 1, "location": "Mare Tranquilitatis", "profile_banner_url": "https://pbs.twimg.com/profile_banners/3765991992/1443845573", "description": "Tweeting pics from the Project Apollo Archive, four times a day. Not affiliated with the Project Apollo Archive. // a bot by @tinysubversions", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": -28800, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/650161232540909568/yyvPEOnF_normal.jpg", "geo_enabled": false, "time_zone": "Pacific Time (US & Canada)", "name": "Moon Shot Bot", "id_str": "3765991992", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "flickr.com/photos/project\u2026", "indices": [0, 23], "expanded_url": "https://www.flickr.com/photos/projectapolloarchive/", "url": "https://t.co/8fYJI1TWEs"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "photo", "id": 693470001316171776, "media_url": "http://pbs.twimg.com/media/CZ-zSsLXEAAhEia.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-zSsLXEAAhEia.jpg", "display_url": "pic.twitter.com/2j5ezW6i9G", "indices": [103, 126], "expanded_url": "http://twitter.com/moonshotbot/status/693470003249684481/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 1024, "h": 1070}, "small": {"resize": "fit", "w": 340, "h": 355}, "medium": {"resize": "fit", "w": 600, "h": 627}}, "id_str": "693470001316171776", "url": "https://t.co/2j5ezW6i9G"}]}, "truncated": false, "id": 693470003249684481, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "\ud83c\udf0c\ud83c\udf18\ud83c\udf1b\nApollo 15 Hasselblad image from film magazine 97/O - lunar orbit view\n\ud83d\ude80\ud83c\udf1a\ud83c\udf19\n https://t.co/n4WH1ZTyuZ https://t.co/2j5ezW6i9G", "id_str": "693470003249684481", "entities": {"media": [{"type": "photo", "id": 693470001316171776, "media_url": "http://pbs.twimg.com/media/CZ-zSsLXEAAhEia.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-zSsLXEAAhEia.jpg", "display_url": "pic.twitter.com/2j5ezW6i9G", "indices": [103, 126], "expanded_url": "http://twitter.com/moonshotbot/status/693470003249684481/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 1024, "h": 1070}, "small": {"resize": "fit", "w": 340, "h": 355}, "medium": {"resize": "fit", "w": 600, "h": 627}}, "id_str": "693470001316171776", "url": "https://t.co/2j5ezW6i9G"}], "hashtags": [], "urls": [{"display_url": "flickr.com/photos/project\u2026", "indices": [79, 102], "expanded_url": "https://www.flickr.com/photos/projectapolloarchive/21831461788", "url": "https://t.co/n4WH1ZTyuZ"}], "symbols": [], "user_mentions": []}, "source": "Moon Shot Bot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 16:25:07 +0000 2016"}, "is_translator": false, "screen_name": "moonshotbot", "created_at": "Sat Oct 03 04:03:02 +0000 2015"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 17, "profile_image_url": "http://pbs.twimg.com/profile_images/629374160993710081/q-lr9vsE_normal.jpg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/mRUwkqVO7i", "statuses_count": 598, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 3406094211, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 133, "friends_count": 0, "location": "Not affiliated with the FBI", "description": "I tweet random pages from FOIA-requested FBI records, 4x/day. I'm a bot by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/629374160993710081/q-lr9vsE_normal.jpg", "geo_enabled": false, "time_zone": null, "name": "FBI Bot", "id_str": "3406094211", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "vault.fbi.gov", "indices": [0, 22], "expanded_url": "http://vault.fbi.gov", "url": "http://t.co/mRUwkqVO7i"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "photo", "id": 693483330080210944, "media_url": "http://pbs.twimg.com/media/CZ-_ahsWAAADnwA.png", "media_url_https": "https://pbs.twimg.com/media/CZ-_ahsWAAADnwA.png", "display_url": "pic.twitter.com/Dey5JLxCvb", "indices": [63, 86], "expanded_url": "http://twitter.com/FBIbot/status/693483330218622976/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 600, "h": 776}, "small": {"resize": "fit", "w": 340, "h": 439}, "large": {"resize": "fit", "w": 1000, "h": 1294}}, "id_str": "693483330080210944", "url": "https://t.co/Dey5JLxCvb"}]}, "truncated": false, "id": 693483330218622976, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "The FBI was watching Fred G. Randaccio https://t.co/NAkQc4FYKp https://t.co/Dey5JLxCvb", "id_str": "693483330218622976", "entities": {"media": [{"type": "photo", "id": 693483330080210944, "media_url": "http://pbs.twimg.com/media/CZ-_ahsWAAADnwA.png", "media_url_https": "https://pbs.twimg.com/media/CZ-_ahsWAAADnwA.png", "display_url": "pic.twitter.com/Dey5JLxCvb", "indices": [63, 86], "expanded_url": "http://twitter.com/FBIbot/status/693483330218622976/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 600, "h": 776}, "small": {"resize": "fit", "w": 340, "h": 439}, "large": {"resize": "fit", "w": 1000, "h": 1294}}, "id_str": "693483330080210944", "url": "https://t.co/Dey5JLxCvb"}], "hashtags": [], "urls": [{"display_url": "vault.fbi.gov/frank-randaccio", "indices": [39, 62], "expanded_url": "https://vault.fbi.gov/frank-randaccio", "url": "https://t.co/NAkQc4FYKp"}], "symbols": [], "user_mentions": []}, "source": "FBIBot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 17:18:04 +0000 2016"}, "is_translator": false, "screen_name": "FBIbot", "created_at": "Thu Aug 06 19:28:10 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 23, "profile_image_url": "http://pbs.twimg.com/profile_images/631231593164607488/R4hRHjBI_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/6cpr8h0hGa", "statuses_count": 664, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 3312790286, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "4A913C", "followers_count": 239, "friends_count": 0, "location": "", "description": "Animal videos sourced from @macaulaylibrary. Turn up the sound! // A bot by @tinysubversions. Not affiliated with the Macaulay Library.", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/631231593164607488/R4hRHjBI_normal.png", "geo_enabled": false, "time_zone": null, "name": "Animal Video Bot", "id_str": "3312790286", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "macaulaylibrary.org", "indices": [0, 22], "expanded_url": "http://macaulaylibrary.org/", "url": "http://t.co/6cpr8h0hGa"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "ro", "extended_entities": {"media": [{"type": "video", "video_info": {"variants": [{"content_type": "video/webm", "bitrate": 832000, "url": "https://video.twimg.com/ext_tw_video/693439580935094273/pu/vid/640x360/hY01KGCSXl-isZzt.webm"}, {"content_type": "video/mp4", "bitrate": 320000, "url": "https://video.twimg.com/ext_tw_video/693439580935094273/pu/vid/320x180/3NEGIMyzX2tdBm5i.mp4"}, {"content_type": "video/mp4", "bitrate": 832000, "url": "https://video.twimg.com/ext_tw_video/693439580935094273/pu/vid/640x360/hY01KGCSXl-isZzt.mp4"}, {"content_type": "application/x-mpegURL", "url": "https://video.twimg.com/ext_tw_video/693439580935094273/pu/pl/G53mlN6oslnMAWd5.m3u8"}, {"content_type": "application/dash+xml", "url": "https://video.twimg.com/ext_tw_video/693439580935094273/pu/pl/G53mlN6oslnMAWd5.mpd"}], "aspect_ratio": [16, 9], "duration_millis": 20021}, "id": 693439580935094273, "media_url": "http://pbs.twimg.com/ext_tw_video_thumb/693439580935094273/pu/img/K0BdyKh0qQc3P5_N.jpg", "media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/693439580935094273/pu/img/K0BdyKh0qQc3P5_N.jpg", "display_url": "pic.twitter.com/aaGNPmPpni", "indices": [85, 108], "expanded_url": "http://twitter.com/AnimalVidBot/status/693439595946479617/video/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 640, "h": 360}, "small": {"resize": "fit", "w": 340, "h": 191}, "medium": {"resize": "fit", "w": 600, "h": 338}}, "id_str": "693439580935094273", "url": "https://t.co/aaGNPmPpni"}]}, "truncated": false, "id": 693439595946479617, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Maui Parrotbill\n\nPseudonestor xanthophrys\nBarksdale, Timothy https://t.co/c6n9M2Cjnv https://t.co/aaGNPmPpni", "id_str": "693439595946479617", "entities": {"media": [{"type": "photo", "id": 693439580935094273, "media_url": "http://pbs.twimg.com/ext_tw_video_thumb/693439580935094273/pu/img/K0BdyKh0qQc3P5_N.jpg", "media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/693439580935094273/pu/img/K0BdyKh0qQc3P5_N.jpg", "display_url": "pic.twitter.com/aaGNPmPpni", "indices": [85, 108], "expanded_url": "http://twitter.com/AnimalVidBot/status/693439595946479617/video/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 640, "h": 360}, "small": {"resize": "fit", "w": 340, "h": 191}, "medium": {"resize": "fit", "w": 600, "h": 338}}, "id_str": "693439580935094273", "url": "https://t.co/aaGNPmPpni"}], "hashtags": [], "urls": [{"display_url": "macaulaylibrary.org/video/428118", "indices": [61, 84], "expanded_url": "http://macaulaylibrary.org/video/428118", "url": "https://t.co/c6n9M2Cjnv"}], "symbols": [], "user_mentions": []}, "source": "Animal Video Bot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 14:24:17 +0000 2016"}, "is_translator": false, "screen_name": "AnimalVidBot", "created_at": "Tue Aug 11 22:25:35 +0000 2015"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 10, "profile_image_url": "http://pbs.twimg.com/profile_images/624358417818238976/CfSPOEr4_normal.jpg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/YEWmunbcFZ", "statuses_count": 4, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 3300809963, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 56, "friends_count": 0, "location": "The Most Relevant Ad Agency", "description": "The most happenin' new trends on the internet. // A bot by @tinysubversions.", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/624358417818238976/CfSPOEr4_normal.jpg", "geo_enabled": false, "time_zone": null, "name": "Trend Reportz", "id_str": "3300809963", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "slideshare.net/trendreportz", "indices": [0, 22], "expanded_url": "http://www.slideshare.net/trendreportz", "url": "http://t.co/YEWmunbcFZ"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 1, "lang": "en", "truncated": false, "id": 638389840472600576, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "NEW REPORT! Learn what steamers mean to 7-18 y/o men http://t.co/veQGWz1Lqn", "id_str": "638389840472600576", "entities": {"hashtags": [], "urls": [{"display_url": "slideshare.net/trendreportz/t\u2026", "indices": [53, 75], "expanded_url": "http://www.slideshare.net/trendreportz/trends-in-steamers", "url": "http://t.co/veQGWz1Lqn"}], "symbols": [], "user_mentions": []}, "source": "Trend Reportz", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Mon Aug 31 16:36:13 +0000 2015"}, "is_translator": false, "screen_name": "TrendReportz", "created_at": "Wed May 27 19:43:37 +0000 2015"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 14, "profile_image_url": "http://pbs.twimg.com/profile_images/585540228439498752/OPHSe1Yw_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/lrCYkDGPrm", "statuses_count": 888, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 3145355109, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 106, "friends_count": 1, "location": "The Middle East", "profile_banner_url": "https://pbs.twimg.com/profile_banners/3145355109/1428438665", "description": "Public domain photos from the Qatar Digital Library of Middle Eastern (& nearby) history. Tweets 4x/day. // bot by @tinysubversions, not affiliated with the QDL", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/585540228439498752/OPHSe1Yw_normal.png", "geo_enabled": false, "time_zone": null, "name": "Middle East History", "id_str": "3145355109", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "qdl.qa/en/search/site\u2026", "indices": [0, 22], "expanded_url": "http://www.qdl.qa/en/search/site/?f%5B0%5D=document_source%3Aarchive_source", "url": "http://t.co/lrCYkDGPrm"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "photo", "id": 693479308619300866, "media_url": "http://pbs.twimg.com/media/CZ-7wclWQAIpxlu.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-7wclWQAIpxlu.jpg", "display_url": "pic.twitter.com/xojYCSnUZu", "indices": [72, 95], "expanded_url": "http://twitter.com/MidEastHistory/status/693479308745113600/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 1024, "h": 1024}, "small": {"resize": "fit", "w": 340, "h": 340}, "medium": {"resize": "fit", "w": 600, "h": 600}}, "id_str": "693479308619300866", "url": "https://t.co/xojYCSnUZu"}]}, "truncated": false, "id": 693479308745113600, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "'Distant View of Hormuz.' Photographer: Unknown https://t.co/hkDmSbTLgT https://t.co/xojYCSnUZu", "id_str": "693479308745113600", "entities": {"media": [{"type": "photo", "id": 693479308619300866, "media_url": "http://pbs.twimg.com/media/CZ-7wclWQAIpxlu.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-7wclWQAIpxlu.jpg", "display_url": "pic.twitter.com/xojYCSnUZu", "indices": [72, 95], "expanded_url": "http://twitter.com/MidEastHistory/status/693479308745113600/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 1024, "h": 1024}, "small": {"resize": "fit", "w": 340, "h": 340}, "medium": {"resize": "fit", "w": 600, "h": 600}}, "id_str": "693479308619300866", "url": "https://t.co/xojYCSnUZu"}], "hashtags": [], "urls": [{"display_url": "qdl.qa//en/archive/81\u2026", "indices": [48, 71], "expanded_url": "http://www.qdl.qa//en/archive/81055/vdc_100024111424.0x00000f", "url": "https://t.co/hkDmSbTLgT"}], "symbols": [], "user_mentions": []}, "source": "Mid east history pics", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 17:02:06 +0000 2016"}, "is_translator": false, "screen_name": "MidEastHistory", "created_at": "Tue Apr 07 20:27:15 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 99, "profile_image_url": "http://pbs.twimg.com/profile_images/584076161325473793/gufAEGJv_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/s6OmUwb6Bn", "statuses_count": 91531, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 3131670665, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "ABB8C2", "followers_count": 46186, "friends_count": 1, "location": "Hogwarts", "description": "I'm the Sorting Hat and I'm here to say / I love sorting students in a major way // a bot by @tinysubversions, follow to get sorted!", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 1, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/584076161325473793/gufAEGJv_normal.png", "geo_enabled": false, "time_zone": null, "name": "The Sorting Hat Bot", "id_str": "3131670665", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "tinysubversions.com/notes/sorting-\u2026", "indices": [0, 22], "expanded_url": "http://tinysubversions.com/notes/sorting-bot/", "url": "http://t.co/s6OmUwb6Bn"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "truncated": false, "id": 693474779018362880, "place": null, "favorite_count": 0, "in_reply_to_status_id_str": null, "in_reply_to_user_id": 1210222826, "in_reply_to_status_id": null, "in_reply_to_screen_name": "Nyrfall", "text": "@Nyrfall The Sorting time is here at last, I know each time you send\nI've figured out that Slytherin's the right place for your blend", "id_str": "693474779018362880", "entities": {"hashtags": [], "urls": [], "symbols": [], "user_mentions": [{"indices": [0, 8], "id": 1210222826, "name": "Desi Sobrino", "screen_name": "Nyrfall", "id_str": "1210222826"}]}, "source": "The Sorting Hat Bot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": "1210222826", "retweeted": false, "created_at": "Sat Jan 30 16:44:06 +0000 2016"}, "is_translator": false, "screen_name": "SortingBot", "created_at": "Fri Apr 03 19:27:31 +0000 2015"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 34, "profile_image_url": "http://pbs.twimg.com/profile_images/580173179236196352/nWsIPbqH_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/2YPE0x0Knw", "statuses_count": 1233, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 3105672877, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 424, "friends_count": 0, "location": "NYC", "profile_banner_url": "https://pbs.twimg.com/profile_banners/3105672877/1427159206", "description": "Posting random flyers from hip hop's formative years every 6 hours. Most art by Buddy Esquire and Phase 2. Full collection at link. // a bot by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/580173179236196352/nWsIPbqH_normal.png", "geo_enabled": false, "time_zone": null, "name": "Old School Flyers", "id_str": "3105672877", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "toledohiphop.org/images/old_sch\u2026", "indices": [0, 22], "expanded_url": "http://www.toledohiphop.org/images/old_school_source_code/", "url": "http://t.co/2YPE0x0Knw"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "und", "extended_entities": {"media": [{"type": "photo", "id": 693422418480742400, "media_url": "http://pbs.twimg.com/media/CZ-IBATWQAAhkZA.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-IBATWQAAhkZA.jpg", "display_url": "pic.twitter.com/B7jv7lwB9S", "indices": [0, 23], "expanded_url": "http://twitter.com/oldschoolflyers/status/693422418929524736/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 503, "h": 646}, "small": {"resize": "fit", "w": 340, "h": 435}, "medium": {"resize": "fit", "w": 503, "h": 646}}, "id_str": "693422418480742400", "url": "https://t.co/B7jv7lwB9S"}]}, "truncated": false, "id": 693422418929524736, "place": null, "favorite_count": 1, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "https://t.co/B7jv7lwB9S", "id_str": "693422418929524736", "entities": {"media": [{"type": "photo", "id": 693422418480742400, "media_url": "http://pbs.twimg.com/media/CZ-IBATWQAAhkZA.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ-IBATWQAAhkZA.jpg", "display_url": "pic.twitter.com/B7jv7lwB9S", "indices": [0, 23], "expanded_url": "http://twitter.com/oldschoolflyers/status/693422418929524736/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 503, "h": 646}, "small": {"resize": "fit", "w": 340, "h": 435}, "medium": {"resize": "fit", "w": 503, "h": 646}}, "id_str": "693422418480742400", "url": "https://t.co/B7jv7lwB9S"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "oldschoolflyers", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 13:16:02 +0000 2016"}, "is_translator": false, "screen_name": "oldschoolflyers", "created_at": "Tue Mar 24 00:55:10 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 41, "profile_image_url": "http://pbs.twimg.com/profile_images/561971159927771136/sEQ5u1zM_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 1396, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 3010688583, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "961EE4", "followers_count": 243, "friends_count": 1, "location": "", "profile_banner_url": "https://pbs.twimg.com/profile_banners/3010688583/1422819642", "description": "DAD: weird conversation joke is bae ME: ugh dad no DAD: [something unhip] // a bot by @tinysubversions", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/561971159927771136/sEQ5u1zM_normal.png", "geo_enabled": false, "time_zone": null, "name": "Weird Convo Bot", "id_str": "3010688583", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "truncated": false, "id": 693429980093620224, "place": null, "favorite_count": 0, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "DOG: Wtf are you bae'\nME: fart. Ugh.\nDOG: so sex with me is sex vape\nME: Wtf are you skeleton'", "id_str": "693429980093620224", "entities": {"hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "WeirdConvoBot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 13:46:05 +0000 2016"}, "is_translator": false, "screen_name": "WeirdConvoBot", "created_at": "Sun Feb 01 19:05:21 +0000 2015"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 110, "profile_image_url": "http://pbs.twimg.com/profile_images/556129692554493953/82ISdQxF_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/3gDtETAFpu", "statuses_count": 1510, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 2981339967, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 1308, "friends_count": 1, "location": "Frankfurt School", "profile_banner_url": "https://pbs.twimg.com/profile_banners/2981339967/1421426775", "description": "We love #innovation and are always thinking of the next #disruptive startup #idea! // a bot by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 1, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/556129692554493953/82ISdQxF_normal.png", "geo_enabled": false, "time_zone": null, "name": "Hottest Startups", "id_str": "2981339967", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "marxists.org/archive/index.\u2026", "indices": [0, 22], "expanded_url": "http://www.marxists.org/archive/index.htm", "url": "http://t.co/3gDtETAFpu"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "truncated": false, "id": 693477791883350016, "place": null, "favorite_count": 0, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Startup idea: The architect thinks of the building contractor as a layman who tells him what he needs and what he can pay.", "id_str": "693477791883350016", "entities": {"hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "hottest startups", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 16:56:04 +0000 2016"}, "is_translator": false, "screen_name": "HottestStartups", "created_at": "Fri Jan 16 16:33:45 +0000 2015"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 40, "profile_image_url": "http://pbs.twimg.com/profile_images/549979314541039617/fZ_XDnWz_normal.jpeg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 6748, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 2951486632, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "FFCC4D", "followers_count": 3155, "friends_count": 1, "location": "(bot by @tinysubversions)", "description": "We are the Academy For Annual Recognition and each year we give out the Yearly Awards. Follow (or re-follow) for your award! Warning: sometimes it's mean.", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 2, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/549979314541039617/fZ_XDnWz_normal.jpeg", "geo_enabled": false, "time_zone": null, "name": "The Yearly Awards", "id_str": "2951486632", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "animated_gif", "video_info": {"variants": [{"content_type": "video/mp4", "bitrate": 0, "url": "https://pbs.twimg.com/tweet_video/CZ-2l2fWwAEH6MB.mp4"}], "aspect_ratio": [4, 3]}, "id": 693473629036789761, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CZ-2l2fWwAEH6MB.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CZ-2l2fWwAEH6MB.png", "display_url": "pic.twitter.com/XGDD3tMJPF", "indices": [86, 109], "expanded_url": "http://twitter.com/YearlyAwards/status/693473629766598656/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 400, "h": 300}, "small": {"resize": "fit", "w": 340, "h": 255}, "large": {"resize": "fit", "w": 400, "h": 300}}, "id_str": "693473629036789761", "url": "https://t.co/XGDD3tMJPF"}]}, "truncated": false, "id": 693473629766598656, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": 4863901738, "in_reply_to_status_id": null, "in_reply_to_screen_name": "know_fast", "text": "@know_fast It's official: you're the Least Prodigiously Despondent Confidant of 2015! https://t.co/XGDD3tMJPF", "id_str": "693473629766598656", "entities": {"media": [{"type": "photo", "id": 693473629036789761, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CZ-2l2fWwAEH6MB.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CZ-2l2fWwAEH6MB.png", "display_url": "pic.twitter.com/XGDD3tMJPF", "indices": [86, 109], "expanded_url": "http://twitter.com/YearlyAwards/status/693473629766598656/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 400, "h": 300}, "small": {"resize": "fit", "w": 340, "h": 255}, "large": {"resize": "fit", "w": 400, "h": 300}}, "id_str": "693473629036789761", "url": "https://t.co/XGDD3tMJPF"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": [{"indices": [0, 10], "id": 4863901738, "name": "Know Fast", "screen_name": "know_fast", "id_str": "4863901738"}]}, "source": "The Yearly Awards", "contributors": null, "favorited": false, "in_reply_to_user_id_str": "4863901738", "retweeted": false, "created_at": "Sat Jan 30 16:39:32 +0000 2016"}, "is_translator": false, "screen_name": "YearlyAwards", "created_at": "Tue Dec 30 16:59:34 +0000 2014"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 20, "profile_image_url": "http://pbs.twimg.com/profile_images/512673377283080194/eFnJQJSp_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 1972, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 2817629347, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "ABB8C2", "followers_count": 93, "friends_count": 1, "location": "", "profile_banner_url": "https://pbs.twimg.com/profile_banners/2817629347/1411065932", "description": "Wise sayings, four times daily. by @tinysubversions", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/512673377283080194/eFnJQJSp_normal.png", "geo_enabled": false, "time_zone": null, "name": "Received Wisdom", "id_str": "2817629347", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "truncated": false, "id": 693418402392719360, "place": null, "favorite_count": 0, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Water is thicker than blood.", "id_str": "693418402392719360", "entities": {"hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Received Wisdom", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 13:00:04 +0000 2016"}, "is_translator": false, "screen_name": "received_wisdom", "created_at": "Thu Sep 18 18:32:38 +0000 2014"}, {"notifications": false, "profile_use_background_image": false, "has_extended_profile": false, "listed_count": 11, "profile_image_url": "http://pbs.twimg.com/profile_images/517404591218900992/kf2iYD1f_normal.jpeg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 1767, "profile_text_color": "000000", "profile_background_tile": false, "follow_request_sent": false, "id": 2798799669, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "4A913C", "followers_count": 40, "friends_count": 0, "location": "Everywhere", "profile_banner_url": "https://pbs.twimg.com/profile_banners/2798799669/1412195008", "description": "Chronicling men doing things. // A bot by @tinysubversions, Powered By Giphy", "profile_sidebar_fill_color": "000000", "default_profile": false, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "000000", "is_translation_enabled": false, "profile_sidebar_border_color": "000000", "profile_image_url_https": "https://pbs.twimg.com/profile_images/517404591218900992/kf2iYD1f_normal.jpeg", "geo_enabled": false, "time_zone": null, "name": "Men Doing Things", "id_str": "2798799669", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "animated_gif", "video_info": {"variants": [{"content_type": "video/mp4", "bitrate": 0, "url": "https://pbs.twimg.com/tweet_video/CZ-WORAWQAED6It.mp4"}], "aspect_ratio": [167, 104]}, "id": 693438039465541633, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CZ-WORAWQAED6It.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CZ-WORAWQAED6It.png", "display_url": "pic.twitter.com/wsk2GyEsGh", "indices": [37, 60], "expanded_url": "http://twitter.com/MenDoing/status/693438039801073664/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 334, "h": 208}, "small": {"resize": "fit", "w": 334, "h": 208}, "large": {"resize": "fit", "w": 334, "h": 208}}, "id_str": "693438039465541633", "url": "https://t.co/wsk2GyEsGh"}]}, "truncated": false, "id": 693438039801073664, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Men Authoring Landmarks in Manhattan https://t.co/wsk2GyEsGh", "id_str": "693438039801073664", "entities": {"media": [{"type": "photo", "id": 693438039465541633, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CZ-WORAWQAED6It.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CZ-WORAWQAED6It.png", "display_url": "pic.twitter.com/wsk2GyEsGh", "indices": [37, 60], "expanded_url": "http://twitter.com/MenDoing/status/693438039801073664/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 334, "h": 208}, "small": {"resize": "fit", "w": 334, "h": 208}, "large": {"resize": "fit", "w": 334, "h": 208}}, "id_str": "693438039465541633", "url": "https://t.co/wsk2GyEsGh"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Men Doing Things", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 14:18:06 +0000 2016"}, "is_translator": false, "screen_name": "MenDoing", "created_at": "Wed Oct 01 19:59:55 +0000 2014"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 139, "profile_image_url": "http://pbs.twimg.com/profile_images/495988901790482432/le2-dKgs_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/r2HzjsqHTU", "statuses_count": 2157, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 2704554914, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 1172, "friends_count": 1, "location": "", "profile_banner_url": "https://pbs.twimg.com/profile_banners/2704554914/1407087962", "description": "A bot that picks a word and then draws randomly until an OCR library (http://t.co/XmDeI5TWoF) reads that word. 4x daily. Also on Tumblr. // by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/495988901790482432/le2-dKgs_normal.png", "geo_enabled": false, "time_zone": null, "name": "Reverse OCR", "id_str": "2704554914", "entities": {"description": {"urls": [{"display_url": "antimatter15.com/ocrad.js/demo.\u2026", "indices": [70, 92], "expanded_url": "http://antimatter15.com/ocrad.js/demo.html", "url": "http://t.co/XmDeI5TWoF"}]}, "url": {"urls": [{"display_url": "reverseocr.tumblr.com", "indices": [0, 22], "expanded_url": "http://reverseocr.tumblr.com", "url": "http://t.co/r2HzjsqHTU"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "und", "extended_entities": {"media": [{"type": "photo", "id": 693404391072776192, "media_url": "http://pbs.twimg.com/media/CZ93nq-WwAAdSAo.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ93nq-WwAAdSAo.jpg", "display_url": "pic.twitter.com/WbD9lkNarf", "indices": [8, 31], "expanded_url": "http://twitter.com/reverseocr/status/693404391160860673/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 600, "h": 150}, "small": {"resize": "fit", "w": 340, "h": 85}, "large": {"resize": "fit", "w": 800, "h": 200}}, "id_str": "693404391072776192", "url": "https://t.co/WbD9lkNarf"}]}, "truncated": false, "id": 693404391160860673, "place": null, "favorite_count": 2, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "larceny https://t.co/WbD9lkNarf", "id_str": "693404391160860673", "entities": {"media": [{"type": "photo", "id": 693404391072776192, "media_url": "http://pbs.twimg.com/media/CZ93nq-WwAAdSAo.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ93nq-WwAAdSAo.jpg", "display_url": "pic.twitter.com/WbD9lkNarf", "indices": [8, 31], "expanded_url": "http://twitter.com/reverseocr/status/693404391160860673/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 600, "h": 150}, "small": {"resize": "fit", "w": 340, "h": 85}, "large": {"resize": "fit", "w": 800, "h": 200}}, "id_str": "693404391072776192", "url": "https://t.co/WbD9lkNarf"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Reverse OCR", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 12:04:24 +0000 2016"}, "is_translator": false, "screen_name": "reverseocr", "created_at": "Sun Aug 03 17:26:28 +0000 2014"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 5, "profile_image_url": "http://pbs.twimg.com/profile_images/479836451182362624/0fAtv_AN_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": null, "statuses_count": 2, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 2577963498, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 14, "friends_count": 1, "location": "", "description": "Deploying GIFs every six hours. // by @tinysubvesions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/479836451182362624/0fAtv_AN_normal.png", "geo_enabled": false, "time_zone": null, "name": "GIF Deployer", "id_str": "2577963498", "entities": {"description": {"urls": []}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "en", "extended_entities": {"media": [{"type": "animated_gif", "video_info": {"variants": [{"content_type": "video/mp4", "bitrate": 0, "url": "https://pbs.twimg.com/tweet_video/BqkTB2fIEAA8zCs.mp4"}], "aspect_ratio": [1, 1]}, "id": 479935757818531840, "media_url": "http://pbs.twimg.com/tweet_video_thumb/BqkTB2fIEAA8zCs.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/BqkTB2fIEAA8zCs.png", "display_url": "pic.twitter.com/WEYISUSsJR", "indices": [21, 43], "expanded_url": "http://twitter.com/gifDeployer/status/479935760033153024/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 600, "h": 600}, "small": {"resize": "fit", "w": 340, "h": 340}, "large": {"resize": "fit", "w": 612, "h": 612}}, "id_str": "479935757818531840", "url": "http://t.co/WEYISUSsJR"}]}, "truncated": false, "id": 479935760033153024, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Vietnamese decadence http://t.co/WEYISUSsJR", "id_str": "479935760033153024", "entities": {"media": [{"type": "photo", "id": 479935757818531840, "media_url": "http://pbs.twimg.com/tweet_video_thumb/BqkTB2fIEAA8zCs.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/BqkTB2fIEAA8zCs.png", "display_url": "pic.twitter.com/WEYISUSsJR", "indices": [21, 43], "expanded_url": "http://twitter.com/gifDeployer/status/479935760033153024/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 600, "h": 600}, "small": {"resize": "fit", "w": 340, "h": 340}, "large": {"resize": "fit", "w": 612, "h": 612}}, "id_str": "479935757818531840", "url": "http://t.co/WEYISUSsJR"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "GIF Deployer", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Fri Jun 20 10:36:16 +0000 2014"}, "is_translator": false, "screen_name": "gifDeployer", "created_at": "Fri Jun 20 03:56:13 +0000 2014"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 48, "profile_image_url": "http://pbs.twimg.com/profile_images/479364877551538176/HN0wLHbt_normal.png", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/XJeqwaDhQg", "statuses_count": 11970, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 2575445382, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 428, "friends_count": 1, "location": "", "description": "Generating new aesthetics every hour. Botpunk. // by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": -18000, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 0, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/479364877551538176/HN0wLHbt_normal.png", "geo_enabled": false, "time_zone": "Eastern Time (US & Canada)", "name": "Brand New Aesthetics", "id_str": "2575445382", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "brand-new-aesthetics.tumblr.com", "indices": [0, 22], "expanded_url": "http://brand-new-aesthetics.tumblr.com", "url": "http://t.co/XJeqwaDhQg"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "da", "extended_entities": {"media": [{"type": "animated_gif", "video_info": {"variants": [{"content_type": "video/mp4", "bitrate": 0, "url": "https://pbs.twimg.com/tweet_video/CX5BF-lWMAEyoPA.mp4"}], "aspect_ratio": [3, 2]}, "id": 684055764361687041, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CX5BF-lWMAEyoPA.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CX5BF-lWMAEyoPA.png", "display_url": "pic.twitter.com/d4ZGIYqyt9", "indices": [13, 36], "expanded_url": "http://twitter.com/neweraesthetics/status/684055764768522240/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 300, "h": 200}, "small": {"resize": "fit", "w": 300, "h": 200}, "large": {"resize": "fit", "w": 300, "h": 200}}, "id_str": "684055764361687041", "url": "https://t.co/d4ZGIYqyt9"}]}, "truncated": false, "id": 684055764768522240, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "SOLUTIONPUNK https://t.co/d4ZGIYqyt9", "id_str": "684055764768522240", "entities": {"media": [{"type": "photo", "id": 684055764361687041, "media_url": "http://pbs.twimg.com/tweet_video_thumb/CX5BF-lWMAEyoPA.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/CX5BF-lWMAEyoPA.png", "display_url": "pic.twitter.com/d4ZGIYqyt9", "indices": [13, 36], "expanded_url": "http://twitter.com/neweraesthetics/status/684055764768522240/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 300, "h": 200}, "small": {"resize": "fit", "w": 300, "h": 200}, "large": {"resize": "fit", "w": 300, "h": 200}}, "id_str": "684055764361687041", "url": "https://t.co/d4ZGIYqyt9"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Brand New Aesthetics", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Mon Jan 04 16:56:18 +0000 2016"}, "is_translator": false, "screen_name": "neweraesthetics", "created_at": "Wed Jun 18 20:39:25 +0000 2014"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 12, "profile_image_url": "http://pbs.twimg.com/profile_images/479355596076892160/p_jT5KqM_normal.jpeg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/DZYA6d8tU5", "statuses_count": 8587, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 2575407888, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 113, "friends_count": 1, "location": "Bodymore, Murdaland", "description": "This Twitter account automatically tweets GIFs of The Wire. Also a Tumblr. Updates hourly. // by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 1, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/479355596076892160/p_jT5KqM_normal.jpeg", "geo_enabled": false, "time_zone": null, "name": "Scenes from The Wire", "id_str": "2575407888", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "wirescenes.tumblr.com", "indices": [0, 22], "expanded_url": "http://wirescenes.tumblr.com/", "url": "http://t.co/DZYA6d8tU5"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 0, "lang": "und", "extended_entities": {"media": [{"type": "animated_gif", "video_info": {"variants": [{"content_type": "video/mp4", "bitrate": 0, "url": "https://pbs.twimg.com/tweet_video/COXAcgVU8AACS4W.mp4"}], "aspect_ratio": [132, 119]}, "id": 641130117918420992, "media_url": "http://pbs.twimg.com/tweet_video_thumb/COXAcgVU8AACS4W.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/COXAcgVU8AACS4W.png", "display_url": "pic.twitter.com/XBoaB2klZq", "indices": [0, 22], "expanded_url": "http://twitter.com/wirescenes/status/641130118132314112/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 264, "h": 238}, "small": {"resize": "fit", "w": 264, "h": 238}, "medium": {"resize": "fit", "w": 264, "h": 238}}, "id_str": "641130117918420992", "url": "http://t.co/XBoaB2klZq"}]}, "truncated": false, "id": 641130118132314112, "place": null, "favorite_count": 0, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "http://t.co/XBoaB2klZq", "id_str": "641130118132314112", "entities": {"media": [{"type": "photo", "id": 641130117918420992, "media_url": "http://pbs.twimg.com/tweet_video_thumb/COXAcgVU8AACS4W.png", "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/COXAcgVU8AACS4W.png", "display_url": "pic.twitter.com/XBoaB2klZq", "indices": [0, 22], "expanded_url": "http://twitter.com/wirescenes/status/641130118132314112/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 264, "h": 238}, "small": {"resize": "fit", "w": 264, "h": 238}, "medium": {"resize": "fit", "w": 264, "h": 238}}, "id_str": "641130117918420992", "url": "http://t.co/XBoaB2klZq"}], "hashtags": [], "urls": [], "symbols": [], "user_mentions": []}, "source": "Scenes from The Wire", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Tue Sep 08 06:05:06 +0000 2015"}, "is_translator": false, "screen_name": "wirescenes", "created_at": "Wed Jun 18 20:08:31 +0000 2014"}, {"notifications": false, "profile_use_background_image": true, "has_extended_profile": false, "listed_count": 229, "profile_image_url": "http://pbs.twimg.com/profile_images/468570294253150208/DlK5sGe2_normal.jpeg", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "verified": false, "lang": "en", "protected": false, "url": "http://t.co/qTVWPkaIgo", "statuses_count": 2026, "profile_text_color": "333333", "profile_background_tile": false, "follow_request_sent": false, "id": 2508960524, "default_profile_image": false, "contributors_enabled": false, "following": false, "profile_link_color": "0084B4", "followers_count": 4176, "friends_count": 1, "location": "(not affiliated with the Met)", "description": "I am a bot that tweets a random high-res Open Access image from the Metropolitan Museum of Art, four times a day. // by @tinysubversions", "profile_sidebar_fill_color": "DDEEF6", "default_profile": true, "utc_offset": null, "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "favourites_count": 3, "profile_background_color": "C0DEED", "is_translation_enabled": false, "profile_sidebar_border_color": "C0DEED", "profile_image_url_https": "https://pbs.twimg.com/profile_images/468570294253150208/DlK5sGe2_normal.jpeg", "geo_enabled": false, "time_zone": null, "name": "Museum Bot", "id_str": "2508960524", "entities": {"description": {"urls": []}, "url": {"urls": [{"display_url": "metmuseum.org/about-the-muse\u2026", "indices": [0, 22], "expanded_url": "http://metmuseum.org/about-the-museum/press-room/news/2014/oasc-access", "url": "http://t.co/qTVWPkaIgo"}]}}, "status": {"coordinates": null, "is_quote_status": false, "geo": null, "retweet_count": 3, "lang": "en", "extended_entities": {"media": [{"type": "photo", "id": 693407092623949824, "media_url": "http://pbs.twimg.com/media/CZ96E7CWQAAVYYz.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ96E7CWQAAVYYz.jpg", "display_url": "pic.twitter.com/mRktzdlEB1", "indices": [33, 56], "expanded_url": "http://twitter.com/MuseumBot/status/693407092863033344/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 1024, "h": 1247}, "small": {"resize": "fit", "w": 340, "h": 414}, "medium": {"resize": "fit", "w": 600, "h": 730}}, "id_str": "693407092623949824", "url": "https://t.co/mRktzdlEB1"}]}, "truncated": false, "id": 693407092863033344, "place": null, "favorite_count": 2, "possibly_sensitive": false, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_status_id": null, "in_reply_to_screen_name": null, "text": "Nativity https://t.co/OpNseJO3oL https://t.co/mRktzdlEB1", "id_str": "693407092863033344", "entities": {"media": [{"type": "photo", "id": 693407092623949824, "media_url": "http://pbs.twimg.com/media/CZ96E7CWQAAVYYz.jpg", "media_url_https": "https://pbs.twimg.com/media/CZ96E7CWQAAVYYz.jpg", "display_url": "pic.twitter.com/mRktzdlEB1", "indices": [33, 56], "expanded_url": "http://twitter.com/MuseumBot/status/693407092863033344/photo/1", "sizes": {"thumb": {"resize": "crop", "w": 150, "h": 150}, "large": {"resize": "fit", "w": 1024, "h": 1247}, "small": {"resize": "fit", "w": 340, "h": 414}, "medium": {"resize": "fit", "w": 600, "h": 730}}, "id_str": "693407092623949824", "url": "https://t.co/mRktzdlEB1"}], "hashtags": [], "urls": [{"display_url": "metmuseum.org/collection/the\u2026", "indices": [9, 32], "expanded_url": "http://www.metmuseum.org/collection/the-collection-online/search/462886?rpp=30&pg=1413&rndkey=20160130&ao=on&ft=*&pos=42373", "url": "https://t.co/OpNseJO3oL"}], "symbols": [], "user_mentions": []}, "source": "Museum bot", "contributors": null, "favorited": false, "in_reply_to_user_id_str": null, "retweeted": false, "created_at": "Sat Jan 30 12:15:08 +0000 2016"}, "is_translator": false, "screen_name": "MuseumBot", "created_at": "Tue May 20 01:32:24 +0000 2014"}], "previous_cursor_str": "4611686020936348428", "next_cursor_str": "4611686020936348428", "previous_cursor": 4611686020936348428} diff --git a/tests/test_api_30.py b/tests/test_api_30.py index d6e349e9..fc69705b 100644 --- a/tests/test_api_30.py +++ b/tests/test_api_30.py @@ -809,6 +809,7 @@ def testGetListMembers(self): resp = self.api.GetListMembers(list_id=93527328) self.assertTrue(type(resp[0]) is twitter.User) self.assertEqual(resp[0].id, 4048395140) + self.assertEqual(len(resp), 47) @responses.activate def testGetListMembersPaged(self): @@ -820,8 +821,10 @@ def testGetListMembersPaged(self): body=resp_data, match_querystring=True, status=200) - resp = self.api.GetListMembersPaged(list_id=93527328, cursor=4611686020936348428) + _, _, resp = self.api.GetListMembersPaged(list_id=93527328, + cursor=4611686020936348428) self.assertTrue([isinstance(u, twitter.User) for u in resp]) + self.assertEqual(len(resp), 20) with open('testdata/get_list_members_extra_params.json') as f: resp_data = f.read() @@ -837,6 +840,7 @@ def testGetListMembersPaged(self): include_entities=False, count=100) self.assertFalse(resp[0].status) + self.assertEqual(len(resp), 27) @responses.activate def testGetListTimeline(self): diff --git a/twitter/api.py b/twitter/api.py index 13d88799..f663736c 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -4283,7 +4283,7 @@ def GetListMembers(self, include_entities=include_entities) result += users - if next_cursor == 0 or next_cursor == previous_cursor: + if next_cursor == 0: break else: cursor = next_cursor From 3c7fca8e40b9ba5ab5b7ec2adfecf94a78d4cc41 Mon Sep 17 00:00:00 2001 From: Wanda Evans <39149409+derpwanda@users.noreply.github.com> Date: Wed, 17 Feb 2021 13:55:36 -0600 Subject: [PATCH 71/83] update comments update comments on lines 3552 & 3554, from 'mark' to 'unmark' --- twitter/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index 13d88799..b7a96b1d 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -3549,9 +3549,9 @@ def DestroyFavorite(self, Args: status_id (int, optional): - The id of the twitter status to mark as a favorite. + The id of the twitter status to unmark as a favorite. status (twitter.Status, optional): - The twitter.Status object to mark as a favorite. + The twitter.Status object to unmark as a favorite. include_entities (bool, optional): The entities node will be omitted when set to False. From 9eca1901ae80e0333ae764ee645205df3245b003 Mon Sep 17 00:00:00 2001 From: Wanda Evans <39149409+derpwanda@users.noreply.github.com> Date: Wed, 17 Feb 2021 14:26:17 -0600 Subject: [PATCH 72/83] update comments in api.py update comments on lines 3518 & 3520 from "mark" to "unmark" --- twitter/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index b7a96b1d..e3ecd836 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -3515,9 +3515,9 @@ def CreateFavorite(self, Args: status_id (int, optional): - The id of the twitter status to mark as a favorite. + The id of the twitter status to unmark as a favorite. status (twitter.Status, optional): - The twitter.Status object to mark as a favorite. + The twitter.Status object to unmark as a favorite. include_entities (bool, optional): The entities node will be omitted when set to False. From 91e711420525dffc44f81ed286f292fd527711bc Mon Sep 17 00:00:00 2001 From: Wanda Evans <39149409+derpwanda@users.noreply.github.com> Date: Wed, 17 Feb 2021 17:56:57 -0600 Subject: [PATCH 73/83] correction Changed lines 3518 & 3520 from "unmark" to "mark" --- twitter/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index e3ecd836..b7a96b1d 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -3515,9 +3515,9 @@ def CreateFavorite(self, Args: status_id (int, optional): - The id of the twitter status to unmark as a favorite. + The id of the twitter status to mark as a favorite. status (twitter.Status, optional): - The twitter.Status object to unmark as a favorite. + The twitter.Status object to mark as a favorite. include_entities (bool, optional): The entities node will be omitted when set to False. From cbcf58ba9a07fa40ec4d75719625ccda5cbf0231 Mon Sep 17 00:00:00 2001 From: Wanda Evans <39149409+derpwanda@users.noreply.github.com> Date: Thu, 18 Feb 2021 23:01:56 -0600 Subject: [PATCH 74/83] corrected comments for DestoryBlock, DestroyMute DestroyBlock comments: block to unblock. DestroyMute comments: mute to unmute. --- twitter/api.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index b7a96b1d..832932e6 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -2209,17 +2209,17 @@ def DestroyBlock(self, Args: user_id (int, optional) - The numerical ID of the user to block. + The numerical ID of the user to unblock. screen_name (str, optional): - The screen name of the user to block. + The screen name of the user to unblock. include_entities (bool, optional): The entities node will not be included if set to False. skip_status (bool, optional): - When set to False, the blocked User's statuses will not be included + When set to False, the unblocked User's statuses will not be included with the returned User object. Returns: - A twitter.User instance representing the blocked user. + A twitter.User instance representing the unblocked user. """ return self._BlockMute(action='destroy', endpoint='block', @@ -2265,13 +2265,13 @@ def DestroyMute(self, Args: user_id (int, optional) - The numerical ID of the user to mute. + The numerical ID of the user to unmute. screen_name (str, optional): - The screen name of the user to mute. + The screen name of the user to unmute. include_entities (bool, optional): The entities node will not be included if set to False. skip_status (bool, optional): - When set to False, the muted User's statuses will not be included + When set to False, the unmuted User's statuses will not be included with the returned User object. Returns: From 1ade878f8d912264b0f2723f10e4d8886dd37392 Mon Sep 17 00:00:00 2001 From: yu9824 <58211916+yu9824@users.noreply.github.com> Date: Sat, 10 Jul 2021 19:43:54 +0900 Subject: [PATCH 75/83] Specifies the text type of long_description. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 1a0a4120..39c67ef5 100755 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ def extract_metaitem(meta): long_description=(read('README.rst') + '\n\n' + read('AUTHORS.rst') + '\n\n' + read('CHANGES')), + long_description_content_type = 'text/x-rst', author=extract_metaitem('author'), author_email=extract_metaitem('email'), maintainer=extract_metaitem('author'), From 719a507d8128a3e65d25e8b15640041b284835e5 Mon Sep 17 00:00:00 2001 From: yu9824 <58211916+yu9824@users.noreply.github.com> Date: Sat, 10 Jul 2021 19:50:30 +0900 Subject: [PATCH 76/83] Change project name to debug no pypitest server because I can not upload to project 'python-twitter'. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 39c67ef5..d415b2c4 100755 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ def extract_metaitem(meta): raise RuntimeError('Unable to find __{meta}__ string.'.format(meta=meta)) setup( - name='python-twitter', + name='python-twitter_test', version=extract_metaitem('version'), license=extract_metaitem('license'), description=extract_metaitem('description'), From 378447974fdbc18841cf6d9ae8bb5051f39e47e8 Mon Sep 17 00:00:00 2001 From: yu9824 <58211916+yu9824@users.noreply.github.com> Date: Sat, 10 Jul 2021 20:03:07 +0900 Subject: [PATCH 77/83] Modify README.rst to deal with syntax errors. --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 6e7931a3..5d006db7 100644 --- a/README.rst +++ b/README.rst @@ -111,9 +111,9 @@ Using The library provides a Python wrapper around the Twitter API and the Twitter data model. To get started, check out the examples in the examples/ folder or read the documentation at https://python-twitter.readthedocs.io which contains information about getting your authentication keys from Twitter and using the library. ----- +------------------ Using with Django ----- +------------------ Additional template tags that expand tweet urls and urlize tweet text. See the django template tags available for use with python-twitter: https://github.com/radzhome/python-twitter-django-tags @@ -195,9 +195,9 @@ or check out the inline documentation with:: $ pydoc twitter.Api ----- +------ Todo ----- +------ Patches, pull requests, and bug reports are `welcome `_, just please keep the style consistent with the original source. From 84ddd920c534e390fdaa75e4da3dbb48b96cb426 Mon Sep 17 00:00:00 2001 From: yu9824 <58211916+yu9824@users.noreply.github.com> Date: Sat, 10 Jul 2021 21:45:11 +0900 Subject: [PATCH 78/83] Define a function that converts text that causes errors due to indentation. --- setup.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d415b2c4..b4b52e19 100755 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ # limitations under the License. import os +from pdb import set_trace import re import codecs @@ -30,6 +31,19 @@ def read(filename): with codecs.open(os.path.join(cwd, filename), 'rb', 'utf-8') as h: return h.read() +def convert_txt(txt): + lst_txt = txt.split('\n') + for i in range(len(lst_txt)): + line = lst_txt[i] + match = re.match(r' +(\S.+)', line) + if match is not None: + lst_txt[i] = '\n| {}'.format(match.group(1)) + else: + match_date = re.match(r'(\d+-\d+-\d+)', line) + if match_date is not None: + lst_txt[i] = '\n**{}**'.format(match_date.group(1)) + return '\n'.join(lst_txt) + metadata = read(os.path.join(cwd, 'twitter', '__init__.py')) def extract_metaitem(meta): @@ -41,13 +55,13 @@ def extract_metaitem(meta): raise RuntimeError('Unable to find __{meta}__ string.'.format(meta=meta)) setup( - name='python-twitter_test', + name='python-twitter', version=extract_metaitem('version'), license=extract_metaitem('license'), description=extract_metaitem('description'), long_description=(read('README.rst') + '\n\n' + read('AUTHORS.rst') + '\n\n' + - read('CHANGES')), + convert_txt(read('CHANGES'))), long_description_content_type = 'text/x-rst', author=extract_metaitem('author'), author_email=extract_metaitem('email'), From 202e62c3e55164c57f0e5b3eaf227223ec5ecf38 Mon Sep 17 00:00:00 2001 From: yu9824 <58211916+yu9824@users.noreply.github.com> Date: Sat, 10 Jul 2021 21:48:18 +0900 Subject: [PATCH 79/83] Delete from pdb import set_trace --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index b4b52e19..0e0f0e36 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,6 @@ # limitations under the License. import os -from pdb import set_trace import re import codecs From eee5167dacff3d4b690987bd47def0ec05c5ef4f Mon Sep 17 00:00:00 2001 From: Dash <16400857+analogdash@users.noreply.github.com> Date: Sun, 25 Jul 2021 00:03:40 +0800 Subject: [PATCH 80/83] Fix docs to fit expected keyword argument --- twitter/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twitter/api.py b/twitter/api.py index 832932e6..307644c2 100755 --- a/twitter/api.py +++ b/twitter/api.py @@ -3318,11 +3318,11 @@ def ShowFriendship(self, """Returns information about the relationship between the two users. Args: - source_id: + source_user_id: The user_id of the subject user [Optional] source_screen_name: The screen_name of the subject user [Optional] - target_id: + target_user_id: The user_id of the target user [Optional] target_screen_name: The screen_name of the target user [Optional] From 9a11ef2666210365656bccfacac26d7cc9bb2f2f Mon Sep 17 00:00:00 2001 From: sharkykh Date: Thu, 2 May 2019 01:08:45 +0300 Subject: [PATCH 81/83] Remove `future` dependency (again) Accidentally re-added in #573. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dece197e..95c7832e 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def extract_metaitem(meta): download_url=extract_metaitem('download_url'), packages=find_packages(exclude=('tests', 'docs')), platforms=['Any'], - install_requires=['future', 'requests', 'requests-oauthlib'], + install_requires=['requests', 'requests-oauthlib'], tests_require=['pytest'], keywords='twitter api', classifiers=[ From ce60ed52674b4aa6ea3ef05cfb8e83df8d7fd892 Mon Sep 17 00:00:00 2001 From: Mike Taylor Date: Wed, 7 Aug 2024 19:14:34 -0400 Subject: [PATCH 82/83] Update README.rst Adding a note to mark that I'm archiving this repo --- README.rst | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 5d006db7..0b831bfa 100644 --- a/README.rst +++ b/README.rst @@ -4,29 +4,11 @@ A Python wrapper around the Twitter API. By the `Python-Twitter Developers `_ -.. image:: https://img.shields.io/pypi/v/python-twitter.svg - :target: https://pypi.python.org/pypi/python-twitter/ - :alt: Downloads - -.. image:: https://readthedocs.org/projects/python-twitter/badge/?version=latest - :target: http://python-twitter.readthedocs.org/en/latest/?badge=latest - :alt: Documentation Status - -.. image:: https://circleci.com/gh/bear/python-twitter.svg?style=svg - :target: https://circleci.com/gh/bear/python-twitter - :alt: Circle CI - -.. image:: http://codecov.io/github/bear/python-twitter/coverage.svg?branch=master - :target: http://codecov.io/github/bear/python-twitter - :alt: Codecov - -.. image:: https://requires.io/github/bear/python-twitter/requirements.svg?branch=master - :target: https://requires.io/github/bear/python-twitter/requirements/?branch=master - :alt: Requirements Status - -.. image:: https://dependencyci.com/github/bear/python-twitter/badge - :target: https://dependencyci.com/github/bear/python-twitter - :alt: Dependency Status +============- +NOTICE +============ +I've archived this repo to mark that I'm not going to be maintaining it. It's open-source so anyone using it can fork or take it over. +Thank you to all the people that contributed to it in the past ============ Introduction From da1e9986f179622fa2d85f5b125a3f12f3e63177 Mon Sep 17 00:00:00 2001 From: Mike Taylor Date: Wed, 7 Aug 2024 19:14:50 -0400 Subject: [PATCH 83/83] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0b831bfa..bed3afb3 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ A Python wrapper around the Twitter API. By the `Python-Twitter Developers `_ -============- +============ NOTICE ============ I've archived this repo to mark that I'm not going to be maintaining it. It's open-source so anyone using it can fork or take it over.