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/.gitignore b/.gitignore index cba5cd9e..f5955911 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ htmlcov nosetests.xml htmlcov coverage.xml +.hypothesis # PyCharm data .idea @@ -52,3 +53,9 @@ violations.flake8.txt # Built docs doc/_build/** + +# Mypy cache +**/.mypy_cache + +# VS Code +**/.vscode 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 diff --git a/Makefile b/Makefile index d9a47f53..76039c40 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: @@ -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 @@ -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 dev 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) diff --git a/README.rst b/README.rst index a735cfab..bed3afb3 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 @@ -34,7 +16,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 @@ -111,9 +93,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 @@ -143,7 +125,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 +155,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]) @@ -195,9 +177,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. @@ -220,7 +202,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. diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 3c318ad5..00000000 --- a/circle.yml +++ /dev/null @@ -1,12 +0,0 @@ -dependencies: - override: - - pip install -U pip - - make dev - -test: - pre: - - make info - - uname -a - - lsb_release -a - override: - - make ci 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/doc/getting_started.rst b/doc/getting_started.rst index 40716d4f..b5044a18 100644 --- a/doc/getting_started.rst +++ b/doc/getting_started.rst @@ -26,26 +26,32 @@ _________ 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:: 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. -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. 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 00000000..f88e18b7 Binary files /dev/null and b/doc/python-twitter-app-creation-part2-new.png differ 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 00000000..3de5bf44 Binary files /dev/null and b/doc/python-twitter-app-creation-part3-1-new.png differ 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 00000000..739d6162 Binary files /dev/null and b/doc/python-twitter-app-creation-part3-new.png differ 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') diff --git a/get_access_token.py b/get_access_token.py index d3010fe6..84845c11 100755 --- a/get_access_token.py +++ b/get_access_token.py @@ -1,6 +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. @@ -13,11 +14,18 @@ # 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 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' @@ -25,14 +33,22 @@ 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') - 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) @@ -68,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) 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 a4a493ed..56c660d0 100644 --- a/requirements.testing.txt +++ b/requirements.testing.txt @@ -1,6 +1,5 @@ -future requests -requests_oauthlib +requests-oauthlib responses pytest diff --git a/requirements.txt b/requirements.txt index d539fa58..fc12ad28 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -future requests -requests_oauthlib +requests-oauthlib +filetype 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/setup.py b/setup.py index 99635fee..33d2cfda 100755 --- a/setup.py +++ b/setup.py @@ -30,6 +30,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): @@ -47,7 +60,8 @@ def extract_metaitem(meta): 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'), maintainer=extract_metaitem('author'), @@ -56,8 +70,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'], - setup_requires=['pytest-runner'], + install_requires=['requests', 'requests-oauthlib', 'filetype'], tests_require=['pytest'], keywords='twitter api', classifiers=[ 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/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/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/testdata/media/happy.jpg b/testdata/media/happy.jpg new file mode 100644 index 00000000..e6a80f42 Binary files /dev/null and b/testdata/media/happy.jpg differ 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/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/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_api_30.py b/tests/test_api_30.py index c12b0662..fc69705b 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,13 +802,14 @@ 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) 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): @@ -816,18 +817,20 @@ 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) - 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() 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) @@ -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): @@ -844,7 +848,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 +859,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 +884,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 +962,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 +978,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 +993,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) @@ -1017,7 +1021,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): @@ -1025,7 +1029,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 +1041,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 +1165,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) @@ -1289,7 +1293,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 +1316,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) @@ -1330,13 +1334,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 +1370,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 +1380,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 +1395,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 +1405,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) @@ -1548,6 +1552,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: @@ -1643,16 +1728,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_direct_messages.py b/tests/test_direct_messages.py index 4f60e0d7..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,30 +40,44 @@ 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] 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', - text='https://t.co/L4MIplKUwR') + responses.add(POST, DEFAULT_BASE_URL, body=f.read()) + resp = api.PostDirectMessage(user_id='372018022', + text='hello') assert isinstance(resp, twitter.DirectMessage) - assert resp.text == 'https://t.co/L4MIplKUwR' + assert resp.text == 'hello' + + +@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_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.jpg', + media_type='dm_image') + assert isinstance(resp, twitter.DirectMessage) + assert resp.text == 'hello' @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/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") 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/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/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, 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/tests/test_status_place.py b/tests/test_status_place.py new file mode 100644 index 00000000..628bc229 --- /dev/null +++ b/tests/test_status_place.py @@ -0,0 +1,54 @@ +import unittest +import re +import sys +import twitter +import responses +from responses import GET, POST + +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) + + @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') 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/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] diff --git a/twitter/__init__.py b/twitter/__init__.py index 24cec746..79546f22 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.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' @@ -46,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 old mode 100644 new mode 100755 index f0541b85..ecba5d92 --- 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. @@ -49,6 +49,7 @@ Category, DirectMessage, List, + Place, Status, Trend, User, @@ -120,7 +121,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) @@ -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,21 +256,12 @@ 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' - 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 @@ -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): """ @@ -449,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 @@ -521,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) @@ -1037,15 +1039,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 @@ -1158,9 +1160,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: @@ -1262,7 +1268,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'}) @@ -1272,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 @@ -1282,6 +1288,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')) @@ -1403,12 +1413,12 @@ 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: ID of the uploaded media returned by the Twitter API. Raises if - unsuccesful. + unsuccessful. """ media_id, media_fp, filename = self._UploadMediaChunkedInit(media=media, @@ -1429,6 +1439,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): @@ -1606,14 +1691,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. @@ -1622,9 +1707,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), } @@ -2067,6 +2152,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, @@ -2104,17 +2224,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', @@ -2160,13 +2280,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: @@ -2819,8 +2939,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')) @@ -2996,6 +3114,8 @@ def GetSentDirectMessages(self, def PostDirectMessage(self, text, user_id=None, + media_file_path=None, + media_type=None, screen_name=None, return_json=False): """Post a twitter direct message from the authenticated user. @@ -3003,30 +3123,72 @@ def PostDirectMessage(self, 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. + media_file_path: + The file path to the media to be posted + media_type: + The media type. Accepted media types: dm_image, dm_gif or dm_video 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 + # 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 + + # Default + message_data_value = { + 'text': text + } + if media_file_path is not None: + try: + media = open(media_file_path, 'rb') + except IOError: + raise TwitterError({'message': 'Media file could not be opened.'}) + + response_media_id = self.UploadMediaChunked(media=media, media_category=media_type) + + # With media + message_data_value = { + 'text': text, + "attachment": { + "type": "media", + "media": { + "id": response_media_id + } + } + } - resp = self._RequestUrl(url, 'POST', data=data) + event = { + 'event': { + 'type': 'message_create', + 'message_create': { + 'target': { + 'recipient_id': user_id + }, + 'message_data': message_data_value + } + } + } + + resp = self._RequestUrl(url, 'POST', json=event) data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 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. @@ -3171,11 +3333,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] @@ -3402,9 +3564,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. @@ -4114,10 +4276,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 @@ -4136,7 +4298,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 @@ -4323,7 +4485,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. @@ -4335,6 +4498,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: @@ -4353,7 +4518,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 @@ -4418,7 +4584,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. @@ -4450,14 +4616,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 @@ -4468,8 +4640,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 @@ -4646,6 +4822,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. @@ -4679,6 +4861,32 @@ def VerifyCredentials(self, include_entities=None, skip_status=None, include_ema return User.NewFromJsonDict(data) + 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): + 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. @@ -4882,7 +5090,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 @@ -4913,7 +5121,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)) @@ -4950,29 +5160,30 @@ 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: 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, 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 = self._session.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 = self._session.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 = self._session.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 = self._session.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 - 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) @@ -5002,14 +5213,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 diff --git a/twitter/models.py b/twitter/models.py index a79515df..861f9832 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 @@ -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) @@ -498,6 +490,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 +505,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,4 +531,37 @@ 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): + """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) 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) diff --git a/twitter/twitter_utils.py b/twitter/twitter_utils.py index e68804cc..02e1ded3 100644 --- a/twitter/twitter_utils.py +++ b/twitter/twitter_utils.py @@ -1,7 +1,8 @@ -# encoding: utf-8 +# -*- coding: utf-8 -*- +"""Collection of utilities for use in API calls, functions.""" from __future__ import unicode_literals -import mimetypes +import filetype import os import re import sys @@ -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): @@ -216,12 +225,12 @@ def http_to_file(http): return data_file -def parse_media_file(passed_media): - """ Parses a media file and attempts to return a file-like object and - information about the media file. +def parse_media_file(passed_media, async_upload=False): + """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. + async_upload: flag, for validation media file attributes. Returns: file-like object, the filename of the media file, the file size, and @@ -229,9 +238,12 @@ def parse_media_file(passed_media): """ img_formats = ['image/jpeg', 'image/png', - 'image/gif', 'image/bmp', 'image/webp'] + long_img_formats = [ + 'image/gif' + ] + subtitle_formats = ['text/srt'] video_formats = ['video/mp4', 'video/quicktime'] @@ -262,22 +274,28 @@ def parse_media_file(passed_media): except Exception as e: pass - media_type = mimetypes.guess_type(os.path.basename(filename))[0] + 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.'}) - elif media_type in video_formats and file_size > 15 * 1048576: + 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 not in img_formats and media_type not in video_formats: + 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 + long_img_formats + subtitle_formats + video_formats: raise TwitterError({'message': 'Media type could not be determined.'}) return data_file, filename, file_size, media_type 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: @@ -300,6 +318,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)