├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── feature-request.md │ └── question-others.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── COMPAT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── Makefile ├── api.rst ├── conf.py ├── faq.rst ├── index.rst ├── make.bat └── usage.rst ├── examples ├── pagination.py └── savesettings_logincallback.py ├── instagram_private_api ├── __init__.py ├── client.py ├── compat.py ├── compatpatch.py ├── constants.py ├── endpoints │ ├── __init__.py │ ├── accounts.py │ ├── collections.py │ ├── common.py │ ├── discover.py │ ├── feed.py │ ├── friendships.py │ ├── highlights.py │ ├── igtv.py │ ├── live.py │ ├── locations.py │ ├── media.py │ ├── misc.py │ ├── tags.py │ ├── upload.py │ ├── users.py │ └── usertags.py ├── errors.py ├── http.py └── utils.py ├── instagram_web_api ├── __init__.py ├── client.py ├── common.py ├── compat.py ├── compatpatch.py ├── errors.py └── http.py ├── misc └── checkpoint.py ├── requirements-dev.txt ├── setup.py └── tests ├── __init__.py ├── common.py ├── private ├── __init__.py ├── accounts.py ├── apiutils.py ├── client.py ├── collections.py ├── compatpatch.py ├── discover.py ├── feed.py ├── friendships.py ├── highlights.py ├── igtv.py ├── live.py ├── locations.py ├── media.py ├── misc.py ├── tags.py ├── upload.py ├── users.py └── usertags.py ├── test_private_api.py ├── test_web_api.py └── web ├── __init__.py ├── client.py ├── compatpatch.py ├── feed.py ├── media.py ├── unauthenticated.py ├── upload.py └── user.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://buymeacoffee.com/ping/'] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Please follow the guide below 2 | 3 | - Issues submitted without this template format will be **ignored**. 4 | - Please read the questions **carefully** and answer completely. 5 | - Do not post screenshots of error messages or code. 6 | - Put an `x` into all the boxes [ ] relevant to your issue (==> [x] *no* spaces). 7 | - Use the *Preview* tab to see how your issue will actually look like. 8 | - Issues about reverse engineering is out of scope and will be closed without response. 9 | - Any mention of spam-like actions or spam-related tools/libs/etc is strictly **not allowed**. 10 | 11 | --- 12 | 13 | ### Before submitting an issue, make sure you have: 14 | - [ ] Updated to the lastest version v1.6.0 15 | - [ ] Read the [README](https://github.com/ping/instagram_private_api/blob/master/README.md) and [docs](https://instagram-private-api.readthedocs.io/en/latest/) 16 | - [ ] [Searched](https://github.com/ping/instagram_private_api/search?type=Issues) the bugtracker for similar issues including **closed** ones 17 | - [ ] Reviewed the sample code in [tests](https://github.com/ping/instagram_private_api/tree/master/tests) and [examples](https://github.com/ping/instagram_private_api/tree/master/examples) 18 | 19 | ### Which client are you using? 20 | - [ ] app (``instagram_private_api/``) 21 | - [ ] web (``instagram_web_api/``) 22 | 23 | --- 24 | 25 | ### Describe your issue 26 | 27 | Please make sure the description is worded well enough to be understood with as much context and examples as possible. 28 | 29 | If describing a problem or a bug, code to replicate the issue *must* be provided below. 30 | 31 | --- 32 | 33 | Paste the output of ``python -V`` here: 34 | 35 | Code: 36 | 37 | ```python 38 | # Example code that will produce the error reported 39 | from instagram_web_api import Client 40 | 41 | web_api = Client() 42 | user_feed_info = web_api.user_feed('1234567890', count=10) 43 | ``` 44 | 45 | Error/Debug Log: 46 | 47 | ```python 48 | Traceback (most recent call last): 49 | File "", line 1, in 50 | ZeroDivisionError: integer division or modulo by zero 51 | ``` 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report an error or problem 4 | 5 | --- 6 | 7 | ## Please follow the guide below 8 | 9 | - Issues submitted without this template format will be **ignored**. 10 | - Please read the questions **carefully** and answer completely. 11 | - Do not post screenshots of error messages or code. 12 | - Put an `x` into all the boxes [ ] relevant to your issue (==> [x] *NO* spaces). 13 | - Use the *Preview* tab to see how your issue will actually look like. 14 | - Issues about reverse engineering is out of scope and will be closed without response. 15 | - Any mention of spam-like actions or spam-related tools/libs/etc is strictly **not allowed**. 16 | 17 | --- 18 | 19 | ### Before submitting an issue, make sure you have: 20 | - [ ] Updated to the lastest version v1.6.0 21 | - [ ] Read the [README](https://github.com/ping/instagram_private_api/blob/master/README.md) and [docs](https://instagram-private-api.readthedocs.io/en/latest/) 22 | - [ ] [Searched](https://github.com/ping/instagram_private_api/search?type=Issues) the bugtracker for similar issues including **closed** ones 23 | - [ ] Reviewed the sample code in [tests](https://github.com/ping/instagram_private_api/tree/master/tests) and [examples](https://github.com/ping/instagram_private_api/tree/master/examples) 24 | 25 | ### Which client are you using? 26 | - [ ] app (``instagram_private_api/``) 27 | - [ ] web (``instagram_web_api/``) 28 | 29 | --- 30 | 31 | ### Describe the Bug/Error: 32 | 33 | Please make sure the description is worded well enough to be understood with as much context and examples as possible. 34 | 35 | Code to replicate the error must be provided below. 36 | 37 | --- 38 | 39 | Paste the output of ``python -V`` here: 40 | 41 | Code: 42 | 43 | ```python 44 | # Example code that will produce the error reported 45 | from instagram_web_api import Client 46 | 47 | web_api = Client() 48 | user_feed_info = web_api.user_feed('1234567890', count=10) 49 | ``` 50 | 51 | Error/Debug Log: 52 | 53 | ```python 54 | Traceback (most recent call last): 55 | File "", line 1, in 56 | ZeroDivisionError: integer division or modulo by zero 57 | ``` 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Request for new functionality 4 | 5 | --- 6 | 7 | ## Please follow the guide below 8 | 9 | - Issues submitted without this template format will be **ignored**. 10 | - Read the questions **carefully** and answer completely. 11 | - Do not post screenshots of error messages or code. 12 | - Put an `x` into all the boxes [ ] relevant to your issue (==> [x] *no* spaces). 13 | - Use the *Preview* tab to see how your issue will actually look like. 14 | - Issues about reverse engineering is out of scope and will be closed without response. 15 | - Any mention of spam-like actions or spam-related tools/libs/etc is strictly **not allowed**. 16 | 17 | --- 18 | 19 | ### Before submitting an issue, make sure you have: 20 | - [ ] Updated to the lastest version v1.6.0 21 | - [ ] Read the [README](https://github.com/ping/instagram_private_api/blob/master/README.md) and [docs](https://instagram-private-api.readthedocs.io/en/latest/) 22 | - [ ] [Searched](https://github.com/ping/instagram_private_api/search?type=Issues) the bugtracker for similar issues including **closed** ones 23 | - [ ] Reviewed the sample code in [tests](https://github.com/ping/instagram_private_api/tree/master/tests) and [examples](https://github.com/ping/instagram_private_api/tree/master/examples) 24 | 25 | ### Which client are you using? 26 | - [ ] app (``instagram_private_api/``) 27 | - [ ] web (``instagram_web_api/``) 28 | 29 | --- 30 | 31 | ### Describe your Feature Request: 32 | 33 | Please make sure the description is worded well enough to be understood with as much context and examples as possible. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-others.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question/Others 3 | about: Not an error or feature request 4 | 5 | --- 6 | 7 | ## Please follow the guide below 8 | 9 | - Issues submitted without this template format will be **ignored**. 10 | - Rlease read them **carefully** and answer completely. 11 | - Do not post screenshots of error messages or code. 12 | - Put an `x` into all the boxes [ ] relevant to your issue (==> [x] *no* spaces). 13 | - Use the *Preview* tab to see how your issue will actually look like. 14 | - Issues about reverse engineering is out of scope and will be closed without response. 15 | - Any mention of spam-like actions or spam-related tools/libs/etc is strictly **not allowed**. 16 | 17 | --- 18 | 19 | ### Before submitting an issue, make sure you have: 20 | - [ ] Updated to the lastest version v1.6.0 21 | - [ ] Read the [README](https://github.com/ping/instagram_private_api/blob/master/README.md) and [docs](https://instagram-private-api.readthedocs.io/en/latest/) 22 | - [ ] [Searched](https://github.com/ping/instagram_private_api/search?type=Issues) the bugtracker for similar issues including **closed** ones 23 | - [ ] Reviewed the sample code in [tests](https://github.com/ping/instagram_private_api/tree/master/tests) and [examples](https://github.com/ping/instagram_private_api/tree/master/examples) 24 | 25 | ### Which client are you using? 26 | - [ ] app (``instagram_private_api/``) 27 | - [ ] web (``instagram_web_api/``) 28 | 29 | --- 30 | 31 | ### Describe your Question/Issue: 32 | 33 | Please make sure the description is worded well enough to be understood with as much context and examples as possible. 34 | 35 | --- 36 | 37 | Paste the output of ``python -V`` here: 38 | 39 | Code: 40 | 41 | ```python 42 | # Example code that will produce the error reported 43 | from instagram_web_api import Client 44 | 45 | web_api = Client() 46 | user_feed_info = web_api.user_feed('1234567890', count=10) 47 | ``` 48 | 49 | Error/Debug Log: 50 | 51 | ```python 52 | Traceback (most recent call last): 53 | File "", line 1, in 54 | ZeroDivisionError: integer division or modulo by zero 55 | ``` 56 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What does this PR do? 2 | 3 | (Briefly describe what this PR is about.) 4 | 5 | ## Why was this PR needed? 6 | 7 | (Briefly describe reasons.) 8 | 9 | ## What are the relevant issue numbers? 10 | 11 | (List issue numbers here.) 12 | 13 | ## Does this PR meet the acceptance criteria? 14 | 15 | - [ ] Passes flake8 (refer to ``.travis.yml``) 16 | - [ ] Docs are buildable 17 | - [ ] Branch has no merge conflicts with ``master`` 18 | - [ ] Is covered by a test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coveragerc 2 | coverage.sh 3 | cov_html/ 4 | 5 | venv/ 6 | venv3/ 7 | .vscode/ 8 | docs/_build 9 | docs/_static 10 | docs/_templates 11 | 12 | *.iml 13 | .idea/ 14 | 15 | # OS generated files # 16 | .DS_Store 17 | .DS_Store? 18 | ._* 19 | .Spotlight-V100 20 | .Trashes 21 | ehthumbs.db 22 | Thumbs.db 23 | 24 | # Byte-compiled / optimized / DLL files 25 | __pycache__/ 26 | *.py[cod] 27 | *$py.class 28 | 29 | # C extensions 30 | *.so 31 | 32 | # Distribution / packaging 33 | .Python 34 | env/ 35 | build/ 36 | develop-eggs/ 37 | dist/ 38 | downloads/ 39 | eggs/ 40 | .eggs/ 41 | lib/ 42 | lib64/ 43 | parts/ 44 | sdist/ 45 | var/ 46 | *.egg-info/ 47 | .installed.cfg 48 | *.egg 49 | 50 | # PyInstaller 51 | # Usually these files are written by a python script from a template 52 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 53 | *.manifest 54 | *.spec 55 | 56 | # Installer logs 57 | pip-log.txt 58 | pip-delete-this-directory.txt 59 | 60 | # Unit test / coverage reports 61 | htmlcov/ 62 | .tox/ 63 | .coverage 64 | .coverage.* 65 | .cache 66 | nosetests.xml 67 | coverage.xml 68 | *,cover 69 | .hypothesis/ 70 | 71 | # Translations 72 | *.mo 73 | *.pot 74 | 75 | # Django stuff: 76 | *.log 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | #Ipython Notebook 85 | .ipynb_checkpoints 86 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.5" 6 | 7 | install: 8 | - pip install flake8 'pylint<2.0' 9 | 10 | script: 11 | - flake8 --max-line-length=120 --exclude=./setup.py,./instagram_private_api/compat.py,./instagram_web_api/compat.py,./instagram_private_api/endpoints/__init__.py 12 | - pylint -E instagram_private_api instagram_web_api 13 | 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /COMPAT.md: -------------------------------------------------------------------------------- 1 | ### Support for Official Endpoints 2 | 3 | Note: ``api.*()`` methods refer to using the [app api client](instagram_private_api/) while ``web.*()`` methods refer to the [web api client](instagram_web_api/) 4 | 5 | Official Endpoints | Availability | Notes | 6 | ------- | :----------: | ----- | 7 | [Users](https://www.instagram.com/developer/endpoints/users/) | 8 | /users/self | Yes | [``api.current_user()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.current_user), [``web.user_info()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.user_info) with account userid 9 | /users/``user-id`` | Yes | [``api.user_info()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.user_info), [``web.user_info()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.user_info) 10 | /users/self/media/recent | Yes | Use [``api.user_feed()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.user_feed), [``web.user_feed()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.user_feed) with account userid 11 | /users/``user-id``/media/recent | Yes | [``api.user_feed()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.user_feed), [``web.user_feed()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.user_feed) 12 | /users/self/media/liked | Yes | [``api.feed_liked()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.feed_liked) 13 | [Relationships](https://www.instagram.com/developer/endpoints/relationships/) | 14 | /users/self/follows | Yes | Use [``api.user_following()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.user_following), [``web.user_following()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.user_following) with account userid 15 | /users/self/followed-by | Yes | Use [``api.user_followers()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.user_followers), [``web.user_followers()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.user_followers) with account userid 16 | /users/self/requested-by | Yes | [``api.friendships_pending()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.friendships_pending) 17 | /users/``user-id``/relationship | Yes | [``api.friendships_show()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.friendships_show), [``api.friendships_create()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.friendships_create), [``api.friendships_destroy()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.friendships_destroy), [``web.friendships_create()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.friendships_create), [``web.friendships_destroy()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.friendships_destroy) 18 | [Media](https://www.instagram.com/developer/endpoints/media/) | 19 | /media/``media-id`` | Yes | [``api.media_info()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.media_info), [``web.media_info()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.media_info) 20 | /media/shortcode/``shortcode`` | Yes | Use [``api.oembed()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.oembed) to get the media_id and then call [``api.media_info()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.media_info) with it 21 | /media/search | No 22 | [Comments](https://www.instagram.com/developer/endpoints/comments/) | 23 | /media/``media-id``/comments | Yes | [``api.media_comments()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.media_comments), [``api.media_n_comments()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.media_n_comments), [``api.post_comment()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.post_comment), [``web.media_comments()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.media_comments), [``web.post_comment()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.post_comment) 24 | /media/``media-id``/comments/``comment-id`` | Yes | [``api.delete_comment()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.delete_comment), [``web.delete_comment()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.delete_comment) 25 | [Likes](https://www.instagram.com/developer/endpoints/likes/) | 26 | /media/``media-id``/likes | Yes | [``api.media_likers()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.media_likers), [``api.post_like()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.post_like), [``api.delete_like()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.delete_like), [``web.post_like()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.post_like), [``web.delete_like()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.delete_like) 27 | [Tags](https://www.instagram.com/developer/endpoints/tags/) | 28 | /tags/``tag-name`` | Yes | [``api.tag_info()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.tag_info) 29 | /tags/``tag-name``/media/recent | Yes | [``api.feed_tag()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.feed_tag) 30 | /tags/search | Yes | [``api.tag_search()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.tag_search), [``web.search()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.search) 31 | [Locations](https://www.instagram.com/developer/endpoints/locations/) | 32 | /locations/``location-id`` | Yes | [``api.location_info()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.location_info) 33 | /locations/``location-id``/media/recent | Yes | [``api.feed_location()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.feed_location) 34 | /locations/search | Yes | [``api.location_search()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.location_search), [``web.search()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.search) 35 | [Embedding](https://www.instagram.com/developer/embedding/) | 36 | /embed | Yes | [``api.oembed()``](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.oembed) 37 | /p/``shortcode``/media | No 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issues 4 | When submitting an [issue report](https://github.com/ping/instagram_private_api/issues/new), please make sure to fill up the details as specified in the [issue template](.github/ISSUE_TEMPLATE.md). 5 | 6 | > This is a strict requirement, and failure to do so will get your issue closed without response. 7 | 8 | ## Pull Requests 9 | Here are a few simple guidelines to follow if you wish to submit a pull request: 10 | 11 | - [**Submit an Issue**](https://github.com/ping/instagram_private_api/issues/new) (mark as "Other") describing what you intend to implement if it's a substantial change. Allow me time to provide feedback so that there is less risk of rework or rejection. 12 | - New endpoints should be accompanied by a **relevant test case**. 13 | - Backward compatibility should not be broken without very good reason. 14 | - I try to maintain a **small dependency footprint**. If you intend to add a new dependency, make sure that there is a strong case for it. 15 | - Run ``flake8 --max-line-length=120`` on your changes before pushing. 16 | - Make sure docs are buildable by running ``make html`` in the ``docs/`` folder (after you've installed the dev requirements). 17 | - **Please do not take a rejection of a PR personally**. I appreciate your contribution but I reserve the right to be the final arbiter for any changes. You're free to fork my work and tailor it for your needs, it's fine! 18 | 19 | Thank you for your interest. 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 ping 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Instagram Private API 2 | 3 | A Python wrapper for the Instagram private API with no 3rd party dependencies. Supports both the app and web APIs. 4 | 5 | ![Python 2.7, 3.5](https://img.shields.io/badge/Python-2.7%2C%203.5-3776ab.svg?maxAge=2592000) 6 | [![Release](https://img.shields.io/github/release/ping/instagram_private_api.svg?colorB=ff7043)](https://github.com/ping/instagram_private_api/releases) 7 | [![Docs](https://img.shields.io/badge/docs-readthedocs.io-ff4980.svg?maxAge=2592000)](https://instagram-private-api.readthedocs.io/en/latest/) 8 | [![Build](https://img.shields.io/travis/com/ping/instagram_private_api.svg)](https://travis-ci.com/ping/instagram_private_api) 9 | 10 | [![Build](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/ping) 11 | 12 | ## Overview 13 | 14 | I wrote this to access Instagram's API when they clamped down on developer access. Because this is meant to achieve [parity](COMPAT.md) with the [official public API](https://www.instagram.com/developer/endpoints/), methods not available in the public API will generally have lower priority. 15 | 16 | Problems? Please check the [docs](https://instagram-private-api.readthedocs.io/en/latest/) before submitting an issue. 17 | 18 | ## Features 19 | 20 | - Supports many functions that are only available through the official app, such as: 21 | * Multiple feeds, such as [user feed](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.user_feed), [location feed](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.feed_location), [tag feed](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.feed_tag), [popular feed](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.feed_popular) 22 | * Post a [photo](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.post_photo) or [video](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.post_video) to your feed or stories 23 | * [Like](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.post_like)/[unlike](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.delete_like) posts 24 | * Get [post comments](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.media_comments) 25 | * [Post](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.post_comment)/[delete](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.delete_comment) comments 26 | * [Like](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.comment_like)/[unlike](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.comment_unlike) comments 27 | * [Follow](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.friendships_create)/[unfollow](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.friendships_destroy) users 28 | * User [stories](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client.user_story_feed) 29 | * And [more](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.Client)! 30 | - The web api client supports a subset of functions that do not require login, such as: 31 | * Get user [info](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.user_info) and [feed](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.user_feed) 32 | * Get [post comments](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client.media_comments) 33 | * And [more](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.Client)! 34 | - Compatible with functions available through the public API using the ClientCompatPatch ([app](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_private_api.ClientCompatPatch)/[web](https://instagram-private-api.readthedocs.io/en/latest/api.html#instagram_web_api.ClientCompatPatch)) utility class 35 | - Beta Python 3 support 36 | 37 | An [extension module](https://github.com/ping/instagram_private_api_extensions) is available to help with common tasks like pagination, posting photos or videos. 38 | 39 | ## Documentation 40 | 41 | Documentation is available at https://instagram-private-api.readthedocs.io/en/latest/ 42 | 43 | ## Install 44 | 45 | Install with pip: 46 | 47 | ``pip install git+https://git@github.com/ping/instagram_private_api.git@1.6.0`` 48 | 49 | To update: 50 | 51 | ``pip install git+https://git@github.com/ping/instagram_private_api.git@1.6.0 --upgrade`` 52 | 53 | To update with latest repo code: 54 | 55 | ``pip install git+https://git@github.com/ping/instagram_private_api.git --upgrade --force-reinstall`` 56 | 57 | Tested on Python 2.7 and 3.5. 58 | 59 | ## Usage 60 | 61 | The [app API client](instagram_private_api/) emulates the official app and has a larger set of functions. The [web API client](instagram_web_api/) has a smaller set but can be used without logging in. 62 | 63 | Your choice will depend on your use case. 64 | 65 | The [``examples/``](examples/) and [``tests/``](tests/) are a good source of detailed sample code on how to use the clients, including a simple way to save the auth cookie for reuse. 66 | 67 | ### Option 1: Use the [official app's API](instagram_private_api/) 68 | 69 | ```python 70 | 71 | from instagram_private_api import Client, ClientCompatPatch 72 | 73 | user_name = 'YOUR_LOGIN_USER_NAME' 74 | password = 'YOUR_PASSWORD' 75 | 76 | api = Client(user_name, password) 77 | results = api.feed_timeline() 78 | items = [item for item in results.get('feed_items', []) 79 | if item.get('media_or_ad')] 80 | for item in items: 81 | # Manually patch the entity to match the public api as closely as possible, optional 82 | # To automatically patch entities, initialise the Client with auto_patch=True 83 | ClientCompatPatch.media(item['media_or_ad']) 84 | print(item['media_or_ad']['code']) 85 | ``` 86 | 87 | ### Option 2: Use the [official website's API](instagram_web_api/) 88 | 89 | ```python 90 | 91 | from instagram_web_api import Client, ClientCompatPatch, ClientError, ClientLoginError 92 | 93 | # Without any authentication 94 | web_api = Client(auto_patch=True, drop_incompat_keys=False) 95 | user_feed_info = web_api.user_feed('329452045', count=10) 96 | for post in user_feed_info: 97 | print('%s from %s' % (post['link'], post['user']['username'])) 98 | 99 | # Some endpoints, e.g. user_following are available only after authentication 100 | authed_web_api = Client( 101 | auto_patch=True, authenticate=True, 102 | username='YOUR_USERNAME', password='YOUR_PASSWORD') 103 | 104 | following = authed_web_api.user_following('123456') 105 | for user in following: 106 | print(user['username']) 107 | 108 | # Note: You can and should cache the cookie even for non-authenticated sessions. 109 | # This saves the overhead of a single http request when the Client is initialised. 110 | ``` 111 | 112 | ### Avoiding Re-login 113 | 114 | You are advised to persist/cache the auth cookie details to avoid logging in every time you make an api call. Excessive logins is a surefire way to get your account flagged for removal. It's also advisable to cache the client details such as user agent, etc together with the auth details. 115 | 116 | The saved auth cookie can be reused for up to **90 days**. 117 | 118 | ## Donate 119 | 120 | Want to keep this project going? Please donate generously [https://www.buymeacoffee.com/ping](https://www.buymeacoffee.com/ping) 121 | 122 | [![Build](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/ping) 123 | 124 | ## Support 125 | 126 | Make sure to review the [contributing documentation](CONTRIBUTING.md) before submitting an issue report or pull request. 127 | 128 | ## Legal 129 | 130 | Disclaimer: This is not affliated, endorsed or certified by Instagram. This is an independent and unofficial API. Strictly **not for spam**. Use at your own risk. 131 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = instagram_private_api 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | Developer Interface 4 | =================== 5 | 6 | This page of the documentation will cover all methods and classes available to the developer. 7 | 8 | The api currently has two main interfaces: 9 | 10 | - `App API`_ 11 | - :class:`instagram_private_api.Client` 12 | - :class:`instagram_private_api.ClientCompatPatch` 13 | - :class:`instagram_private_api.ClientError` 14 | - :class:`instagram_private_api.ClientLoginError` 15 | - :class:`instagram_private_api.ClientLoginRequiredError` 16 | - :class:`instagram_private_api.ClientCookieExpiredError` 17 | - :class:`instagram_private_api.ClientThrottledError` 18 | - :class:`instagram_private_api.ClientReqHeadersTooLargeError` 19 | - :class:`instagram_private_api.ClientConnectionError` 20 | - :class:`instagram_private_api.ClientCheckpointRequiredError` 21 | - :class:`instagram_private_api.ClientChallengeRequiredError` 22 | - :class:`instagram_private_api.ClientSentryBlockError` 23 | - :class:`instagram_private_api.MediaRatios` 24 | - :class:`instagram_private_api.MediaTypes` 25 | 26 | - `Web API`_ 27 | - :class:`instagram_web_api.Client` 28 | - :class:`instagram_web_api.ClientCompatPatch` 29 | - :class:`instagram_web_api.ClientError` 30 | - :class:`instagram_web_api.ClientCookieExpiredError` 31 | - :class:`instagram_web_api.ClientConnectionError` 32 | - :class:`instagram_web_api.ClientBadRequestError` 33 | - :class:`instagram_web_api.ClientForbiddenError` 34 | - :class:`instagram_web_api.ClientThrottledError` 35 | 36 | 37 | App API 38 | ----------- 39 | 40 | .. automodule:: instagram_private_api 41 | 42 | .. autoclass:: Client 43 | :special-members: __init__ 44 | :inherited-members: 45 | 46 | .. autoclass:: ClientCompatPatch 47 | :special-members: __init__ 48 | :inherited-members: 49 | 50 | .. autoexception:: ClientError 51 | .. autoexception:: ClientLoginError 52 | .. autoexception:: ClientLoginRequiredError 53 | .. autoexception:: ClientCookieExpiredError 54 | 55 | .. autoclass:: MediaRatios 56 | :members: 57 | 58 | .. autoclass:: MediaTypes 59 | :members: 60 | 61 | Web API 62 | ------------------- 63 | 64 | .. automodule:: instagram_web_api 65 | 66 | .. autoclass:: Client 67 | :special-members: __init__ 68 | :inherited-members: 69 | 70 | .. autoclass:: ClientCompatPatch 71 | :special-members: __init__ 72 | :inherited-members: 73 | 74 | .. autoexception:: ClientError 75 | .. autoexception:: ClientLoginError 76 | .. autoexception:: ClientCookieExpiredError 77 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # instagram_private_api documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Jan 13 15:42:33 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | import os 20 | import sys 21 | sys.path.insert(0, os.path.abspath('..')) 22 | 23 | # import instagram_private_api 24 | # import instagram_web_api 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.viewcode'] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # 46 | # source_suffix = ['.rst', '.md'] 47 | source_suffix = '.rst' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = u'instagram_private_api' 54 | copyright = u'2017, ping' 55 | author = u'ping' 56 | 57 | # The version info for the project you're documenting, acts as replacement for 58 | # |version| and |release|, also used in various other places throughout the 59 | # built documents. 60 | # 61 | # The short X.Y version. 62 | version = u'1.6.0' 63 | # The full version, including alpha/beta/rc tags. 64 | release = u'1.6.0' 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # 69 | # This is also used if you do content translation via gettext catalogs. 70 | # Usually you set "language" from the command line for these cases. 71 | language = None 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | # This patterns also effect to html_static_path and html_extra_path 76 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 77 | 78 | # The name of the Pygments (syntax highlighting) style to use. 79 | pygments_style = 'sphinx' 80 | 81 | # If true, `todo` and `todoList` produce output, else they produce nothing. 82 | todo_include_todos = False 83 | 84 | 85 | # -- Options for HTML output ---------------------------------------------- 86 | 87 | # The theme to use for HTML and HTML Help pages. See the documentation for 88 | # a list of builtin themes. 89 | # 90 | html_theme = 'sphinx_rtd_theme' 91 | 92 | # Theme options are theme-specific and customize the look and feel of a theme 93 | # further. For a list of options available for each theme, see the 94 | # documentation. 95 | # 96 | # html_theme_options = {} 97 | 98 | # Add any paths that contain custom static files (such as style sheets) here, 99 | # relative to this directory. They are copied after the builtin static files, 100 | # so a file named "default.css" will overwrite the builtin "default.css". 101 | html_static_path = ['_static'] 102 | 103 | 104 | # -- Options for HTMLHelp output ------------------------------------------ 105 | 106 | # Output file base name for HTML help builder. 107 | htmlhelp_basename = 'instagram_private_apidoc' 108 | 109 | 110 | # -- Options for LaTeX output --------------------------------------------- 111 | 112 | latex_elements = { 113 | # The paper size ('letterpaper' or 'a4paper'). 114 | # 115 | # 'papersize': 'letterpaper', 116 | 117 | # The font size ('10pt', '11pt' or '12pt'). 118 | # 119 | # 'pointsize': '10pt', 120 | 121 | # Additional stuff for the LaTeX preamble. 122 | # 123 | # 'preamble': '', 124 | 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, 132 | # author, documentclass [howto, manual, or own class]). 133 | latex_documents = [ 134 | (master_doc, 'instagram_private_api.tex', u'instagram\\_private\\_api Documentation', 135 | u'ping', 'manual'), 136 | ] 137 | 138 | 139 | # -- Options for manual page output --------------------------------------- 140 | 141 | # One entry per manual page. List of tuples 142 | # (source start file, name, description, authors, manual section). 143 | man_pages = [ 144 | (master_doc, 'instagram_private_api', u'instagram_private_api Documentation', 145 | [author], 1) 146 | ] 147 | 148 | 149 | # -- Options for Texinfo output ------------------------------------------- 150 | 151 | # Grouping the document tree into Texinfo files. List of tuples 152 | # (source start file, target name, title, author, 153 | # dir menu entry, description, category) 154 | texinfo_documents = [ 155 | (master_doc, 'instagram_private_api', u'instagram_private_api Documentation', 156 | author, 'instagram_private_api', 'One line description of project.', 157 | 'Miscellaneous'), 158 | ] 159 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | .. _faq: 2 | 3 | FAQ 4 | === 5 | 6 | .. contents:: 7 | :local: 8 | :backlinks: top 9 | 10 | Can I create accounts with this library? 11 | ---------------------------------------- 12 | No. This library will not support account creation because of abuse by spammers. If you need an account, use the official app or website. 13 | 14 | Can I _____ with this library? 15 | --------------------------------- 16 | 17 | This library is limited to what the mobile app/web interface can do. If you can't do it on those platforms, you can't do it through the library. 18 | 19 | What does error code XXX mean? 20 | ------------------------------ 21 | 22 | - **400**: Bad request. Please check the parameters specified. 23 | - **403**: The method requires authentication (web client) or the request has been denied by IG. 24 | - **404**: The entity requested is not found (web client) or the endpoint does not exist. 25 | - **429**: Too many requests. You're making too many calls. 26 | 27 | IG may also return other 4XX or 5XX codes. 28 | 29 | "Your version of Instagram is out of date. Please upgrade your app to log in to Instagram." 30 | ------------------------------------------------------------------------------------------- 31 | 32 | Instagram is rejecting the app version that the lib is using. 33 | 34 | If discarding the cached auth and relogging in does not work, you may need to: 35 | 36 | #. update the lib, or 37 | #. extract the latest signature key and version from the latest `Instagram APK`_ or from https://github.com/mgp25/Instagram-API/blob/master/src/Constants.php. 38 | 39 | .. _Instagram APK: http://www.apkmirror.com/apk/instagram/instagram-instagram 40 | 41 | With the new sig key and app version, you can modify the client like so 42 | 43 | .. code-block:: python 44 | 45 | new_app_version = '10.3.2' 46 | new_sig_key = '5ad7d6f013666cc93c88fc8af940348bd067b68f0dce3c85122a923f4f74b251' 47 | new_key_ver = '4' # does not freq change 48 | new_ig_capa = '3ToAAA==' # does not freq change 49 | 50 | api = Client( 51 | user_name, password, 52 | app_version=new_app_version, 53 | signature_key=new_sig_key, 54 | key_version= new_key_ver, 55 | ig_capabilities=new_ig_capa) 56 | 57 | How to direct message/share? 58 | ---------------------------- 59 | There are no plans to implement direct messaging/sharing functions. 60 | 61 | What does ``sentry_block`` error mean? 62 | -------------------------------------- 63 | This is the response for detected spam/bot behavior. Stop using the api in whatever way that triggered this reponse. 64 | 65 | Why are the captions not posted? 66 | -------------------------------- 67 | This is due to your account / access location (IP) being soft-blocked. 68 | 69 | What does ``checkpoint_challenge_required``, ``challenge_required`` mean? 70 | ------------------------------------------------------------------------- 71 | Your access attempt has been flagged. Login manually to pass the required challenge. 72 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. instagram_private_api documentation master file, created by 2 | sphinx-quickstart on Fri Jan 13 15:42:33 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | instagram_private_api 7 | ====================== 8 | 9 | A Python wrapper for the Instagram private API with no 3rd party dependencies. Supports both the app and web APIs. 10 | 11 | 12 | Features 13 | -------- 14 | - Supports many functions that are only available through the official app 15 | - The web api client supports a subset of functions that do not require login 16 | - Compatibility patch available to match the public API methods 17 | - Beta Python 3 support. 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | :caption: Usage 22 | 23 | usage 24 | faq 25 | 26 | .. toctree:: 27 | :maxdepth: 2 28 | :caption: API Documentation 29 | 30 | api 31 | 32 | .. toctree:: 33 | :caption: Links 34 | 35 | Repository 36 | Bug Tracker 37 | Examples 38 | Tests 39 | Public API Compatibility 40 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=instagram_private_api 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | Installation 4 | ============ 5 | 6 | Pip 7 | --- 8 | 9 | Install via pip 10 | 11 | .. code-block:: bash 12 | 13 | $ pip install git+https://git@github.com/ping/instagram_private_api.git@1.6.0 14 | 15 | Update your install with the latest release 16 | 17 | .. code-block:: bash 18 | 19 | $ pip install git+https://git@github.com/ping/instagram_private_api.git@1.6.0 --upgrade 20 | 21 | Force an update from source 22 | 23 | .. code-block:: bash 24 | 25 | $ pip install git+https://git@github.com/ping/instagram_private_api.git --upgrade --force-reinstall 26 | 27 | 28 | Source Code 29 | ----------- 30 | 31 | The library is maintained on GitHub. Feel free to clone the repository. 32 | 33 | .. code-block:: bash 34 | 35 | git clone git://github.com/ping/instagram_private_api.git 36 | 37 | 38 | Usage 39 | ===== 40 | 41 | The private app API client emulates the official app and has a larger number of functions. 42 | The web API client has a smaller set but can be used without logging in. 43 | 44 | Your choice will depend on your use case. 45 | 46 | App API 47 | ----------- 48 | 49 | .. code-block:: python 50 | 51 | from instagram_private_api import Client, ClientCompatPatch 52 | 53 | user_name = 'YOUR_LOGIN_USER_NAME' 54 | password = 'YOUR_PASSWORD' 55 | 56 | api = Client(user_name, password) 57 | results = api.feed_timeline() 58 | items = results.get('items', []) 59 | for item in items: 60 | # Manually patch the entity to match the public api as closely as possible, optional 61 | # To automatically patch entities, initialise the Client with auto_patch=True 62 | ClientCompatPatch.media(item) 63 | print(media['code']) 64 | 65 | 66 | Web API 67 | ------- 68 | 69 | .. code-block:: python 70 | 71 | from instagram_web_api import Client, ClientCompatPatch, ClientError, ClientLoginError 72 | 73 | # Without any authentication 74 | web_api = Client(auto_patch=True, drop_incompat_keys=False) 75 | user_feed_info = web_api.user_feed('329452045', count=10) 76 | for post in user_feed_info: 77 | print('%s from %s' % (post['link'], post['user']['username'])) 78 | 79 | # Some endpoints, e.g. user_following are available only after authentication 80 | authed_web_api = Client( 81 | auto_patch=True, authenticate=True, 82 | username='YOUR_USERNAME', password='YOUR_PASSWORD') 83 | 84 | following = authed_web_api.user_following('123456') 85 | for user in following: 86 | print(user['username']) 87 | 88 | # Note: You can and should cache the cookie even for non-authenticated sessions. 89 | # This saves the overhead of a single http request when the Client is initialised. 90 | 91 | 92 | Avoiding Re-login 93 | ----------------- 94 | 95 | You are advised to persist/cache the auth cookie details to avoid logging in every time you make an api call. Excessive logins is a surefire way to get your account flagged for removal. It's also advisable to cache the client details such as user agent, etc together with the auth details. 96 | 97 | The saved auth cookie can be reused for up to 90 days. 98 | 99 | An example of how to save and reuse the auth setting can be found in the examples_. 100 | 101 | .. _examples: https://github.com/ping/instagram_private_api/blob/master/examples/savesettings_logincallback.py 102 | -------------------------------------------------------------------------------- /examples/pagination.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os.path 3 | import logging 4 | import argparse 5 | try: 6 | from instagram_private_api import ( 7 | Client, __version__ as client_version) 8 | except ImportError: 9 | import sys 10 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 11 | from instagram_private_api import ( 12 | Client, __version__ as client_version) 13 | 14 | 15 | if __name__ == '__main__': 16 | 17 | logging.basicConfig() 18 | logger = logging.getLogger('instagram_private_api') 19 | logger.setLevel(logging.WARNING) 20 | 21 | # Example command: 22 | # python examples/savesettings_logincallback.py -u "yyy" -p "zzz" -settings "test_credentials.json" 23 | parser = argparse.ArgumentParser(description='Pagination demo') 24 | parser.add_argument('-u', '--username', dest='username', type=str, required=True) 25 | parser.add_argument('-p', '--password', dest='password', type=str, required=True) 26 | parser.add_argument('-debug', '--debug', action='store_true') 27 | 28 | args = parser.parse_args() 29 | if args.debug: 30 | logger.setLevel(logging.DEBUG) 31 | 32 | print('Client version: {0!s}'.format(client_version)) 33 | api = Client(args.username, args.password) 34 | 35 | # ---------- Pagination with max_id ---------- 36 | user_id = '2958144170' 37 | updates = [] 38 | results = api.user_feed(user_id) 39 | updates.extend(results.get('items', [])) 40 | 41 | next_max_id = results.get('next_max_id') 42 | while next_max_id: 43 | results = api.user_feed(user_id, max_id=next_max_id) 44 | updates.extend(results.get('items', [])) 45 | if len(updates) >= 30: # get only first 30 or so 46 | break 47 | next_max_id = results.get('next_max_id') 48 | 49 | updates.sort(key=lambda x: x['pk']) 50 | # print list of IDs 51 | print(json.dumps([u['pk'] for u in updates], indent=2)) 52 | 53 | # ---------- Pagination with rank_token and exclusion list ---------- 54 | rank_token = Client.generate_uuid() 55 | has_more = True 56 | tag_results = [] 57 | while has_more and rank_token and len(tag_results) < 60: 58 | results = api.tag_search( 59 | 'cats', rank_token, exclude_list=[t['id'] for t in tag_results]) 60 | tag_results.extend(results.get('results', [])) 61 | has_more = results.get('has_more') 62 | rank_token = results.get('rank_token') 63 | print(json.dumps([t['name'] for t in tag_results], indent=2)) 64 | -------------------------------------------------------------------------------- /examples/savesettings_logincallback.py: -------------------------------------------------------------------------------- 1 | import json 2 | import codecs 3 | import datetime 4 | import os.path 5 | import logging 6 | import argparse 7 | try: 8 | from instagram_private_api import ( 9 | Client, ClientError, ClientLoginError, 10 | ClientCookieExpiredError, ClientLoginRequiredError, 11 | __version__ as client_version) 12 | except ImportError: 13 | import sys 14 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 15 | from instagram_private_api import ( 16 | Client, ClientError, ClientLoginError, 17 | ClientCookieExpiredError, ClientLoginRequiredError, 18 | __version__ as client_version) 19 | 20 | 21 | def to_json(python_object): 22 | if isinstance(python_object, bytes): 23 | return {'__class__': 'bytes', 24 | '__value__': codecs.encode(python_object, 'base64').decode()} 25 | raise TypeError(repr(python_object) + ' is not JSON serializable') 26 | 27 | 28 | def from_json(json_object): 29 | if '__class__' in json_object and json_object['__class__'] == 'bytes': 30 | return codecs.decode(json_object['__value__'].encode(), 'base64') 31 | return json_object 32 | 33 | 34 | def onlogin_callback(api, new_settings_file): 35 | cache_settings = api.settings 36 | with open(new_settings_file, 'w') as outfile: 37 | json.dump(cache_settings, outfile, default=to_json) 38 | print('SAVED: {0!s}'.format(new_settings_file)) 39 | 40 | 41 | if __name__ == '__main__': 42 | 43 | logging.basicConfig() 44 | logger = logging.getLogger('instagram_private_api') 45 | logger.setLevel(logging.WARNING) 46 | 47 | # Example command: 48 | # python examples/savesettings_logincallback.py -u "yyy" -p "zzz" -settings "test_credentials.json" 49 | parser = argparse.ArgumentParser(description='login callback and save settings demo') 50 | parser.add_argument('-settings', '--settings', dest='settings_file_path', type=str, required=True) 51 | parser.add_argument('-u', '--username', dest='username', type=str, required=True) 52 | parser.add_argument('-p', '--password', dest='password', type=str, required=True) 53 | parser.add_argument('-debug', '--debug', action='store_true') 54 | 55 | args = parser.parse_args() 56 | if args.debug: 57 | logger.setLevel(logging.DEBUG) 58 | 59 | print('Client version: {0!s}'.format(client_version)) 60 | 61 | device_id = None 62 | try: 63 | 64 | settings_file = args.settings_file_path 65 | if not os.path.isfile(settings_file): 66 | # settings file does not exist 67 | print('Unable to find file: {0!s}'.format(settings_file)) 68 | 69 | # login new 70 | api = Client( 71 | args.username, args.password, 72 | on_login=lambda x: onlogin_callback(x, args.settings_file_path)) 73 | else: 74 | with open(settings_file) as file_data: 75 | cached_settings = json.load(file_data, object_hook=from_json) 76 | print('Reusing settings: {0!s}'.format(settings_file)) 77 | 78 | device_id = cached_settings.get('device_id') 79 | # reuse auth settings 80 | api = Client( 81 | args.username, args.password, 82 | settings=cached_settings) 83 | 84 | except (ClientCookieExpiredError, ClientLoginRequiredError) as e: 85 | print('ClientCookieExpiredError/ClientLoginRequiredError: {0!s}'.format(e)) 86 | 87 | # Login expired 88 | # Do relogin but use default ua, keys and such 89 | api = Client( 90 | args.username, args.password, 91 | device_id=device_id, 92 | on_login=lambda x: onlogin_callback(x, args.settings_file_path)) 93 | 94 | except ClientLoginError as e: 95 | print('ClientLoginError {0!s}'.format(e)) 96 | exit(9) 97 | except ClientError as e: 98 | print('ClientError {0!s} (Code: {1:d}, Response: {2!s})'.format(e.msg, e.code, e.error_response)) 99 | exit(9) 100 | except Exception as e: 101 | print('Unexpected Exception: {0!s}'.format(e)) 102 | exit(99) 103 | 104 | # Show when login expires 105 | cookie_expiry = api.cookie_jar.auth_expires 106 | print('Cookie Expiry: {0!s}'.format(datetime.datetime.fromtimestamp(cookie_expiry).strftime('%Y-%m-%dT%H:%M:%SZ'))) 107 | 108 | # Call the api 109 | results = api.user_feed('2958144170') 110 | assert len(results.get('items', [])) > 0 111 | 112 | print('All ok') 113 | -------------------------------------------------------------------------------- /instagram_private_api/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .client import Client 4 | from .compatpatch import ClientCompatPatch 5 | from .errors import ( 6 | ClientError, ClientLoginError, ClientLoginRequiredError, 7 | ClientCookieExpiredError, ClientThrottledError, ClientConnectionError, 8 | ClientCheckpointRequiredError, ClientChallengeRequiredError, 9 | ClientSentryBlockError, ClientReqHeadersTooLargeError, 10 | ) 11 | from .endpoints.upload import MediaRatios 12 | from .endpoints.common import MediaTypes 13 | 14 | 15 | __version__ = '1.6.0' 16 | -------------------------------------------------------------------------------- /instagram_private_api/compat.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # pylint: disable=unused-import 3 | try: 4 | import urllib.request as compat_urllib_request 5 | except ImportError: # Python 2 6 | import urllib2 as compat_urllib_request 7 | 8 | try: 9 | import urllib.error as compat_urllib_error 10 | except ImportError: # Python 2 11 | import urllib2 as compat_urllib_error 12 | 13 | try: 14 | import urllib.parse as compat_urllib_parse 15 | except ImportError: # Python 2 16 | import urllib as compat_urllib_parse 17 | 18 | try: 19 | from urllib.parse import urlparse as compat_urllib_parse_urlparse 20 | except ImportError: # Python 2 21 | from urlparse import urlparse as compat_urllib_parse_urlparse 22 | 23 | try: 24 | import http.cookiejar as compat_cookiejar 25 | except ImportError: # Python 2 26 | import cookielib as compat_cookiejar 27 | 28 | try: 29 | import http.cookies as compat_cookies 30 | except ImportError: # Python 2 31 | import Cookie as compat_cookies 32 | 33 | try: 34 | import cPickle as compat_pickle 35 | except ImportError: 36 | import pickle as compat_pickle 37 | 38 | try: 39 | import http.client as compat_http_client 40 | except ImportError: # Python 2 41 | import httplib as compat_http_client 42 | -------------------------------------------------------------------------------- /instagram_private_api/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .accounts import AccountsEndpointsMixin 3 | from .discover import DiscoverEndpointsMixin 4 | from .feed import FeedEndpointsMixin 5 | from .friendships import FriendshipsEndpointsMixin 6 | from .live import LiveEndpointsMixin 7 | from .media import MediaEndpointsMixin 8 | from .misc import MiscEndpointsMixin 9 | from .locations import LocationsEndpointsMixin 10 | from .tags import TagsEndpointsMixin 11 | from .upload import UploadEndpointsMixin 12 | from .users import UsersEndpointsMixin 13 | from .usertags import UsertagsEndpointsMixin 14 | from .collections import CollectionsEndpointsMixin 15 | from .highlights import HighlightsEndpointsMixin 16 | from .igtv import IGTVEndpointsMixin 17 | 18 | from .common import ( 19 | ClientDeprecationWarning, 20 | ClientPendingDeprecationWarning, 21 | ClientExperimentalWarning, 22 | ) 23 | -------------------------------------------------------------------------------- /instagram_private_api/endpoints/collections.py: -------------------------------------------------------------------------------- 1 | import json 2 | from ..compatpatch import ClientCompatPatch 3 | 4 | 5 | class CollectionsEndpointsMixin(object): 6 | """For endpoints in related to collections functionality.""" 7 | 8 | def list_collections(self): 9 | return self._call_api('collections/list/') 10 | 11 | def collection_feed(self, collection_id, **kwargs): 12 | """ 13 | Get the items in a collection. 14 | 15 | :param collection_id: Collection ID 16 | :return: 17 | """ 18 | endpoint = 'feed/collection/{collection_id!s}/'.format(**{'collection_id': collection_id}) 19 | res = self._call_api(endpoint, query=kwargs) 20 | if self.auto_patch and res.get('items'): 21 | [ClientCompatPatch.media(m['media'], drop_incompat_keys=self.drop_incompat_keys) 22 | for m in res.get('items', []) if m.get('media')] 23 | return res 24 | 25 | def create_collection(self, name, added_media_ids=None): 26 | """ 27 | Create a new collection. 28 | 29 | :param name: Name for the collection 30 | :param added_media_ids: list of media_ids 31 | :return: 32 | .. code-block:: javascript 33 | 34 | { 35 | "status": "ok", 36 | "collection_id": "1700000000123", 37 | "cover_media": { 38 | "media_type": 1, 39 | "original_width": 1080, 40 | "original_height": 1080, 41 | "id": 1492726080000000, 42 | "image_versions2": { 43 | "candidates": [ 44 | { 45 | "url": "http://scontent-xx4-1.cdninstagram.com/...123.jpg", 46 | "width": 1080, 47 | "height": 1080 48 | }, 49 | ... 50 | ] 51 | } 52 | }, 53 | "collection_name": "A Collection" 54 | } 55 | """ 56 | params = {'name': name} 57 | if added_media_ids and isinstance(added_media_ids, str): 58 | added_media_ids = [added_media_ids] 59 | if added_media_ids: 60 | params['added_media_ids'] = json.dumps(added_media_ids, separators=(',', ':')) 61 | params.update(self.authenticated_params) 62 | return self._call_api('collections/create/', params=params) 63 | 64 | def edit_collection(self, collection_id, added_media_ids): 65 | """ 66 | Add media IDs to an existing collection. 67 | 68 | :param collection_id: Collection ID 69 | :param added_media_ids: list of media IDs 70 | :return: Returns same object as :meth:`create_collection` 71 | """ 72 | if isinstance(added_media_ids, str): 73 | added_media_ids = [added_media_ids] 74 | params = { 75 | 'added_media_ids': json.dumps(added_media_ids, separators=(',', ':')) 76 | } 77 | params.update(self.authenticated_params) 78 | endpoint = 'collections/{collection_id!s}/edit/'.format(**{'collection_id': collection_id}) 79 | return self._call_api(endpoint, params=params) 80 | 81 | def delete_collection(self, collection_id): 82 | """ 83 | Delete a collection. 84 | 85 | :param collection_id: Collection ID 86 | :return: 87 | .. code-block:: javascript 88 | 89 | { 90 | "status": "ok" 91 | } 92 | """ 93 | params = self.authenticated_params 94 | endpoint = 'collections/{collection_id!s}/delete/'.format(**{'collection_id': collection_id}) 95 | return self._call_api(endpoint, params=params) 96 | -------------------------------------------------------------------------------- /instagram_private_api/endpoints/common.py: -------------------------------------------------------------------------------- 1 | 2 | class ClientDeprecationWarning(DeprecationWarning): 3 | pass 4 | 5 | 6 | class ClientPendingDeprecationWarning(PendingDeprecationWarning): 7 | pass 8 | 9 | 10 | class ClientExperimentalWarning(UserWarning): 11 | pass 12 | 13 | 14 | class MediaTypes(object): 15 | """Psuedo enum-ish/lookup class for media types.""" 16 | 17 | PHOTO = 1 #: Photo type 18 | VIDEO = 2 #: Video type 19 | CAROUSEL = 8 #: Carousel/Album type 20 | 21 | ALL = (PHOTO, VIDEO, CAROUSEL) 22 | 23 | __media_type_map = { 24 | 'image': PHOTO, 25 | 'video': VIDEO, 26 | 'carousel': CAROUSEL, 27 | } 28 | 29 | @staticmethod 30 | def id_to_name(media_type_id): 31 | """Convert a media type ID to its name""" 32 | try: 33 | return [k for k, v in MediaTypes.__media_type_map.items() if v == media_type_id][0] 34 | except IndexError: 35 | raise ValueError('Invalid media ID') 36 | 37 | @staticmethod 38 | def name_to_id(media_type_name): 39 | """Convert a media type name to its ID""" 40 | try: 41 | return MediaTypes.__media_type_map[media_type_name] 42 | except KeyError: 43 | raise ValueError('Invalid media name') 44 | -------------------------------------------------------------------------------- /instagram_private_api/endpoints/discover.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from .common import ClientDeprecationWarning 4 | from ..compatpatch import ClientCompatPatch 5 | 6 | 7 | class DiscoverEndpointsMixin(object): 8 | """For endpoints in ``/discover/``.""" 9 | 10 | def explore(self, **kwargs): 11 | """ 12 | Get explore items 13 | 14 | :param kwargs: 15 | - **max_id**: For pagination 16 | :return: 17 | """ 18 | query = {'is_prefetch': 'false', 'is_from_promote': 'false'} 19 | query.update(kwargs) 20 | res = self._call_api('discover/explore/', query=query) 21 | if self.auto_patch: 22 | [ClientCompatPatch.media(item['media'], drop_incompat_keys=self.drop_incompat_keys) 23 | if item.get('media') else item for item in res['items']] 24 | return res 25 | 26 | def discover_channels_home(self): # pragma: no cover 27 | """Discover channels home""" 28 | warnings.warn( 29 | 'This endpoint is believed to be obsolete. Do not use.', 30 | ClientDeprecationWarning) 31 | 32 | res = self._call_api('discover/channels_home/') 33 | if self.auto_patch: 34 | for item in res.get('items', []): 35 | for row_item in item.get('row_items', []): 36 | if row_item.get('media'): 37 | ClientCompatPatch.media(row_item['media']) 38 | return res 39 | 40 | def discover_chaining(self, user_id): 41 | """ 42 | Get suggested users 43 | 44 | :param user_id: 45 | :return: 46 | """ 47 | res = self._call_api('discover/chaining/', query={'target_id': user_id}) 48 | if self.auto_patch: 49 | [ClientCompatPatch.list_user(user) for user in res.get('users', [])] 50 | return res 51 | 52 | def discover_top_live(self, **kwargs): 53 | """ 54 | Get top live broadcasts 55 | 56 | :param kwargs: 57 | - max_id: For pagination 58 | :return: 59 | """ 60 | return self._call_api('discover/top_live/', query=kwargs) 61 | 62 | def top_live_status(self, broadcast_ids): 63 | """ 64 | Get status for a list of broadcast_ids 65 | 66 | :return: 67 | """ 68 | if isinstance(broadcast_ids, str): 69 | broadcast_ids = [broadcast_ids] 70 | broadcast_ids = [str(x) for x in broadcast_ids] 71 | params = {'broadcast_ids': broadcast_ids} 72 | params.update(self.authenticated_params) 73 | return self._call_api('discover/top_live_status/', params=params) 74 | -------------------------------------------------------------------------------- /instagram_private_api/endpoints/highlights.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class HighlightsEndpointsMixin(object): 5 | """For endpoints in ``/highlights/`` or related to the highlights feature.""" 6 | 7 | def stories_archive(self, **kwargs): 8 | """ 9 | Returns the authenticated user's story archive. The returned items's id 10 | value is passed to ``reels_media()`` to retrieve 11 | 12 | Example: 13 | .. code-block:: python 14 | 15 | archived_stories = api.stories_archive() 16 | if archived_stories.get('items): 17 | item_ids = [a['id'] for a in archived_stories['items']] 18 | archived_stories_media = api.reels_media(user_ids=item_ids) 19 | 20 | :return: 21 | .. code-block:: javascript 22 | 23 | { 24 | "items": [{ 25 | "timestamp": 1510090000, 26 | "media_count": 3, 27 | "id": "archiveDay:1710000000", 28 | "reel_type": "archive_day_reel", 29 | "latest_reel_media": 1510090000 30 | }], 31 | "num_results": 1, 32 | "more_available": false, 33 | "max_id": null, 34 | "status": "ok" 35 | } 36 | """ 37 | query = {'include_cover': '0'} 38 | if kwargs: 39 | query.update(kwargs) 40 | return self._call_api('archive/reel/day_shells/', query=query) 41 | 42 | def highlights_user_feed(self, user_id): 43 | """ 44 | Returns a user's highlight tray 45 | 46 | :param user_id: 47 | """ 48 | endpoint = 'highlights/{user_id!s}/highlights_tray/'.format(user_id=user_id) 49 | return self._call_api(endpoint) 50 | 51 | def highlight_create( 52 | self, media_ids, cover_media_id=None, 53 | title='Highlights', source='self_profile'): 54 | """ 55 | Create a new highlight 56 | 57 | :param media_ids: A list of media_ids 58 | :param cover_media_id: The media_id for the highlight cover image 59 | :param title: Title of the highlight 60 | :param module: The UI module via which the highlight is created 61 | """ 62 | if not (media_ids and isinstance(media_ids, list)): 63 | raise ValueError('media_ids must be a non-empty list') 64 | 65 | if not cover_media_id: 66 | cover_media_id = media_ids[0] 67 | 68 | if not title: 69 | title = 'Highlights' 70 | 71 | if len(title) > 16: 72 | raise ValueError('title must not exceed 16 characters') 73 | 74 | cover = { 75 | 'media_id': cover_media_id, 76 | 'crop_rect': json.dumps( 77 | [0.0, 0.21830457, 1.0, 0.78094524], separators=(',', ':')) 78 | } 79 | params = { 80 | 'media_ids': json.dumps(media_ids, separators=(',', ':')), 81 | 'cover': json.dumps(cover, separators=(',', ':')), 82 | 'source': source, 83 | 'title': title, 84 | } 85 | params.update(self.authenticated_params) 86 | return self._call_api('highlights/create_reel/', params=params) 87 | 88 | def highlight_edit( 89 | self, highlight_id, cover_media_id=None, 90 | added_media_ids=[], removed_media_ids=[], 91 | title=None, source='story_viewer'): 92 | """ 93 | Edits a highlight 94 | 95 | :param highlight_id: highlight_id, example 'highlight:1770000' 96 | :param cover_media_id: The media_id for the highlight cover image 97 | :param added_media_ids: List of media_id to be added 98 | :param removed_media_ids: List of media_id to be removed 99 | :param title: Title of the highlight 100 | :param module: The UI module via which the highlight is created 101 | """ 102 | endpoint = 'highlights/{highlight_id!s}/edit_reel/'.format( 103 | highlight_id=highlight_id 104 | ) 105 | 106 | # sanitise inputs 107 | if not added_media_ids: 108 | added_media_ids = [] 109 | elif not isinstance(added_media_ids, list): 110 | raise ValueError('added_media_ids must be a list') 111 | 112 | if not removed_media_ids: 113 | removed_media_ids = [] 114 | elif not isinstance(removed_media_ids, list): 115 | raise ValueError('removed_media_ids must be a list') 116 | 117 | if title and len(title) > 16: 118 | raise ValueError('title must not exceed 16 characters') 119 | 120 | if not (added_media_ids or removed_media_ids or cover_media_id or title): 121 | raise ValueError('No edited values') 122 | 123 | params = { 124 | 'added_media_ids': json.dumps(added_media_ids, separators=(',', ':')), 125 | 'removed_media_ids': json.dumps(removed_media_ids, separators=(',', ':')), 126 | 'source': source, 127 | } 128 | if title: 129 | params['title'] = title 130 | if cover_media_id: 131 | cover = { 132 | 'media_id': cover_media_id, 133 | 'crop_rect': json.dumps( 134 | [0.0, 0.21830457, 1.0, 0.78094524], separators=(',', ':')) 135 | } 136 | params['cover'] = json.dumps(cover, separators=(',', ':')) 137 | 138 | params.update(self.authenticated_params) 139 | return self._call_api(endpoint, params=params) 140 | 141 | def highlight_delete(self, highlight_id): 142 | """ 143 | Deletes specified highlight 144 | 145 | :param highlight_id: highlight_id, example 'highlight:1770000' 146 | """ 147 | endpoint = 'highlights/{highlight_id!s}/delete_reel/'.format( 148 | highlight_id=highlight_id 149 | ) 150 | return self._call_api(endpoint, params=self.authenticated_params) 151 | -------------------------------------------------------------------------------- /instagram_private_api/endpoints/igtv.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from ..compatpatch import ClientCompatPatch 4 | 5 | USER_CHANNEL_ID_RE = r'^user_[1-9]\d+$' 6 | 7 | 8 | class IGTVEndpointsMixin(object): 9 | """For endpoints in ``/igtv/``.""" 10 | 11 | def tvchannel(self, channel_id, **kwargs): 12 | """ 13 | Get channel 14 | 15 | :param channel_id: One of 'for_you', 'chrono_following', 'popular', 'continue_watching' 16 | (as returned by :meth:`tvguide`) or for a user 'user_12345' where user_id = '12345' 17 | """ 18 | if (channel_id not in ('for_you', 'chrono_following', 'popular', 'continue_watching') 19 | and not re.match(USER_CHANNEL_ID_RE, channel_id)): 20 | raise ValueError('Invalid channel_id: {}'.format(channel_id)) 21 | 22 | endpoint = 'igtv/channel/' 23 | params = {'id': channel_id} 24 | params.update(self.authenticated_params) 25 | if kwargs: 26 | params.update(kwargs) 27 | res = self._call_api(endpoint, params=params) 28 | 29 | if self.auto_patch: 30 | [ClientCompatPatch.media(m, drop_incompat_keys=self.drop_incompat_keys) 31 | for m in res.get('items', [])] 32 | 33 | return res 34 | 35 | def tvguide(self): 36 | """TV guide to popular, following, suggested channels, etc""" 37 | res = self._call_api('igtv/tv_guide/') 38 | if self.auto_patch: 39 | for c in res.get('channels', []): 40 | [ClientCompatPatch.media(m, drop_incompat_keys=self.drop_incompat_keys) 41 | for m in c.get('items', [])] 42 | [ClientCompatPatch.media(m, drop_incompat_keys=self.drop_incompat_keys) 43 | for m in res.get('my_channel', {}).get('items', [])] 44 | return res 45 | 46 | def search_igtv(self, text): 47 | """ 48 | Search igtv 49 | 50 | :param text: Search term 51 | """ 52 | text = text.strip() 53 | if not text.strip(): 54 | raise ValueError('Search text cannot be empty') 55 | 56 | res = self._call_api('igtv/search/', query={'query': text}) 57 | if self.auto_patch: 58 | for r in res.get('results', []): 59 | [ClientCompatPatch.media(m, drop_incompat_keys=self.drop_incompat_keys) 60 | for m in r.get('channel', {}).get('items', [])] 61 | if r.get('user'): 62 | ClientCompatPatch.user(r['user'], drop_incompat_keys=self.drop_incompat_keys) 63 | return res 64 | -------------------------------------------------------------------------------- /instagram_private_api/endpoints/live.py: -------------------------------------------------------------------------------- 1 | from ..utils import gen_user_breadcrumb 2 | from ..compatpatch import ClientCompatPatch 3 | 4 | 5 | class LiveEndpointsMixin(object): 6 | """For endpoints in ``/live/``.""" 7 | 8 | def user_broadcast(self, user_id): 9 | """ 10 | Helper method to get a user's broadcast if there is one currently live. Returns none otherwise. 11 | 12 | :param user_id: 13 | :return: 14 | """ 15 | results = self.user_story_feed(user_id) 16 | return results.get('broadcast') 17 | 18 | def broadcast_like(self, broadcast_id, like_count=1): 19 | """ 20 | Like a live broadcast 21 | 22 | :param broadcast_id: Broadcast id 23 | :param like_count: 24 | :return: 25 | """ 26 | if not 1 <= like_count <= 5: 27 | raise ValueError('Invalid like_count') 28 | broadcast_id = str(broadcast_id) 29 | endpoint = 'live/{broadcast_id!s}/like/'.format(**{'broadcast_id': broadcast_id}) 30 | params = {'user_like_count': str(like_count)} 31 | params.update(self.authenticated_params) 32 | return self._call_api(endpoint, params=params) 33 | 34 | def broadcast_like_count(self, broadcast_id, like_ts=0): 35 | """ 36 | Get a live broadcast's like count 37 | 38 | :param broadcast_id: Broadcast id 39 | :return: 40 | """ 41 | broadcast_id = str(broadcast_id) 42 | endpoint = 'live/{broadcast_id!s}/get_like_count/'.format(**{'broadcast_id': broadcast_id}) 43 | return self._call_api(endpoint, query={'like_ts': like_ts}) 44 | 45 | def broadcast_comments(self, broadcast_id, last_comment_ts=0): 46 | """ 47 | Get a live broadcast's latest comments 48 | 49 | :param broadcast_id: Broadcast id 50 | :param last_comment_ts: 51 | :return: 52 | """ 53 | broadcast_id = str(broadcast_id) 54 | endpoint = 'live/{broadcast_id!s}/get_comment/'.format(**{'broadcast_id': broadcast_id}) 55 | res = self._call_api(endpoint, query={'last_comment_ts': last_comment_ts}) 56 | if self.auto_patch and res.get('comments'): 57 | [ClientCompatPatch.comment(c) for c in res.get('comments', [])] 58 | if res.get('pinned_comment'): 59 | ClientCompatPatch.comment(res['pinned_comment']) 60 | return res 61 | 62 | def broadcast_heartbeat_and_viewercount(self, broadcast_id): 63 | """ 64 | Get a live broadcast's heartbeat and viewer count 65 | 66 | :param broadcast_id: Broadcast id 67 | :return: 68 | """ 69 | broadcast_id = str(broadcast_id) 70 | endpoint = 'live/{broadcast_id!s}/heartbeat_and_get_viewer_count/'.format(**{'broadcast_id': broadcast_id}) 71 | params = { 72 | '_csrftoken': self.csrftoken, 73 | '_uuid': self.uuid 74 | } 75 | return self._call_api(endpoint, params=params, unsigned=True) 76 | 77 | def broadcast_comment(self, broadcast_id, comment_text): 78 | """ 79 | Post a comment to a live broadcast 80 | 81 | :param broadcast_id: Broadcast id 82 | :param comment_text: Comment text 83 | :return: 84 | """ 85 | broadcast_id = str(broadcast_id) 86 | endpoint = 'live/{broadcast_id!s}/comment/'.format(**{'broadcast_id': broadcast_id}) 87 | params = { 88 | 'live_or_vod': '1', 89 | 'offset_to_video_start': '0', 90 | 'comment_text': comment_text, 91 | 'user_breadcrumb': gen_user_breadcrumb(len(comment_text)), 92 | 'idempotence_token': self.generate_uuid(), 93 | } 94 | params.update(self.authenticated_params) 95 | res = self._call_api(endpoint, params=params) 96 | if self.auto_patch and res.get('comment'): 97 | ClientCompatPatch.comment(res['comment']) 98 | return res 99 | 100 | def broadcast_info(self, broadcast_id): 101 | """ 102 | Get broadcast information. 103 | Known broadcast_status values: 'active', 'interrupted', 'stopped', 'hard_stop' 104 | 105 | :param broadcast_id: Broadcast Id 106 | :return: 107 | .. code-block:: javascript 108 | 109 | { 110 | "status": "ok", 111 | "broadcast_status": "active", 112 | "media_id": "12345678934374208_123456789", 113 | "cover_frame_url": "https://scontent-hkg3-1.cdninstagram.com/something.jpg", 114 | "broadcast_owner": { 115 | "username": "abc", 116 | "friendship_status": { 117 | "incoming_request": false, 118 | "followed_by": false, 119 | "outgoing_request": false, 120 | "following": false, 121 | "blocking": false, 122 | "is_private": false 123 | }, 124 | "profile_pic_url": "http://scontent-hkg3-1.cdninstagram.com/somethingelse.jpg", 125 | "profile_pic_id": "1234567850644676241_123456789", 126 | "full_name": "ABC", 127 | "pk": 123456789, 128 | "is_verified": true, 129 | "is_private": false 130 | }, 131 | "dash_abr_playback_url": null, 132 | "broadcast_message": "", 133 | "published_time": 1485312576, 134 | "dash_playback_url": "https://scontent-hkg3-1.cdninstagram.com/hvideo-ash1/v/dash-hd/spmething.mpd", 135 | "rtmp_playback_url": "rtmp://svelivestream007.16.ash1.facebook.com:16000/live-hd/something", 136 | "id": 178591123456789, 137 | "viewer_count": 9000.0 138 | } 139 | """ 140 | broadcast_id = str(broadcast_id) 141 | endpoint = 'live/{broadcast_id!s}/info/'.format(**{'broadcast_id': broadcast_id}) 142 | return self._call_api(endpoint) 143 | 144 | def suggested_broadcasts(self, **kwargs): 145 | """ 146 | Get sugggested broadcasts 147 | 148 | :param kwargs: 149 | :return: 150 | """ 151 | return self._call_api('live/get_suggested_broadcasts/', query=kwargs) 152 | 153 | def replay_broadcast_comments( 154 | self, broadcast_id, starting_offset=0, 155 | encoding_tag='instagram_dash_remuxed'): 156 | """ 157 | Get comments for a post live broadcast. 158 | 159 | :param broadcast_id: 160 | :param starting_offset: 161 | :param encoding_tag: 162 | :return: 163 | """ 164 | broadcast_id = str(broadcast_id) 165 | query = { 166 | 'starting_offset': starting_offset, 167 | 'encoding_tag': encoding_tag, 168 | } 169 | endpoint = 'live/{broadcast_id!s}/get_post_live_comments/'.format( 170 | **{'broadcast_id': broadcast_id}) 171 | res = self._call_api(endpoint, query=query) 172 | if self.auto_patch and res.get('comments'): 173 | [ClientCompatPatch.comment(c['comment']) for c in res.get('comments', []) 174 | if c.get('comment')] 175 | return res 176 | 177 | def replay_broadcast_likes( 178 | self, broadcast_id, starting_offset=0, 179 | encoding_tag='instagram_dash_remuxed'): 180 | """ 181 | Get likes for a post live broadcast. 182 | 183 | :param broadcast_id: 184 | :param starting_offset: 185 | :param encoding_tag: 186 | :return: 187 | """ 188 | broadcast_id = str(broadcast_id) 189 | query = { 190 | 'starting_offset': starting_offset, 191 | 'encoding_tag': encoding_tag, 192 | } 193 | endpoint = 'live/{broadcast_id!s}/get_post_live_likes/'.format( 194 | **{'broadcast_id': broadcast_id}) 195 | return self._call_api(endpoint, query=query) 196 | -------------------------------------------------------------------------------- /instagram_private_api/endpoints/locations.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from ..utils import raise_if_invalid_rank_token 5 | from ..compatpatch import ClientCompatPatch 6 | 7 | 8 | class LocationsEndpointsMixin(object): 9 | """For endpoints related to location functionality.""" 10 | 11 | def location_info(self, location_id): 12 | """ 13 | Get a location info 14 | 15 | :param location_id: 16 | :return: 17 | .. code-block:: javascript 18 | 19 | { 20 | "status": "ok", 21 | "location": { 22 | "external_source": "facebook_places", 23 | "city": "", 24 | "name": "Berlin Brandenburger Tor", 25 | "facebook_places_id": 114849465334163, 26 | "address": "Pariser Platz", 27 | "lat": 52.51588, 28 | "pk": 229573811, 29 | "lng": 13.37892 30 | } 31 | } 32 | """ 33 | endpoint = 'locations/{location_id!s}/info/'.format(**{'location_id': location_id}) 34 | return self._call_api(endpoint) 35 | 36 | def location_related(self, location_id, **kwargs): 37 | """ 38 | Get related locations 39 | 40 | :param location_id: 41 | :return: 42 | """ 43 | endpoint = 'locations/{location_id!s}/related/'.format(**{'location_id': location_id}) 44 | query = { 45 | 'visited': json.dumps([{'id': location_id, 'type': 'location'}], separators=(',', ':')), 46 | 'related_types': json.dumps(['location'], separators=(',', ':'))} 47 | query.update(kwargs) 48 | return self._call_api(endpoint, query=query) 49 | 50 | def location_search(self, latitude, longitude, query=None, **kwargs): 51 | """ 52 | Location search 53 | 54 | :param latitude: 55 | :param longitude: 56 | :param query: 57 | :return: 58 | """ 59 | query_params = { 60 | 'rank_token': self.rank_token, 61 | 'latitude': latitude, 62 | 'longitude': longitude, 63 | 'timestamp': int(time.time()) 64 | } 65 | if query: 66 | query_params['search_query'] = query 67 | query_params.update(kwargs) 68 | return self._call_api('location_search/', query=query_params) 69 | 70 | def location_fb_search(self, query, rank_token, exclude_list=[], **kwargs): 71 | """ 72 | Search for locations by query text 73 | 74 | :param query: search terms 75 | :param rank_token: Required for paging through a single feed. See examples/pagination.py 76 | :param exclude_list: List of numerical location IDs to exclude 77 | :param kwargs: 78 | - **max_id**: For pagination 79 | :return: 80 | """ 81 | raise_if_invalid_rank_token(rank_token) 82 | 83 | if not exclude_list: 84 | exclude_list = [] 85 | 86 | query_params = { 87 | 'query': query, 88 | 'timezone_offset': self.timezone_offset, 89 | 'count': 30, 90 | 'exclude_list': json.dumps(exclude_list, separators=(',', ':')), 91 | 'rank_token': rank_token, 92 | } 93 | query_params.update(kwargs) 94 | return self._call_api('fbsearch/places/', query=query_params) 95 | 96 | def location_section(self, location_id, rank_token, tab='ranked', **kwargs): 97 | """ 98 | Get a location feed 99 | 100 | :param location_id: 101 | :param rank_token: Required for paging through a single feed and can be generated with 102 | :meth:`generate_uuid`. You should use the same rank_token for paging through a single location. 103 | :param tab: One of 'ranked', 'recent' 104 | :kwargs: 105 | **extract**: return the array of media items only 106 | **page**: for pagination 107 | **next_media_ids**: array of media_id (int) for pagination 108 | **max_id**: for pagination 109 | :return: 110 | """ 111 | raise_if_invalid_rank_token(rank_token) 112 | if tab not in ('ranked', 'recent'): 113 | raise ValueError('Invalid tab: {}'.format(tab)) 114 | 115 | extract_media_only = kwargs.pop('extract', False) 116 | endpoint = 'locations/{location_id!s}/sections/'.format(**{'location_id': location_id}) 117 | params = { 118 | 'rank_token': rank_token, 119 | 'tab': tab, 120 | 'session_id': self.session_id, 121 | } 122 | 123 | # explicitly set known paging parameters to avoid triggering server-side errors 124 | if kwargs.get('max_id'): 125 | params['max_id'] = kwargs.pop('max_id') 126 | if kwargs.get('page'): 127 | params['page'] = kwargs.pop('page') 128 | if kwargs.get('next_media_ids'): 129 | params['next_media_ids'] = json.dumps(kwargs.pop('next_media_ids'), separators=(',', ':')) 130 | kwargs.pop('max_id', None) 131 | kwargs.pop('page', None) 132 | kwargs.pop('next_media_ids', None) 133 | 134 | params.update(kwargs) 135 | results = self._call_api(endpoint, params=params, unsigned=True) 136 | extracted_medias = [] 137 | if self.auto_patch: 138 | for s in results.get('sections', []): 139 | for m in s.get('layout_content', {}).get('medias', []): 140 | if m.get('media'): 141 | ClientCompatPatch.media(m['media'], drop_incompat_keys=self.drop_incompat_keys) 142 | if extract_media_only: 143 | extracted_medias.append(m['media']) 144 | if extract_media_only: 145 | return extracted_medias 146 | return results 147 | 148 | def location_stories(self, location_id, **kwargs): 149 | """ 150 | Get a location story feed 151 | 152 | :param location_id: 153 | :param rank_token: Required for paging through a single feed and can be generated with 154 | :meth:`generate_uuid`. You should use the same rank_token for paging through a single location. 155 | :return: 156 | """ 157 | endpoint = 'locations/{location_id!s}/story/'.format(**{'location_id': location_id}) 158 | # params = { 159 | # 'rank_token': rank_token, 160 | # 'tab': tab, 161 | # 'session_id': self.session_id, 162 | # } 163 | # params.update(kwargs) 164 | # return self._call_api(endpoint, params=params) 165 | return self._call_api(endpoint) 166 | -------------------------------------------------------------------------------- /instagram_private_api/endpoints/misc.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from .common import ClientDeprecationWarning 4 | from ..constants import Constants 5 | from ..compatpatch import ClientCompatPatch 6 | 7 | 8 | class MiscEndpointsMixin(object): 9 | """For miscellaneous functions.""" 10 | 11 | def sync(self, prelogin=False): 12 | """Synchronise experiments.""" 13 | if prelogin: 14 | params = { 15 | 'id': self.generate_uuid(), 16 | 'experiments': Constants.LOGIN_EXPERIMENTS 17 | } 18 | else: 19 | params = { 20 | 'id': self.authenticated_user_id, 21 | 'experiments': Constants.EXPERIMENTS 22 | } 23 | params.update(self.authenticated_params) 24 | return self._call_api('qe/sync/', params=params) 25 | 26 | def expose(self, experiment='ig_android_profile_contextual_feed'): # pragma: no cover 27 | warnings.warn( 28 | 'This endpoint is believed to be obsolete. Do not use.', 29 | ClientDeprecationWarning) 30 | 31 | params = { 32 | 'id': self.authenticated_user_id, 33 | 'experiment': experiment 34 | } 35 | params.update(self.authenticated_params) 36 | return self._call_api('qe/expose/', params=params) 37 | 38 | def megaphone_log(self, log_type='feed_aysf', action='seen', reason='', **kwargs): 39 | """ 40 | A tracking endpoint of sorts 41 | 42 | :param log_type: 43 | :param action: 44 | :param reason: 45 | :param kwargs: 46 | :return: 47 | """ 48 | params = { 49 | 'type': log_type, 50 | 'action': action, 51 | 'reason': reason, 52 | '_uuid': self.uuid, 53 | 'device_id': self.device_id, 54 | '_csrftoken': self.csrftoken, 55 | 'uuid': self.generate_uuid(return_hex=True) 56 | } 57 | params.update(kwargs) 58 | return self._call_api('megaphone/log/', params=params, unsigned=True) 59 | 60 | def ranked_recipients(self): 61 | """Get ranked recipients""" 62 | res = self._call_api('direct_v2/ranked_recipients/', query={'show_threads': 'true'}) 63 | return res 64 | 65 | def recent_recipients(self): 66 | """Get recent recipients""" 67 | res = self._call_api('direct_share/recent_recipients/') 68 | return res 69 | 70 | def news(self, **kwargs): 71 | """ 72 | Get news feed of accounts the logged in account is following. 73 | This returns the items in the 'Following' tab. 74 | """ 75 | return self._call_api('news/', query=kwargs) 76 | 77 | def news_inbox(self): 78 | """ 79 | Get inbox feed of activity related to the logged in account. 80 | This returns the items in the 'You' tab. 81 | """ 82 | return self._call_api( 83 | 'news/inbox/', query={'limited_activity': 'true', 'show_su': 'true'}) 84 | 85 | def direct_v2_inbox(self): 86 | """Get v2 inbox""" 87 | return self._call_api('direct_v2/inbox/') 88 | 89 | def oembed(self, url, **kwargs): 90 | """ 91 | Get oembed info 92 | 93 | :param url: 94 | :param kwargs: 95 | :return: 96 | """ 97 | query = {'url': url} 98 | query.update(kwargs) 99 | res = self._call_api('oembed/', query=query) 100 | return res 101 | 102 | def translate(self, object_id, object_type): 103 | """ 104 | 105 | :param object_id: id value for the object 106 | :param object_type: One of [1, 2, 3] where 107 | 1 = CAPTION - unsupported 108 | 2 = COMMENT - unsupported 109 | 3 = BIOGRAPHY 110 | :return: 111 | """ 112 | warnings.warn('This endpoint is not tested fully.', UserWarning) 113 | res = self._call_api( 114 | 'language/translate/', 115 | query={'id': object_id, 'type': object_type}) 116 | return res 117 | 118 | def bulk_translate(self, comment_ids): 119 | """ 120 | Get translations of comments 121 | 122 | :param comment_ids: list of comment/caption IDs 123 | :return: 124 | """ 125 | if isinstance(comment_ids, str): 126 | comment_ids = [comment_ids] 127 | query = {'comment_ids': ','.join(comment_ids)} 128 | res = self._call_api('language/bulk_translate/', query=query) 129 | return res 130 | 131 | def top_search(self, query): 132 | """ 133 | Search for top matching hashtags, users, locations 134 | 135 | :param query: search terms 136 | :return: 137 | """ 138 | res = self._call_api( 139 | 'fbsearch/topsearch/', 140 | query={'context': 'blended', 'ranked_token': self.rank_token, 'query': query}) 141 | if self.auto_patch and res.get('users', []): 142 | [ClientCompatPatch.list_user(u['user']) for u in res['users']] 143 | return res 144 | 145 | def stickers(self, sticker_type='static_stickers', location=None): 146 | """ 147 | Get sticker assets 148 | 149 | :param sticker_type: One of ['static_stickers'] 150 | :param location: dict containing 'lat', 'lng', 'horizontalAccuracy'. 151 | Example: {'lat': '', 'lng': '', 'horizontalAccuracy': ''} 152 | 'horizontalAccuracy' is a float in meters representing the estimated horizontal accuracy 153 | https://developer.android.com/reference/android/location/Location.html#getAccuracy() 154 | :return: 155 | """ 156 | if sticker_type not in ['static_stickers']: 157 | raise ValueError('Invalid sticker_type: {0!s}'.format(sticker_type)) 158 | if location and not ('lat' in location and 'lng' in location and 'horizontalAccuracy' in location): 159 | raise ValueError('Invalid location') 160 | params = { 161 | 'type': sticker_type 162 | } 163 | if location: 164 | params['lat'] = location['lat'] 165 | params['lng'] = location['lng'] 166 | params['horizontalAccuracy'] = location['horizontalAccuracy'] 167 | params.update(self.authenticated_params) 168 | return self._call_api('creatives/assets/', params=params) 169 | -------------------------------------------------------------------------------- /instagram_private_api/endpoints/tags.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from ..compat import compat_urllib_parse 4 | from ..utils import raise_if_invalid_rank_token 5 | from ..compatpatch import ClientCompatPatch 6 | 7 | 8 | class TagsEndpointsMixin(object): 9 | """For endpoints in ``/tags/``.""" 10 | 11 | def tag_info(self, tag): 12 | """ 13 | Get tag info 14 | 15 | :param tag: 16 | :return: 17 | """ 18 | endpoint = 'tags/{tag!s}/info/'.format( 19 | **{'tag': compat_urllib_parse.quote(tag.encode('utf8'))}) 20 | res = self._call_api(endpoint) 21 | return res 22 | 23 | def tag_related(self, tag, **kwargs): 24 | """ 25 | Get related tags 26 | 27 | :param tag: 28 | :return: 29 | """ 30 | endpoint = 'tags/{tag!s}/related/'.format( 31 | **{'tag': compat_urllib_parse.quote(tag.encode('utf8'))}) 32 | query = { 33 | 'visited': json.dumps([{'id': tag, 'type': 'hashtag'}], separators=(',', ':')), 34 | 'related_types': json.dumps(['hashtag', 'location'], separators=(',', ':'))} 35 | res = self._call_api(endpoint, query=query) 36 | return res 37 | 38 | def tag_search(self, text, rank_token, exclude_list=[], **kwargs): 39 | """ 40 | Search tag 41 | 42 | :param text: Search term 43 | :param rank_token: Required for paging through a single feed. See examples/pagination.py 44 | :param exclude_list: List of numerical tag IDs to exclude 45 | :param kwargs: 46 | - **max_id**: For pagination 47 | :return: 48 | """ 49 | raise_if_invalid_rank_token(rank_token) 50 | if not exclude_list: 51 | exclude_list = [] 52 | query = { 53 | 'q': text, 54 | 'timezone_offset': self.timezone_offset, 55 | 'count': 30, 56 | 'exclude_list': json.dumps(exclude_list, separators=(',', ':')), 57 | 'rank_token': rank_token, 58 | } 59 | query.update(kwargs) 60 | res = self._call_api('tags/search/', query=query) 61 | return res 62 | 63 | def tags_user_following(self, user_id): 64 | """ 65 | Get tags a user is following 66 | 67 | :param user_id: 68 | :return: 69 | """ 70 | endpoint = 'users/{user_id!s}/following_tags_info/'.format(user_id=user_id) 71 | return self._call_api(endpoint) 72 | 73 | def tag_follow_suggestions(self): 74 | """Get suggestions for tags to follow""" 75 | return self._call_api('tags/suggested/') 76 | 77 | def tag_follow(self, tag): 78 | """ 79 | Follow a tag 80 | 81 | :param tag: 82 | :return: 83 | """ 84 | endpoint = 'tags/follow/{hashtag!s}/'.format( 85 | hashtag=compat_urllib_parse.quote(tag.encode('utf-8'))) 86 | return self._call_api(endpoint, params=self.authenticated_params) 87 | 88 | def tag_unfollow(self, tag): 89 | """ 90 | Unfollow a tag 91 | 92 | :param tag: 93 | :return: 94 | """ 95 | endpoint = 'tags/unfollow/{hashtag!s}/'.format( 96 | hashtag=compat_urllib_parse.quote(tag.encode('utf-8'))) 97 | return self._call_api(endpoint, params=self.authenticated_params) 98 | 99 | def tag_section(self, tag, tab='top', **kwargs): 100 | """ 101 | Get a tag feed section 102 | 103 | :param tag: tag text (without '#') 104 | :param tab: One of 'top', 'recent', 'places' 105 | :kwargs: 106 | **extract**: return the array of media items only 107 | **page**: for pagination 108 | **next_media_ids**: array of media_id (int) for pagination 109 | **max_id**: for pagination 110 | :return: 111 | """ 112 | valid_tabs = ['top', 'recent', 'places'] 113 | if tab not in valid_tabs: 114 | raise ValueError('Invalid tab: {}'.format(tab)) 115 | 116 | extract_media_only = kwargs.pop('extract', False) 117 | endpoint = 'tags/{tag!s}/sections/'.format( 118 | **{'tag': compat_urllib_parse.quote(tag.encode('utf8'))}) 119 | 120 | params = { 121 | 'supported_tabs': json.dumps(valid_tabs, separators=(',', ':')), 122 | 'tab': tab, 123 | 'include_persistent': True, 124 | } 125 | 126 | # explicitly set known paging parameters to avoid triggering server-side errors 127 | if kwargs.get('max_id'): 128 | params['max_id'] = kwargs.pop('max_id') 129 | if kwargs.get('page'): 130 | params['page'] = kwargs.pop('page') 131 | if kwargs.get('next_media_ids'): 132 | params['next_media_ids'] = json.dumps(kwargs.pop('next_media_ids'), separators=(',', ':')) 133 | kwargs.pop('max_id', None) 134 | kwargs.pop('page', None) 135 | kwargs.pop('next_media_ids', None) 136 | 137 | params.update(kwargs) 138 | results = self._call_api(endpoint, params=params, unsigned=True) 139 | extracted_medias = [] 140 | if self.auto_patch: 141 | for s in results.get('sections', []): 142 | for m in s.get('layout_content', {}).get('medias', []): 143 | if m.get('media'): 144 | ClientCompatPatch.media(m['media'], drop_incompat_keys=self.drop_incompat_keys) 145 | if extract_media_only: 146 | extracted_medias.append(m['media']) 147 | if extract_media_only: 148 | return extracted_medias 149 | return results 150 | -------------------------------------------------------------------------------- /instagram_private_api/endpoints/users.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from .common import ClientExperimentalWarning, ClientDeprecationWarning 4 | from ..compatpatch import ClientCompatPatch 5 | 6 | 7 | class UsersEndpointsMixin(object): 8 | """For endpoints in ``/users/``.""" 9 | 10 | def user_info(self, user_id): 11 | """ 12 | Get user info for a specified user id 13 | 14 | :param user_id: 15 | :return: 16 | """ 17 | res = self._call_api('users/{user_id!s}/info/'.format(**{'user_id': user_id})) 18 | if self.auto_patch: 19 | ClientCompatPatch.user(res['user'], drop_incompat_keys=self.drop_incompat_keys) 20 | return res 21 | 22 | def username_info(self, user_name): 23 | """ 24 | Get user info for a specified user name 25 | :param user_name: 26 | :return: 27 | """ 28 | res = self._call_api('users/{user_name!s}/usernameinfo/'.format(**{'user_name': user_name})) 29 | if self.auto_patch: 30 | ClientCompatPatch.user(res['user'], drop_incompat_keys=self.drop_incompat_keys) 31 | return res 32 | 33 | def user_detail_info(self, user_id, **kwargs): 34 | """ 35 | EXPERIMENTAL ENDPOINT, INADVISABLE TO USE. 36 | Get user detailed info 37 | 38 | :param user_id: 39 | :param kwargs: 40 | - **max_id**: For pagination 41 | - **min_timestamp**: For pagination 42 | :return: 43 | """ 44 | warnings.warn('This endpoint is experimental. Do not use.', ClientExperimentalWarning) 45 | 46 | endpoint = 'users/{user_id!s}/full_detail_info/'.format(**{'user_id': user_id}) 47 | res = self._call_api(endpoint, query=kwargs) 48 | if self.auto_patch: 49 | ClientCompatPatch.user(res['user_detail']['user'], drop_incompat_keys=self.drop_incompat_keys) 50 | [ClientCompatPatch.media(m, drop_incompat_keys=self.drop_incompat_keys) 51 | for m in res.get('feed', {}).get('items', [])] 52 | [ClientCompatPatch.media(m, drop_incompat_keys=self.drop_incompat_keys) 53 | for m in res.get('reel_feed', {}).get('items', [])] 54 | [ClientCompatPatch.media(m, drop_incompat_keys=self.drop_incompat_keys) 55 | for m in res.get('user_story', {}).get('reel', {}).get('items', [])] 56 | return res 57 | 58 | def user_map(self, user_id): # pragma: no cover 59 | """ 60 | Get a list of geo-tagged media from a user 61 | 62 | :param user_id: User id 63 | :return: 64 | """ 65 | warnings.warn( 66 | 'This endpoint is believed to be obsolete. Do not use.', 67 | ClientDeprecationWarning) 68 | 69 | endpoint = 'maps/user/{user_id!s}/'.format(**{'user_id': user_id}) 70 | return self._call_api(endpoint) 71 | 72 | def search_users(self, query, **kwargs): 73 | """ 74 | Search users 75 | 76 | :param query: Search string 77 | :return: 78 | """ 79 | query_params = { 80 | 'q': query, 81 | 'timezone_offset': self.timezone_offset, 82 | 'count': 50, 83 | } 84 | query_params.update(kwargs) 85 | res = self._call_api('users/search/', query=query_params) 86 | if self.auto_patch: 87 | [ClientCompatPatch.list_user(u, drop_incompat_keys=self.drop_incompat_keys) 88 | for u in res.get('users', [])] 89 | return res 90 | 91 | def check_username(self, username): 92 | """ 93 | Check username 94 | 95 | :param username: 96 | :return: 97 | .. code-block:: javascript 98 | 99 | { 100 | "status": "ok", 101 | "available": false, 102 | "username": "xxx", 103 | "error_type": "username_is_taken", 104 | "error": "The username xxx is not available." 105 | } 106 | """ 107 | params = {'username': username} 108 | return self._call_api('users/check_username/', params=params) 109 | 110 | def blocked_user_list(self, **kwargs): 111 | """ 112 | Get list of blocked users 113 | 114 | :param kwargs: 115 | - **max_id**: For pagination 116 | """ 117 | return self._call_api('users/blocked_list/', query=kwargs) 118 | 119 | def user_reel_settings(self): 120 | """ 121 | Get user reel settings 122 | """ 123 | res = self._call_api('users/reel_settings/') 124 | if self.auto_patch and res.get('blocked_reels', {}).get('users'): 125 | [ClientCompatPatch.list_user(u, drop_incompat_keys=self.drop_incompat_keys) 126 | for u in res.get('blocked_reels', {}).get('users', [])] 127 | return res 128 | 129 | def set_reel_settings( 130 | self, message_prefs, 131 | allow_story_reshare=None, reel_auto_archive=None, 132 | save_to_camera_roll=None): 133 | """ 134 | Set story message replies settings 135 | 136 | :param message_prefs: One of 'anyone', 'following', 'off' 137 | :param allow_story_reshare: bool 138 | :param auto_archive: One of 'on', 'off' 139 | :param save_to_camera_roll: bool 140 | :return: 141 | .. code-block:: javascript 142 | 143 | { 144 | "message_prefs": "off", 145 | "status": "ok" 146 | } 147 | """ 148 | if message_prefs not in ['anyone', 'following', 'off']: 149 | raise ValueError('Invalid message_prefs: {0!s}'.format(message_prefs)) 150 | params = {'message_prefs': message_prefs} 151 | if allow_story_reshare is not None: 152 | params['allow_story_reshare'] = '1' if allow_story_reshare else '0' 153 | if reel_auto_archive is not None: 154 | if reel_auto_archive not in ['on', 'off']: 155 | raise ValueError('Invalid auto_archive: {0!s}'.format(reel_auto_archive)) 156 | params['reel_auto_archive'] = reel_auto_archive 157 | if save_to_camera_roll is not None: 158 | params['save_to_camera_roll'] = '1' if save_to_camera_roll else '0' 159 | params.update(self.authenticated_params) 160 | return self._call_api('users/set_reel_settings/', params=params) 161 | -------------------------------------------------------------------------------- /instagram_private_api/endpoints/usertags.py: -------------------------------------------------------------------------------- 1 | from ..compatpatch import ClientCompatPatch 2 | 3 | 4 | class UsertagsEndpointsMixin(object): 5 | """For endpoints in ``/usertags/``.""" 6 | 7 | def usertag_feed(self, user_id, **kwargs): 8 | """ 9 | Get a usertag feed 10 | 11 | :param user_id: 12 | :param kwargs: 13 | :return: 14 | """ 15 | endpoint = 'usertags/{user_id!s}/feed/'.format(**{'user_id': user_id}) 16 | query = {'rank_token': self.rank_token, 'ranked_content': 'true'} 17 | query.update(kwargs) 18 | res = self._call_api(endpoint, query=query) 19 | if self.auto_patch: 20 | [ClientCompatPatch.media(m, drop_incompat_keys=self.drop_incompat_keys) 21 | for m in res.get('items', [])] 22 | return res 23 | 24 | def usertag_self_remove(self, media_id): 25 | """ 26 | Remove your own user tag from a media post 27 | 28 | :param media_id: Media id 29 | :return: 30 | """ 31 | endpoint = 'usertags/{media_id!s}/remove/'.format(**{'media_id': media_id}) 32 | res = self._call_api(endpoint, params=self.authenticated_params) 33 | if self.auto_patch: 34 | ClientCompatPatch.media(res.get('media')) 35 | return res 36 | -------------------------------------------------------------------------------- /instagram_private_api/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import json 4 | import re 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class ClientErrorCodes(object): 10 | """Holds static constant values for the http error codes returned from IG""" 11 | 12 | INTERNAL_SERVER_ERROR = 500 13 | BAD_REQUEST = 400 14 | NOT_FOUND = 404 15 | TOO_MANY_REQUESTS = 429 16 | REQ_HEADERS_TOO_LARGE = 431 17 | 18 | 19 | class ClientError(Exception): 20 | """Generic error class, catch-all for most client issues. 21 | """ 22 | def __init__(self, msg, code=None, error_response=''): 23 | self.code = code or 0 24 | self.error_response = error_response 25 | super(ClientError, self).__init__(msg) 26 | 27 | @property 28 | def msg(self): 29 | return self.args[0] 30 | 31 | 32 | class ClientLoginError(ClientError): 33 | """Raised when login fails.""" 34 | pass 35 | 36 | 37 | class ClientLoginRequiredError(ClientError): 38 | """Raised when login is required.""" 39 | pass 40 | 41 | 42 | class ClientCookieExpiredError(ClientError): 43 | """Raised when cookies have expired.""" 44 | pass 45 | 46 | 47 | class ClientThrottledError(ClientError): 48 | """Raised when client detects an http 429 Too Many Requests response.""" 49 | pass 50 | 51 | 52 | class ClientReqHeadersTooLargeError(ClientError): 53 | """Raised when client detects an http 431 Request Header Fields Too Large response.""" 54 | pass 55 | 56 | 57 | class ClientConnectionError(ClientError): 58 | """Raised due to network connectivity-related issues""" 59 | pass 60 | 61 | 62 | class ClientCheckpointRequiredError(ClientError): 63 | """Raise when IG detects suspicious activity from your account""" 64 | 65 | @property 66 | def challenge_url(self): 67 | try: 68 | error_info = json.loads(self.error_response) 69 | return error_info.get('challenge', {}).get('url') or error_info.get('checkpoint_url') 70 | except ValueError as ve: 71 | logger.warning('Error parsing error response: {}'.format(str(ve))) 72 | return None 73 | 74 | 75 | class ClientChallengeRequiredError(ClientCheckpointRequiredError): 76 | """Raise when IG detects suspicious activity from your account""" 77 | 78 | 79 | class ClientSentryBlockError(ClientError): 80 | """Raise when IG has flagged your account for spam or abusive behavior""" 81 | pass 82 | 83 | 84 | class ClientFeedbackRequiredError(ClientError): 85 | """Raise when IG has flagged your account for spam or abusive behavior""" 86 | pass 87 | 88 | 89 | class ErrorHandler(object): 90 | 91 | KNOWN_ERRORS_MAP = [ 92 | {'patterns': ['bad_password', 'invalid_user'], 'error': ClientLoginError}, 93 | {'patterns': ['login_required'], 'error': ClientLoginRequiredError}, 94 | { 95 | 'patterns': ['checkpoint_required', 'checkpoint_challenge_required', 'checkpoint_logged_out'], 96 | 'error': ClientCheckpointRequiredError 97 | }, 98 | {'patterns': ['challenge_required'], 'error': ClientChallengeRequiredError}, 99 | {'patterns': ['sentry_block'], 'error': ClientSentryBlockError}, 100 | {'patterns': ['feedback_required'], 'error': ClientFeedbackRequiredError}, 101 | ] 102 | 103 | @staticmethod 104 | def process(http_error, error_response): 105 | """ 106 | Tries to process an error meaningfully 107 | 108 | :param http_error: an instance of compat_urllib_error.HTTPError 109 | :param error_response: body of the error response 110 | """ 111 | error_msg = http_error.reason 112 | if http_error.code == ClientErrorCodes.REQ_HEADERS_TOO_LARGE: 113 | raise ClientReqHeadersTooLargeError( 114 | error_msg, 115 | code=http_error.code, 116 | error_response=error_response) 117 | 118 | try: 119 | error_obj = json.loads(error_response) 120 | error_message_type = error_obj.get('error_type', '') or error_obj.get('message', '') 121 | if http_error.code == ClientErrorCodes.TOO_MANY_REQUESTS: 122 | raise ClientThrottledError( 123 | error_obj.get('message'), code=http_error.code, 124 | error_response=json.dumps(error_obj)) 125 | 126 | for error_info in ErrorHandler.KNOWN_ERRORS_MAP: 127 | for p in error_info['patterns']: 128 | if re.search(p, error_message_type): 129 | raise error_info['error']( 130 | error_message_type, code=http_error.code, 131 | error_response=json.dumps(error_obj) 132 | ) 133 | if error_message_type: 134 | error_msg = '{0!s}: {1!s}'.format(http_error.reason, error_message_type) 135 | else: 136 | error_msg = http_error.reason 137 | except ValueError as ve: 138 | # do nothing else, prob can't parse json 139 | logger.warning('Error parsing error response: {}'.format(str(ve))) 140 | 141 | raise ClientError(error_msg, http_error.code, error_response) 142 | -------------------------------------------------------------------------------- /instagram_private_api/http.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | import sys 3 | import codecs 4 | import mimetypes 5 | import random 6 | import string 7 | from .compat import compat_cookiejar, compat_pickle 8 | 9 | 10 | class ClientCookieJar(compat_cookiejar.CookieJar): 11 | """Custom CookieJar that can be pickled to/from strings 12 | """ 13 | def __init__(self, cookie_string=None, policy=None): 14 | compat_cookiejar.CookieJar.__init__(self, policy) 15 | if cookie_string: 16 | if isinstance(cookie_string, bytes): 17 | self._cookies = compat_pickle.loads(cookie_string) 18 | else: 19 | self._cookies = compat_pickle.loads(cookie_string.encode('utf-8')) 20 | 21 | @property 22 | def auth_expires(self): 23 | for cookie in self: 24 | if cookie.name in ('ds_user_id', 'ds_user'): 25 | return cookie.expires 26 | return None 27 | 28 | @property 29 | def expires_earliest(self): 30 | """For backward compatibility""" 31 | return self.auth_expires 32 | 33 | def dump(self): 34 | return compat_pickle.dumps(self._cookies) 35 | 36 | 37 | class MultipartFormDataEncoder(object): 38 | """ 39 | Modified from 40 | http://stackoverflow.com/questions/1270518/python-standard-library-to-post-multipart-form-data-encoded-data 41 | """ 42 | def __init__(self, boundary=None): 43 | self.boundary = boundary or \ 44 | ''.join(random.choice(string.ascii_letters + string.digits + '_-') for _ in range(30)) 45 | self.content_type = 'multipart/form-data; boundary={}'.format(self.boundary) 46 | 47 | @classmethod 48 | def u(cls, s): 49 | if sys.hexversion < 0x03000000 and isinstance(s, str): 50 | s = s.decode('utf-8') 51 | if sys.hexversion >= 0x03000000 and isinstance(s, bytes): 52 | s = s.decode('utf-8') 53 | return s 54 | 55 | def iter(self, fields, files): 56 | """ 57 | :param fields: sequence of (name, value) elements for regular form fields 58 | :param files: sequence of (name, filename, contenttype, filedata) elements for data to be uploaded as files 59 | :return: 60 | """ 61 | encoder = codecs.getencoder('utf-8') 62 | for (key, value) in fields: 63 | key = self.u(key) 64 | yield encoder('--{}\r\n'.format(self.boundary)) 65 | yield encoder(self.u('Content-Disposition: form-data; name="{}"\r\n').format(key)) 66 | yield encoder('\r\n') 67 | if isinstance(value, (int, float)): 68 | value = str(value) 69 | yield encoder(self.u(value)) 70 | yield encoder('\r\n') 71 | for (key, filename, contenttype, fd) in files: 72 | key = self.u(key) 73 | filename = self.u(filename) 74 | yield encoder('--{}\r\n'.format(self.boundary)) 75 | yield encoder(self.u('Content-Disposition: form-data; name="{}"; filename="{}"\r\n').format(key, filename)) 76 | yield encoder('Content-Type: {}\r\n'.format( 77 | contenttype or mimetypes.guess_type(filename)[0] or 'application/octet-stream')) 78 | yield encoder('Content-Transfer-Encoding: binary\r\n') 79 | yield encoder('\r\n') 80 | yield (fd, len(fd)) 81 | yield encoder('\r\n') 82 | yield encoder('--{}--\r\n'.format(self.boundary)) 83 | 84 | def encode(self, fields, files): 85 | body = BytesIO() 86 | for chunk, _ in self.iter(fields, files): 87 | body.write(chunk) 88 | return self.content_type, body.getvalue() 89 | -------------------------------------------------------------------------------- /instagram_web_api/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .client import Client 4 | from .compatpatch import ClientCompatPatch 5 | from .errors import ( 6 | ClientError, ClientLoginError, ClientCookieExpiredError, 7 | ClientConnectionError, ClientForbiddenError, 8 | ClientThrottledError,ClientBadRequestError, 9 | ) 10 | from .common import ClientDeprecationWarning 11 | 12 | 13 | __version__ = '1.6.0' 14 | -------------------------------------------------------------------------------- /instagram_web_api/common.py: -------------------------------------------------------------------------------- 1 | 2 | class ClientDeprecationWarning(DeprecationWarning): 3 | pass 4 | 5 | 6 | class ClientPendingDeprecationWarning(PendingDeprecationWarning): 7 | pass 8 | 9 | 10 | class ClientExperimentalWarning(UserWarning): 11 | pass 12 | -------------------------------------------------------------------------------- /instagram_web_api/compat.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # pylint: disable=unused-import 3 | try: 4 | import urllib.request as compat_urllib_request 5 | except ImportError: # Python 2 6 | import urllib2 as compat_urllib_request 7 | 8 | try: 9 | import urllib.error as compat_urllib_error 10 | except ImportError: # Python 2 11 | import urllib2 as compat_urllib_error 12 | 13 | try: 14 | import urllib.parse as compat_urllib_parse 15 | except ImportError: # Python 2 16 | import urllib as compat_urllib_parse 17 | 18 | try: 19 | from urllib.parse import urlparse as compat_urllib_parse_urlparse 20 | except ImportError: # Python 2 21 | from urlparse import urlparse as compat_urllib_parse_urlparse 22 | 23 | try: 24 | import http.cookiejar as compat_cookiejar 25 | except ImportError: # Python 2 26 | import cookielib as compat_cookiejar 27 | 28 | try: 29 | import http.cookies as compat_cookies 30 | except ImportError: # Python 2 31 | import Cookie as compat_cookies 32 | 33 | try: 34 | import cPickle as compat_pickle 35 | except ImportError: 36 | import pickle as compat_pickle 37 | 38 | try: 39 | import http.client as compat_http_client 40 | except ImportError: # Python 2 41 | import httplib as compat_http_client 42 | -------------------------------------------------------------------------------- /instagram_web_api/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class ClientError(Exception): 5 | """Generic error class, catch-all for most client issues. 6 | """ 7 | def __init__(self, msg, code=None): 8 | self.code = code or 0 9 | super(ClientError, self).__init__(msg) 10 | 11 | @property 12 | def msg(self): 13 | return self.args[0] 14 | 15 | 16 | class ClientLoginError(ClientError): 17 | """Raised when login fails.""" 18 | pass 19 | 20 | 21 | class ClientCookieExpiredError(ClientError): 22 | """Raised when cookies have expired.""" 23 | pass 24 | 25 | 26 | class ClientConnectionError(ClientError): 27 | """Raised due to network connectivity-related issues""" 28 | pass 29 | 30 | 31 | class ClientBadRequestError(ClientError): 32 | """Raised due to a HTTP 400 response""" 33 | pass 34 | 35 | 36 | class ClientForbiddenError(ClientError): 37 | """Raised due to a HTTP 403 response""" 38 | pass 39 | 40 | 41 | class ClientThrottledError(ClientError): 42 | """Raised due to a HTTP 429 response""" 43 | pass 44 | -------------------------------------------------------------------------------- /instagram_web_api/http.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | import sys 3 | import codecs 4 | import mimetypes 5 | import random 6 | import string 7 | 8 | from .compat import compat_cookiejar, compat_pickle 9 | 10 | 11 | class ClientCookieJar(compat_cookiejar.CookieJar): 12 | """Custom CookieJar that can be pickled to/from strings 13 | """ 14 | def __init__(self, cookie_string=None, policy=None): 15 | compat_cookiejar.CookieJar.__init__(self, policy) 16 | if cookie_string: 17 | if isinstance(cookie_string, bytes): 18 | self._cookies = compat_pickle.loads(cookie_string) 19 | else: 20 | self._cookies = compat_pickle.loads(cookie_string.encode('utf-8')) 21 | 22 | @property 23 | def auth_expires(self): 24 | try: 25 | return min([ 26 | cookie.expires for cookie in self 27 | if cookie.name in ('sessionid', 'ds_user_id', 'ds_user') 28 | and cookie.expires]) 29 | except ValueError: 30 | # empty sequence 31 | pass 32 | return None 33 | 34 | @property 35 | def expires_earliest(self): 36 | """For backward compatibility""" 37 | return self.auth_expires 38 | 39 | def dump(self): 40 | return compat_pickle.dumps(self._cookies) 41 | 42 | 43 | class MultipartFormDataEncoder(object): 44 | """ 45 | Modified from 46 | http://stackoverflow.com/questions/1270518/python-standard-library-to-post-multipart-form-data-encoded-data 47 | """ 48 | def __init__(self, boundary=None): 49 | self.boundary = boundary or \ 50 | ''.join(random.choice(string.ascii_letters + string.digits + '_-') for _ in range(30)) 51 | self.content_type = 'multipart/form-data; boundary={}'.format(self.boundary) 52 | 53 | @classmethod 54 | def u(cls, s): 55 | if sys.hexversion < 0x03000000 and isinstance(s, str): 56 | s = s.decode('utf-8') 57 | if sys.hexversion >= 0x03000000 and isinstance(s, bytes): 58 | s = s.decode('utf-8') 59 | return s 60 | 61 | def iter(self, fields, files): 62 | """ 63 | :param fields: sequence of (name, value) elements for regular form fields 64 | :param files: sequence of (name, filename, contenttype, filedata) elements for data to be uploaded as files 65 | :return: 66 | """ 67 | encoder = codecs.getencoder('utf-8') 68 | for (key, value) in fields: 69 | key = self.u(key) 70 | yield encoder('--{}\r\n'.format(self.boundary)) 71 | yield encoder(self.u('Content-Disposition: form-data; name="{}"\r\n').format(key)) 72 | yield encoder('\r\n') 73 | if isinstance(value, (int, float)): 74 | value = str(value) 75 | yield encoder(self.u(value)) 76 | yield encoder('\r\n') 77 | for (key, filename, contenttype, fd) in files: 78 | key = self.u(key) 79 | filename = self.u(filename) 80 | yield encoder('--{}\r\n'.format(self.boundary)) 81 | yield encoder(self.u('Content-Disposition: form-data; name="{}"; filename="{}"\r\n').format(key, filename)) 82 | yield encoder('Content-Type: {}\r\n'.format( 83 | contenttype or mimetypes.guess_type(filename)[0] or 'application/octet-stream')) 84 | yield encoder('Content-Transfer-Encoding: binary\r\n') 85 | yield encoder('\r\n') 86 | yield (fd, len(fd)) 87 | yield encoder('\r\n') 88 | yield encoder('--{}--\r\n'.format(self.boundary)) 89 | 90 | def encode(self, fields, files): 91 | body = BytesIO() 92 | for chunk, _ in self.iter(fields, files): 93 | body.write(chunk) 94 | return self.content_type, body.getvalue() 95 | -------------------------------------------------------------------------------- /misc/checkpoint.py: -------------------------------------------------------------------------------- 1 | import re 2 | import gzip 3 | from io import BytesIO 4 | try: 5 | # python 2.x 6 | from urllib2 import urlopen, Request 7 | from urllib import urlencode, unquote_plus 8 | except ImportError: 9 | # python 3.x 10 | from urllib.request import urlopen, Request 11 | from urllib.parse import urlencode, unquote_plus 12 | 13 | import sys 14 | 15 | 16 | class Checkpoint: 17 | """OBSOLETE. No longer working or supported.""" 18 | 19 | USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_3 like Mac OS X) ' \ 20 | 'AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13G34 ' \ 21 | 'Instagram 9.2.0 (iPhone7,2; iPhone OS 9_3_3; en_US; en-US; scale=2.00; 750x1334)' 22 | 23 | def __init__(self, user_id, **kwargs): 24 | self.user_id = user_id 25 | self.csrftoken = '' 26 | self.cookie = '' 27 | self.endpoint = 'https://i.instagram.com/integrity/checkpoint/' \ 28 | 'checkpoint_logged_out_main/%(user_id)s/?%(params)s' % \ 29 | { 30 | 'user_id': self.user_id, 31 | 'params': urlencode({'next': 'instagram://checkpoint/dismiss'}) 32 | } 33 | self.timeout = kwargs.pop('timeout', 15) 34 | 35 | def trigger_checkpoint(self): 36 | headers = { 37 | 'User-Agent': self.USER_AGENT, 38 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 39 | 'Accept-Language': 'en-US', 40 | 'Accept-Encoding': 'gzip', 41 | 'Connection': 'keep-alive', 42 | } 43 | req = Request(self.endpoint, headers=headers) 44 | res = urlopen(req, timeout=15) 45 | 46 | csrf_mobj = re.search(r'csrftoken=(?P[^;]+?);', res.info().get('set-cookie') or '') 47 | if not csrf_mobj: 48 | raise Exception('Unable to retrieve csrf token.') 49 | 50 | csrf = csrf_mobj.group('csrf') 51 | self.csrftoken = csrf 52 | 53 | cookie_val = res.info().get('set-cookie') or '' 54 | cookie = '' 55 | for c in ['sessionid', 'checkpoint_step', 'mid', 'csrftoken']: 56 | cookie_mobj = re.search(r'{0!s}=(?P[^;]+?);'.format(c), cookie_val) 57 | if cookie_mobj: 58 | cookie += '{0!s}={1!s}; '.format(c, unquote_plus(cookie_mobj.group('val'))) 59 | 60 | self.cookie = cookie 61 | data = {'csrfmiddlewaretoken': csrf, 'email': 'Verify by Email'} # 'sms': 'Verify by SMS' 62 | 63 | headers['Referer'] = self.endpoint 64 | headers['Origin'] = 'https://i.instagram.com' 65 | headers['Content-Type'] = 'application/x-www-form-urlencoded' 66 | headers['Cookie'] = self.cookie 67 | 68 | req = Request(self.endpoint, headers=headers) 69 | res = urlopen(req, data=urlencode(data).encode('ascii'), timeout=self.timeout) 70 | 71 | if res.info().get('Content-Encoding') == 'gzip': 72 | buf = BytesIO(res.read()) 73 | content = gzip.GzipFile(fileobj=buf).read().decode('utf-8') 74 | else: 75 | content = res.read().decode('utf-8') 76 | 77 | if 'id_response_code' in content: 78 | return True 79 | 80 | return False 81 | 82 | def respond_to_checkpoint(self, response_code): 83 | headers = { 84 | 'User-Agent': self.USER_AGENT, 85 | 'Origin': 'https://i.instagram.com', 86 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 87 | 'Accept-Language': 'en-US', 88 | 'Accept-Encoding': 'gzip', 89 | 'Referer': self.endpoint, 90 | 'Cookie': self.cookie, 91 | } 92 | 93 | req = Request(self.endpoint, headers=headers) 94 | data = {'csrfmiddlewaretoken': self.csrftoken, 'response_code': response_code} 95 | res = urlopen(req, data=urlencode(data).encode('ascii'), timeout=self.timeout) 96 | 97 | if res.info().get('Content-Encoding') == 'gzip': 98 | buf = BytesIO(res.read()) 99 | content = gzip.GzipFile(fileobj=buf).read().decode('utf-8') 100 | else: 101 | content = res.read().decode('utf-8') 102 | 103 | return res.code, content 104 | 105 | 106 | if __name__ == '__main__': 107 | print('------------------------------------') 108 | print('** THIS IS UNLIKELY TO BE WORKING **') 109 | print('------------------------------------') 110 | try: 111 | user_id = None 112 | while not user_id: 113 | user_id = input('User ID (numeric): ') 114 | 115 | client = Checkpoint(user_id) 116 | successful = client.trigger_checkpoint() 117 | 118 | if not successful: 119 | print('Unable to trigger checkpoint challenge.') 120 | 121 | response_code = None 122 | while not response_code: 123 | response_code = input('Response Code (6-digit numeric code): ') 124 | 125 | status_code, final_response = client.respond_to_checkpoint(response_code) 126 | 127 | if status_code != 200 or 'has been verified' not in final_response: 128 | print(final_response) 129 | print('-------------------------------\n[!] Unable to verify checkpoint.') 130 | else: 131 | print('[i] Checkpoint successfully verified.') 132 | 133 | except KeyboardInterrupt: 134 | sys.exit(0) 135 | except Exception as e: 136 | print('Unexpected error: {0!s}'.format(str(e))) 137 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8>=3.3.0 2 | Sphinx>=1.5.1 3 | sphinx-rtd-theme>=0.1.9 4 | pylint 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | import io 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | try: 9 | import unittest.mock 10 | has_mock = True 11 | except ImportError: 12 | has_mock = False 13 | 14 | __author__ = 'ping ' 15 | __version__ = '1.6.0' 16 | 17 | packages = [ 18 | 'instagram_private_api', 19 | 'instagram_private_api.endpoints', 20 | 'instagram_web_api' 21 | ] 22 | test_reqs = [] if has_mock else ['mock'] 23 | 24 | with io.open(path.join(path.abspath(path.dirname(__file__)), 'README.md'), encoding='utf-8') as f: 25 | long_description = f.read() 26 | 27 | setup( 28 | name='instagram_private_api', 29 | version=__version__, 30 | author='ping', 31 | author_email='lastmodified@gmail.com', 32 | license='MIT', 33 | url='https://github.com/ping/instagram_private_api/tree/master', 34 | install_requires=[], 35 | test_requires=test_reqs, 36 | keywords='instagram private api', 37 | description='A client interface for the private Instagram API.', 38 | long_description=long_description, 39 | long_description_content_type='text/markdown', 40 | packages=packages, 41 | platforms=['any'], 42 | classifiers=[ 43 | 'Development Status :: 4 - Beta', 44 | 'Intended Audience :: Developers', 45 | 'License :: OSI Approved :: MIT License', 46 | 'Topic :: Software Development :: Libraries :: Python Modules', 47 | 'Programming Language :: Python :: 2.7', 48 | 'Programming Language :: Python :: 3.5', 49 | 'Programming Language :: Python :: 3.6', 50 | ] 51 | ) 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/instagram_private_api/0fda0369ac02bc03d56f5f3f99bc9de2cc25ffaa/tests/__init__.py -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import unittest 3 | import time 4 | import codecs 5 | try: 6 | import unittest.mock as compat_mock 7 | except ImportError: 8 | import mock as compat_mock 9 | import sys 10 | import os 11 | try: 12 | from instagram_private_api import ( 13 | __version__, Client, ClientError, ClientLoginError, 14 | ClientCookieExpiredError, ClientThrottledError, ClientCompatPatch, 15 | ClientLoginRequiredError, MediaTypes, 16 | ClientSentryBlockError, ClientCheckpointRequiredError, 17 | ClientChallengeRequiredError) 18 | from instagram_private_api.utils import ( 19 | InstagramID, gen_user_breadcrumb, 20 | max_chunk_size_generator, max_chunk_count_generator, get_file_size, 21 | ig_chunk_generator 22 | ) # noqa 23 | from instagram_private_api.constants import Constants 24 | from instagram_private_api.compat import compat_urllib_parse 25 | except ImportError: 26 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 27 | from instagram_private_api import ( 28 | __version__, Client, ClientError, ClientLoginError, 29 | ClientCookieExpiredError, ClientThrottledError, ClientCompatPatch, 30 | ClientLoginRequiredError, MediaTypes, 31 | ClientSentryBlockError, ClientCheckpointRequiredError, 32 | ClientChallengeRequiredError) 33 | from instagram_private_api.utils import ( 34 | InstagramID, gen_user_breadcrumb, 35 | max_chunk_size_generator, max_chunk_count_generator, get_file_size, 36 | ig_chunk_generator 37 | ) # noqa 38 | from instagram_private_api.constants import Constants 39 | from instagram_private_api.compat import compat_urllib_parse 40 | 41 | try: 42 | from instagram_web_api import ( 43 | __version__ as __webversion__, 44 | Client as WebClient, 45 | ClientError as WebClientError, 46 | ClientLoginError as WebClientLoginError, 47 | ClientCookieExpiredError as WebClientCookieExpiredError, 48 | ClientCompatPatch as WebClientCompatPatch) 49 | from instagram_web_api.compat import compat_urllib_error 50 | except ImportError: 51 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 52 | from instagram_web_api import ( 53 | __version__ as __webversion__, 54 | Client as WebClient, 55 | ClientError as WebClientError, 56 | ClientLoginError as WebClientLoginError, 57 | ClientCookieExpiredError as WebClientCookieExpiredError, 58 | ClientCompatPatch as WebClientCompatPatch) 59 | from instagram_web_api.compat import compat_urllib_error 60 | 61 | 62 | def to_json(python_object): 63 | if isinstance(python_object, bytes): 64 | return {'__class__': 'bytes', 65 | '__value__': codecs.encode(python_object, 'base64').decode()} 66 | raise TypeError(repr(python_object) + ' is not JSON serializable') 67 | 68 | 69 | def from_json(json_object): 70 | if '__class__' in json_object and json_object['__class__'] == 'bytes': 71 | return codecs.decode(json_object['__value__'].encode(), 'base64') 72 | return json_object 73 | 74 | 75 | class ApiTestBase(unittest.TestCase): 76 | """Main base class for private api tests.""" 77 | 78 | def __init__(self, testname, api, user_id=None, media_id=None): 79 | super(ApiTestBase, self).__init__(testname) 80 | self.api = api 81 | self.test_user_id = user_id 82 | self.test_media_id = media_id 83 | self.sleep_interval = 2.5 84 | if testname.endswith('_mock'): 85 | self.sleep_interval = 0 # sleep a bit between tests to avoid HTTP429 errors 86 | 87 | @classmethod 88 | def setUpClass(cls): 89 | pass 90 | 91 | @classmethod 92 | def tearDownClass(cls): 93 | pass 94 | 95 | def setUp(self): 96 | pass 97 | 98 | def tearDown(self): 99 | time.sleep(self.sleep_interval) 100 | 101 | 102 | class WebApiTestBase(unittest.TestCase): 103 | """Main base class for web api tests.""" 104 | 105 | def __init__(self, testname, api): 106 | super(WebApiTestBase, self).__init__(testname) 107 | self.api = api 108 | self.sleep_interval = 2.5 109 | if testname.endswith('_mock'): 110 | self.sleep_interval = 0 # sleep a bit between tests to avoid HTTP429 errors 111 | 112 | @classmethod 113 | def setUpClass(cls): 114 | pass 115 | 116 | @classmethod 117 | def tearDownClass(cls): 118 | pass 119 | 120 | def setUp(self): 121 | self.test_user_id = '25025320' 122 | self.test_user_name = 'instagram' 123 | self.test_media_shortcode = 'BJL-gjsDyo1' 124 | self.test_media_shortcode2 = 'BVRqQxmj2TA' 125 | self.test_media_id = '1009392755603152985' 126 | self.test_comment_id = '1234567890' 127 | 128 | def tearDown(self): 129 | time.sleep(self.sleep_interval) 130 | 131 | 132 | class MockResponse(object): 133 | """A mock class to emulate api responses.""" 134 | 135 | def __init__(self, code=200, content_type='', body=''): 136 | self.code = 200 137 | self.content_type = content_type 138 | self.body = body 139 | 140 | def info(self): 141 | return {'Content-Type': self.content_type} 142 | 143 | def read(self): 144 | return self.body.encode('utf8') 145 | -------------------------------------------------------------------------------- /tests/private/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .accounts import AccountTests 3 | from .collections import CollectionsTests 4 | from .discover import DiscoverTests 5 | from .feed import FeedTests 6 | from .friendships import FriendshipTests 7 | from .live import LiveTests 8 | from .locations import LocationTests 9 | from .media import MediaTests 10 | from .misc import MiscTests 11 | from .tags import TagsTests 12 | from .upload import UploadTests 13 | from .users import UsersTests 14 | from .usertags import UsertagsTests 15 | from .highlights import HighlightsTests 16 | from .igtv import IGTVTests 17 | 18 | from .apiutils import ApiUtilsTests 19 | from .client import ClientTests 20 | from .compatpatch import CompatPatchTests 21 | -------------------------------------------------------------------------------- /tests/private/apiutils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ..common import ( 4 | InstagramID, MediaTypes, ig_chunk_generator, 5 | max_chunk_size_generator, max_chunk_count_generator, 6 | ) 7 | 8 | 9 | class ApiUtilsTests(unittest.TestCase): 10 | """Tests for the utility functions.""" 11 | 12 | @staticmethod 13 | def init_all(): 14 | return [ 15 | { 16 | 'name': 'test_expand_code', 17 | 'test': ApiUtilsTests('test_expand_code') 18 | }, 19 | { 20 | 'name': 'test_shorten_id', 21 | 'test': ApiUtilsTests('test_shorten_id') 22 | }, 23 | { 24 | 'name': 'test_shorten_media_id', 25 | 'test': ApiUtilsTests('test_shorten_media_id') 26 | }, 27 | { 28 | 'name': 'test_weblink_from_media_id', 29 | 'test': ApiUtilsTests('test_weblink_from_media_id') 30 | }, 31 | { 32 | 'name': 'test_chunk_generators', 33 | 'test': ApiUtilsTests('test_chunk_generators') 34 | }, 35 | { 36 | 'name': 'test_mediatypes', 37 | 'test': ApiUtilsTests('test_mediatypes') 38 | }, 39 | ] 40 | 41 | def __init__(self, testname): 42 | super(ApiUtilsTests, self).__init__(testname) 43 | 44 | def test_expand_code(self): 45 | id = InstagramID.expand_code('BRo7njqD75U') 46 | self.assertEqual(id, 1470687481426853460) 47 | 48 | def test_shorten_id(self): 49 | shortcode = InstagramID.shorten_id(1470687481426853460) 50 | self.assertEqual(shortcode, 'BRo7njqD75U') 51 | 52 | def test_shorten_media_id(self): 53 | shortcode = InstagramID.shorten_media_id('1470654893538426156_25025320') 54 | self.assertEqual(shortcode, 'BRo0NV0jD0s') 55 | 56 | def test_weblink_from_media_id(self): 57 | weblink = InstagramID.weblink_from_media_id('1470517649007430315_25025320') 58 | self.assertEqual(weblink, 'https://www.instagram.com/p/BRoVAK5B8qr/') 59 | 60 | def test_chunk_generators(self): 61 | file_data = '.' * 1000000 62 | chunks_generated = [] 63 | for chunk, data in ig_chunk_generator(file_data): 64 | self.assertEqual(chunk.length, len(data)) 65 | chunks_generated.append(chunk) 66 | 67 | self.assertEqual( 68 | sum([c.length for c in chunks_generated]), len(file_data), 69 | 'ig_chunk_generator: incorrect chunk total') 70 | self.assertEqual( 71 | len(chunks_generated), 3, 'ig_chunk_generator: incorrect chunk count') 72 | 73 | chunks_generated = [] 74 | for chunk, data in max_chunk_size_generator(200000, file_data): 75 | self.assertEqual(chunk.length, len(data)) 76 | chunks_generated.append(chunk) 77 | self.assertEqual( 78 | sum([c.length for c in chunks_generated]), len(file_data), 79 | 'max_chunk_size_generator: incorrect chunk total') 80 | self.assertEqual( 81 | len(chunks_generated), 5, 'max_chunk_size_generator: incorrect chunk count') 82 | 83 | chunks_generated = [] 84 | for chunk, data in max_chunk_count_generator(4, file_data): 85 | self.assertEqual(chunk.length, len(data)) 86 | chunks_generated.append(chunk) 87 | self.assertEqual( 88 | sum([c.length for c in chunks_generated]), len(file_data), 89 | 'max_chunk_count_generator: incorrect chunk total') 90 | self.assertEqual( 91 | len(chunks_generated), 4, 'max_chunk_count_generator: incorrect chunk count') 92 | 93 | def test_mediatypes(self): 94 | self.assertEqual(MediaTypes.id_to_name(MediaTypes.PHOTO), 'image') 95 | self.assertEqual(MediaTypes.name_to_id('image'), MediaTypes.PHOTO) 96 | 97 | with self.assertRaises(ValueError): 98 | MediaTypes.id_to_name(-1) 99 | 100 | with self.assertRaises(ValueError): 101 | MediaTypes.name_to_id('x') 102 | -------------------------------------------------------------------------------- /tests/private/collections.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | 4 | from ..common import ( 5 | ApiTestBase, compat_mock 6 | ) 7 | 8 | 9 | class CollectionsTests(ApiTestBase): 10 | """Tests for CollectionsEndpointsMixin.""" 11 | 12 | @staticmethod 13 | def init_all(api): 14 | return [ 15 | { 16 | 'name': 'test_create_collection', 17 | 'test': CollectionsTests('test_create_collection', api) 18 | }, 19 | { 20 | 'name': 'test_create_collection_mock', 21 | 'test': CollectionsTests('test_create_collection_mock', api) 22 | }, 23 | { 24 | 'name': 'test_collection_feed', 25 | 'test': CollectionsTests('test_collection_feed', api) 26 | }, 27 | { 28 | 'name': 'test_edit_collection', 29 | 'test': CollectionsTests('test_edit_collection', api) 30 | }, 31 | { 32 | 'name': 'test_edit_collection_mock', 33 | 'test': CollectionsTests('test_edit_collection_mock', api) 34 | }, 35 | { 36 | 'name': 'test_delete_collection', 37 | 'test': CollectionsTests('test_delete_collection', api) 38 | }, 39 | { 40 | 'name': 'test_delete_collection_mock', 41 | 'test': CollectionsTests('test_delete_collection_mock', api) 42 | }, 43 | ] 44 | 45 | def test_collection_feed(self): 46 | results = self.api.list_collections() 47 | self.assertTrue(results.get('items'), 'No collection') 48 | 49 | first_collection_id = results['items'][0]['collection_id'] 50 | results = self.api.collection_feed(first_collection_id) 51 | self.assertEqual(results.get('status'), 'ok') 52 | self.assertEqual(str(results.get('collection_id', '')), first_collection_id) 53 | self.assertIsNotNone(results.get('items')) 54 | 55 | @unittest.skip('Modifies data.') 56 | def test_create_collection(self): 57 | name = 'A Collection' 58 | results = self.api.create_collection(name) 59 | self.assertEqual(results.get('status'), 'ok') 60 | self.assertIsNotNone(results.get('collection_id')) 61 | self.assertEqual(results.get('collection_name'), name) 62 | 63 | @compat_mock.patch('instagram_private_api.Client._call_api') 64 | def test_create_collection_mock(self, call_api): 65 | name = 'A Collection' 66 | call_api.return_value = { 67 | 'status': 'ok', 68 | 'collection_id': 123, 'collection_name': name} 69 | 70 | media_ids = ['1495028858729943288_25025320'] 71 | params = {'name': name, 'added_media_ids': json.dumps(media_ids, separators=(',', ':'))} 72 | params.update(self.api.authenticated_params) 73 | self.api.create_collection(name, media_ids) 74 | call_api.assert_called_with( 75 | 'collections/create/', 76 | params=params) 77 | self.api.create_collection(name, media_ids[0]) 78 | call_api.assert_called_with( 79 | 'collections/create/', 80 | params=params) 81 | 82 | @unittest.skip('Modifies data.') 83 | def test_edit_collection(self): 84 | results = self.api.list_collections() 85 | self.assertTrue(results.get('items'), 'No collections') 86 | 87 | first_collection_id = results['items'][0]['collection_id'] 88 | results = self.api.edit_collection(first_collection_id, ['1495028858729943288_25025320']) 89 | self.assertEqual(results.get('status'), 'ok') 90 | self.assertIsNotNone(results.get('collection_id')) 91 | 92 | @compat_mock.patch('instagram_private_api.Client._call_api') 93 | def test_edit_collection_mock(self, call_api): 94 | collection_id = 123 95 | call_api.return_value = { 96 | 'status': 'ok', 97 | 'collection_id': collection_id, 'collection_name': 'A Collection'} 98 | 99 | media_ids = ['1495028858729943288_25025320'] 100 | params = {'added_media_ids': json.dumps(media_ids, separators=(',', ':'))} 101 | params.update(self.api.authenticated_params) 102 | 103 | self.api.edit_collection(collection_id, media_ids) 104 | call_api.assert_called_with( 105 | 'collections/{collection_id!s}/edit/'.format(**{'collection_id': collection_id}), 106 | params=params) 107 | self.api.edit_collection(collection_id, media_ids[0]) 108 | call_api.assert_called_with( 109 | 'collections/{collection_id!s}/edit/'.format(**{'collection_id': collection_id}), 110 | params=params) 111 | 112 | @unittest.skip('Modifies data.') 113 | def test_delete_collection(self): 114 | results = self.api.list_collections() 115 | self.assertTrue(results.get('items'), 'No collections') 116 | 117 | first_collection_id = results['items'][0]['collection_id'] 118 | results = self.api.delete_collection(first_collection_id) 119 | self.assertEqual(results.get('status'), 'ok') 120 | 121 | @compat_mock.patch('instagram_private_api.Client._call_api') 122 | def test_delete_collection_mock(self, call_api): 123 | collection_id = 123 124 | call_api.return_value = {'status': 'ok'} 125 | 126 | self.api.delete_collection(collection_id) 127 | call_api.assert_called_with( 128 | 'collections/{collection_id!s}/delete/'.format(**{'collection_id': collection_id}), 129 | params=self.api.authenticated_params) 130 | -------------------------------------------------------------------------------- /tests/private/compatpatch.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from ..common import ApiTestBase, ClientCompatPatch 4 | 5 | 6 | class CompatPatchTests(ApiTestBase): 7 | """Tests for the ClientCompatPatch class.""" 8 | 9 | @staticmethod 10 | def init_all(api): 11 | test_media_id = '1962809196194057623_25025320' 12 | return [ 13 | { 14 | 'name': 'test_compat_media', 15 | 'test': CompatPatchTests('test_compat_media', api, media_id=test_media_id) 16 | }, 17 | { 18 | 'name': 'test_compat_comment', 19 | 'test': CompatPatchTests('test_compat_comment', api, media_id=test_media_id) 20 | }, 21 | { 22 | 'name': 'test_compat_user', 23 | 'test': CompatPatchTests('test_compat_user', api, user_id='124317') 24 | }, 25 | { 26 | 'name': 'test_compat_user_list', 27 | 'test': CompatPatchTests('test_compat_user_list', api, user_id='124317') 28 | }, 29 | ] 30 | 31 | def test_compat_media(self): 32 | self.api.auto_patch = False 33 | results = self.api.media_info(self.test_media_id) 34 | self.api.auto_patch = True 35 | media = results.get('items', [])[0] 36 | media_patched = copy.deepcopy(media) 37 | ClientCompatPatch.media(media_patched) 38 | 39 | self.assertIsNone(media.get('link')) 40 | self.assertIsNotNone(media_patched.get('link')) 41 | self.assertIsNone(media.get('created_time')) 42 | self.assertIsNotNone(media_patched.get('created_time')) 43 | self.assertIsNone(media.get('images')) 44 | self.assertIsNotNone(media_patched.get('images')) 45 | self.assertIsNone(media.get('type')) 46 | self.assertIsNotNone(media_patched.get('type')) 47 | self.assertIsNone(media.get('filter')) 48 | self.assertIsNotNone(media_patched.get('filter')) 49 | self.assertIsNone(media.get('user', {}).get('id')) 50 | self.assertIsNotNone(media_patched.get('user', {}).get('id')) 51 | self.assertIsNone(media.get('user', {}).get('profile_picture')) 52 | self.assertIsNotNone(media_patched.get('user', {}).get('profile_picture')) 53 | if media['caption']: 54 | self.assertIsNone(media.get('caption', {}).get('id')) 55 | self.assertIsNotNone(media_patched['caption']['id']) 56 | self.assertIsNone(media.get('caption', {}).get('from')) 57 | self.assertIsNotNone(media_patched['caption']['from']) 58 | media_dropped = copy.deepcopy(media) 59 | ClientCompatPatch.media(media_dropped, drop_incompat_keys=True) 60 | self.assertIsNone(media_dropped.get('pk')) 61 | 62 | def test_compat_comment(self): 63 | self.api.auto_patch = False 64 | results = self.api.media_comments(self.test_media_id) 65 | self.api.auto_patch = True 66 | self.assertGreater(len(results.get('comments', [])), 0, 'No items returned.') 67 | comment = results.get('comments', [{}])[0] 68 | comment_patched = copy.deepcopy(comment) 69 | ClientCompatPatch.comment(comment_patched) 70 | self.assertIsNone(comment.get('id')) 71 | self.assertIsNotNone(comment_patched.get('id')) 72 | self.assertIsNone(comment.get('created_time')) 73 | self.assertIsNotNone(comment_patched.get('created_time')) 74 | self.assertIsNone(comment.get('from')) 75 | self.assertIsNotNone(comment_patched.get('from')) 76 | 77 | comment_dropped = copy.deepcopy(comment) 78 | ClientCompatPatch.comment(comment_dropped, drop_incompat_keys=True) 79 | self.assertIsNone(comment_dropped.get('pk')) 80 | 81 | def test_compat_user(self): 82 | self.api.auto_patch = False 83 | results = self.api.user_info(self.test_user_id) 84 | self.api.auto_patch = True 85 | user = results.get('user', {}) 86 | user_patched = copy.deepcopy(user) 87 | ClientCompatPatch.user(user_patched) 88 | self.assertIsNone(user.get('id')) 89 | self.assertIsNotNone(user_patched.get('id')) 90 | self.assertIsNone(user.get('bio')) 91 | self.assertIsNotNone(user_patched.get('bio')) 92 | self.assertIsNone(user.get('profile_picture')) 93 | self.assertIsNotNone(user_patched.get('profile_picture')) 94 | self.assertIsNone(user.get('website')) 95 | self.assertIsNotNone(user_patched.get('website')) 96 | 97 | user_dropped = copy.deepcopy(user) 98 | ClientCompatPatch.user(user_dropped, drop_incompat_keys=True) 99 | self.assertIsNone(user_dropped.get('pk')) 100 | 101 | def test_compat_user_list(self): 102 | self.api.auto_patch = False 103 | rank_token = self.api.generate_uuid() 104 | results = self.api.user_following(self.test_user_id, rank_token) 105 | self.api.auto_patch = True 106 | user = results.get('users', [{}])[0] 107 | user_patched = copy.deepcopy(user) 108 | ClientCompatPatch.list_user(user_patched) 109 | self.assertIsNone(user.get('id')) 110 | self.assertIsNotNone(user_patched.get('id')) 111 | self.assertIsNone(user.get('profile_picture')) 112 | self.assertIsNotNone(user_patched.get('profile_picture')) 113 | 114 | user_dropped = copy.deepcopy(user) 115 | ClientCompatPatch.list_user(user_dropped, drop_incompat_keys=True) 116 | self.assertIsNone(user_dropped.get('pk')) 117 | -------------------------------------------------------------------------------- /tests/private/discover.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ..common import ApiTestBase 4 | 5 | 6 | class DiscoverTests(ApiTestBase): 7 | """Tests for DiscoverEndpointsMixin.""" 8 | 9 | @staticmethod 10 | def init_all(api): 11 | return [ 12 | { 13 | 'name': 'test_discover_channels_home', 14 | 'test': DiscoverTests('test_discover_channels_home', api) 15 | }, 16 | { 17 | 'name': 'test_discover_chaining', 18 | 'test': DiscoverTests('test_discover_chaining', api, user_id='329452045') 19 | }, 20 | { 21 | 'name': 'test_explore', 22 | 'test': DiscoverTests('test_explore', api) 23 | }, 24 | { 25 | 'name': 'test_discover_top_live', 26 | 'test': DiscoverTests('test_discover_top_live', api) 27 | }, 28 | { 29 | 'name': 'test_top_live_status', 30 | 'test': DiscoverTests('test_top_live_status', api) 31 | }, 32 | ] 33 | 34 | def test_explore(self): 35 | results = self.api.explore() 36 | self.assertEqual(results.get('status'), 'ok') 37 | self.assertGreater(len(results.get('items', [])), 0, 'No items returned.') 38 | 39 | @unittest.skip('Deprecated.') 40 | def test_discover_channels_home(self): 41 | results = self.api.discover_channels_home() 42 | self.assertEqual(results.get('status'), 'ok') 43 | self.assertGreater(len(results.get('items', [])), 0, 'No items returned.') 44 | 45 | def test_discover_chaining(self): 46 | results = self.api.discover_chaining(self.test_user_id) 47 | self.assertEqual(results.get('status'), 'ok') 48 | self.assertGreater(len(results.get('users', [])), 0, 'No users returned.') 49 | 50 | def test_discover_top_live(self): 51 | results = self.api.discover_top_live() 52 | self.assertEqual(results.get('status'), 'ok') 53 | self.assertTrue('broadcasts' in results) 54 | 55 | def test_top_live_status(self): 56 | results = self.api.discover_top_live() 57 | broadcast_ids = [b['id'] for b in results.get('broadcasts', [])] 58 | if broadcast_ids: 59 | results = self.api.top_live_status(broadcast_ids) 60 | self.assertEqual(results.get('status'), 'ok') 61 | self.assertGreater(len(results.get('broadcast_status_items', [])), 0, 'No broadcast_status_items returned.') 62 | 63 | results = self.api.top_live_status(str(broadcast_ids[0])) 64 | self.assertEqual(results.get('status'), 'ok') 65 | self.assertGreater(len(results.get('broadcast_status_items', [])), 0, 'No broadcast_status_items returned.') 66 | -------------------------------------------------------------------------------- /tests/private/feed.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ..common import ApiTestBase, ClientError 4 | 5 | 6 | class FeedTests(ApiTestBase): 7 | """Tests for FeedEndpointsMixin.""" 8 | 9 | @staticmethod 10 | def init_all(api): 11 | return [ 12 | { 13 | 'name': 'test_feed_timeline', 14 | 'test': FeedTests('test_feed_timeline', api) 15 | }, 16 | { 17 | 'name': 'test_feed_liked', 18 | 'test': FeedTests('test_feed_liked', api) 19 | }, 20 | { 21 | 'name': 'test_self_feed', 22 | 'test': FeedTests('test_self_feed', api) 23 | }, 24 | { 25 | 'name': 'test_user_feed', 26 | 'test': FeedTests('test_user_feed', api, user_id='124317') 27 | }, 28 | { 29 | 'name': 'test_username_feed', 30 | 'test': FeedTests('test_username_feed', api, user_id='maruhanamogu') 31 | }, 32 | { 33 | 'name': 'test_private_user_feed', 34 | 'test': FeedTests('test_private_user_feed', api, user_id='426095486') 35 | }, 36 | { 37 | 'name': 'test_reels_tray', 38 | 'test': FeedTests('test_reels_tray', api) 39 | }, 40 | { 41 | 'name': 'test_user_reel_media', 42 | 'test': FeedTests('test_user_reel_media', api, user_id='329452045') 43 | }, 44 | { 45 | 'name': 'test_reels_media', 46 | 'test': FeedTests('test_reels_media', api, user_id='329452045') 47 | }, 48 | { 49 | 'name': 'test_user_story_feed', 50 | 'test': FeedTests('test_user_story_feed', api, user_id='329452045') 51 | }, 52 | { 53 | 'name': 'test_location_feed', 54 | 'test': FeedTests('test_location_feed', api) 55 | }, 56 | { 57 | 'name': 'test_feed_tag', 58 | 'test': FeedTests('test_feed_tag', api) 59 | }, 60 | { 61 | 'name': 'test_saved_feed', 62 | 'test': FeedTests('test_saved_feed', api) 63 | }, 64 | { 65 | 'name': 'test_feed_popular', 66 | 'test': FeedTests('test_feed_popular', api) 67 | }, 68 | { 69 | 'name': 'test_feed_only_me', 70 | 'test': FeedTests('test_feed_only_me', api) 71 | }, 72 | ] 73 | 74 | def test_feed_liked(self): 75 | results = self.api.feed_liked() 76 | self.assertEqual(results.get('status'), 'ok') 77 | 78 | def test_feed_timeline(self): 79 | results = self.api.feed_timeline() 80 | self.assertEqual(results.get('status'), 'ok') 81 | self.assertGreater(len(results.get('feed_items', [])), 0, 'No items returned.') 82 | self.assertIsNotNone(results.get('feed_items', [])[0]['media_or_ad'].get('link')) 83 | 84 | @unittest.skip('Deprecated.') 85 | def test_feed_popular(self): 86 | results = self.api.feed_popular() 87 | self.assertEqual(results.get('status'), 'ok') 88 | self.assertGreater(len(results.get('items', [])), 0, 'No items returned.') 89 | 90 | def test_user_feed(self): 91 | results = self.api.user_feed(self.test_user_id) 92 | self.assertEqual(results.get('status'), 'ok') 93 | self.assertGreater(len(results.get('items', [])), 0, 'No items returned.') 94 | 95 | def test_private_user_feed(self): 96 | with self.assertRaises(ClientError) as ce: 97 | self.api.user_feed(self.test_user_id) 98 | self.assertEqual(ce.exception.code, 400) 99 | 100 | def test_self_feed(self): 101 | results = self.api.self_feed() 102 | self.assertEqual(results.get('status'), 'ok') 103 | self.assertGreater(len(results.get('items', [])), 0, 'No items returned.') 104 | 105 | def test_username_feed(self): 106 | results = self.api.username_feed(self.test_user_id) 107 | self.assertEqual(results.get('status'), 'ok') 108 | self.assertGreater(len(results.get('items', [])), 0, 'No items returned.') 109 | 110 | def test_reels_tray(self): 111 | results = self.api.reels_tray() 112 | self.assertEqual(results.get('status'), 'ok') 113 | 114 | def test_user_reel_media(self): 115 | results = self.api.user_reel_media(self.test_user_id) 116 | self.assertEqual(results.get('status'), 'ok') 117 | 118 | def test_reels_media(self): 119 | results = self.api.reels_media([self.test_user_id]) 120 | self.assertEqual(results.get('status'), 'ok') 121 | 122 | def test_feed_tag(self): 123 | rank_token = self.api.generate_uuid() 124 | results = self.api.feed_tag('catsofinstagram', rank_token) 125 | self.assertEqual(results.get('status'), 'ok') 126 | self.assertGreater(len(results.get('items', [])), 0, 'No items returned.') 127 | self.assertGreater(len(results.get('ranked_items', [])), 0, 'No ranked_items returned.') 128 | if results.get('story'): # Returned only in version >= 10.22.0 129 | self.assertGreater(len(results.get('story', {}).get('items', [])), 0, 'No story items returned.') 130 | 131 | def test_user_story_feed(self): 132 | results = self.api.user_story_feed(self.test_user_id) 133 | self.assertEqual(results.get('status'), 'ok') 134 | 135 | @unittest.skip('Deprecated.') 136 | def test_location_feed(self): 137 | rank_token = self.api.generate_uuid() 138 | # 213012122 - Yosemite National Park 139 | # 218551172247829 - Mount Fuji 140 | results = self.api.feed_location(218551172247829, rank_token) 141 | self.assertEqual(results.get('status'), 'ok') 142 | self.assertGreater(len(results.get('items', [])), 0, 'No items returned.') 143 | self.assertGreater(len(results.get('ranked_items', [])), 0, 'No ranked_items returned.') 144 | if results.get('story'): # Returned only in version >= 10.22.0 145 | self.assertGreater(len(results.get('story', {}).get('items', [])), 0, 'No story items returned.') 146 | 147 | def test_saved_feed(self): 148 | results = self.api.saved_feed() 149 | self.assertEqual(results.get('status'), 'ok') 150 | 151 | def test_feed_only_me(self): 152 | results = self.api.feed_only_me() 153 | self.assertEqual(results.get('status'), 'ok') 154 | -------------------------------------------------------------------------------- /tests/private/highlights.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from ..common import ( 4 | ApiTestBase, compat_mock 5 | ) 6 | 7 | 8 | class HighlightsTests(ApiTestBase): 9 | """Tests for HighlightsEndpointsMixin.""" 10 | 11 | @staticmethod 12 | def init_all(api): 13 | return [ 14 | { 15 | 'name': 'test_stories_archive', 16 | 'test': HighlightsTests('test_stories_archive', api) 17 | }, 18 | { 19 | 'name': 'test_highlights_user_feed', 20 | 'test': HighlightsTests('test_highlights_user_feed', api, user_id='25025320') 21 | }, 22 | { 23 | 'name': 'test_highlight_create_mock', 24 | 'test': HighlightsTests('test_highlight_create_mock', api) 25 | }, 26 | { 27 | 'name': 'test_highlight_edit_mock', 28 | 'test': HighlightsTests('test_highlight_edit_mock', api) 29 | }, 30 | { 31 | 'name': 'test_highlight_delete_mock', 32 | 'test': HighlightsTests('test_highlight_delete_mock', api) 33 | }, 34 | ] 35 | 36 | def test_stories_archive(self): 37 | results = self.api.stories_archive() 38 | self.assertEqual(results.get('status'), 'ok') 39 | self.assertIn('items', results) 40 | 41 | def test_highlights_user_feed(self): 42 | results = self.api.highlights_user_feed(self.test_user_id) 43 | self.assertEqual(results.get('status'), 'ok') 44 | self.assertIn('tray', results) 45 | 46 | @compat_mock.patch('instagram_private_api.Client._call_api') 47 | def test_highlight_create_mock(self, call_api): 48 | call_api.return_value = { 49 | 'status': 'ok', 'reel': { 50 | 'id': 'highlight:1710000000' 51 | } 52 | } 53 | media_ids = ['123456700_001', '123456701_001'] 54 | 55 | cover = { 56 | 'media_id': media_ids[0], 57 | 'crop_rect': json.dumps( 58 | [0.0, 0.21830457, 1.0, 0.78094524], separators=(',', ':')) 59 | } 60 | params = { 61 | 'media_ids': json.dumps(media_ids, separators=(',', ':')), 62 | 'cover': json.dumps(cover, separators=(',', ':')), 63 | 'source': 'x', 64 | 'title': 'Test', 65 | } 66 | params.update(self.api.authenticated_params) 67 | 68 | self.api.highlight_create( 69 | media_ids, title=params['title'], source=params['source']) 70 | call_api.assert_called_with( 71 | 'highlights/create_reel/', 72 | params=params) 73 | 74 | with self.assertRaises(ValueError): 75 | self.api.highlight_create( 76 | media_ids, title='A title that is much too long' 77 | ) 78 | 79 | with self.assertRaises(ValueError): 80 | self.api.highlight_create('x') 81 | 82 | @compat_mock.patch('instagram_private_api.Client._call_api') 83 | def test_highlight_edit_mock(self, call_api): 84 | call_api.return_value = { 85 | 'status': 'ok', 'reel': { 86 | 'id': 'highlight:1710000000' 87 | } 88 | } 89 | highlight_id = 'highlight:123456' 90 | endpoint = 'highlights/{highlight_id!s}/edit_reel/'.format( 91 | highlight_id=highlight_id 92 | ) 93 | 94 | cover = { 95 | 'media_id': '123456789_001', 96 | 'crop_rect': json.dumps( 97 | [0.0, 0.21830457, 1.0, 0.78094524], separators=(',', ':')) 98 | } 99 | params = { 100 | 'added_media_ids': json.dumps([], separators=(',', ':')), 101 | 'removed_media_ids': json.dumps([], separators=(',', ':')), 102 | 'cover': json.dumps(cover, separators=(',', ':')), 103 | 'source': 'x', 104 | 'title': 'Test', 105 | } 106 | params.update(self.api.authenticated_params) 107 | 108 | with self.assertRaises(ValueError): 109 | # test empty edit 110 | self.api.highlight_edit(highlight_id) 111 | 112 | self.api.highlight_edit( 113 | highlight_id, cover_media_id=cover['media_id'], 114 | added_media_ids=[], removed_media_ids=[], 115 | title=params['title'], source=params['source']) 116 | call_api.assert_called_with(endpoint, params=params) 117 | 118 | with self.assertRaises(ValueError): 119 | self.api.highlight_edit( 120 | highlight_id, title='A title that is much too long' 121 | ) 122 | 123 | with self.assertRaises(ValueError): 124 | self.api.highlight_edit( 125 | highlight_id, added_media_ids='x' 126 | ) 127 | 128 | with self.assertRaises(ValueError): 129 | self.api.highlight_edit( 130 | highlight_id, removed_media_ids='x' 131 | ) 132 | 133 | @compat_mock.patch('instagram_private_api.Client._call_api') 134 | def test_highlight_delete_mock(self, call_api): 135 | call_api.return_value = {'status': 'ok'} 136 | 137 | highlight_id = 'highlight:1000' 138 | endpoint = 'highlights/{highlight_id!s}/delete_reel/'.format( 139 | highlight_id=highlight_id 140 | ) 141 | self.api.highlight_delete(highlight_id) 142 | call_api.assert_called_with( 143 | endpoint, params=self.api.authenticated_params) 144 | -------------------------------------------------------------------------------- /tests/private/igtv.py: -------------------------------------------------------------------------------- 1 | 2 | from ..common import ApiTestBase 3 | 4 | 5 | class IGTVTests(ApiTestBase): 6 | """Tests for IGTVEndpointsMixin.""" 7 | 8 | @staticmethod 9 | def init_all(api): 10 | return [ 11 | { 12 | 'name': 'test_tvchannel', 13 | 'test': IGTVTests('test_tvchannel', api) 14 | }, 15 | { 16 | 'name': 'test_tvguide', 17 | 'test': IGTVTests('test_tvguide', api) 18 | }, 19 | { 20 | 'name': 'test_search_igtv', 21 | 'test': IGTVTests('test_search_igtv', api) 22 | }, 23 | ] 24 | 25 | def test_tvchannel(self): 26 | results = self.api.tvchannel('for_you') 27 | self.assertGreater(len(results.get('items', [])), 0) 28 | 29 | def test_tvguide(self): 30 | results = self.api.tvguide() 31 | self.assertGreater(len(results.get('channels', [])), 0) 32 | 33 | def test_search_igtv(self): 34 | results = self.api.search_igtv('cooking') 35 | self.assertGreater(len(results.get('results', [])), 0) 36 | -------------------------------------------------------------------------------- /tests/private/locations.py: -------------------------------------------------------------------------------- 1 | 2 | from ..common import ApiTestBase 3 | 4 | 5 | class LocationTests(ApiTestBase): 6 | """Tests for LocationsEndpointsMixin.""" 7 | 8 | @staticmethod 9 | def init_all(api): 10 | return [ 11 | { 12 | 'name': 'test_location_info', 13 | 'test': LocationTests('test_location_info', api) 14 | }, 15 | { 16 | 'name': 'test_location_related', 17 | 'test': LocationTests('test_location_related', api) 18 | }, 19 | { 20 | 'name': 'test_location_search', 21 | 'test': LocationTests('test_location_search', api) 22 | }, 23 | { 24 | 'name': 'test_location_fb_search', 25 | 'test': LocationTests('test_location_fb_search', api) 26 | }, 27 | { 28 | 'name': 'test_location_section', 29 | 'test': LocationTests('test_location_section', api) 30 | }, 31 | ] 32 | 33 | def test_location_info(self): 34 | results = self.api.location_info(229573811) 35 | self.assertEqual(results.get('status'), 'ok') 36 | self.assertIsNotNone(results.get('location')) 37 | 38 | def test_location_related(self): 39 | results = self.api.location_related(229573811) 40 | self.assertEqual(results.get('status'), 'ok') 41 | self.assertIsNotNone(results.get('related')) 42 | 43 | def test_location_search(self): 44 | results = self.api.location_search('40.7484445', '-73.9878531', query='Empire') 45 | self.assertEqual(results.get('status'), 'ok') 46 | self.assertGreater(len(results.get('venues', [])), 0, 'No venues returned.') 47 | 48 | def test_location_fb_search(self): 49 | rank_token = self.api.generate_uuid() 50 | results = self.api.location_fb_search('Paris, France', rank_token) 51 | self.assertEqual(results.get('status'), 'ok') 52 | self.assertGreater(len(results.get('items', [])), 0, 'No items returned.') 53 | 54 | def test_location_section(self): 55 | results = self.api.location_section(229573811, self.api.generate_uuid()) 56 | self.assertEqual(results.get('status'), 'ok') 57 | self.assertIn('sections', results) 58 | self.assertGreater(len(results.get('sections', [])), 0, 'No results returned.') 59 | -------------------------------------------------------------------------------- /tests/private/misc.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ..common import ApiTestBase 4 | 5 | 6 | class MiscTests(ApiTestBase): 7 | """Tests for MiscEndpointsMixin.""" 8 | 9 | @staticmethod 10 | def init_all(api): 11 | return [ 12 | { 13 | 'name': 'test_sync', 14 | 'test': MiscTests('test_sync', api) 15 | }, 16 | { 17 | 'name': 'test_ranked_recipients', 18 | 'test': MiscTests('test_ranked_recipients', api) 19 | }, 20 | { 21 | 'name': 'test_recent_recipients', 22 | 'test': MiscTests('test_recent_recipients', api) 23 | }, 24 | { 25 | 'name': 'test_news', 26 | 'test': MiscTests('test_news', api) 27 | }, 28 | { 29 | 'name': 'test_news_inbox', 30 | 'test': MiscTests('test_news_inbox', api) 31 | }, 32 | { 33 | 'name': 'test_direct_v2_inbox', 34 | 'test': MiscTests('test_direct_v2_inbox', api) 35 | }, 36 | { 37 | 'name': 'test_oembed', 38 | 'test': MiscTests('test_oembed', api) 39 | }, 40 | { 41 | 'name': 'test_bulk_translate', 42 | 'test': MiscTests('test_bulk_translate', api) 43 | }, 44 | { 45 | 'name': 'test_translate', 46 | 'test': MiscTests('test_translate', api) 47 | }, 48 | { 49 | 'name': 'test_megaphone_log', 50 | 'test': MiscTests('test_megaphone_log', api) 51 | }, 52 | { 53 | 'name': 'test_expose', 54 | 'test': MiscTests('test_expose', api) 55 | }, 56 | { 57 | 'name': 'test_top_search', 58 | 'test': MiscTests('test_top_search', api) 59 | }, 60 | { 61 | 'name': 'test_stickers', 62 | 'test': MiscTests('test_stickers', api) 63 | }, 64 | ] 65 | 66 | def test_sync(self): 67 | results = self.api.sync() 68 | self.assertEqual(results.get('status'), 'ok') 69 | self.assertGreater(len(results.get('experiments', [])), 0, 'No experiments returned.') 70 | 71 | @unittest.skip('Deprecated.') 72 | def test_expose(self): 73 | results = self.api.expose() 74 | self.assertEqual(results.get('status'), 'ok') 75 | 76 | @unittest.skip('Posts data') 77 | def test_megaphone_log(self): 78 | results = self.api.megaphone_log('turn_on_push') 79 | self.assertEqual(results.get('status'), 'ok') 80 | self.assertTrue(results.get('success')) 81 | 82 | def test_ranked_recipients(self): 83 | results = self.api.ranked_recipients() 84 | self.assertEqual(results.get('status'), 'ok') 85 | self.assertIsNotNone(results.get('ranked_recipients')) 86 | 87 | def test_recent_recipients(self): 88 | results = self.api.recent_recipients() 89 | self.assertEqual(results.get('status'), 'ok') 90 | 91 | def test_news(self): 92 | results = self.api.news() 93 | self.assertEqual(results.get('status'), 'ok') 94 | 95 | def test_news_inbox(self): 96 | results = self.api.news_inbox() 97 | self.assertEqual(results.get('status'), 'ok') 98 | 99 | def test_direct_v2_inbox(self): 100 | results = self.api.direct_v2_inbox() 101 | self.assertEqual(results.get('status'), 'ok') 102 | 103 | def test_oembed(self): 104 | results = self.api.oembed('https://www.instagram.com/p/BJL-gjsDyo1/') 105 | self.assertIsNotNone(results.get('html')) 106 | 107 | def test_translate(self): 108 | results = self.api.translate('1390480622', '3') 109 | self.assertEqual(results.get('status'), 'ok') 110 | self.assertIsNotNone(results.get('translation')) 111 | 112 | def test_bulk_translate(self): 113 | results = self.api.bulk_translate('17851953262114589') 114 | self.assertEqual(results.get('status'), 'ok') 115 | self.assertGreater(len(results.get('comment_translations', [])), 0, 'No translations returned.') 116 | 117 | def test_top_search(self): 118 | results = self.api.top_search('cats') 119 | self.assertEqual(results.get('status'), 'ok') 120 | 121 | def test_stickers(self): 122 | results = self.api.stickers(location={'lat': '40.7484445', 'lng': '-73.9878531', 'horizontalAccuracy': 5.8}) 123 | self.assertEqual(results.get('status'), 'ok') 124 | self.assertIsNotNone(results.get('static_stickers')) 125 | 126 | self.assertRaises(ValueError, lambda: self.api.stickers('x')) 127 | self.assertRaises(ValueError, lambda: self.api.stickers(location={'x': 1})) 128 | -------------------------------------------------------------------------------- /tests/private/tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | from ..common import ApiTestBase, compat_mock, compat_urllib_parse 5 | 6 | 7 | class TagsTests(ApiTestBase): 8 | """Tests for TagsEndpointsMixin.""" 9 | 10 | @staticmethod 11 | def init_all(api): 12 | return [ 13 | { 14 | 'name': 'test_tag_info', 15 | 'test': TagsTests('test_tag_info', api) 16 | }, 17 | { 18 | 'name': 'test_tag_related', 19 | 'test': TagsTests('test_tag_related', api) 20 | }, 21 | { 22 | 'name': 'test_tag_search', 23 | 'test': TagsTests('test_tag_search', api) 24 | }, 25 | { 26 | 'name': 'test_tag_follow_suggestions', 27 | 'test': TagsTests('test_tag_follow_suggestions', api) 28 | }, 29 | { 30 | 'name': 'test_tags_user_following', 31 | 'test': TagsTests('test_tags_user_following', api, user_id='25025320') 32 | }, 33 | { 34 | 'name': 'test_tag_follow_mock', 35 | 'test': TagsTests('test_tag_follow_mock', api) 36 | }, 37 | { 38 | 'name': 'test_tag_unfollow_mock', 39 | 'test': TagsTests('test_tag_unfollow_mock', api) 40 | }, 41 | { 42 | 'name': 'test_tag_section', 43 | 'test': TagsTests('test_tag_section', api) 44 | }, 45 | ] 46 | 47 | def test_tag_info(self): 48 | results = self.api.tag_info('catsofinstagram') 49 | self.assertEqual(results.get('status'), 'ok') 50 | self.assertGreater(results.get('media_count'), 0, 'No media_count returned.') 51 | 52 | time.sleep(self.sleep_interval) 53 | 54 | results = self.api.tag_info(u'日本') 55 | self.assertEqual(results.get('status'), 'ok') 56 | self.assertGreater(results.get('media_count'), 0, 'No media_count returned.') 57 | 58 | def test_tag_related(self): 59 | results = self.api.tag_related('catsofinstagram') 60 | self.assertEqual(results.get('status'), 'ok') 61 | self.assertGreater(len(results.get('related', [])), 0, 'No media_count returned.') 62 | 63 | def test_tag_search(self): 64 | rank_token = self.api.generate_uuid() 65 | results = self.api.tag_search('cats', rank_token) 66 | self.assertEqual(results.get('status'), 'ok') 67 | self.assertGreater(len(results.get('results', [])), 0, 'No results returned.') 68 | 69 | def test_tag_follow_suggestions(self): 70 | results = self.api.tag_follow_suggestions() 71 | self.assertEqual(results.get('status'), 'ok') 72 | self.assertGreater(len(results.get('tags', [])), 0, 'No results returned.') 73 | 74 | def test_tags_user_following(self): 75 | results = self.api.tags_user_following(self.test_user_id) 76 | self.assertEqual(results.get('status'), 'ok') 77 | self.assertIn('tags', results) 78 | self.assertGreater(len(results.get('tags', [])), 0, 'No results returned.') 79 | 80 | @compat_mock.patch('instagram_private_api.Client._call_api') 81 | def test_tag_follow_mock(self, call_api): 82 | tag = 'catsofinstagram' 83 | call_api.return_value = { 84 | 'status': 'ok', 85 | } 86 | self.api.tag_follow(tag) 87 | call_api.assert_called_with( 88 | 'tags/follow/{hashtag!s}/'.format( 89 | hashtag=compat_urllib_parse.quote(tag.encode('utf-8'))), 90 | params=self.api.authenticated_params) 91 | 92 | @compat_mock.patch('instagram_private_api.Client._call_api') 93 | def test_tag_unfollow_mock(self, call_api): 94 | tag = 'catsofinstagram' 95 | call_api.return_value = { 96 | 'status': 'ok', 97 | } 98 | self.api.tag_unfollow(tag) 99 | call_api.assert_called_with( 100 | 'tags/unfollow/{hashtag!s}/'.format( 101 | hashtag=compat_urllib_parse.quote(tag.encode('utf-8'))), 102 | params=self.api.authenticated_params) 103 | 104 | def test_tag_section(self): 105 | results = self.api.tag_section('catsofinstagram') 106 | self.assertEqual(results.get('status'), 'ok') 107 | self.assertIn('sections', results) 108 | self.assertGreater(len(results.get('sections', [])), 0, 'No results returned.') 109 | -------------------------------------------------------------------------------- /tests/private/users.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ..common import ( 4 | ApiTestBase, compat_mock, ClientError 5 | ) 6 | 7 | 8 | class UsersTests(ApiTestBase): 9 | """Tests for UsersEndpointsMixin.""" 10 | 11 | @staticmethod 12 | def init_all(api): 13 | return [ 14 | { 15 | 'name': 'test_user_info', 16 | 'test': UsersTests('test_user_info', api, user_id='124317') 17 | }, 18 | { 19 | # private user 20 | 'name': 'test_user_info2', 21 | 'test': UsersTests('test_user_info', api, user_id='426095486') 22 | }, 23 | { 24 | 'name': 'test_deleted_user_info', 25 | 'test': UsersTests('test_deleted_user_info', api, user_id='322244991') 26 | }, 27 | { 28 | 'name': 'test_username_info', 29 | 'test': UsersTests('test_username_info', api, user_id='maruhanamogu') 30 | }, 31 | { 32 | 'name': 'test_user_detail_info', 33 | 'test': UsersTests('test_user_detail_info', api, user_id='124317') 34 | }, 35 | { 36 | 'name': 'test_search_users', 37 | 'test': UsersTests('test_search_users', api) 38 | }, 39 | { 40 | 'name': 'test_user_map', 41 | 'test': UsersTests('test_user_map', api, user_id='2958144170') 42 | }, 43 | { 44 | 'name': 'test_check_username', 45 | 'test': UsersTests('test_check_username', api) 46 | }, 47 | { 48 | 'name': 'test_user_reel_settings', 49 | 'test': UsersTests('test_user_reel_settings', api) 50 | }, 51 | { 52 | 'name': 'test_set_reel_settings_mock', 53 | 'test': UsersTests('test_set_reel_settings_mock', api) 54 | }, 55 | { 56 | 'name': 'test_blocked_user_list', 57 | 'test': UsersTests('test_blocked_user_list', api) 58 | }, 59 | ] 60 | 61 | def test_user_info(self): 62 | results = self.api.user_info(self.test_user_id) 63 | self.assertEqual(results.get('status'), 'ok') 64 | self.assertIsNotNone(results.get('user', {}).get('profile_picture')) 65 | 66 | def test_deleted_user_info(self): 67 | with self.assertRaises(ClientError) as ce: 68 | self.api.user_info(self.test_user_id) 69 | self.assertEqual(ce.exception.code, 404) 70 | 71 | def test_username_info(self): 72 | results = self.api.username_info(self.test_user_id) 73 | self.assertEqual(results.get('status'), 'ok') 74 | self.assertIsNotNone(results.get('user', {}).get('profile_picture')) 75 | 76 | def test_user_detail_info(self): 77 | results = self.api.user_detail_info(self.test_user_id) 78 | self.assertEqual(results.get('status'), 'ok') 79 | self.assertGreater(len(results.get('feed', {}).get('items', [])), 0, 'No items returned.') 80 | 81 | @unittest.skip('Deprecated.') 82 | def test_user_map(self): 83 | results = self.api.user_map(self.test_user_id) 84 | self.assertEqual(results.get('status'), 'ok') 85 | self.assertIsNotNone(results.get('geo_media')) 86 | 87 | def test_search_users(self): 88 | results = self.api.search_users('maruhanamogu') 89 | self.assertEqual(results.get('status'), 'ok') 90 | 91 | def test_check_username(self): 92 | results = self.api.check_username('instagram') 93 | self.assertEqual(results.get('status'), 'ok') 94 | self.assertIsNotNone(results.get('available')) 95 | self.assertIsNotNone(results.get('error')) 96 | self.assertIsNotNone(results.get('error_type')) 97 | 98 | def test_blocked_user_list(self): 99 | results = self.api.blocked_user_list() 100 | self.assertEqual(results.get('status'), 'ok') 101 | self.assertTrue('blocked_list' in results) 102 | 103 | def test_user_reel_settings(self): 104 | results = self.api.user_reel_settings() 105 | self.assertEqual(results.get('status'), 'ok') 106 | self.assertIsNotNone(results.get('message_prefs')) 107 | self.assertTrue('blocked_reels' in results) 108 | 109 | @compat_mock.patch('instagram_private_api.Client._call_api') 110 | def test_set_reel_settings_mock(self, call_api): 111 | call_api.return_value = {'status': 'ok', 'message_prefs': 'anyone'} 112 | params = {'message_prefs': call_api.return_value['message_prefs']} 113 | params.update(self.api.authenticated_params) 114 | self.api.set_reel_settings(call_api.return_value['message_prefs']) 115 | call_api.assert_called_with('users/set_reel_settings/', params=params) 116 | 117 | with self.assertRaises(ValueError) as ve: 118 | self.api.set_reel_settings('x') 119 | self.assertEqual(str(ve.exception), 'Invalid message_prefs: x') 120 | -------------------------------------------------------------------------------- /tests/private/usertags.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ..common import ApiTestBase, compat_mock 4 | 5 | 6 | class UsertagsTests(ApiTestBase): 7 | """Tests for UsertagsEndpointsMixin.""" 8 | 9 | @staticmethod 10 | def init_all(api): 11 | return [ 12 | { 13 | 'name': 'test_usertag_feed', 14 | 'test': UsertagsTests('test_usertag_feed', api, user_id='329452045') 15 | }, 16 | { 17 | 'name': 'test_usertag_self_remove', 18 | 'test': UsertagsTests('test_usertag_self_remove', api, media_id='???') 19 | }, 20 | { 21 | 'name': 'test_usertag_self_remove_mock', 22 | 'test': UsertagsTests('test_usertag_self_remove_mock', api, media_id='???') 23 | }, 24 | ] 25 | 26 | def test_usertag_feed(self): 27 | results = self.api.usertag_feed(self.test_user_id) 28 | self.assertEqual(results.get('status'), 'ok') 29 | self.assertGreater(len(results.get('items', [])), 0, 'No items returned.') 30 | 31 | @unittest.skip('Modifies data. Needs info setup.') 32 | def test_usertag_self_remove(self): 33 | results = self.api.usertag_self_remove(self.test_media_id) 34 | self.assertEqual(results.get('status'), 'ok') 35 | self.assertIsNotNone(results.get('media')) 36 | 37 | @compat_mock.patch('instagram_private_api.Client._call_api') 38 | def test_usertag_self_remove_mock(self, call_api): 39 | media_id = '123' 40 | call_api.return_value = { 41 | 'status': 'ok', 42 | 'media': { 43 | 'pk': 123, 'code': 'abc', 'taken_at': 1234567890, 44 | 'media_type': 1, 'caption': None, 45 | 'user': { 46 | 'pk': 123, 'biography': '', 47 | 'profile_pic_url': 'https://example.com/x.jpg', 48 | 'external_url': '' 49 | } 50 | } 51 | } 52 | self.api.usertag_self_remove(media_id) 53 | call_api.assert_called_with( 54 | 'usertags/{media_id!s}/remove/'.format(**{'media_id': media_id}), 55 | params=self.api.authenticated_params) 56 | -------------------------------------------------------------------------------- /tests/test_private_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import argparse 3 | import os 4 | import json 5 | import sys 6 | import logging 7 | import re 8 | import warnings 9 | 10 | from .private import ( 11 | AccountTests, CollectionsTests, DiscoverTests, 12 | FeedTests, FriendshipTests, LiveTests, 13 | LocationTests, MediaTests, MiscTests, 14 | TagsTests, UploadTests, UsersTests, 15 | UsertagsTests, HighlightsTests, 16 | ClientTests, ApiUtilsTests, CompatPatchTests, 17 | IGTVTests, 18 | ) 19 | from .common import ( 20 | Client, ClientError, ClientLoginError, ClientCookieExpiredError, 21 | __version__, to_json, from_json 22 | ) 23 | 24 | 25 | if __name__ == '__main__': 26 | 27 | warnings.simplefilter('ignore', UserWarning) 28 | logging.basicConfig(format='%(name)s %(message)s', stream=sys.stdout) 29 | logger = logging.getLogger('instagram_private_api') 30 | logger.setLevel(logging.WARNING) 31 | 32 | # Example command: 33 | # python test_private_api.py -u "xxx" -p "xxx" -settings "saved_auth.json" -save 34 | 35 | parser = argparse.ArgumentParser(description='Test instagram_private_api.py') 36 | parser.add_argument('-settings', '--settings', dest='settings_file_path', type=str, required=True) 37 | parser.add_argument('-u', '--username', dest='username', type=str, required=True) 38 | parser.add_argument('-p', '--password', dest='password', type=str, required=True) 39 | parser.add_argument('-d', '--device_id', dest='device_id', type=str) 40 | parser.add_argument('-uu', '--uuid', dest='uuid', type=str) 41 | parser.add_argument('-save', '--save', action='store_true') 42 | parser.add_argument('-tests', '--tests', nargs='+') 43 | parser.add_argument('-debug', '--debug', action='store_true') 44 | 45 | args = parser.parse_args() 46 | if args.debug: 47 | logger.setLevel(logging.DEBUG) 48 | 49 | print('Client version: {0!s}'.format(__version__)) 50 | 51 | cached_auth = None 52 | if args.settings_file_path and os.path.isfile(args.settings_file_path): 53 | with open(args.settings_file_path) as file_data: 54 | cached_auth = json.load(file_data, object_hook=from_json) 55 | 56 | # Optional. You can custom the device settings instead of using the default one 57 | my_custom_device = { 58 | 'phone_manufacturer': 'LGE/lge', 59 | 'phone_model': 'RS988', 60 | 'phone_device': 'h1', 61 | 'android_release': '6.0.1', 62 | 'android_version': 23, 63 | 'phone_dpi': '640dpi', 64 | 'phone_resolution': '1440x2392', 65 | 'phone_chipset': 'h1' 66 | } 67 | 68 | api = None 69 | if not cached_auth: 70 | 71 | ts_seed = str(int(os.path.getmtime(__file__))) 72 | if not args.uuid: 73 | # Example of how to generate a uuid. 74 | # You can generate a fixed uuid if you use a fixed value seed 75 | uuid = Client.generate_uuid( 76 | seed='{pw!s}.{usr!s}.{ts!s}'.format(**{'pw': args.username, 'usr': args.password, 'ts': ts_seed})) 77 | else: 78 | uuid = args.uuid 79 | 80 | if not args.device_id: 81 | # Example of how to generate a device id. 82 | # You can generate a fixed device id if you use a fixed value seed 83 | device_id = Client.generate_deviceid( 84 | seed='{usr!s}.{ts!s}.{pw!s}'.format(**{'pw': args.password, 'usr': args.username, 'ts': ts_seed})) 85 | else: 86 | device_id = args.device_id 87 | 88 | # start afresh without existing auth 89 | try: 90 | api = Client( 91 | args.username, args.password, 92 | auto_patch=True, drop_incompat_keys=False, 93 | guid=uuid, device_id=device_id, 94 | # custom device settings 95 | **my_custom_device) 96 | 97 | except ClientLoginError: 98 | print('Login Error. Please check your username and password.') 99 | sys.exit(99) 100 | 101 | # stuff that you should cache 102 | cached_auth = api.settings 103 | if args.save: 104 | # this auth cache can be re-used for up to 90 days 105 | with open(args.settings_file_path, 'w') as outfile: 106 | json.dump(cached_auth, outfile, default=to_json) 107 | 108 | else: 109 | try: 110 | # remove previous app version specific info so that we 111 | # can test the new sig key whenever there's an update 112 | for k in ['app_version', 'signature_key', 'key_version', 'ig_capabilities']: 113 | cached_auth.pop(k, None) 114 | api = Client( 115 | args.username, args.password, 116 | auto_patch=True, drop_incompat_keys=False, 117 | settings=cached_auth, 118 | **my_custom_device) 119 | 120 | except ClientCookieExpiredError: 121 | print('Cookie Expired. Please discard cached auth and login again.') 122 | sys.exit(99) 123 | 124 | tests = [] 125 | tests.extend(AccountTests.init_all(api)) 126 | tests.extend(CollectionsTests.init_all(api)) 127 | tests.extend(DiscoverTests.init_all(api)) 128 | tests.extend(FeedTests.init_all(api)) 129 | tests.extend(FriendshipTests.init_all(api)) 130 | tests.extend(LiveTests.init_all(api)) 131 | tests.extend(LocationTests.init_all(api)) 132 | tests.extend(MediaTests.init_all(api)) 133 | tests.extend(MiscTests.init_all(api)) 134 | tests.extend(TagsTests.init_all(api)) 135 | tests.extend(UploadTests.init_all(api)) 136 | tests.extend(UsersTests.init_all(api)) 137 | tests.extend(UsertagsTests.init_all(api)) 138 | tests.extend(HighlightsTests.init_all(api)) 139 | tests.extend(IGTVTests.init_all(api)) 140 | 141 | tests.extend(ClientTests.init_all(api)) 142 | tests.extend(CompatPatchTests.init_all(api)) 143 | tests.extend(ApiUtilsTests.init_all()) 144 | 145 | def match_regex(test_name): 146 | for test_re in args.tests: 147 | test_re = r'{0!s}'.format(test_re) 148 | if re.match(test_re, test_name): 149 | return True 150 | return False 151 | 152 | if args.tests: 153 | tests = filter(lambda x: match_regex(x['name']), tests) 154 | 155 | try: 156 | suite = unittest.TestSuite() 157 | for test in tests: 158 | suite.addTest(test['test']) 159 | result = unittest.TextTestRunner(verbosity=2).run(suite) 160 | sys.exit(not result.wasSuccessful()) 161 | 162 | except ClientError as e: 163 | print('Unexpected ClientError {0!s} (Code: {1:d}, Response: {2!s})'.format( 164 | e.msg, e.code, e.error_response)) 165 | -------------------------------------------------------------------------------- /tests/test_web_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import argparse 3 | import os 4 | import json 5 | import sys 6 | import logging 7 | import re 8 | import warnings 9 | 10 | from .common import ( 11 | __webversion__ as __version__, 12 | to_json, from_json, 13 | WebClient as Client, 14 | WebClientError as ClientError, 15 | WebClientLoginError as ClientLoginError, 16 | WebClientCookieExpiredError as ClientCookieExpiredError 17 | ) 18 | from .web import ( 19 | ClientTests, MediaTests, UserTests, 20 | CompatPatchTests, UploadTests, 21 | FeedTests, UnauthenticatedTests, 22 | ) 23 | 24 | if __name__ == '__main__': 25 | 26 | warnings.simplefilter('ignore', UserWarning) 27 | logging.basicConfig(format='%(name)s %(message)s', stream=sys.stdout) 28 | logger = logging.getLogger('instagram_web_api') 29 | logger.setLevel(logging.WARNING) 30 | 31 | # Example command: 32 | # python test_web_api.py -u "xxx" -p "xxx" -save -settings "web_settings.json" 33 | 34 | parser = argparse.ArgumentParser(description='Test instagram_web_api.py') 35 | parser.add_argument('-settings', '--settings', dest='settings_file_path', type=str, required=True) 36 | parser.add_argument('-u', '--username', dest='username', type=str) 37 | parser.add_argument('-p', '--password', dest='password', type=str) 38 | parser.add_argument('-save', '--save', action='store_true') 39 | parser.add_argument('-tests', '--tests', nargs='+') 40 | parser.add_argument('-debug', '--debug', action='store_true') 41 | 42 | args = parser.parse_args() 43 | if args.debug: 44 | logger.setLevel(logging.DEBUG) 45 | 46 | print('Client version: {0!s}'.format(__version__)) 47 | 48 | cached_auth = None 49 | if args.settings_file_path and os.path.isfile(args.settings_file_path): 50 | with open(args.settings_file_path) as file_data: 51 | cached_auth = json.load(file_data, object_hook=from_json) 52 | 53 | api = None 54 | if not cached_auth and args.username and args.password: 55 | # start afresh without existing auth 56 | try: 57 | print('New login.') 58 | api = Client( 59 | auto_patch=True, drop_incompat_keys=False, 60 | username=args.username, password=args.password, authenticate=True) 61 | except ClientLoginError: 62 | print('Login Error. Please check your username and password.') 63 | sys.exit(99) 64 | 65 | cached_auth = api.settings 66 | if args.save: 67 | # this auth cache can be re-used for up to 90 days 68 | with open(args.settings_file_path, 'w') as outfile: 69 | json.dump(cached_auth, outfile, default=to_json) 70 | 71 | elif cached_auth and args.username and args.password: 72 | try: 73 | print('Reuse login.') 74 | api = Client( 75 | auto_patch=True, drop_incompat_keys=False, 76 | username=args.username, 77 | password=args.password, 78 | settings=cached_auth) 79 | except ClientCookieExpiredError: 80 | print('Cookie Expired. Please discard cached auth and login again.') 81 | sys.exit(99) 82 | 83 | else: 84 | # unauthenticated client instance 85 | print('Unauthenticated.') 86 | api = Client(auto_patch=True, drop_incompat_keys=False) 87 | 88 | if not api: 89 | raise Exception('Unable to initialise api.') 90 | 91 | tests = [] 92 | tests.extend(ClientTests.init_all(api)) 93 | tests.extend(MediaTests.init_all(api)) 94 | tests.extend(UserTests.init_all(api)) 95 | tests.extend(CompatPatchTests.init_all(api)) 96 | tests.extend(UploadTests.init_all(api)) 97 | tests.extend(FeedTests.init_all(api)) 98 | web_api = Client(auto_patch=True, drop_incompat_keys=False) 99 | tests.extend(UnauthenticatedTests.init_all(web_api)) 100 | 101 | def match_regex(test_name): 102 | for test_re in args.tests: 103 | test_re = r'{0!s}'.format(test_re) 104 | if re.match(test_re, test_name): 105 | return True 106 | return False 107 | 108 | if args.tests: 109 | tests = filter(lambda x: match_regex(x['name']), tests) 110 | 111 | if not api.is_authenticated: 112 | tests = filter(lambda x: not x.get('require_auth', False), tests) 113 | 114 | try: 115 | suite = unittest.TestSuite() 116 | for test in tests: 117 | suite.addTest(test['test']) 118 | result = unittest.TextTestRunner(verbosity=2).run(suite) 119 | sys.exit(not result.wasSuccessful()) 120 | 121 | except ClientError as e: 122 | print('Unexpected ClientError {0!s} (Code: {1:d})'.format(e.msg, e.code)) 123 | -------------------------------------------------------------------------------- /tests/web/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .client import ClientTests 3 | from .media import MediaTests 4 | from .user import UserTests 5 | from .upload import UploadTests 6 | from .feed import FeedTests 7 | from .unauthenticated import UnauthenticatedTests 8 | 9 | from .compatpatch import CompatPatchTests 10 | -------------------------------------------------------------------------------- /tests/web/client.py: -------------------------------------------------------------------------------- 1 | 2 | from ..common import ( 3 | WebApiTestBase, WebClientError as ClientError, 4 | WebClientLoginError as ClientLoginError, 5 | WebClient as Client, 6 | compat_mock, compat_urllib_error 7 | ) 8 | 9 | 10 | class ClientTests(WebApiTestBase): 11 | """Tests for client related functions.""" 12 | 13 | @staticmethod 14 | def init_all(api): 15 | return [ 16 | { 17 | 'name': 'test_search', 18 | 'test': ClientTests('test_search', api), 19 | }, 20 | { 21 | 'name': 'test_client_properties', 22 | 'test': ClientTests('test_client_properties', api), 23 | 'require_auth': True, 24 | }, 25 | { 26 | 'name': 'test_client_errors', 27 | 'test': ClientTests('test_client_errors', api) 28 | }, 29 | { 30 | 'name': 'test_client_init', 31 | 'test': ClientTests('test_client_init', api) 32 | }, 33 | { 34 | 'name': 'test_login_mock', 35 | 'test': ClientTests('test_login_mock', api) 36 | }, 37 | { 38 | 'name': 'test_unauthed_client', 39 | 'test': ClientTests('test_unauthed_client', api) 40 | } 41 | ] 42 | 43 | @compat_mock.patch('instagram_web_api.Client._make_request') 44 | def test_login_mock(self, make_request): 45 | make_request.side_effect = [ 46 | {'status': 'ok', 'authenticated': 'x'}, 47 | {'status': 'fail'} 48 | ] 49 | self.api.on_login = lambda x: self.assertIsNotNone(x) 50 | self.api.login() 51 | self.api.on_login = None 52 | 53 | make_request.assert_called_with( 54 | 'https://www.instagram.com/accounts/login/ajax/', 55 | params={ 56 | 'username': self.api.username, 57 | 'password': self.api.password, 58 | 'queryParams': '{}'}) 59 | with self.assertRaises(ClientLoginError): 60 | self.api.login() 61 | 62 | def test_search(self): 63 | results = self.api.search('maru') 64 | self.assertGreaterEqual(len(results['users']), 0) 65 | self.assertGreaterEqual(len(results['hashtags']), 0) 66 | 67 | def test_client_properties(self): 68 | self.sleep_interval = 0 69 | self.assertIsNotNone(self.api.csrftoken) 70 | self.assertIsNotNone(self.api.authenticated_user_id) 71 | self.assertTrue(self.api.is_authenticated) 72 | settings = self.api.settings 73 | for k in ('cookie', 'created_ts'): 74 | self.assertIsNotNone(settings.get(k)) 75 | self.assertIsNotNone(self.api.cookie_jar.dump()) 76 | 77 | @compat_mock.patch('instagram_web_api.client.compat_urllib_request.OpenerDirector.open') 78 | def test_client_errors(self, open_mock): 79 | self.sleep_interval = 0 80 | open_mock.side_effect = [ 81 | compat_urllib_error.HTTPError('', 404, 'Not Found', None, None), 82 | compat_urllib_error.URLError('No route to host')] 83 | 84 | with self.assertRaises(ClientError): 85 | self.api.search('maru') 86 | 87 | with self.assertRaises(ClientError): 88 | self.api.search('maru') 89 | 90 | @compat_mock.patch('instagram_web_api.Client.csrftoken', 91 | new_callable=compat_mock.PropertyMock, return_value=None) 92 | def test_client_init(self, csrftoken): 93 | with self.assertRaises(ClientError): 94 | self.api.init() 95 | 96 | def test_unauthed_client(self): 97 | api = Client() 98 | self.assertFalse(api.is_authenticated) 99 | 100 | with self.assertRaises(ClientError): 101 | # Test authenticated method 102 | api.user_following(self.test_user_id) 103 | -------------------------------------------------------------------------------- /tests/web/compatpatch.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import re 3 | import time 4 | 5 | from ..common import WebApiTestBase, WebClientCompatPatch as ClientCompatPatch 6 | 7 | 8 | class CompatPatchTests(WebApiTestBase): 9 | """Tests for ClientCompatPatch.""" 10 | 11 | @staticmethod 12 | def init_all(api): 13 | return [ 14 | { 15 | 'name': 'test_compat_media', 16 | 'test': CompatPatchTests('test_compat_media', api), 17 | }, 18 | { 19 | 'name': 'test_compat_comment', 20 | 'test': CompatPatchTests('test_compat_comment', api), 21 | }, 22 | { 23 | 'name': 'test_compat_user', 24 | 'test': CompatPatchTests('test_compat_user', api), 25 | }, 26 | { 27 | 'name': 'test_compat_user_list', 28 | 'test': CompatPatchTests('test_compat_user_list', api), 29 | 'require_auth': True, 30 | }, 31 | ] 32 | 33 | def test_compat_media(self): 34 | self.api.auto_patch = False 35 | media = self.api.media_info2(self.test_media_shortcode) 36 | media_patched = copy.deepcopy(media) 37 | ClientCompatPatch.media(media_patched) 38 | self.api.auto_patch = True 39 | self.assertIsNone(media.get('link')) 40 | self.assertIsNotNone(media_patched.get('link')) 41 | self.assertIsNone(media.get('user')) 42 | self.assertIsNotNone(media_patched.get('user')) 43 | self.assertIsNone(media.get('type')) 44 | self.assertIsNotNone(media_patched.get('type')) 45 | self.assertIsNone(media.get('images')) 46 | self.assertIsNotNone(media_patched.get('images')) 47 | self.assertIsNone(media.get('created_time')) 48 | self.assertIsNotNone(media_patched.get('created_time')) 49 | self.assertIsNotNone(re.match(r'\d+_\d+', media_patched['id'])) 50 | media_dropped = copy.deepcopy(media) 51 | ClientCompatPatch.media(media_dropped, drop_incompat_keys=True) 52 | self.assertIsNone(media_dropped.get('code')) 53 | self.assertIsNone(media_dropped.get('dimensions')) 54 | 55 | time.sleep(self.sleep_interval) 56 | # Test fix for Issue #20 57 | # https://github.com/ping/instagram_private_api/issues/20 58 | media2 = self.api.media_info2(self.test_media_shortcode2) 59 | ClientCompatPatch.media(media2) 60 | 61 | def test_compat_comment(self): 62 | self.api.auto_patch = False 63 | comment = self.api.media_comments(self.test_media_shortcode, count=1)[0] 64 | comment_patched = copy.deepcopy(comment) 65 | self.api.auto_patch = True 66 | ClientCompatPatch.comment(comment_patched) 67 | self.assertIsNone(comment.get('created_time')) 68 | self.assertIsNotNone(comment_patched.get('created_time')) 69 | self.assertIsNone(comment.get('from')) 70 | self.assertIsNotNone(comment_patched.get('from')) 71 | comment_dropped = copy.deepcopy(comment) 72 | ClientCompatPatch.comment(comment_dropped, drop_incompat_keys=True) 73 | self.assertIsNone(comment_dropped.get('created_at')) 74 | self.assertIsNone(comment_dropped.get('user')) 75 | 76 | def test_compat_user(self): 77 | self.api.auto_patch = False 78 | user = self.api.user_info2(self.test_user_name) 79 | user_patched = copy.deepcopy(user) 80 | ClientCompatPatch.user(user_patched) 81 | self.api.auto_patch = True 82 | self.assertIsNone(user.get('bio')) 83 | self.assertIsNotNone(user_patched.get('bio')) 84 | self.assertIsNone(user.get('profile_picture')) 85 | self.assertIsNotNone(user_patched.get('profile_picture')) 86 | self.assertIsNone(user.get('website')) 87 | # no bio link for test account 88 | # self.assertIsNotNone(user_patched.get('website')) 89 | self.assertIsNone(user.get('counts')) 90 | self.assertIsNotNone(user_patched.get('counts')) 91 | user_dropped = copy.deepcopy(user) 92 | ClientCompatPatch.user(user_dropped, drop_incompat_keys=True) 93 | self.assertIsNone(user_dropped.get('biography')) 94 | self.assertIsNone(user_dropped.get('status')) 95 | 96 | def test_compat_user_list(self): 97 | self.api.auto_patch = False 98 | user = self.api.user_followers(self.test_user_id)[0] 99 | user_patched = copy.deepcopy(user) 100 | ClientCompatPatch.list_user(user_patched) 101 | self.api.auto_patch = True 102 | self.assertIsNone(user.get('profile_picture')) 103 | self.assertIsNotNone(user_patched.get('profile_picture')) 104 | user_dropped = copy.deepcopy(user) 105 | ClientCompatPatch.list_user(user_dropped, drop_incompat_keys=True) 106 | self.assertIsNone(user_dropped.get('followed_by_viewer')) 107 | self.assertIsNone(user_dropped.get('requested_by_viewer')) 108 | -------------------------------------------------------------------------------- /tests/web/feed.py: -------------------------------------------------------------------------------- 1 | 2 | from ..common import WebApiTestBase 3 | 4 | 5 | class FeedTests(WebApiTestBase): 6 | """Tests for media related functions.""" 7 | 8 | @staticmethod 9 | def init_all(api): 10 | return [ 11 | { 12 | 'name': 'test_tag_feed', 13 | 'test': FeedTests('test_tag_feed', api), 14 | }, 15 | { 16 | 'name': 'test_location_feed', 17 | 'test': FeedTests('test_location_feed', api), 18 | }, 19 | { 20 | 'name': 'test_timeline_feed', 21 | 'test': FeedTests('test_timeline_feed', api), 22 | }, 23 | { 24 | 'name': 'test_reels_tray', 25 | 'test': FeedTests('test_reels_tray', api), 26 | }, 27 | { 28 | 'name': 'test_reels_feed', 29 | 'test': FeedTests('test_reels_feed', api), 30 | }, 31 | { 32 | 'name': 'test_highlight_reels', 33 | 'test': FeedTests('test_highlight_reels', api), 34 | }, 35 | { 36 | 'name': 'test_tagged_user_feed', 37 | 'test': FeedTests('test_tagged_user_feed', api), 38 | }, 39 | { 40 | 'name': 'test_tag_story_feed', 41 | 'test': FeedTests('test_tag_story_feed', api), 42 | }, 43 | { 44 | 'name': 'test_location_story_feed', 45 | 'test': FeedTests('test_location_story_feed', api), 46 | } 47 | ] 48 | 49 | def test_tag_feed(self): 50 | results = self.api.tag_feed('catsofinstagram').get('data', {}) 51 | self.assertIsNotNone(results.get('hashtag', {}).get('name')) 52 | self.assertGreater( 53 | len(results.get('hashtag', {}).get('edge_hashtag_to_media', {}).get('edges', [])), 0) 54 | self.assertGreater( 55 | len(results.get('hashtag', {}).get('edge_hashtag_to_top_posts', {}).get('edges', [])), 0) 56 | 57 | def test_location_feed(self): 58 | results = self.api.location_feed('212988663').get('data', {}) 59 | self.assertIsNotNone(results.get('location', {}).get('name')) 60 | self.assertGreater( 61 | len(results.get('location', {}).get('edge_location_to_media', {}).get('edges', [])), 0) 62 | self.assertGreater( 63 | len(results.get('location', {}).get('edge_location_to_top_posts', {}).get('edges', [])), 0) 64 | 65 | def test_timeline_feed(self): 66 | results = self.api.timeline_feed().get('data', {}) 67 | self.assertIsNotNone(results.get('user', {}).get('username')) 68 | self.assertGreater( 69 | len(results.get('user', {}).get('edge_web_feed_timeline', {}).get('edges', [])), 0) 70 | 71 | def test_reels_tray(self): 72 | results = self.api.reels_tray().get('data', {}) 73 | self.assertGreater( 74 | len(results.get('user', {}).get( 75 | 'feed_reels_tray', {}).get( 76 | 'edge_reels_tray_to_reel', {}).get('edges', [])), 0) 77 | 78 | def test_reels_feed(self): 79 | results = self.api.reels_feed(['25025320']).get('data', {}) 80 | self.assertTrue('reels_media' in results) 81 | 82 | def test_highlight_reels(self): 83 | results = self.api.highlight_reels('25025320').get('data', {}).get('user', {}) 84 | self.assertTrue('edge_highlight_reels' in results) 85 | 86 | def test_tagged_user_feed(self): 87 | results = self.api.tagged_user_feed('25025320').get('data', {}).get('user', {}) 88 | self.assertTrue('edge_user_to_photos_of_you' in results) 89 | 90 | def test_tag_story_feed(self): 91 | results = self.api.tag_story_feed('catsofinstagram').get('data', {}) 92 | self.assertTrue('reels_media' in results) 93 | 94 | def test_location_story_feed(self): 95 | results = self.api.location_story_feed('7226110').get('data', {}) 96 | self.assertTrue('reels_media' in results) 97 | -------------------------------------------------------------------------------- /tests/web/unauthenticated.py: -------------------------------------------------------------------------------- 1 | 2 | from ..common import WebApiTestBase 3 | 4 | 5 | class UnauthenticatedTests(WebApiTestBase): 6 | """Tests for endpoints with authentication""" 7 | 8 | @staticmethod 9 | def init_all(api): 10 | return [ 11 | { 12 | 'name': 'test_unauthenticated_tag_feed', 13 | 'test': UnauthenticatedTests('test_unauthenticated_tag_feed', api), 14 | }, 15 | { 16 | 'name': 'test_unauthenticated_user_feed', 17 | 'test': UnauthenticatedTests('test_unauthenticated_user_feed', api), 18 | }, 19 | { 20 | 'name': 'test_unauthenticated_location_feed', 21 | 'test': UnauthenticatedTests('test_unauthenticated_location_feed', api), 22 | }, 23 | { 24 | 'name': 'test_unauthenticated_media_comments', 25 | 'test': UnauthenticatedTests('test_unauthenticated_media_comments', api), 26 | }, 27 | { 28 | 'name': 'test_unauthenticated_media_comments_noextract', 29 | 'test': UnauthenticatedTests('test_unauthenticated_media_comments_noextract', api), 30 | }, 31 | { 32 | 'name': 'test_unauthenticated_user_info2', 33 | 'test': UnauthenticatedTests('test_unauthenticated_user_info2', api), 34 | }, 35 | { 36 | 'name': 'test_unauthenticated_tag_story_feed', 37 | 'test': UnauthenticatedTests('test_unauthenticated_tag_story_feed', api), 38 | }, 39 | { 40 | 'name': 'test_unauthenticated_location_story_feed', 41 | 'test': UnauthenticatedTests('test_unauthenticated_location_story_feed', api), 42 | }, 43 | ] 44 | 45 | def test_unauthenticated_tag_feed(self): 46 | results = self.api.tag_feed('catsofinstagram').get('data', {}) 47 | self.assertIsNotNone(results.get('hashtag', {}).get('name')) 48 | self.assertGreater( 49 | len(results.get('hashtag', {}).get('edge_hashtag_to_media', {}).get('edges', [])), 0) 50 | self.assertGreater( 51 | len(results.get('hashtag', {}).get('edge_hashtag_to_top_posts', {}).get('edges', [])), 0) 52 | 53 | def test_unauthenticated_user_feed(self): 54 | results = self.api.user_feed(self.test_user_id) 55 | self.assertGreater(len(results), 0) 56 | self.assertIsInstance(results, list) 57 | self.assertIsInstance(results[0], dict) 58 | 59 | def test_unauthenticated_location_feed(self): 60 | results = self.api.location_feed('212988663').get('data', {}) 61 | self.assertIsNotNone(results.get('location', {}).get('name')) 62 | self.assertGreater( 63 | len(results.get('location', {}).get('edge_location_to_media', {}).get('edges', [])), 0) 64 | self.assertGreater( 65 | len(results.get('location', {}).get('edge_location_to_top_posts', {}).get('edges', [])), 0) 66 | 67 | def test_unauthenticated_media_comments(self): 68 | results = self.api.media_comments(self.test_media_shortcode, count=20) 69 | self.assertGreaterEqual(len(results), 0) 70 | self.assertIsInstance(results, list) 71 | self.assertIsInstance(results[0], dict) 72 | 73 | def test_unauthenticated_media_comments_noextract(self): 74 | results = self.api.media_comments(self.test_media_shortcode, count=20, extract=False) 75 | self.assertIsInstance(results, dict) 76 | 77 | def test_unauthenticated_user_info2(self): 78 | results = self.api.user_info2('instagram') 79 | self.assertIsNotNone(results.get('id')) 80 | 81 | def test_unauthenticated_tag_story_feed(self): 82 | results = self.api.tag_story_feed('catsofinstagram').get('data', {}) 83 | self.assertTrue('reels_media' in results) 84 | 85 | def test_unauthenticated_location_story_feed(self): 86 | results = self.api.location_story_feed('7226110').get('data', {}) 87 | self.assertTrue('reels_media' in results) 88 | -------------------------------------------------------------------------------- /tests/web/upload.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import time 3 | try: 4 | # python 2.x 5 | from urllib2 import urlopen 6 | except ImportError: 7 | # python 3.x 8 | from urllib.request import urlopen 9 | import json 10 | 11 | from ..common import WebApiTestBase, MockResponse, compat_mock 12 | 13 | 14 | class UploadTests(WebApiTestBase): 15 | """Tests for ClientCompatPatch.""" 16 | 17 | @staticmethod 18 | def init_all(api): 19 | return [ 20 | { 21 | 'name': 'test_post_photo', 22 | 'test': UploadTests('test_post_photo', api), 23 | }, 24 | { 25 | 'name': 'test_post_photo_mock', 26 | 'test': UploadTests('test_post_photo_mock', api), 27 | }, 28 | ] 29 | 30 | @unittest.skip('Modifies data') 31 | def test_post_photo(self): 32 | sample_url = 'https://c1.staticflickr.com/5/4103/5059663679_85a7ec3f63_b.jpg' 33 | res = urlopen(sample_url) 34 | photo_data = res.read() 35 | results = self.api.post_photo(photo_data, caption='Feathers #feathers') 36 | self.assertEqual(results.get('status'), 'ok') 37 | self.assertIsNotNone(results.get('media')) 38 | 39 | @compat_mock.patch('instagram_web_api.Client._make_request') 40 | def test_post_photo_mock(self, make_request): 41 | ts_now = time.time() 42 | make_request.return_value = {'status': 'ok', 'upload_id': '123456789'} 43 | with compat_mock.patch( 44 | 'instagram_web_api.client.compat_urllib_request.OpenerDirector.open') as opener, \ 45 | compat_mock.patch('instagram_web_api.client.time.time') as time_mock, \ 46 | compat_mock.patch('instagram_web_api.client.random.choice') as rand_choice, \ 47 | compat_mock.patch('instagram_web_api.Client._read_response') as read_response, \ 48 | compat_mock.patch( 49 | 'instagram_web_api.client.compat_urllib_request.Request') as request: 50 | opener.return_value = MockResponse() 51 | time_mock.return_value = ts_now 52 | rand_choice.return_value = 'x' 53 | # add rhx_gis so that we can reuse the same response for init and uploading 54 | read_response.return_value = json.dumps( 55 | {'status': 'ok', 'upload_id': '123456789', 'rhx_gis': '22aea71b163e335a0ad4479549b530d7'}, 56 | separators=(',', ':') 57 | ) 58 | self.api.post_photo('...'.encode('ascii'), caption='Test') 59 | 60 | headers = { 61 | 'Accept-Language': 'en-US', 62 | 'Accept-Encoding': 'gzip, deflate', 63 | 'Origin': 'https://www.instagram.com', 64 | 'x-csrftoken': self.api.csrftoken, 65 | 'x-instagram-ajax': '1', 66 | 'Accept': '*/*', 67 | 'User-Agent': self.api.mobile_user_agent, 68 | 'Referer': 'https://www.instagram.com/create/details/', 69 | 'x-requested-with': 'XMLHttpRequest', 70 | 'Connection': 'close', 71 | 'Content-Type': 'application/x-www-form-urlencoded'} 72 | 73 | body = '--{boundary}\r\n' \ 74 | 'Content-Disposition: form-data; name="upload_id"\r\n\r\n' \ 75 | '{upload_id}\r\n' \ 76 | '--{boundary}\r\n' \ 77 | 'Content-Disposition: form-data; name="media_type"\r\n\r\n1\r\n' \ 78 | '--{boundary}\r\n' \ 79 | 'Content-Disposition: form-data; name="photo"; filename="photo.jpg"\r\n' \ 80 | 'Content-Type: application/octet-stream\r\n' \ 81 | 'Content-Transfer-Encoding: binary\r\n\r\n...\r\n' \ 82 | '--{boundary}--\r\n'.format( 83 | boundary='----WebKitFormBoundary{}'.format('x' * 16), 84 | upload_id=int(ts_now * 1000)) 85 | request.assert_called_with( 86 | 'https://www.instagram.com/create/upload/photo/', 87 | body.encode('utf-8'), headers=headers) 88 | -------------------------------------------------------------------------------- /tests/web/user.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import time 3 | 4 | from ..common import WebApiTestBase, WebClientError as ClientError, compat_mock 5 | 6 | 7 | class UserTests(WebApiTestBase): 8 | """Tests for user related functions.""" 9 | 10 | @staticmethod 11 | def init_all(api): 12 | return [ 13 | { 14 | 'name': 'test_user_info', 15 | 'test': UserTests('test_user_info', api), 16 | }, 17 | { 18 | 'name': 'test_user_info2', 19 | 'test': UserTests('test_user_info2', api), 20 | }, 21 | { 22 | 'name': 'test_user_feed', 23 | 'test': UserTests('test_user_feed', api), 24 | }, 25 | { 26 | 'name': 'test_notfound_user_feed', 27 | 'test': UserTests('test_notfound_user_feed', api) 28 | }, 29 | { 30 | 'name': 'test_user_feed_noextract', 31 | 'test': UserTests('test_user_feed_noextract', api) 32 | }, 33 | { 34 | 'name': 'test_user_followers', 35 | 'test': UserTests('test_user_followers', api), 36 | 'require_auth': True, 37 | }, 38 | { 39 | 'name': 'test_user_followers_noextract', 40 | 'test': UserTests('test_user_followers_noextract', api), 41 | 'require_auth': True, 42 | }, 43 | { 44 | 'name': 'test_user_following', 45 | 'test': UserTests('test_user_following', api), 46 | 'require_auth': True, 47 | }, 48 | { 49 | 'name': 'test_friendships_create', 50 | 'test': UserTests('test_friendships_create', api), 51 | 'require_auth': True, 52 | }, 53 | { 54 | 'name': 'test_friendships_create_mock', 55 | 'test': UserTests('test_friendships_create_mock', api), 56 | }, 57 | { 58 | 'name': 'test_friendships_destroy', 59 | 'test': UserTests('test_friendships_destroy', api), 60 | 'require_auth': True, 61 | }, 62 | { 63 | 'name': 'test_friendships_destroy_mock', 64 | 'test': UserTests('test_friendships_destroy_mock', api), 65 | }, 66 | ] 67 | 68 | @unittest.skip('Deprecated.') 69 | def test_user_info(self): 70 | results = self.api.user_info(self.test_user_id) 71 | self.assertEqual(results.get('status'), 'ok') 72 | self.assertIsNotNone(results.get('profile_picture')) 73 | 74 | def test_user_info2(self): 75 | results = self.api.user_info2('instagram') 76 | self.assertIsNotNone(results.get('id')) 77 | 78 | def test_user_feed(self): 79 | results = self.api.user_feed(self.test_user_id) 80 | self.assertGreater(len(results), 0) 81 | self.assertIsInstance(results, list) 82 | self.assertIsInstance(results[0], dict) 83 | 84 | def test_notfound_user_feed(self): 85 | self.assertRaises(ClientError, lambda: self.api.user_feed('1')) 86 | 87 | def test_user_feed_noextract(self, extract=True): 88 | results = self.api.user_feed(self.test_user_id, extract=False) 89 | self.assertIsInstance(results, dict) 90 | nodes = [edge['node'] for edge in results.get('data', {}).get('user', {}).get( 91 | 'edge_owner_to_timeline_media', {}).get('edges', [])] 92 | self.assertIsInstance(nodes, list) 93 | self.assertGreater(len(nodes), 0) 94 | first_code = nodes[0]['shortcode'] 95 | end_cursor = results.get('data', {}).get('user', {}).get( 96 | 'edge_owner_to_timeline_media', {}).get('page_info', {}).get('end_cursor') 97 | 98 | time.sleep(self.sleep_interval) 99 | results = self.api.user_feed(self.test_user_id, extract=False, end_cursor=end_cursor) 100 | self.assertNotEqual(first_code, results.get('data', {}).get('user', {}).get( 101 | 'edge_owner_to_timeline_media', {}).get('edges', [])[0]['node']['shortcode']) 102 | 103 | def test_user_followers(self): 104 | results = self.api.user_followers(self.test_user_id) 105 | self.assertGreater(len(results), 0) 106 | self.assertIsInstance(results, list) 107 | self.assertIsInstance(results[0], dict) 108 | 109 | def test_user_followers_noextract(self): 110 | results = self.api.user_followers(self.test_user_id, extract=False) 111 | self.assertIsInstance(results, dict) 112 | 113 | nodes = results.get('data', {}).get('user', {}).get( 114 | 'edge_followed_by', {}).get('edges') 115 | self.assertIsInstance(nodes, list) 116 | self.assertGreater(len(nodes or []), 0) 117 | first_user = nodes[0]['node']['username'] 118 | end_cursor = results.get('data', {}).get('user', {}).get( 119 | 'edge_followed_by', {}).get('page_info', {}).get('end_cursor') 120 | 121 | time.sleep(self.sleep_interval) 122 | results = self.api.user_followers(self.test_user_id, extract=False, end_cursor=end_cursor) 123 | self.assertNotEqual(first_user, results.get('data', {}).get('user', {}).get( 124 | 'edge_followed_by', {}).get('edges')[0]['node']['username']) 125 | 126 | def test_user_following(self): 127 | results = self.api.user_following(self.test_user_id) 128 | self.assertGreater(len(results), 0) 129 | first_user = results[0]['username'] 130 | 131 | time.sleep(self.sleep_interval) 132 | results = self.api.user_following(self.test_user_id, extract=False) 133 | end_cursor = results.get('follows', {}).get('page_info', {}).get('end_cursor') 134 | 135 | time.sleep(self.sleep_interval) 136 | results = self.api.user_following(self.test_user_id, extract=False, end_cursor=end_cursor) 137 | self.assertNotEqual(first_user, results.get('follows', {}).get('nodes', [{}])[0].get('username')) 138 | 139 | @unittest.skip('Modifies data') 140 | def test_friendships_create(self): 141 | results = self.api.friendships_create(self.test_user_id) 142 | self.assertEqual(results.get('status'), 'ok') 143 | 144 | @compat_mock.patch('instagram_web_api.Client._make_request') 145 | def test_friendships_create_mock(self, make_request): 146 | make_request.return_value = {'status': 'ok'} 147 | self.api.friendships_create(self.test_user_id) 148 | make_request.assert_called_with( 149 | 'https://www.instagram.com/web/friendships/{user_id!s}/follow/'.format(**{'user_id': self.test_user_id}), 150 | params='') 151 | 152 | @unittest.skip('Modifies data') 153 | def test_friendships_destroy(self): 154 | results = self.api.friendships_destroy(self.test_user_id) 155 | self.assertEqual(results.get('status'), 'ok') 156 | 157 | @compat_mock.patch('instagram_web_api.Client._make_request') 158 | def test_friendships_destroy_mock(self, make_request): 159 | make_request.return_value = {'status': 'ok'} 160 | self.api.friendships_destroy(self.test_user_id) 161 | make_request.assert_called_with( 162 | 'https://www.instagram.com/web/friendships/{user_id!s}/unfollow/'.format(**{'user_id': self.test_user_id}), 163 | params='') 164 | --------------------------------------------------------------------------------