├── .all-contributorsrc ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .pyre_configuration ├── .watchmanconfig ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── index.rst │ ├── instauto.api.actions.rst │ ├── instauto.api.rst │ ├── instauto.bot.rst │ └── instauto.helpers.rst ├── examples ├── api │ ├── activity │ │ └── get_recent_activity.py │ ├── authentication │ │ ├── change_password.py │ │ └── log_in.py │ ├── direct │ │ ├── get_inbox.py │ │ ├── get_threads.py │ │ ├── send_link.py │ │ ├── send_text_message.py │ │ ├── share_link.py │ │ ├── share_media.py │ │ ├── share_photo.py │ │ └── share_profile.py │ ├── feed │ │ └── get_feed.py │ ├── friendships │ │ ├── approve_follow_request.py │ │ ├── follow_user.py │ │ ├── get_follow_requests.py │ │ ├── get_followers.py │ │ ├── get_following.py │ │ ├── remove_follower.py │ │ ├── retrieve_user.py │ │ └── unfollow_user.py │ ├── post │ │ ├── archive_post.py │ │ ├── comment_post.py │ │ ├── get_commenters.py │ │ ├── get_comments.py │ │ ├── get_likers.py │ │ ├── like_post.py │ │ ├── retrieve_post_by_id.py │ │ ├── retrieve_posts_from_tag.py │ │ ├── retrieve_posts_from_user.py │ │ ├── retrieve_story.py │ │ ├── save_post.py │ │ ├── unarchive_post.py │ │ ├── unlike_post.py │ │ ├── update_caption.py │ │ ├── upload_carousel_to_feed.py │ │ ├── upload_carousel_to_feed_with_usertags.py │ │ ├── upload_image_to_feed.py │ │ ├── upload_image_to_story.py │ │ ├── upload_image_with_location_to_feed.py │ │ ├── upload_image_with_location_to_story.py │ │ └── upload_image_with_usertags_to_feed.py │ ├── profile │ │ ├── get_profile_info.py │ │ ├── update_biography.py │ │ ├── update_gender.py │ │ ├── update_picture.py │ │ └── update_profile.py │ └── search │ │ ├── search_tag.py │ │ └── search_username.py └── helpers │ ├── feed │ └── get_feed.py │ ├── friendships │ ├── follow_user.py │ ├── get_followers.py │ ├── get_following.py │ └── unfollow_user.py │ ├── post │ ├── comment.py │ ├── like.py │ ├── retrieve_commenters.py │ ├── retrieve_from_tag.py │ ├── retrieve_from_user.py │ ├── retrieve_likers.py │ ├── retrieve_stories_from_user.py │ ├── save.py │ ├── unlike.py │ ├── update_caption.py │ ├── upload_image_to_feed.py │ ├── upload_image_to_feed_with_location.py │ └── upload_image_to_story.py │ └── search │ ├── search_tag.py │ └── search_username.py ├── instauto ├── __init__.py ├── api │ ├── __init__.py │ ├── actions │ │ ├── __init__.py │ │ ├── activity.py │ │ ├── authentication.py │ │ ├── challenge.py │ │ ├── direct.py │ │ ├── feed.py │ │ ├── friendships.py │ │ ├── helpers.py │ │ ├── post.py │ │ ├── profile.py │ │ ├── request.py │ │ ├── search.py │ │ ├── structs │ │ │ ├── __init__.py │ │ │ ├── activity.py │ │ │ ├── common.py │ │ │ ├── direct.py │ │ │ ├── feed.py │ │ │ ├── friendships.py │ │ │ ├── post.py │ │ │ ├── profile.py │ │ │ └── search.py │ │ └── stub.py │ ├── client.py │ ├── constants.py │ ├── exceptions.py │ └── structs.py ├── bot │ ├── __init__.py │ ├── bot.py │ └── input.py └── helpers │ ├── __init__.py │ ├── common.py │ ├── feed.py │ ├── friendships.py │ ├── models.py │ ├── post.py │ └── search.py ├── original_requests ├── direct │ ├── linkshare.json │ ├── mediashare.json │ ├── message.json │ ├── photoshare.json │ ├── profileshare.json │ └── videoshare.json ├── friendships │ ├── create.json │ ├── destroy.json │ ├── get_followers.json │ ├── get_following.json │ ├── pending_requests.json │ ├── remove.json │ └── show.json ├── objects │ ├── post.json │ └── user.json ├── post.json ├── post │ ├── retrieve_commenters.json │ ├── retrieve_likers.json │ └── upload.json └── search.json ├── requirements.txt ├── setup.cfg ├── setup.py ├── test_feed.jpg └── test_story.jpg /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "marosgonda", 10 | "name": "Maroš Gonda", 11 | "avatar_url": "https://avatars1.githubusercontent.com/u/16307489?v=4", 12 | "profile": "https://github.com/marosgonda", 13 | "contributions": [ 14 | "test", 15 | "code" 16 | ] 17 | }, 18 | { 19 | "login": "gocnik95", 20 | "name": "Norbert Gocník", 21 | "avatar_url": "https://avatars2.githubusercontent.com/u/68646331?v=4", 22 | "profile": "https://github.com/gocnik95", 23 | "contributions": [ 24 | "code" 25 | ] 26 | }, 27 | { 28 | "login": "juhas96", 29 | "name": "Jakub Juhas", 30 | "avatar_url": "https://avatars3.githubusercontent.com/u/25826778?v=4", 31 | "profile": "https://github.com/juhas96", 32 | "contributions": [ 33 | "code", 34 | "doc", 35 | "test" 36 | ] 37 | }, 38 | { 39 | "login": "Samu1808", 40 | "name": "Samu1808", 41 | "avatar_url": "https://avatars3.githubusercontent.com/u/64809910?v=4", 42 | "profile": "https://github.com/Samu1808", 43 | "contributions": [ 44 | "code" 45 | ] 46 | }, 47 | { 48 | "login": "kevinjon27", 49 | "name": "Kevin Jonathan", 50 | "avatar_url": "https://avatars3.githubusercontent.com/u/12078441?v=4", 51 | "profile": "https://www.kevinjonathan.com", 52 | "contributions": [ 53 | "doc" 54 | ] 55 | }, 56 | { 57 | "login": "marvic2409", 58 | "name": "Martin Nikolov", 59 | "avatar_url": "https://avatars3.githubusercontent.com/u/25594875?v=4", 60 | "profile": "https://github.com/marvic2409", 61 | "contributions": [ 62 | "code" 63 | ] 64 | }, 65 | { 66 | "login": "b177y", 67 | "name": "b177y", 68 | "avatar_url": "https://avatars1.githubusercontent.com/u/34008579?v=4", 69 | "profile": "https://github.com/b177y", 70 | "contributions": [ 71 | "code", 72 | "test", 73 | "doc" 74 | ] 75 | }, 76 | { 77 | "login": "returnWOW", 78 | "name": "wowopo", 79 | "avatar_url": "https://avatars3.githubusercontent.com/u/16145271?v=4", 80 | "profile": "https://github.com/returnWOW", 81 | "contributions": [ 82 | "code" 83 | ] 84 | }, 85 | { 86 | "login": "stanvanrooy", 87 | "name": "Stan van Rooy", 88 | "avatar_url": "https://avatars1.githubusercontent.com/u/49564025?v=4", 89 | "profile": "https://rooy.works", 90 | "contributions": [ 91 | "doc", 92 | "code", 93 | "test" 94 | ] 95 | }, 96 | { 97 | "login": "tibotix", 98 | "name": "Tizian Seehaus", 99 | "avatar_url": "https://avatars3.githubusercontent.com/u/38123657?v=4", 100 | "profile": "https://github.com/tibotix", 101 | "contributions": [ 102 | "code" 103 | ] 104 | }, 105 | { 106 | "login": "ItsFlorkast", 107 | "name": "Florkast", 108 | "avatar_url": "https://avatars.githubusercontent.com/u/43137808?v=4", 109 | "profile": "https://github.com/ItsFlorkast", 110 | "contributions": [ 111 | "doc" 112 | ] 113 | }, 114 | { 115 | "login": "atnartur", 116 | "name": "Artur", 117 | "avatar_url": "https://avatars.githubusercontent.com/u/5189110?v=4", 118 | "profile": "http://atnartur.dev", 119 | "contributions": [ 120 | "code", 121 | "doc" 122 | ] 123 | }, 124 | { 125 | "login": "Fislix", 126 | "name": "Felix Fischer", 127 | "avatar_url": "https://avatars.githubusercontent.com/u/84190063?v=4", 128 | "profile": "https://github.com/Fislix", 129 | "contributions": [ 130 | "code" 131 | ] 132 | }, 133 | { 134 | "login": "alperenkaplan", 135 | "name": "alperenkaplan", 136 | "avatar_url": "https://avatars.githubusercontent.com/u/48252753?v=4", 137 | "profile": "https://github.com/alperenkaplan", 138 | "contributions": [ 139 | "code", 140 | "doc" 141 | ] 142 | }, 143 | { 144 | "login": "javad94", 145 | "name": "Javadz", 146 | "avatar_url": "https://avatars.githubusercontent.com/u/7765309?v=4", 147 | "profile": "https://github.com/javad94", 148 | "contributions": [ 149 | "code", 150 | "doc" 151 | ] 152 | } 153 | ], 154 | "contributorsPerLine": 7, 155 | "projectName": "instauto", 156 | "projectOwner": "stanvanrooy", 157 | "repoType": "github", 158 | "repoHost": "https://github.com", 159 | "skipCi": true 160 | } 161 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'Status: Review Needed, Type: Bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Setup** 24 | Instauto version: 25 | Device profile: 26 | Instagram profile: 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'Status: Review Needed, Type: Enchancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | docs/build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | video.mp4 132 | .idea/ 133 | *.save 134 | main.py 135 | .env 136 | *.jpg 137 | setup.sh 138 | *.swp 139 | -------------------------------------------------------------------------------- /.pyre_configuration: -------------------------------------------------------------------------------- 1 | { 2 | "source_directories": [ 3 | "instauto" 4 | ], 5 | "search_path": [ 6 | "." 7 | ], 8 | "ignore_all_errors": [ 9 | "instauto/api/structs.py", 10 | "instauto/api/actions/stub.py" 11 | ], 12 | "taint_models_path": "/home/stan/.local/lib" 13 | } -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Stan van Rooy 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 | # Instauto 2 | [![All Contributors](https://img.shields.io/badge/all_contributors-15-orange.svg?style=flat-square)](#contributors-) 3 | [![GitHub stars](https://img.shields.io/github/stars/stanvanrooy/instauto)](https://github.com/stanvanrooy/instauto/stargazers) 4 | [![PyPI license](https://img.shields.io/pypi/l/instauto)](https://pypi.python.org/project/instauto/) 5 | [![Downloads](https://pepy.tech/badge/instauto/week)](https://pepy.tech/project/instauto) 6 | [![Documentation Status](https://readthedocs.org/projects/instauto/badge/?version=latest)](https://instauto.readthedocs.io/en/latest/?badge=latest) 7 | 8 | 9 | `instauto` is a Python package for automating Instagram, making use of the private Instagram API. `instauto` tries to have feature parity with the Instagram app. 10 | 11 | ## Overview 12 | Instauto has 3 main api's that can be used: `instauto.api`, `instauto.bot` and `instauto.helpers`. You should probably use `instauto.helpers` and only start using `instauto.api` when you actually need its functionality. 13 | 14 | Everything in `instauto`, is based around the 'core' `instauto.api` package. This package interacts directly with the private Instagram API and contains all functionality. This package is both the most flexible (you can update all requests sent and receive the full response back, for example), but also the most complex. You likely do not need to use this package. 15 | 16 | The `instauto.helpers` package, is an abstraction above the `instauto.api` pacakge. It offers a little bit less flexibility, but is a lot less complex to use. It also offers [typed models](https://github.com/stanvanrooy/instauto/blob/master/instauto/helpers/models.py). 17 | 18 | The `instauto.bot` package, is another abstraction, but this time over the `instauto.helpers` package. This package has pretty much no flexibility, but can be set up in 10 lines of Python code. 19 | 20 | ## Installation 21 | The package can be installed with the following pip command: 22 | ```pip install instauto``` 23 | 24 | ## Getting started 25 | Below are a few examples for getting stared quickly. After getting started, you'll probably want to take a quick look at the more [detailed documentation](https://instauto.readthedocs.io/) on readthedocs. 26 | 27 | ### Authentication 28 | You'll want to do this as little as possible. Instagram sees logging in often as a huge red flag. 29 | ```python 30 | from instauto.api.client import ApiClient 31 | client = ApiClient(username='your_username', password='your_password') 32 | client.log_in() 33 | ``` 34 | 35 | ### Restoring state 36 | Because of that, you can restore your session. 37 | ```python 38 | client.save_to_disk('your-savefile.instauto') 39 | client = ApiClient.initiate_from_file('your-savefile.instauto') 40 | ``` 41 | 42 | ### Making new friends 43 | Ofcourse `instauto` also supports (un)following users. 44 | ```python 45 | from instauto.helpers.friendships import follow_user, unfollow_user 46 | follow_user(client, username='stan000_') 47 | unfollow_user(client, username='stan000_') 48 | ``` 49 | 50 | ### Finding new friends 51 | But before you can follow users, you'll need to find them first. 52 | ```python 53 | from instauto.helpers.search import search_username 54 | users = search_username(client, "username", 10) 55 | ``` 56 | 57 | ### Retrieving 100 of your followers 58 | Getting a list of users that follow you is also super simple. 59 | ```python 60 | from instauto.helpers.friendships import get_followers 61 | followers = get_followers(client, username='your_username', limit=100) 62 | ``` 63 | 64 | ### Uploading images 65 | `instauto` also offers a simple API for uploading images to your feed and story. 66 | ```python 67 | from instauto.helpers.post import upload_image_to_feed 68 | upload_image_to_feed(client, './cat.jpg', 'Hello from instauto!') 69 | ``` 70 | 71 | ### Looking at your feed 72 | Your feed can't be missing, it's pretty much what Instagram is about, isn't it? 73 | ```python 74 | from instato.helpers.feed import get_feed 75 | posts = get_feed(client, 100) 76 | ``` 77 | 78 | ## More examples 79 | Looking for something else? We have more examples: 80 | - [feed helper functions](https://github.com/stanvanrooy/instauto/tree/master/examples/helpers/feed) 81 | - [friendship helper functions](https://github.com/stanvanrooy/instauto/tree/master/examples/helpers/friendships) 82 | - [post helper functions](https://github.com/stanvanrooy/instauto/tree/master/examples/helpers/post) 83 | - [search helper function](https://github.com/stanvanrooy/instauto/tree/master/examples/helpers/search) 84 | - [advanced examples](https://github.com/stanvanrooy/instauto/tree/master/examples/api) 85 | 86 | Stil no look? Submit a feature request! 87 | 88 | ## Tutorials 89 | - Scraping Instagram API with Instauto: https://www.trickster.dev/post/scraping-instagram-api-with-instauto/ 90 | - How I made an instagram bot that posts a quote every day: https://joaoramiro.medium.com/how-i-made-an-instagram-bot-that-publishes-a-post-every-day-cc49e526bc54 91 | 92 | ## Support 93 | This is a hobby project, which means sometimes other things take priority. I will review issues and work on issues when I have the time. Spamming new issues, asking for a ton of updates, or things like that, will not speed up the process. It will probably even give me less motivation to work on it :) 94 | 95 | If you're looking for paid support, please reach out to me at [stanvanrooy6@gmail.com](mailto:stanvanrooy6@gmail.com). 96 | 97 | ## Contributing 98 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 99 | 100 | There's an [article up on the wiki](https://github.com/stanvanrooy/instauto/wiki/Setting-up-a-development-environment), that explains how to set up a development environment. 101 | 102 | ## License 103 | [MIT](https://choosealicense.com/licenses/mit/) 104 | 105 | ## Contributors ✨ 106 | 107 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 |

Maroš Gonda

⚠️ 💻

Norbert Gocník

💻

Jakub Juhas

💻 📖 ⚠️

Samu1808

💻

Kevin Jonathan

📖

Martin Nikolov

💻

b177y

💻 ⚠️ 📖

wowopo

💻

Stan van Rooy

📖 💻 ⚠️

Tizian Seehaus

💻

Florkast

📖

Artur

💻 📖

Felix Fischer

💻

alperenkaplan

💻 📖

Javadz

💻 📖
135 | 136 | 137 | 138 | 139 | 140 | 141 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 142 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 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) 21 | -------------------------------------------------------------------------------- /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=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Instauto' 21 | copyright = '2020, Stan van Rooy' 22 | author = 'Stan van Rooy' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '2.0.6' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.napoleon', 35 | 'sphinx.ext.autodoc', 36 | 'sphinx_rtd_theme', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = [] 46 | 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = "sphinx_rtd_theme" 54 | 55 | html_sidebars = { '**': ['index.html', 'globaltoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html'] } 56 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Instauto's documentation! 2 | ==================================== 3 | 4 | A Python wrapper for the private Instagram API. 5 | 6 | Usage 7 | =================== 8 | Instauto has 3 main api's that can be used: `instauto.api`, `instauto.bot` & `instauto.helpers`. 9 | 10 | Everything in `instauto` is based around the 'core' `instauto.api` package. This package interacts directly 11 | with the private Instagram API and contains all core functionality. This package is both the most flexible (you can 12 | update all requests sent and receive the full response back, for example), but also the most complex. 13 | 14 | The `instauto.helpers` package is an abstraction above the `instauto.api` package. It offers less flexibility, but is 15 | simpler to use. 16 | 17 | The `instauto.bot` package is another abstraction above the `instauto.helpers` package. This package has pretty much no 18 | flexibility, but can be set up in 10 lines of Python code. 19 | 20 | .. toctree:: 21 | :maxdepth: 1 22 | 23 | instauto.api 24 | instauto.helpers 25 | instauto.bot 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/source/instauto.api.actions.rst: -------------------------------------------------------------------------------- 1 | instauto.api.actions package 2 | ============================ 3 | 4 | instauto.api.actions.authentication module 5 | ------------------------------------------ 6 | 7 | .. automodule:: instauto.api.actions.authentication 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | instauto.api.actions.challenge module 13 | ------------------------------------- 14 | 15 | .. automodule:: instauto.api.actions.challenge 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | instauto.api.actions.direct module 21 | ---------------------------------- 22 | 23 | .. automodule:: instauto.api.actions.direct 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | instauto.api.actions.friendships module 29 | --------------------------------------- 30 | 31 | .. automodule:: instauto.api.actions.friendships 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | instauto.api.actions.helpers module 37 | ----------------------------------- 38 | 39 | .. automodule:: instauto.api.actions.helpers 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | instauto.api.actions.post module 45 | -------------------------------- 46 | 47 | .. automodule:: instauto.api.actions.post 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | instauto.api.actions.profile module 53 | ----------------------------------- 54 | 55 | .. automodule:: instauto.api.actions.profile 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | instauto.api.actions.request module 61 | ----------------------------------- 62 | 63 | .. automodule:: instauto.api.actions.request 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | instauto.api.actions.search module 69 | ---------------------------------- 70 | 71 | .. automodule:: instauto.api.actions.search 72 | :members: 73 | :undoc-members: 74 | :show-inheritance: 75 | 76 | instauto.api.actions.stub module 77 | --------------------------------- 78 | 79 | .. automodule:: instauto.api.actions.stub 80 | :members: 81 | :undoc-members: 82 | :show-inheritance: 83 | -------------------------------------------------------------------------------- /docs/source/instauto.api.rst: -------------------------------------------------------------------------------- 1 | instauto.api package 2 | ==================== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | instauto.api.actions 8 | 9 | instauto.api.client module 10 | -------------------------- 11 | 12 | .. automodule:: instauto.api.client 13 | :members: 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | instauto.api.constants module 18 | ----------------------------- 19 | 20 | .. automodule:: instauto.api.constants 21 | :members: 22 | :undoc-members: 23 | :show-inheritance: 24 | 25 | instauto.api.exceptions module 26 | ------------------------------ 27 | 28 | .. automodule:: instauto.api.exceptions 29 | :members: 30 | :undoc-members: 31 | :show-inheritance: 32 | 33 | instauto.api.structs module 34 | --------------------------- 35 | 36 | .. automodule:: instauto.api.structs 37 | :members: 38 | :undoc-members: 39 | :show-inheritance: 40 | -------------------------------------------------------------------------------- /docs/source/instauto.bot.rst: -------------------------------------------------------------------------------- 1 | instauto.bot package 2 | ==================== 3 | 4 | .. autoclass:: instauto.bot.Bot 5 | :members: 6 | :special-members: 7 | :undoc-members: 8 | :exclude-members: input, stop, __annotations__, __dict__, __module__, __weakref__ 9 | 10 | .. autoclass:: instauto.bot.Input 11 | :members: 12 | :undoc-members: 13 | :exclude-members: filtered_accounts 14 | -------------------------------------------------------------------------------- /docs/source/instauto.helpers.rst: -------------------------------------------------------------------------------- 1 | instauto.helpers package 2 | ======================== 3 | 4 | instauto.helpers.friendships module 5 | ----------------------------------- 6 | 7 | .. automodule:: instauto.helpers.friendships 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | instauto.helpers.post module 13 | ---------------------------- 14 | 15 | .. automodule:: instauto.helpers.post 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | instauto.helpers.search module 21 | ------------------------------ 22 | 23 | .. automodule:: instauto.helpers.search 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /examples/api/activity/get_recent_activity.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.activity as act 3 | 4 | 5 | client = ApiClient(username="your_username", password="your_password") 6 | # or ApiClient.initiate_from_file('.instauto.save') 7 | client.save_to_disk('.instauto.save') 8 | 9 | 10 | obj = act.ActivityGet(mark_as_seen=False) 11 | recent_activity = client.activity_get(obj) 12 | 13 | -------------------------------------------------------------------------------- /examples/api/authentication/change_password.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | 3 | # note: to change your password, instauto needs the current password. If you've initialized 4 | # the client from your username and password, it'll be able to get it from there, but if 5 | # you initialized the client from a save file, you'll need to provide it yourself, since 6 | # instauto does not store your password in the save file. 7 | 8 | client = ApiClient(username="your_username", password="your_password") 9 | client.change_password("new_password") 10 | # or 11 | client = ApiClient.initiate_from_file('.instauto.save') 12 | client.change_password("new_password", "current_password") 13 | 14 | -------------------------------------------------------------------------------- /examples/api/authentication/log_in.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | 3 | # There are multiple ways to authenticate in instauto, the first one, 4 | # is by creating a new session 5 | client = ApiClient(username="your_username", password="your_password") 6 | client.log_in() 7 | 8 | # This is simple and fast, but doing this too often, will get your account flagged. 9 | # That's why, instauto also supports saving sessions. 10 | client.save_to_disk('.instauto.save') 11 | # The above statement will save all important information that is necessary 12 | # for reconstructing your session. To reconstruct your session, you can 13 | # call the `initiate_from_file` class method. 14 | client = ApiClient.initiate_from_file('.instauto.save') 15 | 16 | # That covers simple authentication, but what if you have 2FA enabled? No worries, 17 | # that is also supported: 18 | def get_2fa_code(username: str) -> str: 19 | # do something that'll retrieve a valid 2fa code here 20 | return str(random.randint(100000, 999999)) 21 | client = ApiClient(username="your_username", password="your_password", _2fa_function=get_2fa_code) 22 | 23 | # Too much work to implement the `get_2fa_code` function? If it's not provided, instauto 24 | # will prompt you for a 2fa code in the terminal before it continues. 25 | 26 | # More information about authentication? Check out the authentication 27 | # wiki article https://github.com/stanvanrooy/instauto/wiki/Authentication 28 | 29 | -------------------------------------------------------------------------------- /examples/api/direct/get_inbox.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | 3 | client = ApiClient.initiate_from_file('.instauto.save') 4 | assert client.direct_update_inbox() 5 | inbox = client.inbox 6 | 7 | # see the wiki for more information: 8 | # https://github.com/stanvanrooy/instauto/wiki/Using-the-direct-messaging-API 9 | -------------------------------------------------------------------------------- /examples/api/direct/get_threads.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | 3 | client = ApiClient.initiate_from_file('.instauto.save') 4 | assert client.direct_update_inbox() 5 | inbox = client.inbox 6 | 7 | threads = inbox.threads 8 | thread_ids = [t.thread_id for t in threads] 9 | 10 | # see the wiki for more information: 11 | # https://github.com/stanvanrooy/instauto/wiki/Using-the-direct-messaging-API 12 | -------------------------------------------------------------------------------- /examples/api/direct/send_link.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from instauto.api.client import ApiClient 4 | import instauto.api.actions.structs.direct as dr 5 | 6 | client = ApiClient.initiate_from_file('.instauto.save') 7 | client.direct_update_inbox() 8 | random_thread_id = random.choice(client.inbox.threads).thread_id 9 | 10 | user_id = "12345678" 11 | msg = dr.LinkShare( 12 | "Hello from Instauto (https://github.com/stanvanrooy/instauto)!", 13 | ["https://github.com/stanvanrooy/instauto"], 14 | recipients=[[user_id]] 15 | ) 16 | client.direct_send(msg) 17 | 18 | # see the wiki for more information: 19 | # https://github.com/stanvanrooy/instauto/wiki/Using-the-direct-messaging-API 20 | 21 | -------------------------------------------------------------------------------- /examples/api/direct/send_text_message.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from instauto.api.client import ApiClient 4 | import instauto.api.actions.structs.direct as dr 5 | 6 | client = ApiClient.initiate_from_file('.instauto.save') 7 | client.direct_update_inbox() 8 | random_thread_id = random.choice(client.inbox.threads).thread_id 9 | 10 | user_id = "12345678" 11 | msg = dr.Message("Hello from Instauto!", recipients=[[user_id]]) 12 | client.direct_send(msg) 13 | 14 | # see the wiki for more information: 15 | # https://github.com/stanvanrooy/instauto/wiki/Using-the-direct-messaging-API 16 | 17 | -------------------------------------------------------------------------------- /examples/api/direct/share_link.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.direct as dr 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | client.direct_update_inbox() 6 | random_thread_id = client.inbox.threads[0].thread_id 7 | 8 | user_id = "12345678" 9 | msg = dr.LinkShare( 10 | "Hello from Instauto (https://github.com/stanvanrooy/instauto)!", 11 | ["https://github.com/stanvanrooy/instauto"], 12 | recipients=[[user_id]] 13 | ) 14 | client.direct_send(msg) 15 | 16 | # see the wiki for more information: 17 | # https://github.com/stanvanrooy/instauto/wiki/Using-the-direct-messaging-API 18 | 19 | -------------------------------------------------------------------------------- /examples/api/direct/share_media.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.direct as dr 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | client.direct_update_inbox() 6 | random_thread_id = client.inbox.threads[0].thread_id 7 | 8 | recipient = "12345678" 9 | media_id = "123232131231321_12121344" 10 | msg = dr.MediaShare( 11 | media_id, 12 | recipients=[[recipient]] 13 | ) 14 | client.direct_send(msg) 15 | 16 | # see the wiki for more information: 17 | # https://github.com/stanvanrooy/instauto/wiki/Using-the-direct-messaging-API 18 | 19 | -------------------------------------------------------------------------------- /examples/api/direct/share_photo.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.direct as dr 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | client.direct_update_inbox() 6 | random_thread_id = client.inbox.threads[0].thread_id 7 | 8 | # to share an photo, we first have to upload it: 9 | from instauto.api.actions.structs.post import PostNull 10 | post = PostNull('./path-to-image.jpg') 11 | upload_id = client._upload_image(post, quality=70)['upload_id'] 12 | 13 | # and then we can send it 14 | recipient = "12345678" 15 | msg = dr.DirectPhoto( 16 | upload_id, 17 | recipients=[[recipient]] 18 | ) 19 | client.direct_send(photo) 20 | 21 | # see the wiki for more information: 22 | # https://github.com/stanvanrooy/instauto/wiki/Using-the-direct-messaging-API 23 | 24 | -------------------------------------------------------------------------------- /examples/api/direct/share_profile.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.direct as dr 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | client.direct_update_inbox() 6 | random_thread_id = client.inbox.threads[0].thread_id 7 | 8 | recipient_1 = "12345678" 9 | recipient_2 = "87654321" 10 | profile_to_share = "12348765" 11 | msg = dr.ProfileShare( 12 | profile_to_share, 13 | recipients=[[recipient_1], [recipient_2]] 14 | ) 15 | client.direct_send(msg) 16 | 17 | # see the wiki for more information: 18 | # https://github.com/stanvanrooy/instauto/wiki/Using-the-direct-messaging-API 19 | 20 | -------------------------------------------------------------------------------- /examples/api/feed/get_feed.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.feed as feed 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | 6 | # Instauto has an `feed_get` endpoint for retrieving images, videos and ads from your feed. The 7 | # endpoint, returns two values. The input object, and the retrieved response. You can re-use 8 | # the input object, to enable pagination: 9 | obj = feed.FeedGet() 10 | obj, response = client.feed_get(obj) 11 | 12 | posts = response.json()['feed_items'] 13 | 14 | # Let's retrieve the first 50 items from your feed. 15 | while response and len(posts) < 50: 16 | # We check if the response is 'truthy'. This is important, since it will be `False` if 17 | # there are no more items to retrieve from your feed. 18 | obj, response = client.feed_get(obj) 19 | posts.extend(response.json()['feed_items']) 20 | 21 | -------------------------------------------------------------------------------- /examples/api/friendships/approve_follow_request.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.friendships as fs 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | 6 | obj = fs.PendingRequests() 7 | follow_request = client.follow_requests_get(obj)[0] 8 | 9 | obj = fs.ApproveRequest(follow_request['pk']) 10 | client.follow_request_approve(obj) 11 | 12 | -------------------------------------------------------------------------------- /examples/api/friendships/follow_user.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.friendships as fs 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | 6 | user_id = "12345678" 7 | obj = fs.Create(user_id) 8 | client.user_follow(obj) 9 | 10 | -------------------------------------------------------------------------------- /examples/api/friendships/get_follow_requests.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.friendships as fs 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | 6 | obj = fs.PendingRequests() 7 | follow_requests = client.follow_requests_get(obj) 8 | 9 | -------------------------------------------------------------------------------- /examples/api/friendships/get_followers.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.friendships as fs 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | 6 | # Instauto has an `followers_get` endpoint for retrieving followers. The endpoint, returns 7 | # two values. The input object, and the retrieved response. You can re-use the input 8 | # object, to enable pagination: 9 | user_id = "12345678" 10 | obj = fs.GetFollowers(user_id) 11 | obj, response = client.followers_get(obj) 12 | 13 | followers = response.json()['users'] 14 | 15 | # Let's retrieve the first 50 followers of the user. 16 | while response and len(followers) < 50: 17 | # We check if the response is 'truthy'. This is important, since it will be `False` if 18 | # there are no more items to retrieve from your feed. 19 | obj, response = client.followers_get(obj) 20 | followers.extend(response.json()['users']) 21 | 22 | -------------------------------------------------------------------------------- /examples/api/friendships/get_following.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.friendships as fs 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | 6 | # Instauto has an `followers_get` endpoint for retrieving followers. The endpoint, returns 7 | # two values. The input object, and the retrieved response. You can re-use the input 8 | # object, to enable pagination: 9 | user_id = "12345678" 10 | obj = fs.GetFollowing(user_id) 11 | obj, response = client.following_get(obj) 12 | 13 | following= response.json()['users'] 14 | 15 | # Let's retrieve the first 50 users following the user. 16 | while response and len(following) < 50: 17 | # We check if the response is 'truthy'. This is important, since it will be `False` if 18 | # there are no more items to retrieve from your feed. 19 | obj, response = client.following_get(obj) 20 | following.extend(response.json()['users']) 21 | 22 | -------------------------------------------------------------------------------- /examples/api/friendships/remove_follower.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.friendships as fs 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | 6 | user_id = "12345678" 7 | obj = fs.Remove(user_id) 8 | client.follower_remove(obj) 9 | 10 | -------------------------------------------------------------------------------- /examples/api/friendships/retrieve_user.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.friendships as fs 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | 6 | user_id = "12345678" 7 | obj = fs.Show(user_id) 8 | client.follower_show(obj) 9 | 10 | -------------------------------------------------------------------------------- /examples/api/friendships/unfollow_user.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.friendships as fs 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | 6 | user_id = "12345678" 7 | obj = fs.Destroy(user_id) 8 | client.user_unfollow(obj) 9 | 10 | -------------------------------------------------------------------------------- /examples/api/post/archive_post.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = ps.Archive("media_id") 6 | response = client.post_archive(obj) -------------------------------------------------------------------------------- /examples/api/post/comment_post.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = ps.Comment("media_id", "Hello from instauto!") 6 | response = client.post_comment(obj) 7 | 8 | -------------------------------------------------------------------------------- /examples/api/post/get_commenters.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = ps.RetrieveCommenters("media_id") 6 | commenters = client.post_get_commenters(obj) 7 | 8 | -------------------------------------------------------------------------------- /examples/api/post/get_comments.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = ps.RetrieveComments("media_id") 6 | comments = client.post_get_comments(obj) 7 | 8 | -------------------------------------------------------------------------------- /examples/api/post/get_likers.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = ps.RetrieveLikers("media_id") 6 | likers = client.post_get_likers(obj) 7 | 8 | -------------------------------------------------------------------------------- /examples/api/post/like_post.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = ps.Like("media_id") 6 | response = client.post_like(obj) 7 | 8 | -------------------------------------------------------------------------------- /examples/api/post/retrieve_post_by_id.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = ps.RetrieveById("media_id") 6 | post = client.post_retrieve_by_id(obj) 7 | 8 | -------------------------------------------------------------------------------- /examples/api/post/retrieve_posts_from_tag.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | 6 | # Instauto has an `feed_get` endpoint for retrieving images, videos and ads from your feed. The 7 | # endpoint, returns two values. The input object, and the retrieved response. You can re-use 8 | # the input object, to enable pagination: 9 | obj = ps.RetrieveByTag("username") 10 | obj, response = client.post_retrieve_by_tag(obj) 11 | posts = response 12 | 13 | # Let's retrieve the first 50 items posts from the tag. 14 | while response and len(posts) < 25: 15 | # We check if the response is 'truthy'. This is important, since it will be `False` if 16 | # there are no more items to retrieve from your feed. 17 | obj, response = client.post_retrieve_by_tag(obj) 18 | posts.extend(response) 19 | 20 | -------------------------------------------------------------------------------- /examples/api/post/retrieve_posts_from_user.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | 6 | # Instauto has an `feed_get` endpoint for retrieving images, videos and ads from your feed. The 7 | # endpoint, returns two values. The input object, and the retrieved response. You can re-use 8 | # the input object, to enable pagination: 9 | obj = ps.RetrieveByUser("user_id") 10 | obj, response = client.post_retrieve_by_user(obj) 11 | posts = response 12 | 13 | # Let's retrieve the first 50 items posts from the user. 14 | while response and len(posts) < 25: 15 | # We check if the response is 'truthy'. This is important, since it will be `False` if 16 | # there are no more items to retrieve from your feed. 17 | obj, response = client.post_retrieve_by_user(obj) 18 | posts.extend(response) 19 | 20 | -------------------------------------------------------------------------------- /examples/api/post/retrieve_story.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = ps.RetrieveStory(12345678) 6 | post = client.post_retrieve_story(obj) 7 | -------------------------------------------------------------------------------- /examples/api/post/save_post.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = ps.Save("media_id") 6 | response = client.post_save(obj) 7 | 8 | -------------------------------------------------------------------------------- /examples/api/post/unarchive_post.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = ps.Unarchive("media_id") 6 | response = client.post_unarchive(obj) -------------------------------------------------------------------------------- /examples/api/post/unlike_post.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = ps.Unlike("media_id") 6 | response = client.post_unlike(obj) 7 | 8 | -------------------------------------------------------------------------------- /examples/api/post/update_caption.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = ps.UpdateCaption("media_id", "new_caption") 6 | response = client.post_update_caption(obj) 7 | 8 | -------------------------------------------------------------------------------- /examples/api/post/upload_carousel_to_feed.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | posts = [ 6 | ps.PostFeed( 7 | "./test-feed.jpg", 8 | "" 9 | ), 10 | ps.PostFeed( 11 | "./test-feed.jpg", 12 | "" 13 | ), 14 | ] 15 | 16 | resp = client.post_carousel(posts, "Hello from Instauto!", 80) 17 | 18 | -------------------------------------------------------------------------------- /examples/api/post/upload_carousel_to_feed_with_usertags.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | user_tags = ps.UserTags([ 6 | ps.UserTag( 7 | "user_id", 8 | 0.2, 9 | 0.2 10 | ) 11 | ]) 12 | posts = [ 13 | ps.PostFeed( 14 | "./test-feed.jpg", 15 | "", 16 | usertags=user_tags 17 | ), 18 | ps.PostFeed( 19 | "./test-feed.jpg", 20 | "", 21 | usertags=user_tags 22 | ), 23 | ] 24 | 25 | resp = client.post_carousel(posts, "Hello from Instauto!", 80) 26 | 27 | -------------------------------------------------------------------------------- /examples/api/post/upload_image_to_feed.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | post = ps.PostFeed( 6 | "./test-feed.jpg", 7 | "Hello from Instauto!" 8 | ) 9 | 10 | response = client.post_post(post) 11 | 12 | -------------------------------------------------------------------------------- /examples/api/post/upload_image_to_story.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | post = ps.PostStory( 6 | "./test-story.jpg" 7 | ) 8 | 9 | response = client.post_post(post) 10 | 11 | -------------------------------------------------------------------------------- /examples/api/post/upload_image_with_location_to_feed.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file(".instauto.save") 5 | 6 | # Any of the below examples will work. 7 | # location = ps.Location(lat=38.897699584711, lng=-77.036494857373) 8 | # location = ps.Location(name="The white house") 9 | location = ps.Location(lat=68.14259, lng=148.84371, name="The white house") 10 | post = ps.PostFeed( 11 | path='./test_feed.jpg', 12 | caption='Hello from Instauto!', 13 | location=location 14 | ) 15 | 16 | response = client.post_post(post, 80) 17 | 18 | -------------------------------------------------------------------------------- /examples/api/post/upload_image_with_location_to_story.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file(".instauto.save") 5 | 6 | # Any of the below examples will work. 7 | # location = ps.Location(lat=38.897699584711, lng=-77.036494857373) 8 | # location = ps.Location(name="The white house") 9 | location = ps.Location(lat=68.14259, lng=148.84371, name="The white house") 10 | post = ps.PostFeed( 11 | path='./test_feed.jpg', 12 | caption='Hello from Instauto!', 13 | location=location 14 | ) 15 | 16 | response = client.post_post(post, 80) 17 | 18 | -------------------------------------------------------------------------------- /examples/api/post/upload_image_with_usertags_to_feed.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.post as ps 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | post = ps.PostFeed( 6 | "./test-feed.jpg", 7 | "Hello from Instauto!", 8 | usertags=ps.UserTags([ 9 | ps.UserTag("user_id", 0.2, 0.2) 10 | ]) 11 | ) 12 | 13 | response = client.post_post(post) 14 | 15 | -------------------------------------------------------------------------------- /examples/api/profile/get_profile_info.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.profile as pr 3 | 4 | client = ApiClient.initiate_from_file(".instauto.save") 5 | obj = pr.Info("user_id") 6 | info = client.profile_info(obj) 7 | 8 | -------------------------------------------------------------------------------- /examples/api/profile/update_biography.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.profile as pr 3 | 4 | client = ApiClient.initiate_from_file('.instauto.save') 5 | obj = pr.SetBiography("My new biography!") 6 | client.profile_set_biography(obj) 7 | 8 | -------------------------------------------------------------------------------- /examples/api/profile/update_gender.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.profile as pr 3 | from instauto.api.structs import WhichGender 4 | 5 | client = ApiClient.initiate_from_file('.instauto.save') 6 | obj = pr.SetGender(WhichGender.female) 7 | # or 8 | obj = pr.SetGender(WhichGender.other, "Some custom gender") 9 | client.profile_set_gender(obj) 10 | 11 | -------------------------------------------------------------------------------- /examples/api/profile/update_picture.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.profile as pr 3 | import instauto.api.actions.structs.post as ps 4 | 5 | client = ApiClient.initiate_from_file('.instauto.save') 6 | 7 | post = ps.PostNull( 8 | path='./test_feed.jpg', 9 | ) 10 | resp = client.post_post(post, 80) 11 | 12 | upload_id = resp.json()['upload_id'] 13 | p = pr.SetPicture( 14 | upload_id=upload_id 15 | ) 16 | client.profile_set_picture(p) 17 | 18 | -------------------------------------------------------------------------------- /examples/api/profile/update_profile.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.profile as pr 3 | from instauto.api.structs import WhichGender 4 | 5 | client = ApiClient.initiate_from_file('.instauto.save') 6 | # With the update object, you can update multiple attributes at the same time, but you 7 | # can also just set one property. 8 | obj = pr.Update( 9 | "https://github.com/stanvanrooy/instauto", 10 | "your phone number", 11 | "your new username", 12 | "your new first name", 13 | "your new email" 14 | ) 15 | client.profile_update(obj) 16 | 17 | -------------------------------------------------------------------------------- /examples/api/search/search_tag.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.search as se 3 | 4 | # This returns the first 10 search results, for 'instagram'. 5 | client = ApiClient.initiate_from_file('.instauto.save') 6 | obj = se.Tag("instagram", 10) 7 | response = client.search_tag(obj) 8 | 9 | -------------------------------------------------------------------------------- /examples/api/search/search_username.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | import instauto.api.actions.structs.search as se 3 | 4 | # This returns the first 10 search results, for 'instagram'. 5 | client = ApiClient.initiate_from_file('.instauto.save') 6 | obj = se.Username("instagram", 10) 7 | response = client.search_username(obj) 8 | 9 | -------------------------------------------------------------------------------- /examples/helpers/feed/get_feed.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.feed import get_feed 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | posts = get_feed(client, 10) 10 | assert posts is not None 11 | assert len(posts) == 10 12 | 13 | print(posts[0].id) 14 | 15 | -------------------------------------------------------------------------------- /examples/helpers/friendships/follow_user.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.friendships import follow_user 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | # success = follow_user(client, user_id="user_id") 10 | success = follow_user(client, username="instagram") 11 | assert success 12 | 13 | -------------------------------------------------------------------------------- /examples/helpers/friendships/get_followers.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.search import get_user_id_from_username 7 | from instauto.helpers.friendships import get_followers 8 | 9 | # Initiate the client 10 | client = ApiClient.initiate_from_file('../../../.instauto.save') 11 | 12 | # Get the user id by doing one of the below 13 | # user_id = "user_id" 14 | user_id = get_user_id_from_username(client, "instagram") 15 | 16 | # And retrieve 10 followers 17 | followers = get_followers(client, user_id, 10) 18 | assert followers is not None 19 | assert len(followers) == 10 20 | -------------------------------------------------------------------------------- /examples/helpers/friendships/get_following.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.search import get_user_id_from_username 7 | from instauto.helpers.friendships import get_following 8 | 9 | # Initiate the client 10 | client = ApiClient.initiate_from_file('../../../.instauto.save') 11 | # Get the user id by doing one of the below 12 | # user_id = "user_id" 13 | user_id = get_user_id_from_username(client, "instagram") 14 | 15 | # And retrieve 10 users following the user 16 | following = get_following(client, user_id, 10) 17 | 18 | assert following is not None 19 | assert len(following) == 10 20 | 21 | -------------------------------------------------------------------------------- /examples/helpers/friendships/unfollow_user.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.friendships import unfollow_user 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | # unfollow_user(client, user_id="user_id") 10 | # or 11 | success = unfollow_user(client, username="instagram") 12 | assert success 13 | -------------------------------------------------------------------------------- /examples/helpers/post/comment.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import comment_post 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | success = comment_post(client, "MEDIA_ID", "Hello from Instauto!") 10 | assert success 11 | 12 | -------------------------------------------------------------------------------- /examples/helpers/post/like.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import like_post 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | success = like_post(client, "MEDIA_ID") 10 | assert success 11 | 12 | -------------------------------------------------------------------------------- /examples/helpers/post/retrieve_commenters.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import get_commenters_of_post 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | posts = get_commenters_of_post(client, "MEDIA_ID") 10 | assert posts is not None 11 | -------------------------------------------------------------------------------- /examples/helpers/post/retrieve_from_tag.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import retrieve_posts_from_tag 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | posts = retrieve_posts_from_tag(client, "instagram", 10) 10 | print(posts) 11 | 12 | -------------------------------------------------------------------------------- /examples/helpers/post/retrieve_from_user.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import retrieve_posts_from_user 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | # posts = retrieve_posts_from_user(client, 10, user_id="user_id") 10 | # or 11 | posts = retrieve_posts_from_user(client, 10, username="instagram") 12 | assert posts is not None 13 | assert len(posts) == 10 14 | 15 | -------------------------------------------------------------------------------- /examples/helpers/post/retrieve_likers.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import get_likers_of_post 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | posts = get_likers_of_post(client, "3384581222172362999_2120189741") 10 | assert posts is not None 11 | 12 | -------------------------------------------------------------------------------- /examples/helpers/post/retrieve_stories_from_user.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import retrieve_story_from_user 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | # stories = retrieve_story_from_user(client, user_id=12345678) 10 | # or 11 | stories = retrieve_story_from_user(client, username='instagram') 12 | assert stories is not None 13 | -------------------------------------------------------------------------------- /examples/helpers/post/save.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import save_post 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | success = save_post(client, "MEDIA_ID") 10 | assert success 11 | 12 | -------------------------------------------------------------------------------- /examples/helpers/post/unlike.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import unlike_post 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | success = unlike_post(client, "MEDIA_ID") 10 | assert success 11 | 12 | -------------------------------------------------------------------------------- /examples/helpers/post/update_caption.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import update_caption 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | success = update_caption(client, "MEDIA_ID", "Hello from Instauto!") 10 | assert success 11 | 12 | -------------------------------------------------------------------------------- /examples/helpers/post/upload_image_to_feed.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import upload_image_to_feed 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | success = upload_image_to_feed(client, "../../../test_feed.jpg", "Hello from instauto!") 10 | assert success 11 | 12 | -------------------------------------------------------------------------------- /examples/helpers/post/upload_image_to_feed_with_location.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import upload_image_to_feed 7 | from instauto.api.actions.structs.post import Location 8 | 9 | client = ApiClient.initiate_from_file('../../../.instauto.save') 10 | location = Location(name="The white house") 11 | success = upload_image_to_feed(client, "../../../test_feed.jpg", "Hello from instauto!", location) 12 | assert success 13 | 14 | -------------------------------------------------------------------------------- /examples/helpers/post/upload_image_to_story.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.post import upload_image_to_story 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | success = upload_image_to_story(client, "../../../test_story.jpg") 10 | assert success 11 | 12 | -------------------------------------------------------------------------------- /examples/helpers/search/search_tag.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.search import search_tags 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | users = search_tags(client, "instagram", 10) 10 | assert users is not None 11 | assert len(users) > 1 12 | 13 | -------------------------------------------------------------------------------- /examples/helpers/search/search_username.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..')) 3 | # Note: the above 2 lines are not necessary if you have installed the package. 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.helpers.search import search_username 7 | 8 | client = ApiClient.initiate_from_file('../../../.instauto.save') 9 | users = search_username(client, "instagram", 10) 10 | assert users is not None 11 | assert len(users) > 1 12 | -------------------------------------------------------------------------------- /instauto/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanvanrooy/instauto/43866223945c9e8a0e8029ba01bb970ee95ca555/instauto/__init__.py -------------------------------------------------------------------------------- /instauto/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanvanrooy/instauto/43866223945c9e8a0e8029ba01bb970ee95ca555/instauto/api/__init__.py -------------------------------------------------------------------------------- /instauto/api/actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanvanrooy/instauto/43866223945c9e8a0e8029ba01bb970ee95ca555/instauto/api/actions/__init__.py -------------------------------------------------------------------------------- /instauto/api/actions/activity.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from instauto.api.actions.stub import StubMixin 4 | from instauto.api.structs import Method 5 | from instauto.api.actions.structs.activity import ActivityGet 6 | 7 | 8 | class ActivityMixin(StubMixin): 9 | def activity_get(self, obj: ActivityGet) -> requests.Response: 10 | url = 'news/inbox' 11 | qp = { 12 | 'mark_as_seen': obj.mark_as_seen 13 | } 14 | return self._request(url, Method.GET, query=qp) 15 | 16 | -------------------------------------------------------------------------------- /instauto/api/actions/authentication.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import datetime 3 | import base64 4 | import struct 5 | 6 | from typing import Dict, Optional 7 | 8 | # pyre-ignore[21] 9 | from Cryptodome.Cipher import AES, PKCS1_v1_5 10 | # pyre-ignore[21] 11 | from Cryptodome.PublicKey import RSA 12 | # pyre-ignore[21] 13 | from Cryptodome import Random 14 | 15 | from .stub import StubMixin 16 | from ..structs import Method, LoggedInAccountData 17 | 18 | 19 | class AuthenticationMixin(StubMixin): 20 | def log_in(self) -> None: 21 | """Logs in the account with the `username` and `password`""" 22 | self._update_token() 23 | self._sync() 24 | 25 | body = { 26 | 'jazoest': self._create_jazoest(), 27 | 'phone_id': self.state.phone_id, 28 | 'device_id': self.state.android_id, 29 | 'guid': self.state.uuid, 30 | 'adid': self.state.ad_id, 31 | 'google_tokens': '[]', 32 | 'username': self._username, 33 | 'country_codes': "[{\"country_code\":\"31\",\"source\": \"default\"}]", 34 | 'enc_password': self._encoded_password, 35 | 'login_attempt_count': "0", 36 | } 37 | resp = self._request('accounts/login/', Method.POST, body=body, sign_request=True) 38 | try: 39 | self.state.logged_in_account_data = LoggedInAccountData(**self._json_loads(resp.text)['logged_in_user']) 40 | except KeyError as e: 41 | # The response can be empty if challenge was needed. In that 42 | # case, the logged_in_account_data attribute should've been 43 | # set from within in the challenge handler. 44 | if self.state.logged_in_account_data is None: 45 | raise e 46 | 47 | def change_password(self, new_password: str, current_password: Optional[str] = None) -> requests.Response: 48 | cp = current_password or self._plain_password 49 | if cp is None: 50 | raise ValueError("No current password provided") 51 | 52 | return self._request('accounts/change_password/', Method.POST, body={ 53 | '_uid': self.state.user_id, 54 | '_uuid': self.state.uuid, 55 | 'enc_old_password': self._encode_password(current_password), 56 | 'enc_new_password1': self._encode_password(new_password), 57 | 'enc_new_password2': self._encode_password(new_password) 58 | }, sign_request=True) 59 | 60 | def _build_initial_headers(self) -> Dict[str, str]: 61 | """Builds a dictionary that contains all header values required for the first request sent, before login, 62 | to retrieve necessary cookies and header values to send other requests. 63 | 64 | Returns 65 | ------- 66 | d : dict 67 | Dictionary containing the mappings. 68 | """ 69 | d = { 70 | 'x-ig-connection-type': self.state.connection_type, 71 | 'x-ig-capabilities': self.ig_profile.capabilities, 72 | 'x-ig-app-id': self.ig_profile.id, 73 | 'user-agent': self._user_agent, 74 | 'accept-language': 'nl_NL', 75 | 'accept-encoding': 'gzip, deflate', 76 | 'x-fb-http-engine': self.ig_profile.http_engine, 77 | 'x-ig-connection-speed': self.state.connection_speed, 78 | 'x-ig-bandwidth-speed-kbps': self.state.bandwidth_speed_kbps, 79 | 'x-ig-bandwidth-totalbytes-b': self.state.bandwidth_totalbytes_b, 80 | 'x-ig-bandwidth-totaltime-ms': self.state.bandwidth_totaltime_ms, 81 | 'x-ig-www-claim': self.state.www_claim, 82 | } 83 | return d 84 | 85 | def _encode_password(self, password: Optional[str] = None) -> Optional[str]: 86 | """Encrypts the raw password into a form that Instagram accepts.""" 87 | if not self.state.public_api_key: 88 | return 89 | if not any([password, self._plain_password]): 90 | return 91 | 92 | key = Random.get_random_bytes(32) 93 | iv = Random.get_random_bytes(12) 94 | time = int(datetime.datetime.now().timestamp()) 95 | 96 | pubkey = base64.b64decode(self.state.public_api_key) 97 | 98 | rsa_key = RSA.importKey(pubkey) 99 | rsa_cipher = PKCS1_v1_5.new(rsa_key) 100 | encrypted_key = rsa_cipher.encrypt(key) 101 | 102 | aes = AES.new(key, AES.MODE_GCM, nonce=iv) 103 | aes.update(str(time).encode('utf-8')) 104 | 105 | # pyre-ignore[6]: we do check if either password or plain_password 106 | encrypted_password, cipher_tag = aes.encrypt_and_digest(bytes(password or self._plain_password, 'utf-8')) 107 | 108 | encrypted = bytes([1, 109 | int(self.state.public_api_key_id), 110 | *list(iv), 111 | *list(struct.pack(' None: 134 | self.state.refresh(self._gen_uuid) 135 | 136 | def _create_jazoest(self) -> str: 137 | b = bytearray(self.state.phone_id, 'ascii') 138 | s = 0 139 | for c in range(len(b)): 140 | s += b[c] 141 | return f"2{s}" 142 | 143 | def _sync(self): 144 | body = { 145 | 'id': self.state.device_id, 146 | 'sever_config_retrieval': "1", 147 | 'experiments': "ig_android_device_detection_info_upload,ig_android_gmail_oauth_in_reg,ig_android_account_linking_upsell_universe,ig_android_direct_main_tab_universe_v2,ig_android_sign_in_help_only_one_account_family_universe,ig_android_sms_retriever_backtest_universe,ig_android_vc_interop_use_test_igid_universe,ig_android_direct_add_direct_to_android_native_photo_share_sheet,ig_growth_android_profile_pic_prefill_with_fb_pic_2,ig_account_identity_logged_out_signals_global_holdout_universe,ig_android_notification_unpack_universe,ig_android_quickcapture_keep_screen_on,ig_android_device_based_country_verification,ig_android_login_identifier_fuzzy_match,ig_android_reg_modularization_universe,ig_android_video_render_codec_low_memory_gc,ig_android_device_verification_separate_endpoint,ig_android_email_fuzzy_matching_universe,ig_android_suma_landing_page,ig_android_smartlock_hints_universe,ig_android_video_ffmpegutil_pts_fix,ig_android_multi_tap_login_new,ig_android_retry_create_account_universe,ig_android_caption_typeahead_fix_on_o_universe,ig_android_enable_keyboardlistener_redesign,ig_android_reg_nux_headers_cleanup_universe,ig_android_get_cookie_with_concurrent_session_universe,ig_android_nux_add_email_device,ig_android_device_info_foreground_reporting,ig_android_shortcuts_2019,ig_android_device_verification_fb_signup,ig_android_passwordless_account_password_creation_universe,ig_android_black_out_toggle_universe,ig_video_debug_overlay,ig_android_ask_for_permissions_on_reg,ig_assisted_login_universe,ig_android_security_intent_switchoff,ig_android_recovery_one_tap_holdout_universe,ig_android_sim_info_upload,ig_android_mobile_http_flow_device_universe,ig_android_fb_account_linking_sampling_freq_universe,ig_android_access_flow_prefill" 148 | } 149 | # this request retrieves the public key and public key id 150 | _ = self._request('qe/sync/', Method.POST, body=body) 151 | 152 | -------------------------------------------------------------------------------- /instauto/api/actions/challenge.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import uuid 3 | import logging 4 | 5 | from instauto.api.actions.stub import StubMixin 6 | from instauto.api.structs import Method 7 | from instauto.api.exceptions import BadResponse 8 | 9 | logging.basicConfig() 10 | logger = logging.getLogger(__name__) 11 | logger.setLevel(logging.DEBUG) 12 | 13 | 14 | class ChallengeMixin(StubMixin): 15 | def _handle_challenge(self, resp: requests.Response) -> bool: 16 | resp_data = self._json_loads(resp.text) 17 | logger.debug('_handle_challenge -> resp_data: %s', resp_data) 18 | 19 | if resp_data['message'] not in ('challenge_required', 'checkpoint_required'): 20 | raise BadResponse("Challenge required, but no URL provided.") 21 | 22 | assert 'challenge' in resp_data, f"'challenge' not found in resp_data" 23 | assert 'api_path' in resp_data['challenge'], f"'api_path' not found in resp_data" 24 | api_path = resp_data['challenge']['api_path'][1:] 25 | 26 | resp = self._request( 27 | endpoint=api_path, 28 | method=Method.GET, 29 | query={ 30 | "guid": self.state.uuid, 31 | "device_id": self.state.android_id 32 | } 33 | ) 34 | data = self._json_loads(resp.text) 35 | 36 | base_body = { 37 | "_uuid": self.state.uuid, 38 | "bloks_versioning_id": self.state.bloks_version_id, 39 | "post": 1, 40 | } 41 | body = base_body.copy() 42 | body["choice"] = int(data.get("step_data", {}).get("choice", 0)) 43 | 44 | _ = self._request(endpoint=api_path, method=Method.POST, body=body) 45 | 46 | security_code = input("Verification needed. Type verification code here: ") 47 | body = base_body.copy() 48 | body["security_code"] = security_code 49 | _ = self._request(endpoint=api_path, method=Method.POST, body=body) 50 | return True 51 | 52 | def _handle_2fa(self, parsed: dict) -> None: 53 | endpoint = "accounts/two_factor_login/" 54 | username = parsed['two_factor_info']['username'] 55 | code = self._get_2fa_code(username) 56 | 57 | logger.debug("2fa code is: %s", code) 58 | 59 | # 1 = phone verification, 3 = authenticator app verification 60 | verification_method = "1" if parsed['two_factor_info'].get('sms_two_factor_on') else "3" 61 | 62 | body = { 63 | 'verification_code': code, 64 | 'phone_id': uuid.uuid4(), 65 | 'two_factor_identifier': parsed['two_factor_info']['two_factor_identifier'], 66 | 'username': username, 67 | 'trust_this_device': 0, 68 | 'guid': uuid.uuid4(), 69 | 'device_id': self.state.android_id, 70 | 'waterfall_id': uuid.uuid4(), 71 | 'verification_method': verification_method 72 | } 73 | self._request(endpoint, Method.POST, body=body) 74 | 75 | def _get_2fa_code(self, username: str) -> str: 76 | if self._2fa_function: 77 | return self._2fa_function(username) 78 | return input(f"Enter 2fa code for {username}: ") 79 | 80 | -------------------------------------------------------------------------------- /instauto/api/actions/direct.py: -------------------------------------------------------------------------------- 1 | from requests import Response 2 | from typing import Union, List 3 | 4 | from .stub import StubMixin 5 | from ..structs import Method, Thread, Inbox 6 | from .structs.direct import Message, MediaShare, LinkShare, ProfileShare, \ 7 | DirectPhoto, DirectVideo, DirectThread 8 | 9 | 10 | class DirectMixin(StubMixin): 11 | def __init__(self): 12 | self._inbox = None 13 | 14 | @property 15 | def inbox(self): 16 | if self._inbox is None: 17 | raise ValueError("Inbox has not been retrieved") 18 | return self._inbox 19 | 20 | @inbox.setter 21 | def inbox(self, value: Inbox): 22 | self._inbox = value 23 | 24 | def direct_update_inbox(self) -> bool: 25 | """Request your inbox status from Instagram. 26 | 27 | Updates the threads with a distinct set of the old & new threads. Overwrites 28 | all other properties. 29 | 30 | Returns: 31 | bool: True if the inbox has been updated 32 | """ 33 | resp = self._request('direct_v2/inbox', Method.GET) 34 | stat = self._set_inbox_from_json(self._json_loads(resp.text)) 35 | return resp.ok and stat 36 | 37 | def direct_get_thread(self, obj: DirectThread) -> Thread: 38 | """Retrieve more information about a thread. 39 | 40 | If this thread exists in the inbox, it will be updated. If not, it 41 | will be added to the thread lists. 42 | 43 | Returns: 44 | Thread: The retrieved thread. 45 | """ 46 | resp = self._request(f"direct_v2/threads/{obj.thread_id}", Method.GET) 47 | thread = self._json_loads(resp.text)['thread'] 48 | thread = Thread(thread.pop('thread_id'), thread.pop('thread_v2_id'), thread.pop('users'), 49 | thread.pop('left_users'), thread.pop('admin_user_ids'), thread.pop('items'), 50 | {k: v for k, v in thread.items()}) 51 | self._add_or_update_inbox_with_thread(thread) 52 | return thread 53 | 54 | def direct_send(self, obj: Union[Message, MediaShare, LinkShare, 55 | ProfileShare, DirectPhoto, DirectVideo]) -> Response: 56 | """Send a message to a thread.""" 57 | as_dict = obj.to_dict() 58 | return self._request(obj.endpoint, Method.POST, body=as_dict) 59 | 60 | def _build_thread_objects(self, threads_as_dict: List[dict]) -> List[Thread]: 61 | threads: List[Thread] = [] 62 | for thread in threads_as_dict: 63 | threads.append( 64 | Thread(thread.pop('thread_id'), thread.pop('thread_v2_id'), thread.pop('users'), 65 | thread.pop('left_users'), thread.pop('admin_user_ids'), thread.pop('items'), 66 | {k: v for k, v in thread.items()})) 67 | return threads 68 | 69 | def _extend_inbox_threads(self, threads: List[Thread]): 70 | threads.extend(self.inbox.threads) 71 | seen = [] 72 | threads = list(filter(lambda x: x.thread_id not in seen and seen.append(x.thread_id) is None, threads)) 73 | self.inbox.threads = threads 74 | 75 | def _set_inbox_from_json(self, data: dict): 76 | threads = self._build_thread_objects(data['inbox']['threads']) 77 | 78 | self.inbox = Inbox( 79 | threads, data['inbox']['has_older'], data['inbox']['unseen_count'], data['inbox']['unseen_count_ts'], 80 | data['inbox']['oldest_cursor'], data['inbox']['prev_cursor'], data['inbox']['next_cursor'], 81 | data['inbox']['blended_inbox_enabled'], data['seq_id'], data['snapshot_at_ms'], 82 | data['pending_requests_total'], data['has_pending_top_requests'] 83 | ) 84 | self._extend_inbox_threads(threads) 85 | return True 86 | 87 | def _add_or_update_inbox_with_thread(self, thread: Thread): 88 | if self._inbox is None: 89 | return 90 | index_of_existing = [i for i, t in enumerate(self.inbox.threads) if t.thread_id == thread.thread_id] 91 | if index_of_existing: 92 | self.inbox.threads[index_of_existing[0]] = thread 93 | else: 94 | self.inbox.threads.append(thread) 95 | -------------------------------------------------------------------------------- /instauto/api/actions/feed.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Tuple, Union 3 | 4 | import requests 5 | 6 | from instauto.api.actions.structs.feed import FeedGet 7 | from instauto.api.actions.stub import StubMixin 8 | from instauto.api.structs import Method 9 | 10 | 11 | class FeedMixin(StubMixin): 12 | def feed_get(self, obj: FeedGet) -> Tuple[Union[FeedGet], requests.Response]: 13 | as_dict = obj.fill(self).to_dict() 14 | as_dict['request_id'] = str(uuid.uuid4()) 15 | resp = self._request('feed/timeline/', Method.POST, body=as_dict) 16 | 17 | data = self._json_loads(resp.text) 18 | if obj.reason == 'cold_start_fetch': 19 | obj.reason = 'pagination' 20 | 21 | obj.max_id = data['next_max_id'] 22 | return obj, resp 23 | -------------------------------------------------------------------------------- /instauto/api/actions/friendships.py: -------------------------------------------------------------------------------- 1 | from requests import Response 2 | from typing import Union, Tuple, List 3 | from .structs.friendships import Create, Destroy, Remove, Show, \ 4 | GetFollowers, GetFollowing, PendingRequests, ApproveRequest 5 | from .stub import StubMixin 6 | from ..structs import Method 7 | 8 | 9 | class FriendshipsMixin(StubMixin): 10 | def user_follow(self, obj: Create) -> Response: 11 | """Follow a user""" 12 | return self._friendships_act(obj) 13 | 14 | def user_unfollow(self, obj: Destroy) -> Response: 15 | """Unfollow a user""" 16 | return self._friendships_act(obj) 17 | 18 | def follower_remove(self, obj: Remove) -> Response: 19 | """Remove someone from your followers list, that is currently following you""" 20 | return self._friendships_act(obj) 21 | 22 | def follower_show(self, obj: Show) -> Response: 23 | """Retrieve information about a user""" 24 | obj.fill(self) 25 | return self._request(f"friendships/{obj.endpoint}/{obj.user_id}", Method.GET) 26 | 27 | def followers_get(self, obj: GetFollowers) -> Tuple[GetFollowers, Union[Response, bool]]: 28 | """Retrieves the followers of an Instagram user. 29 | 30 | Returns 31 | (GetFollowers, Response || bool): A tuple that contains the object that was passed in 32 | as an argument, but with updated max_id and page attributes, and the response or False. If the 33 | second item is False, there were no more items available. 34 | """ 35 | # pyre-ignore[7] 36 | return self._get_base(obj) 37 | 38 | def following_get(self, obj: GetFollowing) -> Tuple[GetFollowing, Union[Response, bool]]: 39 | """Retrieves the following of an Instagram user. 40 | 41 | Returns: 42 | (GetFollowing, Response || bool): A tuple that contains the object that was passed in 43 | as an argument, but with updated max_id and page attributes, and the response or False. If the 44 | second item is False, there were no more items available. 45 | """ 46 | # pyre-ignore[7] 47 | return self._get_base(obj) 48 | 49 | def follow_requests_get(self, obj: PendingRequests) -> List[dict]: 50 | """Retrieve all follow requests""" 51 | resp = self._request('friendships/pending/', Method.GET) 52 | parsed = self._json_loads(resp.text) 53 | return parsed['users'] 54 | 55 | def follow_request_approve(self, obj: ApproveRequest) -> Response: 56 | """Accept a follow request/""" 57 | obj.fill(self) 58 | return self._request(f'friendships/approve/{obj.user_id}/', Method.POST, body=obj.to_dict()) 59 | 60 | def _friendships_act(self, obj: Union[Create, Destroy, Remove]) -> Response: 61 | obj.fill(self) 62 | return self._request(f"friendships/{obj.endpoint}/{obj.user_id}/", Method.POST, body=obj.to_dict(), sign_request=True) 63 | 64 | def _get_base(self, obj: Union[GetFollowing, GetFollowers]) -> \ 65 | Tuple[Union[GetFollowing, GetFollowers], Union[Response, bool]]: 66 | obj.fill(self) 67 | data = obj.to_dict() 68 | # pyre-ignore[58] 69 | if 'max_id' not in data and data.get('page', 0) > 0: 70 | return obj, False 71 | 72 | query_params = { 73 | 'search_surface': obj.search_surface, 74 | 'order': 'default', 75 | 'enable_groups': "true", 76 | "query": "", 77 | "rank_token": obj.rank_token 78 | } 79 | # pyre-ignore[58] 80 | if data.get('page', 0) > 0: # make sure we don't include max_id on the first request 81 | query_params['max_id'] = obj.max_id 82 | endpoint = 'friendships/{user_id}/followers/' if isinstance(obj, GetFollowers) else 'friendships/{user_id}/following/' 83 | resp = self._request(endpoint.format(user_id=obj.user_id), Method.GET, query=query_params) 84 | as_json = self._json_loads(resp.text) 85 | if 'next_max_id' not in as_json: 86 | return obj, False 87 | obj.max_id = as_json['next_max_id'] 88 | # pyre-ignore[58] 89 | obj.page = data.get('page', 0) + 1 90 | return obj, resp 91 | 92 | -------------------------------------------------------------------------------- /instauto/api/actions/helpers.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Any, Union 3 | 4 | import orjson 5 | 6 | from instauto.api.actions.stub import StubMixin 7 | 8 | 9 | class HelperMixin(StubMixin): 10 | @staticmethod 11 | def get_image_type(p: Union[str, Path]) -> str: 12 | """Returns the type of image, i.e. jpeg or png.""" 13 | if not isinstance(p, Path): 14 | p = Path(p) 15 | return ''.join(p.suffixes).replace('.', '', 1) 16 | 17 | def _build_default_rupload_params(self, obj, quality: int, is_sidecar: bool) -> dict: 18 | """Builds default parameters used to upload media.""" 19 | return { 20 | 'upload_id': obj.upload_id, 21 | 'media_type': 1, 22 | 'retry_context': self._json_dumps({ 23 | 'num_reupload': 0, 24 | 'num_step_auto_retry': 0, 25 | 'num_step_manual_retry': 0, 26 | }), 27 | 'xsharing_user_ids': self._json_dumps([]), 28 | 'image_compression': self._json_dumps({ 29 | 'lib_name': 'moz', 30 | 'lib_version': '3.1.m', 31 | 'quality': str(quality) 32 | }), 33 | "is_sidecar": str(int(is_sidecar)) 34 | } 35 | 36 | def _json_loads(self, text: Union[bytes, str]) -> Any: 37 | return orjson.loads(text) 38 | 39 | def _json_dumps(self, obj: Any) -> str: 40 | return orjson.dumps(obj).decode() 41 | 42 | -------------------------------------------------------------------------------- /instauto/api/actions/post.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | 3 | import requests 4 | from requests import Response 5 | from typing import Union, List, Tuple, Dict, Optional 6 | 7 | from .stub import StubMixin 8 | from ..structs import Method, PostLocation 9 | from .structs.post import PostFeed, PostStory, Comment, UpdateCaption, Save, Like, Unlike, Device, RetrieveByUser, \ 10 | Location, RetrieveByTag, RetrieveLikers, RetrieveCommenters, UserTags, PostNull, RetrieveComments, RetrieveById ,\ 11 | Archive, Unarchive, RetrieveStory 12 | 13 | from ..exceptions import BadResponse 14 | 15 | 16 | class PostMixin(StubMixin): 17 | def post_like(self, obj: Like) -> Response: 18 | """Likes a post""" 19 | return self._post_act(obj) 20 | 21 | def post_unlike(self, obj: Unlike) -> Response: 22 | """Unlikes a post""" 23 | return self._post_act(obj) 24 | 25 | def post_save(self, obj: Save) -> Response: 26 | """Saves a post to your Instagram account""" 27 | return self._post_act(obj) 28 | 29 | def post_comment(self, obj: Comment) -> Response: 30 | """Comments on a post""" 31 | return self._post_act(obj) 32 | 33 | def post_update_caption(self, obj: UpdateCaption) -> Response: 34 | """Updates the caption of a post""" 35 | return self._post_act(obj) 36 | 37 | def post_archive(self, obj: Archive) -> Response: 38 | return self._post_act(obj) 39 | 40 | def post_unarchive(self, obj: Unarchive) -> Response: 41 | return self._post_act(obj) 42 | 43 | def post_post(self, obj: Union[PostStory, PostFeed, PostNull], quality: Optional[int] = None) -> Response: 44 | """Uploads a new picture/video to your Instagram account. 45 | Parameters 46 | ---------- 47 | obj : Post 48 | Should be instantiated with all the required params 49 | quality : int 50 | Quality of the image, defaults to 70. 51 | Returns 52 | ------- 53 | Response 54 | The response returned by the Instagram API. 55 | """ 56 | if quality is None: 57 | quality = 70 58 | resp, as_dict = self._upload_image(obj, quality) 59 | headers = { 60 | 'retry_context': self._json_dumps({"num_reupload": 0, "num_step_auto_retry": 0, "num_step_manual_retry": 0}) 61 | } 62 | if obj.source_type == PostLocation.Feed.value: 63 | return self._request('media/configure/', Method.POST, body=as_dict, headers=headers, sign_request=True) 64 | elif obj.source_type == PostLocation.Story.value: 65 | return self._request('media/configure_to_story/', Method.POST, body=as_dict, headers=headers, sign_request=True) 66 | elif obj.source_type == PostLocation.Null.value: 67 | return resp 68 | else: 69 | raise Exception("{} is not a supported post location.", obj.source_type) 70 | 71 | def post_retrieve_by_id(self, obj: RetrieveById) -> Response: 72 | url = f'media/{obj.media_id}/info/' 73 | return self._request(url, Method.GET) 74 | 75 | def post_retrieve_by_user(self, obj: RetrieveByUser) -> Tuple[RetrieveByUser, Union[dict, bool]]: 76 | """Retrieves 12 posts of the user at a time. If there was a response / if there were any more posts 77 | available, the response can be found in original_requests/post.json:4 78 | 79 | Returns 80 | -------- 81 | PostRetrieveByUser, (dict, bool) 82 | Will return the updated object and the response if there were any posts left, returns the object and 83 | False if not. 84 | """ 85 | as_dict = obj.to_dict() 86 | 87 | if obj.page > 0 and obj.max_id is None: 88 | return obj, False 89 | as_dict.pop('user_id') 90 | 91 | resp = self._request(f'feed/user/{obj.user_id}/', Method.GET, query=as_dict) 92 | resp_as_json = self._json_loads(resp.text) 93 | 94 | obj.max_id = resp_as_json.get('next_max_id') 95 | obj.page += 1 96 | return obj, resp_as_json['items'] 97 | 98 | def post_retrieve_story(self, obj: RetrieveStory) -> requests.Response: 99 | qp = { 100 | 'supported_capabilities_new': obj.capabilities_string 101 | } 102 | return self._request(f'feed/user/{obj.user_id}/story/', Method.GET, qp) 103 | 104 | def post_retrieve_by_tag(self, obj: RetrieveByTag) -> Tuple[RetrieveByTag, Union[dict, bool]]: 105 | as_dict = obj.to_dict() 106 | 107 | if obj.page > 0 and obj.max_id is None: 108 | return obj, False 109 | 110 | as_dict.pop('tag_name') 111 | 112 | resp = self._request(f'feed/tag/{obj.tag_name}/', Method.GET, query=as_dict) 113 | resp_as_json = self._json_loads(resp.text) 114 | 115 | obj.max_id = resp_as_json.get('next_max_id') 116 | obj.page += 1 117 | return obj, resp_as_json['items'] 118 | 119 | def post_get_likers(self, obj: RetrieveLikers) -> List[Dict]: 120 | """Retrieve all likers of specific media_id""" 121 | endpoint = 'media/{media_id}/likers'.format(media_id=obj.media_id) 122 | resp = self._request(endpoint=endpoint, method=Method.GET) 123 | users_as_json = self._json_loads(resp.text)['users'] 124 | return users_as_json 125 | 126 | def post_get_commenters(self, obj: RetrieveCommenters) -> List[Dict]: 127 | endpoint = 'media/{media_id}/comments'.format(media_id=obj.media_id) 128 | resp = self._request(endpoint=endpoint, method=Method.GET) 129 | users_as_json = [c['user'] for c in self._json_loads(resp.text)['comments']] 130 | return users_as_json 131 | 132 | def post_get_comments(self, obj: RetrieveComments) -> Response: 133 | endpoint = 'media/{media_id}/comments'.format(media_id=obj.media_id) 134 | resp = self._request(endpoint=endpoint, method=Method.GET) 135 | return resp 136 | 137 | def post_carousel(self, posts: List[PostFeed], caption: str, quality: int) -> Dict[str, Response]: 138 | upload_id = str(time()).replace('.', '') 139 | data = { 140 | "timezone_offset": posts[0].timezone_offset, 141 | "source_type": str(PostLocation.Feed.value), 142 | "_uid": self.state.user_id, 143 | "device_id": self.state.android_id, 144 | "_uuid": self.state.uuid, 145 | "creation_logger_session_id": self._gen_uuid(), 146 | "caption": caption, 147 | "device": posts[0].to_dict()['device'], 148 | "client_sidecar_id": upload_id, 149 | "children_metadata": [], 150 | } 151 | 152 | for post in posts: 153 | data['children_metadata'].append({ 154 | "scene_capture_type": post.scene_capture_type, 155 | "upload_id": post.upload_id, 156 | "caption": "", 157 | "timezone_offset": post.timezone_offset, 158 | "source_type": str(post.source_type), 159 | "scene_type": None, 160 | "edits": self._json_dumps(post.to_dict()['edits']), 161 | "extra": self._json_dumps(post.to_dict()['extra']), 162 | "device": self._json_dumps(post.to_dict()['device']) 163 | }) 164 | if hasattr(post, 'user_tags'): 165 | self._add_user_tags(data, post.user_tags) 166 | 167 | headers = { 168 | 'retry_context': self._json_dumps({"num_reupload": 0, "num_step_auto_retry": 0, "num_step_manual_retry": 0}) 169 | } 170 | 171 | responses: Dict[str, Response] = dict() 172 | for i, post in enumerate(posts): 173 | responses[f'post{i}'] = self._upload_image(post, quality, True)[0] 174 | 175 | breakpoint() 176 | responses['configure_sidecar'] = self._request('media/configure_sidecar/', Method.POST, body=data, headers=headers, sign_request=True) 177 | return responses 178 | 179 | def _add_user_tags(self, data, user_tags: Optional[UserTags]): 180 | if user_tags is None: 181 | return 182 | tags = user_tags.to_dict() 183 | for i, user_tag in enumerate(tags['in']): 184 | tags['in'][i] = user_tag.to_dict() 185 | data['children_metadata'][-1]['usertags'] = self._json_dumps(tags) 186 | 187 | def _request_fb_places_id(self, obj: Location) -> str: 188 | if obj.lat is None or obj.lng is None: 189 | if obj.name is None: 190 | raise ValueError("Atleast a lat/lng combination or name needs to be specified.") 191 | resp = self._request("location_search", Method.GET, query={ 192 | "search_query": obj.name, 193 | "rankToken": self._gen_uuid() 194 | }) 195 | else: 196 | query = { 197 | "latitude": obj.lat, 198 | "longitude": obj.lng, 199 | } 200 | if obj.name: 201 | # pyre-ignore[6] 202 | query['search_query'] = obj.name 203 | resp = self._request("location_search", Method.GET, query=query) 204 | 205 | as_json = self._json_loads(resp.text) 206 | if as_json['status'] != 'ok': 207 | raise BadResponse 208 | 209 | return str(as_json['venues'][0]['external_id']) 210 | 211 | def _upload_image(self, obj: Union[PostStory, PostFeed], quality: int, is_sidecar: bool = False) -> Tuple[Response, dict]: 212 | if obj.device is None: 213 | d = Device(self.device_profile.manufacturer, self.device_profile.model, 214 | int(self.device_profile.android_sdk_version), self.device_profile.android_release) 215 | obj.device = d 216 | 217 | as_dict = obj.fill(self).to_dict() 218 | headers = { 219 | 'x-fb-photo-waterfall-id': str(as_dict.pop('x_fb_waterfall_id')), 220 | 'x-entity-length': str(as_dict.pop('entity_length')), 221 | 'x-entity-name': as_dict.pop('entity_name'), 222 | 'x-instagram-rupload-params': self._json_dumps(self._build_default_rupload_params(obj, quality, is_sidecar)), 223 | 'x-entity-type': as_dict.pop('entity_type'), 224 | 'offset': '0', 225 | 'scene_capture_type': 'standard', 226 | 'creation_logger_session_id': self.state.session_id 227 | } 228 | 229 | path = obj.image_path 230 | as_dict.pop('image_path') 231 | # pyre-ignore[16] we check if the object has a location attribute. 232 | if hasattr(obj, 'location') and obj.location is not None: 233 | if not obj.location.facebook_places_id: 234 | obj.location.facebook_places_id = self._request_fb_places_id(obj.location) 235 | as_dict['location'] = self._json_dumps(obj.location.__dict__) 236 | 237 | # pyre-ignore[16] we check if the object has a usertags attribute. 238 | if hasattr(obj, 'usertags') and obj.user_tags is not None: 239 | data = obj.user_tags.to_dict() 240 | for i, usertag in enumerate(data['in']): 241 | data['in'][i] = usertag.to_dict() 242 | as_dict['usertags'] = self._json_dumps(data) 243 | 244 | with open(path, 'rb') as f: 245 | resp = self._request(f'https://i.instagram.com/rupload_igphoto/{headers["x-entity-name"]}', Method.POST, 246 | headers=headers, body=f.read()) 247 | return resp, as_dict 248 | 249 | def _post_act(self, obj: Union[Save, Comment, UpdateCaption, Like, Unlike, Archive, Unarchive]): 250 | """Peforms the actual action and calls the Instagram API with the data provided.""" 251 | if obj.feed_position is None: 252 | delattr(obj, 'feed_position') 253 | 254 | endpoint = f'media/{obj.media_id}/{obj.action}/' 255 | return self._request(endpoint, Method.POST, body=obj.fill(self).to_dict(), sign_request=True) 256 | -------------------------------------------------------------------------------- /instauto/api/actions/profile.py: -------------------------------------------------------------------------------- 1 | from requests import Response 2 | from typing import Union, Dict 3 | 4 | from .stub import StubMixin 5 | from ..structs import Method 6 | from .structs.profile import SetGender, SetBiography, Update, Info, SetPicture 7 | 8 | 9 | class ProfileMixin(StubMixin): 10 | def _profile_act(self, obj: Union[Update, SetBiography, SetGender]) -> Response: 11 | # retrieve the existing data for all profile data fields 12 | current_data = self._request('accounts/current_user/', Method.GET, query={'edit': 'true'}).json() 13 | # ensure we don't overwrite existing data to nothing 14 | # TODO: fix Pyre ignores 15 | # pyre-ignore[16] 16 | if obj.phone_number is None: obj.phone_number = current_data['user']['phone_number'] 17 | # pyre-ignore[16] 18 | if obj.first_name is None: obj.first_name = current_data['user']['full_name'] 19 | # pyre-ignore[16] 20 | if obj.external_url is None: obj.external_url = current_data['user']['external_url'] 21 | # pyre-ignore[16] 22 | if obj.email is None: obj.email = current_data['user']['email'] 23 | # pyre-ignore[16] 24 | if obj.username is None: obj.username = current_data['user']['trusted_username'] 25 | # pyre-ignore[16] 26 | if not isinstance(obj, SetBiography) or obj.biography is None: 27 | obj.biography = current_data['user']['biography_with_entities']['raw_text'] 28 | 29 | endpoint = 'accounts/edit_profile/' 30 | obj.fill(self) 31 | return self._request(endpoint, Method.POST, body=obj.to_dict(), sign_request=True) 32 | 33 | def profile_set_biography(self, obj: SetBiography) -> Response: 34 | """Sets the biography of the currently logged in user""" 35 | obj.fill(self) 36 | return self._request('accounts/set_biography/', Method.POST, body=obj.to_dict()) 37 | 38 | def profile_set_gender(self, obj: SetGender) -> Response: 39 | """Sets the gender of the currently logged in user""" 40 | obj.fill(self) 41 | return self._request('accounts/set_gender/', Method.POST, body=obj.to_dict(), sign_request=True) 42 | 43 | def profile_update(self, obj: Update): 44 | """Updates the name, username, email, phone number and url for the currently logged in user.""" 45 | self._profile_act(obj) 46 | 47 | def profile_info(self, obj: Info) -> Union[Dict, int]: 48 | data = self._request(obj.endpoint, Method.GET).json() 49 | if data['status'] == 'ok': 50 | return data['user'] 51 | return data['status'] 52 | 53 | def profile_set_picture(self, obj: SetPicture) -> Response: 54 | def internal() -> None: 55 | """These requests are unrelated, but are always sent in the ig app.""" 56 | self._request("accounts/current_user/?edit=true", Method.GET) 57 | self.profile_update(Update(None)) 58 | 59 | internal() 60 | data = obj.to_dict() 61 | return self._request("accounts/change_profile_picture/", Method.POST, body=data) 62 | -------------------------------------------------------------------------------- /instauto/api/actions/search.py: -------------------------------------------------------------------------------- 1 | from requests import Response 2 | from .structs.search import Username, Tag 3 | from .stub import StubMixin 4 | from ..structs import Method 5 | 6 | 7 | class SearchMixin(StubMixin): 8 | def search_username(self, obj: Username) -> Response: 9 | return self._request('users/search/', Method.GET, query=obj.to_dict()) 10 | 11 | def search_tag(self, obj: Tag) -> Response: 12 | return self._request('tags/search/', Method.GET, query=obj.to_dict()) 13 | -------------------------------------------------------------------------------- /instauto/api/actions/structs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanvanrooy/instauto/43866223945c9e8a0e8029ba01bb970ee95ca555/instauto/api/actions/structs/__init__.py -------------------------------------------------------------------------------- /instauto/api/actions/structs/activity.py: -------------------------------------------------------------------------------- 1 | from . import common as cmmn 2 | 3 | 4 | class ActivityGet(cmmn.Base): 5 | def __init__(self, mark_as_seen: bool = False, *args, **kwargs): 6 | self.mark_as_seen = mark_as_seen 7 | super().__init__(*args, **kwargs) 8 | 9 | -------------------------------------------------------------------------------- /instauto/api/actions/structs/common.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Callable, Dict, Union 3 | import pprint 4 | import inspect 5 | from dataclasses import asdict 6 | 7 | 8 | class Base: 9 | def __init__(self, *args, **kwargs): 10 | for k, v in kwargs.items(): 11 | setattr(self, k, v) 12 | #: list of attributes that will be skipped over in the `to_dict` method 13 | self._exempt = ["REQUEST", "_datapoint_from_client", "_exempt"] 14 | #: list of datapoints that need to be retrieved from the client 15 | self._datapoint_from_client: Dict[str, Callable[["instauto.api.client.ApiClient"], str]] = { 16 | "device_id": lambda c: c.state.android_id, 17 | "_uuid": lambda c: c.state.uuid, 18 | "_uid": lambda c: c.state.user_id, 19 | "phone_id": lambda c: c.state.phone_id, 20 | "battery_level": lambda c: c.state.battery_level, 21 | "timezone_offset": lambda _: str(time.localtime().tm_gmtoff), 22 | "is_charging": lambda c: c.state.is_charging, 23 | "is_dark_mode": lambda c: c.state.is_dark_mode, 24 | "session_id": lambda c: c.state.session_id, 25 | "bloks_versioning_id": lambda c: c.state.bloks_version_id 26 | } 27 | 28 | def fill(self, client) -> "Base": 29 | """Fills all of the datapoints that need to be retrieved from the client.""" 30 | attrs = dir(self) 31 | for k, func in self._datapoint_from_client.items(): 32 | if k in attrs: 33 | setattr(self, k, func(client)) 34 | return self 35 | 36 | def to_dict(self) -> Dict[str, Union[dict, str, int]]: 37 | """Converts the object to a dictionary""" 38 | d = {} 39 | 40 | for k, v in self.__dict__.items(): 41 | if k in self._exempt or v is None: 42 | continue 43 | if '__dataclass_fields__' in dir(v): 44 | d[k] = asdict(v) 45 | elif inspect.isclass(v) and issubclass(v, Base): 46 | d[k] = v.to_dict() 47 | elif hasattr(v, 'value'): # we assume this is an Enum value. 48 | d[k] = v.value 49 | else: 50 | d[k] = v 51 | return d 52 | 53 | def __repr__(self): 54 | return pprint.pformat(self.__dict__) 55 | -------------------------------------------------------------------------------- /instauto/api/actions/structs/direct.py: -------------------------------------------------------------------------------- 1 | import orjson 2 | 3 | from . import common as cmmn 4 | 5 | import logging 6 | 7 | from typing import Optional, List, Union 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class _Base(cmmn.Base): 13 | broadcast_type: str 14 | 15 | def __init__(self, 16 | recipients: Optional[List[List[str]]], 17 | thread_ids: Optional[List[str]], 18 | broadcast_type: str, *args, **kwargs): 19 | if recipients is not None: 20 | self.recipient_users = orjson.dumps(recipients).decode() 21 | self.thread_ids = [] 22 | elif thread_ids is not None: 23 | self.thread_ids = orjson.dumps(thread_ids).decode() 24 | self.recipient_users = [] 25 | else: 26 | raise ValueError("Neither `recipients` or `threads` are provided.") 27 | self.broadcast_type = broadcast_type 28 | super().__init__(*args, **kwargs) 29 | self._exempt.extend(['endpoint', 'broadcast_type']) 30 | 31 | @property 32 | def endpoint(self): 33 | return f'direct_v2/threads/broadcast/{self.broadcast_type}/' 34 | 35 | 36 | class Message(_Base): 37 | REQUEST = 'direct/message.json' 38 | 39 | def __init__(self, message: str, recipients: Optional[List[List[str]]] = None, 40 | threads: Optional[List[str]] = None, *args, **kwargs): 41 | self.text = message 42 | super().__init__(recipients, threads, 'text', *args, **kwargs) 43 | 44 | 45 | class MediaShare(_Base): 46 | REQUEST = 'direct/mediashare.json' 47 | 48 | def __init__(self, media_id: str, recipients: Optional[List[List[str]]] = None, 49 | threads: Optional[List[str]] = None, *args, **kwargs): 50 | self.media_id = media_id 51 | super().__init__(recipients, threads, 'media_share', *args, **kwargs) 52 | 53 | 54 | class LinkShare(_Base): 55 | REQUEST = 'direct/linkshare.json' 56 | 57 | def __init__( 58 | self, 59 | text: str, 60 | links: Union[List[str], str], 61 | recipients: Optional[List[List[str]]] = None, 62 | threads: Optional[List[str]] = None, *args, **kwargs 63 | ): 64 | if type(links) == str: 65 | links = [links] 66 | self.link_text = text 67 | self.link_urls = orjson.dumps(links).decode() 68 | super().__init__(recipients, threads, 'link', *args, **kwargs) 69 | 70 | 71 | class ProfileShare(_Base): 72 | REQUEST = 'direct/profileshare.json' 73 | 74 | def __init__(self, profile_id: str, recipients: Optional[List[List[str]]] = None, 75 | threads: Optional[List[str]] = None, *args, **kwargs): 76 | self.profile_user_id = profile_id 77 | super().__init__(recipients, threads, 'profile', *args, **kwargs) 78 | 79 | 80 | class DirectPhoto(_Base): 81 | REQUEST = 'direct/photoshare.json' 82 | 83 | def __init__(self, upload_id: str, recipients: Optional[List[List[str]]] = None, 84 | threads: Optional[List[str]] = None, *args, **kwargs): 85 | self.upload_id = upload_id 86 | self.allow_full_aspect_ratio = True 87 | super().__init__(recipients, threads, 'configure_photo', *args, **kwargs) 88 | 89 | 90 | class DirectVideo(_Base): 91 | REQUEST = 'direct/videoshare.json' 92 | sampled: bool 93 | video_result: str 94 | 95 | def __init__(self, upload_id: str, recipients: Optional[List[List[str]]] = None, 96 | threads: Optional[List[str]] = None, *args, **kwargs): 97 | self.upload_id = upload_id 98 | self.sampled = True 99 | self.video_result = '' 100 | super().__init__(recipients, threads, 'configure_video', *args, **kwargs) 101 | 102 | 103 | class DirectThread(cmmn.Base): 104 | thread_id: str 105 | 106 | def __init__(self, thread_id: str, *args, **kwargs): 107 | self.thread_id = thread_id 108 | super().__init__(*args, **kwargs) 109 | -------------------------------------------------------------------------------- /instauto/api/actions/structs/feed.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from . import common as cmmn 4 | 5 | 6 | class FeedGet(cmmn.Base): 7 | phone_id: str = '' 8 | battery_level: str = '' 9 | timezone_offset: str = '' 10 | device_id: str = '' 11 | _uuid: str = '' 12 | is_charging: str = '' 13 | is_dark_mode: str = '' 14 | session_id: str = '' 15 | bloks_versioning_id: str = '' 16 | max_id: Optional[str] = None 17 | 18 | def __init__(self, reason: str = 'cold_start_fetch', *args, **kwargs): 19 | self.reason = reason 20 | self.is_pull_to_refresh = reason == 'pull_to_refresh' 21 | self.will_sound_on = 0 22 | super().__init__(*args, **kwargs) 23 | -------------------------------------------------------------------------------- /instauto/api/actions/structs/friendships.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from . import common as cmmn 4 | 5 | import logging 6 | import uuid 7 | from instauto.api.structs import Surface 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class _Base(cmmn.Base): 13 | _csrftoken: str = '' 14 | _uid: str = '' 15 | _uuid: str = '' 16 | 17 | def __init__(self, user_id: str, surface: Optional[Surface] = None, *args, **kwargs) -> None: 18 | # user_id is returned as int by instagram. That makes it error prone, 19 | # since sending the user_id as int will not work. 20 | self.user_id = str(user_id) 21 | self.surface = surface 22 | 23 | super().__init__(*args, **kwargs) 24 | self._exempt.append('endpoint') 25 | 26 | 27 | class Create(_Base): 28 | REQUEST = 'friendships/create.json' 29 | endpoint: str = 'create' 30 | device_id: str = '' 31 | 32 | def __init__(self, user_id: str, radio_type: str = '-none', *args, **kwargs): 33 | """Use this to create a friendship, i.e. follow a user.""" 34 | super().__init__(user_id, None, radio_type=radio_type, *args, **kwargs) 35 | 36 | 37 | class Destroy(_Base): 38 | REQUEST = 'friendships/destroy.json' 39 | endpoint: str = 'destroy' 40 | 41 | def __init__(self, user_id: str, surface: Surface = Surface.profile, radio_type='wifi-none', *args, **kwargs): 42 | """Use this to 'destroy' a friendship, i.e. unfollow.""" 43 | super().__init__(user_id, surface, radio_type=radio_type, *args, **kwargs) 44 | 45 | 46 | class Remove(_Base): 47 | REQUEST = 'friendships/remove.json' 48 | endpoint: str = 'remove_followers' 49 | 50 | def __init__(self, user_id: str, radio_type='wifi-none', *args, **kwargs): 51 | super().__init__(user_id, radio_type=radio_type, *args, **kwargs) 52 | 53 | 54 | class Show(cmmn.Base): 55 | REQUEST: str = 'friendships/show.json' 56 | endpoint: str = 'show' 57 | 58 | def __init__(self, user_id: str, *args, **kwargs): 59 | self.user_id = user_id 60 | super().__init__(*args, **kwargs) 61 | 62 | 63 | class PendingRequests(cmmn.Base): 64 | REQUEST: str = 'friendships/pending_requests.json' 65 | 66 | 67 | class ApproveRequest(_Base): 68 | REQUEST: str = 'friendships/approve_request.json' 69 | radio_type: str = 'wifi-none' 70 | 71 | def __init__(self, user_id: str, surface: Surface = Surface.follow_requests, *args, **kwargs): 72 | self.user_id = user_id 73 | self.surface = str(surface.value) 74 | super().__init__(*args, **kwargs) 75 | 76 | 77 | # pyre-ignore[13]: page is declared when utilized to keep track of how many pages we've retrieved. 78 | class _GetBase(cmmn.Base): 79 | user_id: int 80 | page: int = 0 81 | max_id: str 82 | rank_token: str 83 | search_surface: str 84 | order: str 85 | 86 | def __init__( 87 | self, user_id: int , order='default', 88 | surface: Surface = Surface.follow_list, enable_groups=False, 89 | query="", *args, **kwargs 90 | ): 91 | self.rank_token = str(uuid.uuid4()) 92 | self.order = order 93 | self.query = query 94 | self.enable_groups = enable_groups 95 | self.user_id = user_id 96 | self.search_surface = str(surface.value) 97 | super().__init__(*args, **kwargs) 98 | 99 | 100 | class GetFollowers(_GetBase): 101 | REQUEST = 'friendships/get_followers.json' 102 | 103 | 104 | class GetFollowing(_GetBase): 105 | REQUEST = 'friendships/get_following.json' 106 | -------------------------------------------------------------------------------- /instauto/api/actions/structs/profile.py: -------------------------------------------------------------------------------- 1 | from . import common as cmmn 2 | import logging 3 | 4 | from instauto.api.structs import WhichGender 5 | 6 | from typing import Optional 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class SetGender(cmmn.Base): 12 | _csrftoken: str = '' 13 | _uuid: str = '' 14 | biography: Optional[str] = None 15 | 16 | def __init__(self, gender: Optional[WhichGender] = None, custom_gender: Optional[str] = None, *args, **kwargs): 17 | if gender is None and custom_gender is None: 18 | raise ValueError("Either gender or custom_gender needs to be provided") 19 | 20 | self.gender = gender 21 | self.custom_gender = custom_gender or "" 22 | super().__init__(*args, **kwargs) 23 | 24 | 25 | class SetBiography(cmmn.Base): 26 | _csrftoken: Optional[str] = None 27 | _uid: Optional[str] = None 28 | _uuid: Optional[str] = None 29 | 30 | def __init__(self, raw_text: str, *args, **kwargs): 31 | self.raw_text = raw_text 32 | super().__init__(*args, **kwargs) 33 | 34 | 35 | class Update(cmmn.Base): 36 | _csrftoken: Optional[str] = None 37 | _uid: Optional[str] = None 38 | _uuid: Optional[str] = None 39 | biography: Optional[str] = None 40 | 41 | def __init__(self, external_url: Optional[str], phone_number: Optional[str] = None, username: Optional[str] = None, 42 | first_name: Optional[str] = None, email: Optional[str] = None, *args, **kwargs): 43 | self.external_url = external_url 44 | self.phone_number = phone_number 45 | self.username = username 46 | self.first_name = first_name 47 | self.email = email 48 | super().__init__(*args, **kwargs) 49 | 50 | 51 | class Info(cmmn.Base): 52 | def __init__(self, user_id: Optional[int] = None, username: Optional[str] = None, *args, **kwargs): 53 | if not (user_id or username): 54 | raise ValueError("Argument required for either user_id or username.") 55 | self.user_id = user_id 56 | self.username = username 57 | super().__init__(*args, **kwargs) 58 | 59 | @property 60 | def endpoint(self): 61 | if self.user_id: 62 | return f'users/{self.user_id}/info/' 63 | elif self.username: 64 | return f'users/{self.username}/usernameinfo/' 65 | 66 | 67 | class SetPicture(cmmn.Base): 68 | _csrftoken: Optional[str] = None 69 | _uuid: Optional[str] = None 70 | 71 | def __init__(self, upload_id: int, *args, **kwargs): 72 | self.upload_id = upload_id 73 | self.use_fbuploader = True 74 | super().__init__(*args, **kwargs) 75 | -------------------------------------------------------------------------------- /instauto/api/actions/structs/search.py: -------------------------------------------------------------------------------- 1 | from . import common as cmmn 2 | import time 3 | 4 | 5 | class Username(cmmn.Base): 6 | timezone_offset: str = '' 7 | q: str 8 | count: int 9 | 10 | def __init__(self, q: str, count: int, *args, **kwargs): 11 | self.q = q 12 | self.count = count 13 | super().__init__(*args, **kwargs) 14 | 15 | 16 | class Tag(cmmn.Base): 17 | q: str 18 | count: int 19 | 20 | def __init__(self, q: str, count: int, *args, **kwargs): 21 | self.q = q 22 | self.count = count 23 | super().__init__(*args, **kwargs) 24 | -------------------------------------------------------------------------------- /instauto/api/actions/stub.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Any, Callable, Optional, Union, Dict 3 | 4 | import requests 5 | 6 | from instauto.api.structs import IGProfile, DeviceProfile, State, Method 7 | 8 | 9 | class _request: 10 | def __call__(self, 11 | endpoint: str, 12 | method: Method, 13 | query: dict = None, 14 | body: Union[dict, list, bytes] = None, 15 | headers: Dict[str, str] = None, 16 | add_default_headers: bool = None, 17 | sign_request: bool = None 18 | ) -> requests.Response: ... 19 | 20 | 21 | class _get_image_type: 22 | def __call__(self, p: Union[str, Path]) -> str: ... 23 | 24 | 25 | class _build_default_rupload_params: 26 | def __call__(self, obj, quality: int, is_sidecar: bool) -> dict: ... 27 | 28 | 29 | class _json_loads: 30 | def __call__(self, text: Union[bytes, bytearray, memoryview, str]) -> Any: ... 31 | 32 | 33 | class _json_dumps: 34 | def __call__(self, obj: Any) -> str: ... 35 | 36 | 37 | class StubMixin: 38 | ig_profile: IGProfile 39 | device_profile: DeviceProfile 40 | state: State 41 | _user_agent: str 42 | _encode_password: Callable 43 | _session: requests.Session 44 | _request_finished_callbacks: list 45 | _handle_challenge: Callable 46 | _2fa_function: Optional[Callable[[str], str]] 47 | _handle_2fa: Callable[[dict], None] 48 | _request: _request 49 | _username: Optional[str] 50 | _plain_password: Optional[str] 51 | _encoded_password: Optional[str] 52 | _gen_uuid: Callable[[], str] 53 | _get_image_type: _get_image_type 54 | _build_default_rupload_params: _build_default_rupload_params 55 | _json_loads: _json_loads 56 | _json_dumps: _json_dumps 57 | -------------------------------------------------------------------------------- /instauto/api/client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import uuid 3 | import random 4 | import hmac 5 | 6 | import orjson 7 | import requests 8 | import base64 9 | import time 10 | 11 | from typing import Callable, Optional, Union 12 | 13 | # pyre-ignore[21] 14 | from apscheduler.schedulers.background import BackgroundScheduler 15 | 16 | from .actions.feed import FeedMixin 17 | from .actions.helpers import HelperMixin 18 | from .structs import IGProfile, DeviceProfile, State 19 | from .constants import (DEFAULT_IG_PROFILE, DEFAULT_DEVICE_PROFILE, DEFAULT_STATE) 20 | from .exceptions import StateExpired, NoAuthDetailsProvided, CorruptedSaveData 21 | 22 | from .actions.profile import ProfileMixin 23 | from .actions.authentication import AuthenticationMixin 24 | from .actions.post import PostMixin 25 | from .actions.request import RequestMixin 26 | from .actions.friendships import FriendshipsMixin 27 | from .actions.search import SearchMixin 28 | from .actions.challenge import ChallengeMixin 29 | from .actions.direct import DirectMixin 30 | from .actions.activity import ActivityMixin 31 | 32 | logger = logging.getLogger(__name__) 33 | logging.captureWarnings(True) 34 | 35 | 36 | class ApiClient(ProfileMixin, AuthenticationMixin, PostMixin, 37 | RequestMixin, FriendshipsMixin, SearchMixin, ChallengeMixin, 38 | DirectMixin, HelperMixin, FeedMixin, ActivityMixin): 39 | breadcrumb_private_key = "iN4$aGr0m".encode() 40 | bc_hmac = hmac.HMAC(breadcrumb_private_key, digestmod='SHA256') 41 | 42 | def __init__( 43 | self, ig_profile: Optional[IGProfile] = None, device_profile: 44 | Optional[DeviceProfile] = None, state: Optional[State] = None, 45 | username: Optional[str] = None, password: Optional[str] = None, 46 | session_cookies: Optional[dict] = None, testing=False, 47 | _2fa_function: Optional[Callable[[str], str]] = None 48 | ) -> None: 49 | """Initializes all attributes. Can be instantiated with no params. 50 | 51 | Needs to be provided with either: 1) state and session_cookies, 52 | to resume an old session, in this case all other params are 53 | optional 2) username and password, in this case all other params 54 | are optional 55 | 56 | In the case that the class is initialized without params, or 57 | with a few of the params not provided, they will automatically 58 | be filled with default values from constants.py. 59 | 60 | Using the default values should be fine for pretty much all use 61 | cases, but if for some reason you need to use non-default 62 | values, that can be done by creating any of the profiles 63 | yourself and passing it in as an argument. 64 | """ 65 | super().__init__() 66 | self._2fa_function = _2fa_function 67 | self._username = username 68 | self._plain_password = password 69 | self._encoded_password = None 70 | 71 | self._init_ig_profile(ig_profile) 72 | self._init_device_profile(device_profile) 73 | self._init_state(state) 74 | self._user_agent = self._build_user_agent() 75 | 76 | if (username is None or password is None) and (state is None or session_cookies is None) and not testing: 77 | raise NoAuthDetailsProvided("Neither a username and username or existing state is provided.") 78 | 79 | self._init_session(session_cookies, testing) 80 | self._request_finished_callbacks = [self._update_state_from_headers] 81 | self._init_scheduler() 82 | 83 | def _init_state(self, state) -> None: 84 | if state is not None and not state.valid: 85 | logger.warning("The state argument was provided, but the object provided is no longer valid.") 86 | raise StateExpired() 87 | elif state is None: 88 | self.state = State(**DEFAULT_STATE) 89 | self.state.fill(self._gen_uuid) 90 | logger.info("No state provided. Using default state.") 91 | elif state is not None: 92 | self.state = state 93 | self.state.refresh(self._gen_uuid) 94 | 95 | def _init_device_profile(self, device_profile) -> None: 96 | if not device_profile: 97 | device_profile = DeviceProfile(**DEFAULT_DEVICE_PROFILE) 98 | self.device_profile = device_profile 99 | 100 | def _init_ig_profile(self, ig_profile) -> None: 101 | if not ig_profile: 102 | ig_profile = IGProfile(**DEFAULT_IG_PROFILE) 103 | self.ig_profile = ig_profile 104 | 105 | def _init_session(self, session_cookies, testing: bool) -> None: 106 | self._session = requests.Session() 107 | if session_cookies is not None: 108 | for k, v in session_cookies.items(): 109 | self._session.cookies.set_cookie( 110 | requests.cookies.create_cookie( 111 | name=k, value=v 112 | ) 113 | ) 114 | if testing: 115 | self._session.cookies['csrftoken'] = "test" 116 | 117 | def _init_scheduler(self): 118 | self.scheduler = BackgroundScheduler() 119 | self.scheduler.add_job(self._refresh_session, trigger='interval', seconds=60 * 5, jitter=60 * 2) 120 | self.scheduler.start() 121 | 122 | def _grab_cookies(self) -> dict: 123 | return self._session.cookies.get_dict() 124 | 125 | def to_json(self) -> str: 126 | cookies = self._grab_cookies() 127 | state_as_dict = self.state.__dict__ 128 | try: 129 | logged_in_data = state_as_dict.pop('logged_in_account_data').__dict__ 130 | except KeyError: 131 | logged_in_data = {} 132 | 133 | return self._json_dumps({ 134 | 'State': state_as_dict, 135 | 'IGProfile': self.ig_profile.__dict__, 136 | 'DeviceProfile': self.device_profile.__dict__, 137 | 'LoggedInAccountData': logged_in_data, 138 | 'session.cookies': cookies 139 | }) 140 | 141 | @classmethod 142 | def from_json(cls, j: Union[str, bytes]) -> "ApiClient": 143 | data = orjson.loads(j) 144 | 145 | state = data['State'] 146 | state['logged_in_account_data'] = data['LoggedInAccountData'] 147 | 148 | ig_profile = data['IGProfile'] 149 | device_profile = data['DeviceProfile'] 150 | 151 | session_cookies = data['session.cookies'] 152 | 153 | instance = cls(IGProfile(**ig_profile), DeviceProfile(**device_profile), State(**state), session_cookies=session_cookies) 154 | instance._update_token() 155 | instance._sync() 156 | 157 | return instance 158 | 159 | def save_to_disk(self, file_name: str, overwrite: bool = False) -> bool: 160 | file_mode = "w" if not overwrite else "w+" 161 | 162 | try: 163 | f = open(file_name, file_mode, encoding="utf-8") 164 | as_json = self.to_json() 165 | f.write(as_json) 166 | except Exception as e: 167 | return False 168 | finally: 169 | f.close() 170 | return True 171 | 172 | @classmethod 173 | def initiate_from_file(cls, file_name: str) -> "ApiClient": 174 | with open(file_name, "r", encoding="utf-8") as f: 175 | try: 176 | return cls.from_json(f.read()) 177 | except orjson.JSONDecodeError: 178 | raise CorruptedSaveData(f"Save file {file_name} couldn't be parsed.") 179 | 180 | @staticmethod 181 | # pyre-ignore[40]: invalid override 182 | def _gen_uuid() -> str: 183 | return str(uuid.uuid4()) 184 | 185 | def _generate_user_breadcrumb(self, comment_length: int) -> str: 186 | """Generates the user_breadcrumb, which is necessary for posting comments. The breadcrumb stores information 187 | in the following format: `{length of comment} {time to type} {backspaces count} {current time in ms}`. 188 | """ 189 | msg = f"{comment_length} {random.uniform(.3, .5) * 1000 * comment_length} " \ 190 | f"{int(comment_length / random.randint(3, 6))} {int(round(time.time() * 1000))}" 191 | self.bc_hmac.update(msg.encode()) 192 | return base64.b64encode(self.bc_hmac.digest()).decode() 193 | -------------------------------------------------------------------------------- /instauto/api/constants.py: -------------------------------------------------------------------------------- 1 | # ************************************ 2 | # default instagram profile values 3 | # ************************************ 4 | #: Can change overtime, but it's pretty easy to extract, see: 5 | #: https://mokhdzanifaeq.github.io/2015/09/28/extracting-instagram-signature-key-2/ 6 | DEFAULT_SIGNATURE_KEY = "19ce5f445dbfd9d29c59dc2a78c616a7fc090a8e018b9267bc4240a30244c53b" 7 | DEFAULT_SIGNATURE_KEY_V = "4" 8 | DEFAULT_HTTP_ENGINE = "Liger" 9 | DEFAULT_IG_CAPABILITIES = "3brTvw8=" 10 | DEFAULT_APP_ID = "567067343352427" 11 | DEFAULT_IG_VERSION = "329.0.0.41.93" 12 | DEFAULT_BUILD_NUMBER = "227298996" 13 | 14 | # ************************************ 15 | # default phone profile values (s10 edge) 16 | # ************************************ 17 | DEFAULT_MANUFACTURER = "samsung" 18 | DEFAULT_ANDROID_SDK = "29" 19 | DEFAULT_ANDROID_RELEASE = "10" 20 | DEFAULT_DEVICE = "SM-973F" 21 | DEFAULT_MODEL = "beyond1" 22 | DEFAULT_DPI = 560 23 | DEFAULT_RESOLUTION = (1440, 2891) 24 | DEFAULT_CHIPSET = "exynos9820" 25 | 26 | 27 | DEFAULT_IG_PROFILE = {'signature_key': DEFAULT_SIGNATURE_KEY, 'signature_key_version': DEFAULT_SIGNATURE_KEY_V, 28 | 'http_engine': DEFAULT_HTTP_ENGINE, 'capabilities': DEFAULT_IG_CAPABILITIES, 'id': DEFAULT_APP_ID, 29 | 'version': DEFAULT_IG_VERSION, 'build_number': DEFAULT_BUILD_NUMBER} 30 | 31 | DEFAULT_DEVICE_PROFILE = {'manufacturer': DEFAULT_MANUFACTURER, 'android_sdk_version': DEFAULT_ANDROID_SDK, 32 | 'android_release': DEFAULT_ANDROID_RELEASE, 'device': DEFAULT_DEVICE, 'model': DEFAULT_MODEL, 33 | 'dpi': DEFAULT_DPI, 'resolution': DEFAULT_RESOLUTION, 'chipset': DEFAULT_CHIPSET} 34 | 35 | 36 | # ************************************ 37 | # state default settings 38 | # ************************************ 39 | DEFAULT_APP_STARTUP_COUNTRY = "US" 40 | DEFAULT_DEVICE_LOCALE = "nl_NL" 41 | DEFAULT_APP_LOCALE = "nl_NL" 42 | DEFAULT_BANDWIDTH_TOTALBYTES_B = '0' 43 | DEFAULT_BANDWIDTH_TOTALTIME_MS = '0' 44 | DEFAULT_CONNECTION_TYPE = 'WIFI' 45 | DEFAULT_ACCEPT_LANGUAGE = 'nl_NL, en_US' 46 | DEFAULT_ACCEPT_ENCODING = 'gzip' 47 | DEFAULT_ACCEPT = '*/*' 48 | DEFAULT_ADS_OPT_OUT = int(False) 49 | DEFAULT_AUTHORIZATION = '' 50 | DEFAULT_WWW_CLAIM = '0' 51 | DEFAULT_RUR = 'VLL' 52 | DEFAULT_BLOKS_VERSION_ID = "5da07fc1b20eb4c7d1b2e6146ee5f197072cbbd193d2d1eb3bb4e825d3c39e28" 53 | DEFAULT_BLOKS_IS_LAYOUT_RTL = "False" 54 | 55 | DEFAULT_STATE = { 56 | 'app_startup_country': DEFAULT_APP_STARTUP_COUNTRY, 'device_locale': DEFAULT_DEVICE_LOCALE, 'app_locale': 57 | DEFAULT_APP_LOCALE, 'www_claim': DEFAULT_WWW_CLAIM, 58 | 'authorization': DEFAULT_AUTHORIZATION, 'bandwidth_totalbytes_b': DEFAULT_BANDWIDTH_TOTALBYTES_B, 59 | 'bandwidth_totaltime_ms': DEFAULT_BANDWIDTH_TOTALTIME_MS, 'connection_type': DEFAULT_CONNECTION_TYPE, 60 | 'accept_language': DEFAULT_ACCEPT_LANGUAGE, 'accept_encoding': DEFAULT_ACCEPT_ENCODING, 'accept': DEFAULT_ACCEPT, 61 | 'bloks_version_id': DEFAULT_BLOKS_VERSION_ID, 'bloks_is_layout_rtl': DEFAULT_BLOKS_IS_LAYOUT_RTL 62 | } 63 | 64 | 65 | # ************************************ 66 | # API stuff 67 | # ************************************ 68 | API_BASE_URL = "https://i.instagram.com/api/v1/{}" 69 | -------------------------------------------------------------------------------- /instauto/api/exceptions.py: -------------------------------------------------------------------------------- 1 | class StateExpired(Exception): 2 | """Raised when saved settings are provided, but not valid anymore""" 3 | pass 4 | 5 | 6 | class NoAuthDetailsProvided(Exception): 7 | """Raised when the login details are not provided, but the client needs them""" 8 | pass 9 | 10 | 11 | class IncorrectLoginDetails(Exception): 12 | """Raised when the provided loging details are incorrect.""" 13 | pass 14 | 15 | 16 | class InvalidUserId(Exception): 17 | """Raised when an invalid user id is provided""" 18 | pass 19 | 20 | 21 | class CorruptedSaveData(Exception): 22 | """Raised when the save data can't be read""" 23 | pass 24 | 25 | 26 | class BadResponse(Exception): 27 | """Raised when Instagram returns a non-ok status code.""" 28 | pass 29 | 30 | 31 | class MissingValue(Exception): 32 | """Raised when an action struct is initiated with a missing value""" 33 | pass 34 | 35 | 36 | class AuthorizationError(Exception): 37 | """Raised when you try to get an object you're not authorized to get""" 38 | pass 39 | 40 | 41 | class NotFoundError(Exception): 42 | """Raised when an entity is not found.""" 43 | pass 44 | -------------------------------------------------------------------------------- /instauto/bot/__init__.py: -------------------------------------------------------------------------------- 1 | from .bot import Bot 2 | from .input import Input 3 | -------------------------------------------------------------------------------- /instauto/bot/bot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import logging 4 | from time import sleep 5 | from typing import List, Tuple, Optional 6 | 7 | from instauto.api.client import ApiClient 8 | from instauto.bot.input import Input 9 | from instauto.helpers import models 10 | from instauto.helpers.friendships import follow_user 11 | from instauto.helpers.post import like_post, comment_post 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class Bot: 17 | stop: bool = False 18 | input: Input 19 | _actions: List = [] 20 | 21 | def __init__( 22 | self, username: Optional[str] = None, password: Optional[str] = None, 23 | client: Optional[ApiClient] = None, delay_between_action: float = 2.0, 24 | delay_variance: float = 0.0 25 | ) -> None: 26 | """Initiate a new `Bot` instance. 27 | 28 | Args: 29 | username: the username of the account 30 | password: the password of the account 31 | client: the `ApiClient` instance the Bot communicates with. If given, it will take precedence over credentials. 32 | delay_between_action: the amount of seconds to wait between actions (each like, follow, etc. is an action) 33 | delay_variance: the amount of variance to add to the delay. Delay will be random number between (delay - variance) - (delay + variance). 34 | """ 35 | 36 | if client is not None: 37 | self._client = client 38 | elif username and password: 39 | self._initialize_client_from_credentials(username, password) 40 | else: 41 | raise Exception("Use either a username/password or an ApiClient") 42 | 43 | self.input = Input(self._client) 44 | self._actions = [] 45 | self._delay = delay_between_action if(delay_between_action) else 0 46 | self._delay_variance = abs(delay_variance) 47 | 48 | def _initialize_client_from_credentials(self, username: str, password: str) -> None: 49 | instauto_save_path = f'.{username}.instauto.save' 50 | if os.path.isfile(instauto_save_path): 51 | self._client = ApiClient.initiate_from_file(instauto_save_path) 52 | else: 53 | self._client = ApiClient(username=username, password=password) 54 | self._client.log_in() 55 | self._client.save_to_disk(instauto_save_path) 56 | 57 | def like(self, chance: int, amount: int) -> "Bot": 58 | """Like posts of users retrieved with the Input pipeline. 59 | 60 | Args: 61 | chance: integer between 0 and 100, represents a percentage between 0 and 100%. 62 | Defines the chance of this action being called for an account. Set to 63 | 25 to call on 1/4 of all accounts, 50 for 1/2 of all accounts, etc. 64 | amount: 65 | The amount of posts to like, if this action is being called for an account. 66 | """ 67 | self._actions.append({ 68 | 'func': like_post, 69 | 'chance': chance, 70 | 'amount': amount, 71 | 'args': ('POST_ID', ) 72 | }) 73 | return self 74 | 75 | def comment(self, chance: int, amount: int, comments: List[str]) -> "Bot": 76 | """Comment on posts of users retrieved with the Input pipeline. 77 | 78 | Args: 79 | chance: integer between 0 and 100, represents a percentage between 0 and 100%. 80 | Defines the chance of this action being called for an account. Set to 81 | 25 to call on 1/4 of all accounts, 50 for 1/2 of all accounts, etc. 82 | amount: 83 | The amount of posts to comment on, if this action is being called for an account. 84 | comments: 85 | A random selected entry out of this list will be used as text to comment. 86 | """ 87 | self._actions.append({ 88 | 'func': comment_post, 89 | 'chance': chance, 90 | 'amount': amount, 91 | 'args': ('POST_ID', (random.choice, comments)) 92 | }) 93 | return self 94 | 95 | def follow(self, chance: int) -> "Bot": 96 | """Follow users retrieved with the Input pipeline. 97 | 98 | Args: 99 | chance: integer between 0 and 100, represents a percentage between 0 and 100%. 100 | Defines the chance of this action being called for an account. Set to 101 | 25 to call on 1/4 of all accounts, 50 for 1/2 of all accounts, etc. 102 | """ 103 | self._actions.append({ 104 | 'func': follow_user, 105 | 'chance': chance, 106 | 'args': ('ACCOUNT_ID', ) 107 | }) 108 | return self 109 | 110 | def start(self): 111 | """Start the bot. 112 | 113 | Once the bot is started, it will run until it went through all retrieved accounts, 114 | or if the `stop` attribute is set to `True`.""" 115 | accounts = self.input.filtered_accounts 116 | while not self.stop: 117 | self._sleep_between_actions() 118 | account = accounts.pop(random.randint(0, len(accounts) - 1)) 119 | for action in self._actions: 120 | if random.randint(0, 100) > action['chance']: 121 | continue 122 | 123 | t = action['args'][0] 124 | if t == 'POST_ID': 125 | posts = self._get_posts(account['username']) 126 | for _ in range(action['amount']): 127 | if not posts: 128 | continue 129 | post = posts.pop(random.randint(0, len(posts) - 1)) 130 | args = self._resolve_args(action['args'], post=post) 131 | try: 132 | action['func'](self._client, *args) 133 | except Exception as e: 134 | logger.warning("Caught exception: ", e) 135 | elif t == 'ACCOUNT_ID': 136 | args = self._resolve_args(action['args'], account=account) 137 | try: 138 | action['func'](self._client, *args) 139 | except Exception as e: 140 | logger.warning("Caught exception: ", e) 141 | 142 | def _sleep_between_actions(self): 143 | min = (self._delay - self._delay_variance) if(self._delay - self._delay_variance) else 0 144 | max = self._delay + self._delay_variance 145 | sleeptime = round(random.uniform(min, max), 2) 146 | sleep(sleeptime) 147 | 148 | def _get_posts(self, account_name: str, force: bool = False) -> List[models.Post]: 149 | return self.input.get_posts(account_name, force) 150 | 151 | @staticmethod 152 | def _resolve_args( 153 | args: Tuple, post: Optional[models.Post] = None, 154 | account: Optional[models.User] = None 155 | ) -> List: 156 | a = list() 157 | for arg in args: 158 | if isinstance(arg, tuple) and callable(arg[0]): 159 | a.append(arg[0](*arg[1::])) 160 | else: 161 | a.append(arg) 162 | for i, arg in enumerate(a.copy()): 163 | if arg == 'POST_ID' and post is not None: 164 | a[i] = post.pk 165 | elif arg == 'ACCOUNT_ID' and account is not None: 166 | a[i] = account.pk 167 | return a 168 | 169 | @classmethod 170 | def from_client(cls, client: ApiClient, delay_between_action: float = 2.0, delay_variance: float = 0.0) -> "Bot": 171 | return cls("", "", client=client, delay_between_action=delay_between_action, delay_variance=delay_variance) 172 | -------------------------------------------------------------------------------- /instauto/bot/input.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, List, Dict 2 | from instauto.api.client import ApiClient 3 | from instauto.helpers import models 4 | from instauto.helpers.friendships import get_followers, get_following 5 | from instauto.helpers.search import get_user_id_from_username 6 | from instauto.helpers.post import get_likers_of_post, get_commenters_of_post 7 | from instauto.helpers.post import retrieve_posts_from_user 8 | 9 | 10 | class Input: 11 | _client: ApiClient 12 | _post_cache: Dict[str, List[models.Post]] = {} 13 | _accounts: List[models.User] = [] 14 | 15 | def __init__(self, client: ApiClient): 16 | self._client = client 17 | 18 | def from_following_of(self, account_name: str, limit: int) -> "Input": 19 | """Retrieves accounts that `account_name` follows. 20 | 21 | Args: 22 | account_name: the account to retrieve from 23 | limit: the amount of accounts to retrieve 24 | """ 25 | user_id = get_user_id_from_username(self._client, account_name) 26 | if user_id is None: 27 | return self 28 | following = get_following(self._client, user_id, limit) 29 | self._accounts.extend(following) 30 | return self 31 | 32 | def from_followers_of(self, account_name: str, limit: int) -> "Input": 33 | """Retrieves accounts that follow `account_name`. 34 | 35 | Args: 36 | account_name: the account to retrieve from 37 | limit: the amount of accounts to retrieve 38 | """ 39 | user_id = get_user_id_from_username(self._client, account_name) 40 | if user_id is None: 41 | return self 42 | followers = get_followers(self._client, limit, user_id) 43 | self._accounts.extend(followers) 44 | return self 45 | 46 | def from_likers_of(self, account_name: str, limit: int) -> "Input": 47 | """Retrieves accounts that have liked recent posts of `account_name`. 48 | 49 | Args: 50 | account_name: the account to retrieve from 51 | limit: the amount of accounts to retrieve 52 | """ 53 | likers = [] 54 | posts = self.get_posts(account_name) 55 | for post in posts: 56 | likers.extend(get_likers_of_post(self._client, post.id)) 57 | if len(likers) > limit: 58 | break 59 | self._post_cache[account_name] = posts 60 | self._accounts.extend(likers[:limit:]) 61 | return self 62 | 63 | def from_commenters_of(self, account_name: str, limit: int) -> "Input": 64 | """Retrieves accounts that have commented on recent posts of `account_name`. 65 | 66 | Args: 67 | account_name: the account to retrieve from 68 | limit: the amount of accounts to retrieve 69 | """ 70 | commenters = [] 71 | posts = self.get_posts(account_name) 72 | for post in posts: 73 | commenters.extend(get_commenters_of_post(self._client, post.id)) 74 | if len(commenters) > limit: 75 | break 76 | self._accounts.extend(commenters[:limit:]) 77 | return self 78 | 79 | def from_user_list(self, accounts: List[models.User]) -> "Input": 80 | """Add supplied accounts to input 81 | 82 | Args: 83 | accounts: List of account objects (objects/user.json) 84 | """ 85 | self._accounts.extend(accounts) 86 | return self 87 | 88 | @property 89 | def filtered_accounts(self) -> List[models.User]: 90 | seen = [] 91 | return list(filter( 92 | lambda x: x.pk not in seen and seen.append(x.pk) is None, 93 | self._accounts 94 | )) 95 | 96 | def get_posts(self, account_name: str, force: bool = False) \ 97 | -> List[models.Post]: 98 | if account_name not in self._post_cache or force: 99 | self._post_cache[account_name] = retrieve_posts_from_user( 100 | self._client, 30, account_name) 101 | return self._post_cache.get(account_name) or [] 102 | 103 | -------------------------------------------------------------------------------- /instauto/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /instauto/helpers/common.py: -------------------------------------------------------------------------------- 1 | import orjson 2 | from requests import Response 3 | 4 | 5 | def is_resp_ok(resp: Response) -> bool: 6 | if not resp.ok: 7 | return False 8 | if not resp.content: 9 | return False 10 | try: 11 | d = orjson.loads(resp.text) 12 | except orjson.JSONDecodeError: 13 | return False 14 | return d['status'] == 'ok' 15 | 16 | -------------------------------------------------------------------------------- /instauto/helpers/feed.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import logging 3 | 4 | import orjson 5 | 6 | from instauto.api.actions.structs.feed import FeedGet 7 | from instauto.api.client import ApiClient 8 | 9 | from instauto.helpers import models 10 | 11 | logging.basicConfig() 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def get_feed(client: ApiClient, limit: int) -> List[models.Post]: 16 | ret = [] 17 | obj = FeedGet() 18 | 19 | while len(ret) < limit: 20 | obj, resp = client.feed_get(obj) 21 | data = orjson.loads(resp.text) 22 | items = list(filter(lambda i: 'media_or_ad' in i, data['feed_items'])) 23 | logger.info("Retrieved {} posts, {} more to go.".format( 24 | len(ret), limit - len(ret)) 25 | ) 26 | if len(items) == 0: 27 | break 28 | ret.extend(items) 29 | return [models.Post.parse(p['media_or_ad']) for p in ret[:limit]] 30 | -------------------------------------------------------------------------------- /instauto/helpers/friendships.py: -------------------------------------------------------------------------------- 1 | from instauto.api.client import ApiClient 2 | from instauto.api.actions.structs.friendships import GetFollowers, Create, \ 3 | GetFollowing, Destroy 4 | from instauto.helpers.search import get_user_id_from_username 5 | from instauto.helpers.common import is_resp_ok 6 | from instauto.helpers import models 7 | 8 | from typing import List, Optional 9 | import logging 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def get_followers( 15 | client: ApiClient, limit: int, user_id: Optional[int]= None, 16 | username: Optional[str] = None 17 | ) -> List[models.User]: 18 | """Retrieve the first x amount of followers from an account. 19 | 20 | Either `user_id` or `username` need to be provided. If both are provided, 21 | the user_id takes precedence. 22 | 23 | Args: 24 | client: your ApiClient 25 | user_id: the user_id of the account to retrieve followers from 26 | limit: the maximum amount of followers to retrieve 27 | username: the username of the account to retrieve followers from 28 | 29 | Returns: 30 | A list containing Instagram user objects (examples/objects/user.json). 31 | """ 32 | if user_id is None and username is not None: 33 | user_id = get_user_id_from_username(client, username) 34 | 35 | if user_id is None: 36 | raise ValueError("Both `user_id` and `username` are not provided.") 37 | 38 | obj = GetFollowers(user_id) 39 | 40 | obj, result = client.followers_get(obj) 41 | followers = [] 42 | while result and len(followers) < limit: 43 | # pyre-ignore[16]: the type of result is `Response` or bool, but because 44 | # of the `result is True` check, it can only be of type bool here. 45 | followers.extend(result.json()["users"]) 46 | logger.info("Retrieved {} followers, {} more to go.".format( 47 | len(followers), limit - len(followers)) 48 | ) 49 | obj, result = client.followers_get(obj) 50 | return [models.User.parse(f) for f in 51 | followers[:min(len(followers), limit)]] 52 | 53 | 54 | def get_following( 55 | client: ApiClient, limit: int, user_id: Optional[int] = None, 56 | username: Optional[str] = None 57 | ) -> List[models.User]: 58 | """Retrieve the first x amount of users that an account is following. 59 | 60 | Either `user_id` or `username` need to be provided. If both are provided, 61 | the user_id takes precedence. 62 | 63 | Args: 64 | client: your ApiClient 65 | user_id: the user_id of the account to retrieve following from 66 | limit: the maximum amount of users to retrieve 67 | username: the username of the account to retrieve following from 68 | 69 | Returns: 70 | A list containing Instagram user objects (examples/objects/user.json). 71 | """ 72 | if user_id is None and username is not None: 73 | user_id = get_user_id_from_username(client, username) 74 | 75 | if user_id is None: 76 | raise ValueError("Both `user_id` and `username` are not provided.") 77 | 78 | obj = GetFollowing(user_id) 79 | 80 | obj, result = client.following_get(obj) 81 | following = [] 82 | while result is True and len(following) < limit: 83 | # pyre-ignore[16]: the type of result is `Response` or bool, but because 84 | # of the `result is True` check, it can only be of type bool here. 85 | following.extend(result.json()["users"]) 86 | logger.info("Retrieved {} of following, {} more to go.".format( 87 | len(following), limit - len(following)) 88 | ) 89 | obj, result = client.following_get(obj) 90 | return [models.User.parse(f) for f in 91 | following[:min(len(following), limit)]] 92 | 93 | 94 | def follow_user( 95 | client: ApiClient, user_id: Optional[int] = None, 96 | username: Optional[str] = None 97 | ) -> bool: 98 | """Send a follow request to a user. 99 | 100 | Either `user_id` or `username` need to be provided. If both are provided, 101 | the user_id takes precedence. 102 | 103 | Args: 104 | client: your ApiClient 105 | user_id: the user_id of the account to follow 106 | username: the username of the account to follow 107 | Returns: 108 | True if success else False 109 | """ 110 | if user_id is None and username is not None: 111 | user_id = get_user_id_from_username(client, username) 112 | 113 | if user_id is None: 114 | raise ValueError("Both `user_id` and `username` are not provided.") 115 | 116 | obj = Create(str(user_id)) 117 | resp = client.user_follow(obj) 118 | return is_resp_ok(resp) 119 | 120 | 121 | def unfollow_user( 122 | client: ApiClient, user_id: Optional[int] = None, 123 | username: Optional[str] = None 124 | ) -> bool: 125 | """Unfollow a user. 126 | 127 | Either `user_id` or `username` need to be provided. If both are provided, 128 | the user_id takes precedence. 129 | 130 | Args: 131 | client: your ApiClient 132 | user_id: the user_id of the account to unfollow 133 | username: the username of the account to unfollow 134 | Returns: 135 | True if success else False 136 | """ 137 | if user_id is None and username is not None: 138 | user_id = get_user_id_from_username(client, username) 139 | 140 | if user_id is None: 141 | raise ValueError("Both `user_id` and `username` are not provided.") 142 | 143 | obj = Destroy(str(user_id)) 144 | resp = client.user_unfollow(obj) 145 | return is_resp_ok(resp) 146 | -------------------------------------------------------------------------------- /instauto/helpers/post.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from instauto.api.client import ApiClient 4 | from instauto.api.actions import post as ps 5 | from instauto.api.actions.structs.post import RetrieveCommenters, RetrieveLikers 6 | from instauto.api.exceptions import NotFoundError 7 | from instauto.helpers.common import is_resp_ok 8 | from instauto.helpers.search import get_user_id_from_username 9 | from instauto.helpers import models 10 | 11 | import logging 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def upload_image_to_feed( 16 | client: ApiClient, image_path: str, 17 | caption: Optional[str] = None, location: Optional[ps.Location] = None 18 | ) -> bool: 19 | """Upload an image to your feed. Location and caption are optional. 20 | 21 | Args: 22 | client: your `ApiClient` 23 | image_path: path to the image to upload 24 | caption: the caption of the post 25 | location: the location tag of the post 26 | 27 | Returns: 28 | `True` if success else `False` 29 | """ 30 | post = ps.PostFeed( 31 | path=image_path, 32 | caption=caption or '', 33 | location=location, 34 | ) 35 | resp = client.post_post(post, 80) 36 | logger.info(f"Uploaded image to feed") 37 | return is_resp_ok(resp) 38 | 39 | 40 | def upload_image_to_story(client: ApiClient, image_path: str) -> bool: 41 | """Upload an image to your story. 42 | 43 | Args: 44 | client: your `ApiClient` 45 | image_path: path to the image to upload 46 | 47 | Returns: 48 | `True` if success else `False` 49 | """ 50 | post = ps.PostStory( 51 | path=image_path 52 | ) 53 | resp = client.post_post(post) 54 | logger.info(f"Uploaded image to story") 55 | return is_resp_ok(resp) 56 | 57 | 58 | def update_caption(client: ApiClient, media_id: str, new_caption: str) -> bool: 59 | """Update the caption of a post. 60 | 61 | Args: 62 | client: your `ApiClient` 63 | media_id: the media_id of a post 64 | new_caption: the new caption 65 | 66 | Returns: 67 | `True` if success else `False` 68 | """ 69 | caption = ps.UpdateCaption( 70 | media_id=media_id, 71 | caption_text=new_caption 72 | ) 73 | resp = client.post_update_caption(caption) 74 | logger.info(f"Updated caption of post {media_id} to {new_caption}") 75 | return is_resp_ok(resp) 76 | 77 | 78 | def like_post(client: ApiClient, media_id: str) -> bool: 79 | """Like a post. 80 | 81 | Args: 82 | client: your `ApiClient` 83 | media_id: the post to like 84 | 85 | Returns: 86 | `True` if success else `False` 87 | """ 88 | like = ps.Like( 89 | media_id=media_id 90 | ) 91 | resp = client.post_like(like) 92 | logger.info(f"liked post {media_id}") 93 | return is_resp_ok(resp) 94 | 95 | 96 | def comment_post(client: ApiClient, media_id: str, comment: str) -> bool: 97 | """Leave a comment on a post. 98 | 99 | Args: 100 | client: your `ApiClient` 101 | media_id: the post to comment on 102 | comment: the comment to place 103 | 104 | Returns: 105 | `True` if success else `False` 106 | """ 107 | obj = ps.Comment(media_id=media_id, comment_text=comment) 108 | resp = client.post_comment(obj) 109 | logger.info(f"Commented {comment} on post {media_id}") 110 | return is_resp_ok(resp) 111 | 112 | 113 | def unlike_post(client: ApiClient, media_id: str) -> bool: 114 | """Undo the liking of a post. 115 | 116 | Args: 117 | client: your `ApiClient` 118 | media_id: the media_id of a post 119 | 120 | Returns: 121 | `True` if success else `False` 122 | """ 123 | like = ps.Unlike( 124 | media_id=media_id 125 | ) 126 | resp = client.post_unlike(like) 127 | logger.info(f"Unliked post {media_id}") 128 | return is_resp_ok(resp) 129 | 130 | 131 | def save_post(client: ApiClient, media_id: str) -> bool: 132 | """Save a post. 133 | 134 | Args: 135 | client: your `ApiClient` 136 | media_id: the media_id of a post 137 | 138 | Returns: 139 | `True` if success else `False` 140 | """ 141 | save = ps.Save( 142 | media_id=media_id 143 | ) 144 | resp = client.post_save(save) 145 | logger.info(f"Saved post {media_id}") 146 | return is_resp_ok(resp) 147 | 148 | 149 | def retrieve_posts_from_user( 150 | client: ApiClient, limit: int, 151 | username: Optional[str] = None, 152 | user_id: Optional[int] = None 153 | ) -> List[models.Post]: 154 | """Retrieve x amount of posts from a user. 155 | 156 | Either `user_id` or `username` need to be provided. If both are provided, 157 | the user_id takes precedence. 158 | 159 | Args: 160 | client: your `ApiClient` 161 | limit: maximum amount of posts to retrieve 162 | username: username of the account to retrieve posts from 163 | user_id: user_id of the account to retrieve posts from 164 | 165 | Returns: 166 | A list of Instagram post objects (objects/post.json). 167 | """ 168 | if username is None and user_id is None: 169 | raise ValueError("Either `username` or `user_id` param need to be provider") 170 | if username is not None and user_id is None: 171 | user_id = get_user_id_from_username(client, username) 172 | elif username is not None and user_id is not None: 173 | logger.warning("Both `username` and `user_id` are provided. `user_id` will be used.") 174 | 175 | if user_id is None: 176 | raise NotFoundError(f"Couldn't find user {username}") 177 | obj = ps.RetrieveByUser(user_id=user_id) 178 | obj, result = client.post_retrieve_by_user(obj) 179 | retrieved_items = [] 180 | 181 | while result and len(retrieved_items) < limit: 182 | logger.info(f"Retrieved {len(retrieved_items)} posts from user {username or user_id}") 183 | # pyre-ignore[6] 184 | retrieved_items.extend(result) 185 | obj, result = client.post_retrieve_by_user(obj) 186 | return [models.Post.parse(p) for p in retrieved_items[:limit:]] 187 | 188 | 189 | def retrieve_posts_from_tag(client: ApiClient, tag: str, limit: int) -> List[models.Post]: 190 | """Retrieve x amount of posts tagged with a tag. 191 | 192 | Args: 193 | client: your `ApiClient` 194 | limit: maximum amount of posts to retrieve 195 | tag: the tag to search for 196 | 197 | Returns: 198 | A list of Instagram post objects (objects/post.json). 199 | """ 200 | obj = ps.RetrieveByTag( 201 | tag_name=tag 202 | ) 203 | obj, result = client.post_retrieve_by_tag(obj) 204 | retrieved_items = [] 205 | 206 | while result and len(retrieved_items) < limit: 207 | logger.info(f"Retrieved {len(retrieved_items)} posts by tag") 208 | # pyre-ignore[6] 209 | retrieved_items.extend(result) 210 | obj, result = client.post_retrieve_by_tag(obj) 211 | return [models.Post.parse(p) for p in retrieved_items[:limit:]] 212 | 213 | 214 | def get_likers_of_post(client: ApiClient, media_id: str) -> List[models.User]: 215 | """Get users that liked a post. 216 | 217 | Args: 218 | client: your `ApiClient` 219 | media_id: the post to retrieve the likers from 220 | 221 | Returns: 222 | A list of Instagram user objects (objects/user.json). 223 | """ 224 | logger.info(f"Getting likers of {media_id}") 225 | return [models.User.parse(l) for l in client.post_get_likers(RetrieveLikers(media_id))] 226 | 227 | 228 | def get_commenters_of_post(client: ApiClient, media_id: str) -> List[models.User]: 229 | """Get users that commented on a post. 230 | 231 | Args: 232 | client: your `ApiClient` 233 | media_id: the post to retrieve the commenters from 234 | 235 | Returns: 236 | A list of Instagram user objects (objects/post.json). 237 | """ 238 | logger.info(f"Getting commenters of {media_id}") 239 | return [models.User.parse(l) for l in client.post_get_likers(RetrieveLikers(media_id))] 240 | 241 | 242 | def retrieve_story_from_user( 243 | client: ApiClient, 244 | username: Optional[str] = None, 245 | user_id: Optional[int] = None 246 | ) -> List[models.Story]: 247 | """Retrieve x amount of posts from a user. 248 | 249 | Either `user_id` or `username` need to be provided. If both are provided, 250 | the user_id takes precedence. 251 | 252 | Args: 253 | client: your `ApiClient` 254 | limit: maximum amount of posts to retrieve 255 | username: username of the account to retrieve posts from 256 | user_id: user_id of the account to retrieve posts from 257 | 258 | Returns: 259 | A list of Instagram post objects (objects/post.json). 260 | """ 261 | if username is None and user_id is None: 262 | raise ValueError("Either `username` or `user_id` param need to be provider") 263 | if username is not None and user_id is None: 264 | user_id = get_user_id_from_username(client, username) 265 | elif username is not None and user_id is not None: 266 | logger.warning("Both `username` and `user_id` are provided. `user_id` will be used.") 267 | 268 | if user_id is None: 269 | raise NotFoundError(f"Couldn't find user {username}") 270 | obj = ps.RetrieveStory(user_id=user_id) 271 | resp = client.post_retrieve_story(obj).json() 272 | if resp['reel'] is None: 273 | return [] 274 | 275 | items = resp['reel'].get('items') 276 | return [models.Story.parse(i) for i in items] 277 | -------------------------------------------------------------------------------- /instauto/helpers/search.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | import orjson 4 | 5 | from instauto.api.client import ApiClient 6 | from instauto.api.actions import search as se 7 | from instauto.helpers import models 8 | 9 | 10 | def search_username(client: ApiClient, username, count: int) -> List[models.User]: 11 | """Search a username on Instagram. 12 | 13 | Args: 14 | client: your `ApiClient` 15 | username: username to search 16 | count: amount of results to retrieve 17 | 18 | Returns: 19 | List of user objects (objects/user.json) that Instagram 20 | matched with the provider username 21 | """ 22 | username = se.Username( 23 | q=username, 24 | count=count 25 | ) 26 | resp = client.search_username(username) 27 | return [models.User.parse(d) for d in orjson.loads(resp.text)['users']] 28 | 29 | 30 | def get_user_by_username(client: ApiClient, username: str) -> Optional[models.User]: 31 | """Retrieve a user by username. 32 | 33 | Args: 34 | client: your `ApiClient` 35 | username: username to search for 36 | 37 | Returns: 38 | None if not found, else a user object (objects/user.json) 39 | containing the found user 40 | """ 41 | users = search_username(client, username, 1) 42 | correct_user = [x for x in users if x.username == username] 43 | if correct_user: 44 | return correct_user[0] 45 | 46 | 47 | def get_user_id_from_username(client: ApiClient, username: str) -> Optional[int]: 48 | """Get the user id of a username. 49 | 50 | Args: 51 | client: your `ApiClient` 52 | username: username to search for 53 | 54 | Returns: 55 | None if not found, else a user id of the found user 56 | """ 57 | user = get_user_by_username(client, username) 58 | if user is not None: 59 | return user.pk 60 | 61 | 62 | def search_tags(client: ApiClient, tag: str, limit: int) -> List[dict]: 63 | s = se.Tag(tag, limit) 64 | resp: dict = client.search_tag(s).json() 65 | return resp['results'] 66 | 67 | -------------------------------------------------------------------------------- /original_requests/direct/linkshare.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "link_text": "test link.com", 4 | "link_urls": ["link.com"], 5 | "recipient_users": [], 6 | "thread_ids": [] 7 | }, 8 | "response": { 9 | "threads": [ 10 | { 11 | "thread_id": "", 12 | "thread_v2_id": "", 13 | "users": [ 14 | { 15 | "pk": 0, 16 | "username": "", 17 | "full_name": "", 18 | "is_private": true, 19 | "profile_pic_url": "", 20 | "profile_pic_id": "", 21 | "friendship_status": { 22 | "following": false, 23 | "blocking": false, 24 | "is_private": true, 25 | "incoming_request": false, 26 | "outgoing_request": false, 27 | "is_bestie": false, 28 | "is_restricted": false 29 | }, 30 | "is_verified": false, 31 | "has_anonymous_profile_picture": false, 32 | "has_threads_app": false, 33 | "is_using_unified_inbox_for_direct": false, 34 | "interop_messaging_user_fbid": 0, 35 | "account_badges": [] 36 | } 37 | ], 38 | "left_users": [], 39 | "admin_user_ids": [], 40 | "items": [ 41 | { 42 | "item_id": "", 43 | "user_id": 0, 44 | "timestamp": 1606941434135449, 45 | "item_type": "", 46 | "link": { 47 | "text": "Link: https://google.com", 48 | "link_context": { 49 | "link_url": "https://google.com", 50 | "link_title": "Google", 51 | "link_summary": "December Holidays 2020 #GoogleDoodle", 52 | "link_image_url": "https://external.xx.fbcdn.net/safe_image.php?d=AQBzeQ5Sufuu-LYA&w=1080&h=600&url=https%3A%2F%2Fwww.google.com%2Flogos%2Fdoodles%2F2020%2Fdecember-holidays-days-2-30-6753651837108830.2-2xa.gif&upscale=1&_nc_cb=1&_nc_hash=AQCUpzlKZPWCLz3x" 53 | }, 54 | "client_context": null, 55 | "mutation_token": null 56 | }, 57 | "show_forward_attribution": false, 58 | "is_shh_mode": false 59 | } 60 | ], 61 | "last_activity_at": 1606941434135449, 62 | "muted": false, 63 | "is_pin": false, 64 | "named": false, 65 | "canonical": true, 66 | "pending": false, 67 | "archived": false, 68 | "thread_type": "private", 69 | "viewer_id": 0, 70 | "thread_title": "", 71 | "folder": 0, 72 | "vc_muted": false, 73 | "is_group": false, 74 | "mentions_muted": false, 75 | "approval_required_for_new_members": false, 76 | "input_mode": 0, 77 | "business_thread_folder": null, 78 | "read_state": null, 79 | "last_non_sender_item_at": 0, 80 | "assigned_admin_id": null, 81 | "shh_mode_enabled": false, 82 | "is_close_friend_thread": false, 83 | "inviter": { 84 | "pk": 0, 85 | "username": "", 86 | "full_name": "", 87 | "is_private": true, 88 | "profile_pic_url": "", 89 | "profile_pic_id": "", 90 | "is_verified": false, 91 | "has_anonymous_profile_picture": false, 92 | "account_badges": [] 93 | }, 94 | "has_older": true, 95 | "has_newer": true, 96 | "last_seen_at": { 97 | "userid": { 98 | "timestamp": "", 99 | "item_id": "", 100 | "created_at": "", 101 | "shh_seen_state": {} 102 | }, 103 | "userid": { 104 | "timestamp": "", 105 | "created_at": "", 106 | "item_id": "", 107 | "shh_seen_state": {} 108 | } 109 | }, 110 | "newest_cursor": "", 111 | "oldest_cursor": "", 112 | "next_cursor": "", 113 | "prev_cursor": "", 114 | "last_permanent_item": { 115 | "item_id": "", 116 | "user_id": 0, 117 | "timestamp": 1606941434135449, 118 | "item_type": "link", 119 | "link": { 120 | "text": "Link: https://google.com", 121 | "link_context": { 122 | "link_url": "https://google.com", 123 | "link_title": "Google", 124 | "link_summary": "December Holidays 2020 #GoogleDoodle", 125 | "link_image_url": "https://external.xx.fbcdn.net/safe_image.php?d=AQBzeQ5Sufuu-LYA&w=1080&h=600&url=https%3A%2F%2Fwww.google.com%2Flogos%2Fdoodles%2F2020%2Fdecember-holidays-days-2-30-6753651837108830.2-2xa.gif&upscale=1&_nc_cb=1&_nc_hash=AQCUpzlKZPWCLz3x" 126 | }, 127 | "client_context": null, 128 | "mutation_token": null 129 | }, 130 | "show_forward_attribution": false, 131 | "is_shh_mode": false 132 | } 133 | } 134 | ], 135 | "status": "ok" 136 | }, 137 | "method": "post", 138 | "endpoint": "direct_v2/threads/broadcast/link/" 139 | } 140 | -------------------------------------------------------------------------------- /original_requests/direct/message.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "text": "", 4 | "recipient_users": [], 5 | "thread_ids": [] 6 | }, 7 | "response": { 8 | "status": "ok", 9 | "threads": [ 10 | { 11 | "thread_id": "", 12 | "thread_v2_id": "", 13 | "users": [ 14 | { 15 | "pk": 0, 16 | "username": "", 17 | "full_name": "", 18 | "is_private": true, 19 | "profile_pic_url": "", 20 | "profile_pic_id": "", 21 | "friendship_status": { 22 | "following": false, 23 | "blocking": false, 24 | "is_private": true, 25 | "incoming_request": false, 26 | "outgoing_request": false, 27 | "is_bestie": false, 28 | "is_restricted": false 29 | }, 30 | "is_verified": false, 31 | "has_anonymous_profile_picture": false, 32 | "has_threads_app": false, 33 | "is_using_unified_inbox_for_direct": false, 34 | "interop_messaging_user_fbid": 0, 35 | "account_badges": [] 36 | } 37 | ], 38 | "left_users": [], 39 | "admin_user_ids": [], 40 | "items": [ 41 | { 42 | "item_id": "", 43 | "user_id": 0, 44 | "timestamp": 0, 45 | "item_type": "text", 46 | "text": "Testing 1 2 3...", 47 | "show_forward_attribution": false, 48 | "is_shh_mode": false 49 | } 50 | ], 51 | "last_activity_at": 1606756641665092, 52 | "muted": false, 53 | "is_pin": false, 54 | "named": false, 55 | "canonical": true, 56 | "pending": false, 57 | "archived": false, 58 | "thread_type": "private", 59 | "viewer_id": 0, 60 | "thread_title": "", 61 | "folder": 0, 62 | "vc_muted": false, 63 | "is_group": false, 64 | "mentions_muted": false, 65 | "approval_required_for_new_members": false, 66 | "input_mode": 0, 67 | "business_thread_folder": null, 68 | "read_state": null, 69 | "last_non_sender_item_at": 0, 70 | "assigned_admin_id": null, 71 | "shh_mode_enabled": false, 72 | "is_close_friend_thread": false, 73 | "inviter": { 74 | "pk": 0, 75 | "username": "", 76 | "full_name": "", 77 | "is_private": true, 78 | "profile_pic_url": "", 79 | "profile_pic_id": "", 80 | "is_verified": false, 81 | "has_anonymous_profile_picture": false, 82 | "account_badges": [] 83 | }, 84 | "has_older": true, 85 | "has_newer": true, 86 | "last_seen_at": { 87 | "idstring": { 88 | "timestamp": "", 89 | "item_id": "", 90 | "shh_seen_state": {} 91 | }, 92 | "idstring2": { 93 | "timestamp": "", 94 | "created_at": "", 95 | "item_id": "", 96 | "shh_seen_state": {} 97 | } 98 | }, 99 | "newest_cursor": "29639428557528997473611914959388672", 100 | "oldest_cursor": "29639428557528997473611914959388672", 101 | "next_cursor": "29639428557528997473611914959388673", 102 | "prev_cursor": "29639428557528997473611914959388671", 103 | "last_permanent_item": { 104 | "item_id": "", 105 | "user_id": 0, 106 | "timestamp": 1606756641665092, 107 | "item_type": "text", 108 | "text": "Testing 1 2 3...", 109 | "show_forward_attribution": false, 110 | "is_shh_mode": false 111 | } 112 | } 113 | ] 114 | }, 115 | "method": "post", 116 | "endpoint": "direct_v2/threads/broadcast/text/" 117 | } 118 | -------------------------------------------------------------------------------- /original_requests/direct/photoshare.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "upload_id": "", 4 | "allow_full_aspect_ratio": true, 5 | "recipient_users": [], 6 | "thread_ids": [] 7 | }, 8 | "response": { 9 | "threads": [ 10 | { 11 | "thread_id": "", 12 | "thread_v2_id": "", 13 | "users": [ 14 | { 15 | "pk": 0, 16 | "username": "", 17 | "full_name": "", 18 | "is_private": true, 19 | "profile_pic_url": "", 20 | "profile_pic_id": "", 21 | "friendship_status": { 22 | "following": false, 23 | "blocking": false, 24 | "is_private": true, 25 | "incoming_request": false, 26 | "outgoing_request": false, 27 | "is_bestie": false, 28 | "is_restricted": false 29 | }, 30 | "is_verified": false, 31 | "has_anonymous_profile_picture": false, 32 | "has_threads_app": false, 33 | "is_using_unified_inbox_for_direct": false, 34 | "interop_messaging_user_fbid": 0, 35 | "account_badges": [] 36 | } 37 | ], 38 | "left_users": [], 39 | "admin_user_ids": [], 40 | "items": [ 41 | { 42 | "item_id": "", 43 | "user_id": 0, 44 | "timestamp": 1606942965005261, 45 | "item_type": "media", 46 | "media": { 47 | "id": 0, 48 | "image_versions2": { 49 | "candidates": [ 50 | { 51 | "width": 1324, 52 | "height": 1446, 53 | "url": "", 54 | "scans_profile": "e35", 55 | "estimated_scans_sizes": [ 56 | 9264, 57 | 18529, 58 | 27794, 59 | 37059, 60 | 46323, 61 | 55912, 62 | 67687, 63 | 75381, 64 | 83383 65 | ] 66 | }, 67 | { 68 | "width": 480, 69 | "height": 524, 70 | "url": "", 71 | "scans_profile": "e35", 72 | "estimated_scans_sizes": [ 73 | 3394, 74 | 6788, 75 | 10183, 76 | 13577, 77 | 16972, 78 | 21187, 79 | 273633, 80 | 30550, 81 | 30550 82 | ] 83 | } 84 | ] 85 | }, 86 | "original_width": 1324, 87 | "original_height": 1446, 88 | "media_type": 1 89 | }, 90 | "show_forward_attribution": false, 91 | "is_shh_mode": false 92 | } 93 | ], 94 | "last_activity_at": 1606942965005261, 95 | "muted": false, 96 | "is_pin": false, 97 | "named": false, 98 | "canonical": true, 99 | "pending": false, 100 | "archived": false, 101 | "thread_type": "private", 102 | "viewer_id": 0, 103 | "thread_title": "", 104 | "folder": 0, 105 | "vc_muted": false, 106 | "is_group": false, 107 | "mentions_muted": false, 108 | "approval_required_for_new_members": false, 109 | "input_mode": 0, 110 | "business_thread_folder": null, 111 | "read_state": null, 112 | "last_non_sender_item_at": 0, 113 | "assigned_admin_id": null, 114 | "shh_mode_enabled": false, 115 | "is_close_friend_thread": false, 116 | "inviter": { 117 | "pk": 0, 118 | "username": "", 119 | "full_name": "", 120 | "is_private": true, 121 | "profile_pic_url": "", 122 | "profile_pic_id": "", 123 | "is_verified": false, 124 | "has_anonymous_profile_picture": false, 125 | "account_badges": [] 126 | }, 127 | "has_older": true, 128 | "has_newer": true, 129 | "last_seen_at": { 130 | "userid": { 131 | "timestamp": "1606941711079868", 132 | "item_id": "", 133 | "created_at": "1606941711079868", 134 | "shh_seen_state": {} 135 | }, 136 | "userid": { 137 | "timestamp": "1606942965005261", 138 | "created_at": "1606942965005261", 139 | "item_id": "", 140 | "shh_seen_state": {} 141 | } 142 | }, 143 | "newest_cursor": "29642865616500053743207367391051776", 144 | "oldest_cursor": "29642865616500053743207367391051776", 145 | "next_cursor": "29642865616500053743207367391051777", 146 | "prev_cursor": "29642865616500053743207367391051775", 147 | "last_permanent_item": { 148 | "item_id": "", 149 | "user_id": 0, 150 | "timestamp": 1606942965005261, 151 | "item_type": "media", 152 | "media": { 153 | "id": 0, 154 | "image_versions2": { 155 | "candidates": [ 156 | { 157 | "width": 1324, 158 | "height": 1446, 159 | "url": "", 160 | "scans_profile": "e35", 161 | "estimated_scans_sizes": [ 162 | 9264, 163 | 18529, 164 | 27794, 165 | 37059, 166 | 46323, 167 | 55912, 168 | 67687, 169 | 75381, 170 | 83383 171 | ] 172 | }, 173 | { 174 | "width": 480, 175 | "height": 524, 176 | "url": "", 177 | "scans_profile": "e35", 178 | "estimated_scans_sizes": [ 179 | 3394, 180 | 6788, 181 | 10183, 182 | 13577, 183 | 16972, 184 | 21187, 185 | 273633, 186 | 30550, 187 | 30550 188 | ] 189 | } 190 | ] 191 | }, 192 | "original_width": 1324, 193 | "original_height": 1446, 194 | "media_type": 1 195 | }, 196 | "show_forward_attribution": false, 197 | "is_shh_mode": false 198 | } 199 | } 200 | ], 201 | "status": "ok" 202 | }, 203 | "method": "post", 204 | "endpoint": "direct_v2/threads/broadcast/configure_photo/" 205 | } 206 | 207 | -------------------------------------------------------------------------------- /original_requests/direct/profileshare.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "profile_user_id": "", 4 | "recipient_users": [], 5 | "thread_ids": [] 6 | }, 7 | "response": { 8 | "threads": [ 9 | { 10 | "thread_id": "", 11 | "thread_v2_id": "", 12 | "users": [ 13 | { 14 | "pk": 0, 15 | "username": "", 16 | "full_name": "", 17 | "is_private": true, 18 | "profile_pic_url": "", 19 | "profile_pic_id": "", 20 | "friendship_status": { 21 | "following": false, 22 | "blocking": false, 23 | "is_private": true, 24 | "incoming_request": false, 25 | "outgoing_request": false, 26 | "is_bestie": false, 27 | "is_restricted": false 28 | }, 29 | "is_verified": false, 30 | "has_anonymous_profile_picture": false, 31 | "has_threads_app": false, 32 | "is_using_unified_inbox_for_direct": false, 33 | "interop_messaging_user_fbid": 0, 34 | "account_badges": [] 35 | } 36 | ], 37 | "left_users": [], 38 | "admin_user_ids": [], 39 | "items": [ 40 | { 41 | "item_id": "", 42 | "user_id": 0, 43 | "timestamp": 1606941711079868, 44 | "item_type": "profile", 45 | "profile": { 46 | "pk": 0, 47 | "username": "", 48 | "full_name": "", 49 | "is_private": true, 50 | "profile_pic_url": "", 51 | "profile_pic_id": "", 52 | "is_verified": false, 53 | "has_anonymous_profile_picture": false, 54 | "account_badges": [] 55 | }, 56 | "show_forward_attribution": false, 57 | "is_shh_mode": false, 58 | "preview_medias": [] 59 | } 60 | ], 61 | "last_activity_at": 1606941711079868, 62 | "muted": false, 63 | "is_pin": false, 64 | "named": false, 65 | "canonical": true, 66 | "pending": false, 67 | "archived": false, 68 | "thread_type": "private", 69 | "viewer_id": 0, 70 | "thread_title": "", 71 | "folder": 0, 72 | "vc_muted": false, 73 | "is_group": false, 74 | "mentions_muted": false, 75 | "approval_required_for_new_members": false, 76 | "input_mode": 0, 77 | "business_thread_folder": null, 78 | "read_state": null, 79 | "last_non_sender_item_at": 0, 80 | "assigned_admin_id": null, 81 | "shh_mode_enabled": false, 82 | "is_close_friend_thread": false, 83 | "inviter": { 84 | "pk": 0, 85 | "username": "", 86 | "full_name": "", 87 | "is_private": true, 88 | "profile_pic_url": "", 89 | "profile_pic_id": "", 90 | "is_verified": false, 91 | "has_anonymous_profile_picture": false, 92 | "account_badges": [] 93 | }, 94 | "has_older": true, 95 | "has_newer": true, 96 | "last_seen_at": { 97 | "userid": { 98 | "timestamp": "1606938850055706", 99 | "item_id": "", 100 | "created_at": "1606938850055706", 101 | "shh_seen_state": {} 102 | }, 103 | "userid": { 104 | "timestamp": "1606941711079868", 105 | "created_at": "1606941711079868", 106 | "item_id": "", 107 | "shh_seen_state": {} 108 | } 109 | }, 110 | "newest_cursor": "29642842485659241546536889444466688", 111 | "oldest_cursor": "29642842485659241546536889444466688", 112 | "next_cursor": "29642842485659241546536889444466689", 113 | "prev_cursor": "29642842485659241546536889444466687", 114 | "last_permanent_item": { 115 | "item_id": "", 116 | "user_id": 0, 117 | "timestamp": 1606941711079868, 118 | "item_type": "", 119 | "profile": { 120 | "pk": 0, 121 | "username": "", 122 | "full_name": "", 123 | "is_private": true, 124 | "profile_pic_url": "", 125 | "profile_pic_id": "", 126 | "is_verified": false, 127 | "has_anonymous_profile_picture": false, 128 | "account_badges": [] 129 | }, 130 | "show_forward_attribution": false, 131 | "is_shh_mode": false, 132 | "preview_medias": [] 133 | } 134 | } 135 | ], 136 | "status": "ok" 137 | }, 138 | "method": "post", 139 | "endpoint": "direct_v2/threads/broadcast/profile/" 140 | } 141 | 142 | -------------------------------------------------------------------------------- /original_requests/direct/videoshare.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "upload_id": "", 4 | "sampled": true, 5 | "video_result": "", 6 | "recipient_users": [], 7 | "thread_ids": [] 8 | }, 9 | "response": { 10 | }, 11 | "method": "post", 12 | "endpoint": "direct_v2/threads/broadcast/configure_video/" 13 | } 14 | 15 | -------------------------------------------------------------------------------- /original_requests/friendships/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "_csrftoken": "", 4 | "radio_type": "-none", 5 | "_uid": "", 6 | "device_id": "", 7 | "_uuid": "", 8 | "user_id": "" 9 | }, 10 | "response": { 11 | "friendship_status": { 12 | "blocking": false, 13 | "followed_by": false, 14 | "following": true, 15 | "incoming_request": false, 16 | "is_bestie": false, 17 | "is_private": false, 18 | "is_restricted": false, 19 | "muting": false, 20 | "outgoing_request": false 21 | }, 22 | "status": "ok" 23 | }, 24 | "method": "post", 25 | "endpoint": "friendships/create/{user_id}" 26 | } 27 | -------------------------------------------------------------------------------- /original_requests/friendships/destroy.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "surface": "following_sheet", 4 | "_csrftoken": "JWNQYfQRyhE5cHXW4Pqp7nw2z7jc3SoE", 5 | "radio_type": "wifi-none", 6 | "_uid":"4478472759", 7 | "_uuid": "ff82e1d2-b663-41e9-87e7-630af2f43268", 8 | "user_id": "" 9 | }, 10 | "response": { 11 | "friendship_status": { 12 | "blocking": false, 13 | "followed_by": false, 14 | "following": false, 15 | "incoming_request": false, 16 | "is_bestie": false, 17 | "is_private": false, 18 | "is_restricted": false, 19 | "muting": false, 20 | "outgoing_request": false 21 | }, 22 | "status": "ok" 23 | }, 24 | "method": "post", 25 | "endpoint": "friendships/destroy/{user_id}" 26 | } -------------------------------------------------------------------------------- /original_requests/friendships/get_followers.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "get", 3 | "endpoint": "friendships/{user_id}/followers", 4 | "request": { 5 | "user_id": "12131223", 6 | "search_surface": "follow_list_page", 7 | "max_id": { 8 | "_": "QVFBeC1uczZCXzREU2dqam5BVlF0WkZERzZlN1hRZ1Y1QWhURktYQTZQUjBUY0d6aDVSaXNxNU54TEtjdVRwMzhMcDU3S3dRV2FoTnIxb0dKNFY3dXp2NA==", 9 | "on_all_requests": false 10 | }, 11 | "order": "default", 12 | "enable_groups": true, 13 | "query": "", 14 | "rank_token": "fab694df-cda6-4d26-b0f5-0e688290a2a3" 15 | }, 16 | "response": { 17 | "big_list": true, 18 | "global_blacklist_sample": null, 19 | "next_max_id": "QVFDN1BnZWpkTTF1SWdkaWVfVzVYbHNJV2NjemFlX2lWV0tjTUVvUGM2bTZrVHdpeGVnSVA4eXBCQVJ1MUVWeXZBNHBvRmtXa3VvWkFoS09PSG1rdVIwMg==", 20 | "page_size": 200, 21 | "sections": null, 22 | "status": "ok", 23 | "users": [ 24 | "objects/user.json" 25 | ] 26 | } 27 | } -------------------------------------------------------------------------------- /original_requests/friendships/get_following.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "get", 3 | "endpoint": "friendships/{user_id}/following/", 4 | "request": { 5 | "user_id": "12131223", 6 | "search_surface": "follow_list_page", 7 | "max_id": { 8 | "_": "100", 9 | "on_all_requests": false 10 | }, 11 | "order": "default", 12 | "enable_groups": true, 13 | "query": "", 14 | "rank_token": "fab694df-cda6-4d26-b0f5-0e688290a2a3" 15 | }, 16 | "response": { 17 | "big_list": true, 18 | "global_blacklist_sample": null, 19 | "next_max_id": "200", 20 | "page_size": 200, 21 | "sections": null, 22 | "status": "ok", 23 | "users": [ 24 | "objects/user.json", 25 | "objects/user.json" 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /original_requests/friendships/pending_requests.json: -------------------------------------------------------------------------------- 1 | { 2 | "endpoint": "friendships/pending/", 3 | "request": { 4 | }, 5 | "response": { 6 | "big_list": false, 7 | "global_blacklist_sample": null, 8 | "next_max_id": null, 9 | "page_size": 200, 10 | "sections": null, 11 | "status": "ok", 12 | "suggested_users": { 13 | "suggestions": [] 14 | }, 15 | "users": [ 16 | "objects/user.json", 17 | "objects/user.json" 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /original_requests/friendships/remove.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "_csrftoken": "JWNQYfQRyhE5cHXW4Pqp7nw2z7jc3SoE", 4 | "radio_type": "wifi-none", 5 | "_uid": "4478472759", 6 | "_uuid": "ff82e1d2-b663-41e9-87e7-630af2f43268", 7 | "user_id": "" 8 | }, 9 | "response": { 10 | "friendship_status": { 11 | "blocking": false, 12 | "followed_by": false, 13 | "following": false, 14 | "incoming_request": false, 15 | "is_bestie": false, 16 | "is_private": false, 17 | "is_restricted": false, 18 | "muting": false, 19 | "outgoing_request": false 20 | }, 21 | "status": "ok" 22 | }, 23 | "method": "post", 24 | "endpoint": "friendships/remove_follower/{user_id}" 25 | } 26 | -------------------------------------------------------------------------------- /original_requests/friendships/show.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "user_id": "343242423" 4 | }, 5 | "response": { 6 | "blocking": false, 7 | "followed_by": false, 8 | "following": false, 9 | "incoming_request": false, 10 | "is_bestie": false, 11 | "is_blocking_reel": false, 12 | "is_muting_reel": false, 13 | "is_private": false, 14 | "is_restricted": false, 15 | "muting": false, 16 | "outgoing_request": false, 17 | "status": "ok" 18 | }, 19 | "method": "get", 20 | "endpoint": "friendships/show/{user_id}" 21 | } -------------------------------------------------------------------------------- /original_requests/objects/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "can_see_insights_as_brand": false, 3 | "can_view_more_preview_comments": false, 4 | "can_viewer_reshare": false, 5 | "can_viewer_save": true, 6 | "caption": { 7 | "bit_flags": 0, 8 | "content_type": "comment", 9 | "created_at": 1498554151, 10 | "created_at_utc": 1498554151, 11 | "did_report_as_spam": false, 12 | "media_id": 1545793193277167087, 13 | "pk": 17863630882140720, 14 | "share_enabled": false, 15 | "status": "Active", 16 | "text": "Comment \"AMAZING\" letter for letter😍🥑\nFollow @viralpos.t (me) for more🍃\n-\nClick the link in my Bio for Awesome emoijs!😍💕", 17 | "type": 1, 18 | "user": { 19 | "account_badges": [], 20 | "full_name": "tienerzinnetje.s", 21 | "has_anonymous_profile_picture": false, 22 | "is_favorite": false, 23 | "is_private": true, 24 | "is_unpublished": false, 25 | "is_verified": false, 26 | "latest_reel_media": 0, 27 | "pk": 4478472759, 28 | "profile_pic_id": "1668785598670642821_4478472759", 29 | "profile_pic_url": "https://scontent-amt2-1.cdninstagram.com/v/t51.2885-19/s150x150/25037686_1559699034114489_801156905606053888_n.jpg?_nc_ht=scontent-amt2-1.cdninstagram.com&_nc_ohc=2iJnWkbO0GwAX8A06J0&oh=6f37effb3890f77843bf65baceb25fa1&oe=5F4B291B", 30 | "username": "tienerzinnetje.s" 31 | }, 32 | "user_id": 4478472759 33 | }, 34 | "caption_is_edited": true, 35 | "client_cache_key": "MTU0NTc5MzE5MzI3NzE2NzA4Nw==.2", 36 | "code": "BVzwrglgJXv67zZCula8x_JTKbpvdPgqJi3bGc0", 37 | "comment_count": 0, 38 | "comment_likes_enabled": true, 39 | "comment_threading_enabled": true, 40 | "device_timestamp": 1498492874, 41 | "filter_type": 0, 42 | "has_audio": true, 43 | "has_liked": false, 44 | "has_more_comments": false, 45 | "id": "1545793193277167087_4478472759", 46 | "image_versions2": { 47 | "candidates": [ 48 | { 49 | "height": 640, 50 | "url": "https://scontent-ams4-1.cdninstagram.com/v/t51.2885-15/e15/19436701_552027881853327_1583884119429873664_n.jpg?_nc_ht=scontent-ams4-1.cdninstagram.com&_nc_cat=103&_nc_ohc=kHHjvSJkMd4AX_P5O80&oh=0da304eb8948ae5dce6b5dd8dea1f920&oe=5F22E3F1", 51 | "width": 640 52 | }, 53 | { 54 | "height": 480, 55 | "url": "https://scontent-ams4-1.cdninstagram.com/v/t51.2885-15/e15/s480x480/19436701_552027881853327_1583884119429873664_n.jpg?_nc_ht=scontent-ams4-1.cdninstagram.com&_nc_cat=103&_nc_ohc=kHHjvSJkMd4AX_P5O80&oh=6bf836b93427160161d9c85e097ce394&oe=5F22D5FC", 56 | "width": 480 57 | } 58 | ] 59 | }, 60 | "inline_composer_display_condition": "impression_trigger", 61 | "inline_composer_imp_trigger_time": 5, 62 | "is_in_profile_grid": false, 63 | "like_count": 3351, 64 | "max_num_visible_preview_comments": 2, 65 | "media_type": 2, 66 | "organic_tracking_token": "eyJ2ZXJzaW9uIjo1LCJwYXlsb2FkIjp7ImlzX2FuYWx5dGljc190cmFja2VkIjp0cnVlLCJ1dWlkIjoiNjJjNzk1Y2NiYzYzNDc0NGFhZDk1ZTliZDFhZmUzNTkxNTQ1NzkzMTkzMjc3MTY3MDg3Iiwic2VydmVyX3Rva2VuIjoiMTU5NTk2NTUzNTkwM3wxNTQ1NzkzMTkzMjc3MTY3MDg3fDIwOTcwMTcwNTJ8NWY1ODlkYzdiMmZjNmNiNGY5ZWE1MDNiOGQ5MzBkYjA4YzUxMzRkMmMzYmU2Njk0ZDAyMjU2OTdmM2UzMjNiNyJ9LCJzaWduYXR1cmUiOiIifQ==", 67 | "original_height": 640, 68 | "original_width": 640, 69 | "photo_of_you": false, 70 | "pk": 1545793193277167087, 71 | "profile_grid_control_enabled": false, 72 | "sharing_friction_info": { 73 | "bloks_app_url": null, 74 | "should_have_sharing_friction": false 75 | }, 76 | "taken_at": 1498492930, 77 | "top_likers": [], 78 | "user": { 79 | "account_badges": [], 80 | "full_name": "tienerzinnetje.s", 81 | "has_anonymous_profile_picture": false, 82 | "is_favorite": false, 83 | "is_private": true, 84 | "is_unpublished": false, 85 | "is_verified": false, 86 | "latest_reel_media": 0, 87 | "pk": 4478472759, 88 | "profile_pic_id": "1668785598670642821_4478472759", 89 | "profile_pic_url": "https://scontent-amt2-1.cdninstagram.com/v/t51.2885-19/s150x150/25037686_1559699034114489_801156905606053888_n.jpg?_nc_ht=scontent-amt2-1.cdninstagram.com&_nc_ohc=2iJnWkbO0GwAX8A06J0&oh=6f37effb3890f77843bf65baceb25fa1&oe=5F4B291B", 90 | "username": "tienerzinnetje.s" 91 | }, 92 | "video_duration": 0.0, 93 | "video_versions": [ 94 | { 95 | "height": 640, 96 | "id": "2534915079913092", 97 | "type": 101, 98 | "url": "https://scontent-amt2-1.cdninstagram.com/v/t50.2886-16/19514839_240388016461674_2673480789433253888_n.mp4?efg=eyJxZV9ncm91cHMiOiJbXCJpZ19wcm9ncmVzc2l2ZV91cmxnZW4ucHJvZHVjdF90eXBlLmZlZWRcIl0ifQ&_nc_ht=scontent-amt2-1.cdninstagram.com&_nc_cat=106&_nc_ohc=t7qahsNwULMAX8kyyIB&oe=5F22BECA&oh=0f2f44feae683e0d327a3be607716f71", 99 | "width": 640 100 | }, 101 | { 102 | "height": 640, 103 | "id": "2534915079913092", 104 | "type": 102, 105 | "url": "https://scontent-amt2-1.cdninstagram.com/v/t50.2886-16/19514839_240388016461674_2673480789433253888_n.mp4?efg=eyJxZV9ncm91cHMiOiJbXCJpZ19wcm9ncmVzc2l2ZV91cmxnZW4ucHJvZHVjdF90eXBlLmZlZWRcIl0ifQ&_nc_ht=scontent-amt2-1.cdninstagram.com&_nc_cat=106&_nc_ohc=t7qahsNwULMAX8kyyIB&oe=5F22BECA&oh=0f2f44feae683e0d327a3be607716f71", 106 | "width": 640 107 | }, 108 | { 109 | "height": 640, 110 | "id": "2534915079913092", 111 | "type": 103, 112 | "url": "https://scontent-amt2-1.cdninstagram.com/v/t50.2886-16/19514839_240388016461674_2673480789433253888_n.mp4?efg=eyJxZV9ncm91cHMiOiJbXCJpZ19wcm9ncmVzc2l2ZV91cmxnZW4ucHJvZHVjdF90eXBlLmZlZWRcIl0ifQ&_nc_ht=scontent-amt2-1.cdninstagram.com&_nc_cat=106&_nc_ohc=t7qahsNwULMAX8kyyIB&oe=5F22BECA&oh=0f2f44feae683e0d327a3be607716f71", 113 | "width": 640 114 | } 115 | ], 116 | "view_count": 4.0 117 | } 118 | -------------------------------------------------------------------------------- /original_requests/objects/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "account_badges": [], 3 | "full_name": "", 4 | "has_anonymous_profile_picture": false, 5 | "is_private": false, 6 | "is_verified": false, 7 | "latest_reel_media": 0, 8 | "pk": 1796107640, 9 | "profile_pic_id": "*******", 10 | "profile_pic_url": "******", 11 | "username": "********" 12 | } 13 | -------------------------------------------------------------------------------- /original_requests/post.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "GET", 4 | "endpoint": "https://i.instagram.com/api/v1/feed/user/{user_id}/", 5 | "query_params": { 6 | "exclude_comment": "true", 7 | "only_fetch_first_carousel_media": "false", 8 | "max_id": "{}" 9 | }, 10 | "response": { 11 | "auto_load_more_enabled": true, 12 | "items": [ 13 | "objects/post.json" 14 | ] 15 | } 16 | } 17 | ] -------------------------------------------------------------------------------- /original_requests/post/retrieve_commenters.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "media_id": "1770154859660826272" 4 | }, 5 | "response": 6 | [ 7 | { 8 | "pk":6000633405, 9 | "username":"husarixj", 10 | "full_name":"Ján Štucka", 11 | "is_private":true, 12 | "profile_pic_url":"https://instagram.fbts8-1.fna.fbcdn.net/v/t51.2885-19/s150x150/100986263_951707295267093_2584242213814796288_n.jpg?_nc_ht=instagram.fbts8-1.fna.fbcdn.net&_nc_ohc=fG_LrFQhu40AX_FJLXX&oh=8d8ff49bc3bc2d01550debce5b12daae&oe=5FCB7B3F", 13 | "profile_pic_id":"2319117107482564970_6000633405", 14 | "is_verified":false, 15 | "latest_reel_media":0, 16 | "story_reel_media_ids":[] 17 | }, 18 | { 19 | "pk":536091762, 20 | "username":"dadtka", 21 | "full_name":"D A D K A", 22 | "is_private":true, 23 | "profile_pic_url":"https://instagram.fbts8-1.fna.fbcdn.net/v/t51.2885-19/s150x150/85127694_1845998595699747_4344528628730560512_n.jpg?_nc_ht=instagram.fbts8-1.fna.fbcdn.net&_nc_ohc=m-RnGusIi5IAX9eo5iV&oh=19e359aec1d2c3b6de7d794b6175b2bf&oe=5FCACC4A", 24 | "profile_pic_id":"2247219510640111047_536091762", 25 | "is_verified":false, 26 | "latest_reel_media":0, 27 | "story_reel_media_ids":[] 28 | } 29 | ], 30 | "method": "get", 31 | "endpoint": "media/{media_id}/comments" 32 | } -------------------------------------------------------------------------------- /original_requests/post/retrieve_likers.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "media_id": "1770154859660826272" 4 | }, 5 | "response": 6 | [ 7 | { 8 | "pk":6000633405, 9 | "username":"husarixj", 10 | "full_name":"Ján Štucka", 11 | "is_private":true, 12 | "profile_pic_url":"https://instagram.fbts8-1.fna.fbcdn.net/v/t51.2885-19/s150x150/100986263_951707295267093_2584242213814796288_n.jpg?_nc_ht=instagram.fbts8-1.fna.fbcdn.net&_nc_ohc=fG_LrFQhu40AX_FJLXX&oh=8d8ff49bc3bc2d01550debce5b12daae&oe=5FCB7B3F", 13 | "profile_pic_id":"2319117107482564970_6000633405", 14 | "is_verified":false, 15 | "latest_reel_media":0, 16 | "story_reel_media_ids":[ 17 | 18 | ] 19 | }, 20 | { 21 | "pk":536091762, 22 | "username":"dadtka", 23 | "full_name":"D A D K A", 24 | "is_private":true, 25 | "profile_pic_url":"https://instagram.fbts8-1.fna.fbcdn.net/v/t51.2885-19/s150x150/85127694_1845998595699747_4344528628730560512_n.jpg?_nc_ht=instagram.fbts8-1.fna.fbcdn.net&_nc_ohc=m-RnGusIi5IAX9eo5iV&oh=19e359aec1d2c3b6de7d794b6175b2bf&oe=5FCACC4A", 26 | "profile_pic_id":"2247219510640111047_536091762", 27 | "is_verified":false, 28 | "latest_reel_media":0, 29 | "story_reel_media_ids":[ 30 | 31 | ] 32 | } 33 | ], 34 | "method": "get", 35 | "endpoint": "media/{media_id}/likers" 36 | } -------------------------------------------------------------------------------- /original_requests/post/upload.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "get", 3 | "endpoint": "https://i.instagram.com/api/v1/feed/user/{user_id}/", 4 | "query_params": { 5 | "exclude_comment": "true", 6 | "only_fetch_first_carousel_media": "false", 7 | "max_id": "{}" 8 | }, 9 | "response": { 10 | "auto_load_more_enabled": true, 11 | "items": [ 12 | { 13 | "can_see_insights_as_brand": false, 14 | "can_view_more_preview_comments": false, 15 | "can_viewer_reshare": false, 16 | "can_viewer_save": true, 17 | "caption": { 18 | "bit_flags": 0, 19 | "content_type": "comment", 20 | "created_at": 1498554151, 21 | "created_at_utc": 1498554151, 22 | "did_report_as_spam": false, 23 | "media_id": 1545793193277167087, 24 | "pk": 17863630882140720, 25 | "share_enabled": false, 26 | "status": "Active", 27 | "text": "Comment \"AMAZING\" letter for letter😍🥑\nFollow @viralpos.t (me) for more🍃\n-\nClick the link in my Bio for Awesome emoijs!😍💕", 28 | "type": 1, 29 | "user": { 30 | "account_badges": [], 31 | "full_name": "tienerzinnetje.s", 32 | "has_anonymous_profile_picture": false, 33 | "is_favorite": false, 34 | "is_private": true, 35 | "is_unpublished": false, 36 | "is_verified": false, 37 | "latest_reel_media": 0, 38 | "pk": 4478472759, 39 | "profile_pic_id": "1668785598670642821_4478472759", 40 | "profile_pic_url": "https://scontent-amt2-1.cdninstagram.com/v/t51.2885-19/s150x150/25037686_1559699034114489_801156905606053888_n.jpg?_nc_ht=scontent-amt2-1.cdninstagram.com&_nc_ohc=2iJnWkbO0GwAX8A06J0&oh=6f37effb3890f77843bf65baceb25fa1&oe=5F4B291B", 41 | "username": "tienerzinnetje.s" 42 | }, 43 | "user_id": 4478472759 44 | }, 45 | "caption_is_edited": true, 46 | "client_cache_key": "MTU0NTc5MzE5MzI3NzE2NzA4Nw==.2", 47 | "code": "BVzwrglgJXv67zZCula8x_JTKbpvdPgqJi3bGc0", 48 | "comment_count": 0, 49 | "comment_likes_enabled": true, 50 | "comment_threading_enabled": true, 51 | "device_timestamp": 1498492874, 52 | "filter_type": 0, 53 | "has_audio": true, 54 | "has_liked": false, 55 | "has_more_comments": false, 56 | "id": "1545793193277167087_4478472759", 57 | "image_versions2": { 58 | "candidates": [ 59 | { 60 | "height": 640, 61 | "url": "https://scontent-ams4-1.cdninstagram.com/v/t51.2885-15/e15/19436701_552027881853327_1583884119429873664_n.jpg?_nc_ht=scontent-ams4-1.cdninstagram.com&_nc_cat=103&_nc_ohc=kHHjvSJkMd4AX_P5O80&oh=0da304eb8948ae5dce6b5dd8dea1f920&oe=5F22E3F1", 62 | "width": 640 63 | }, 64 | { 65 | "height": 480, 66 | "url": "https://scontent-ams4-1.cdninstagram.com/v/t51.2885-15/e15/s480x480/19436701_552027881853327_1583884119429873664_n.jpg?_nc_ht=scontent-ams4-1.cdninstagram.com&_nc_cat=103&_nc_ohc=kHHjvSJkMd4AX_P5O80&oh=6bf836b93427160161d9c85e097ce394&oe=5F22D5FC", 67 | "width": 480 68 | } 69 | ] 70 | }, 71 | "inline_composer_display_condition": "impression_trigger", 72 | "inline_composer_imp_trigger_time": 5, 73 | "is_in_profile_grid": false, 74 | "like_count": 3351, 75 | "max_num_visible_preview_comments": 2, 76 | "media_type": 2, 77 | "organic_tracking_token": "eyJ2ZXJzaW9uIjo1LCJwYXlsb2FkIjp7ImlzX2FuYWx5dGljc190cmFja2VkIjp0cnVlLCJ1dWlkIjoiNjJjNzk1Y2NiYzYzNDc0NGFhZDk1ZTliZDFhZmUzNTkxNTQ1NzkzMTkzMjc3MTY3MDg3Iiwic2VydmVyX3Rva2VuIjoiMTU5NTk2NTUzNTkwM3wxNTQ1NzkzMTkzMjc3MTY3MDg3fDIwOTcwMTcwNTJ8NWY1ODlkYzdiMmZjNmNiNGY5ZWE1MDNiOGQ5MzBkYjA4YzUxMzRkMmMzYmU2Njk0ZDAyMjU2OTdmM2UzMjNiNyJ9LCJzaWduYXR1cmUiOiIifQ==", 78 | "original_height": 640, 79 | "original_width": 640, 80 | "photo_of_you": false, 81 | "pk": 1545793193277167087, 82 | "profile_grid_control_enabled": false, 83 | "sharing_friction_info": { 84 | "bloks_app_url": null, 85 | "should_have_sharing_friction": false 86 | }, 87 | "taken_at": 1498492930, 88 | "top_likers": [], 89 | "user": { 90 | "account_badges": [], 91 | "full_name": "tienerzinnetje.s", 92 | "has_anonymous_profile_picture": false, 93 | "is_favorite": false, 94 | "is_private": true, 95 | "is_unpublished": false, 96 | "is_verified": false, 97 | "latest_reel_media": 0, 98 | "pk": 4478472759, 99 | "profile_pic_id": "1668785598670642821_4478472759", 100 | "profile_pic_url": "https://scontent-amt2-1.cdninstagram.com/v/t51.2885-19/s150x150/25037686_1559699034114489_801156905606053888_n.jpg?_nc_ht=scontent-amt2-1.cdninstagram.com&_nc_ohc=2iJnWkbO0GwAX8A06J0&oh=6f37effb3890f77843bf65baceb25fa1&oe=5F4B291B", 101 | "username": "tienerzinnetje.s" 102 | }, 103 | "video_duration": 0.0, 104 | "video_versions": [ 105 | { 106 | "height": 640, 107 | "id": "2534915079913092", 108 | "type": 101, 109 | "url": "https://scontent-amt2-1.cdninstagram.com/v/t50.2886-16/19514839_240388016461674_2673480789433253888_n.mp4?efg=eyJxZV9ncm91cHMiOiJbXCJpZ19wcm9ncmVzc2l2ZV91cmxnZW4ucHJvZHVjdF90eXBlLmZlZWRcIl0ifQ&_nc_ht=scontent-amt2-1.cdninstagram.com&_nc_cat=106&_nc_ohc=t7qahsNwULMAX8kyyIB&oe=5F22BECA&oh=0f2f44feae683e0d327a3be607716f71", 110 | "width": 640 111 | }, 112 | { 113 | "height": 640, 114 | "id": "2534915079913092", 115 | "type": 102, 116 | "url": "https://scontent-amt2-1.cdninstagram.com/v/t50.2886-16/19514839_240388016461674_2673480789433253888_n.mp4?efg=eyJxZV9ncm91cHMiOiJbXCJpZ19wcm9ncmVzc2l2ZV91cmxnZW4ucHJvZHVjdF90eXBlLmZlZWRcIl0ifQ&_nc_ht=scontent-amt2-1.cdninstagram.com&_nc_cat=106&_nc_ohc=t7qahsNwULMAX8kyyIB&oe=5F22BECA&oh=0f2f44feae683e0d327a3be607716f71", 117 | "width": 640 118 | }, 119 | { 120 | "height": 640, 121 | "id": "2534915079913092", 122 | "type": 103, 123 | "url": "https://scontent-amt2-1.cdninstagram.com/v/t50.2886-16/19514839_240388016461674_2673480789433253888_n.mp4?efg=eyJxZV9ncm91cHMiOiJbXCJpZ19wcm9ncmVzc2l2ZV91cmxnZW4ucHJvZHVjdF90eXBlLmZlZWRcIl0ifQ&_nc_ht=scontent-amt2-1.cdninstagram.com&_nc_cat=106&_nc_ohc=t7qahsNwULMAX8kyyIB&oe=5F22BECA&oh=0f2f44feae683e0d327a3be607716f71", 124 | "width": 640 125 | } 126 | ], 127 | "view_count": 4.0 128 | } 129 | ] 130 | } 131 | } -------------------------------------------------------------------------------- /original_requests/search.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "endpoints": "users/search/", 4 | "query_params": { 5 | "q": "username", 6 | "count": 10, 7 | "timezone_offset": 7200 8 | }, 9 | "response": { 10 | "num_results": 1, 11 | "users": [ 12 | { 13 | "pk": 2283025667, 14 | "username": "username", 15 | "full_name": "", 16 | "is_private": false, 17 | "profile_pic_url": "", 18 | "profile_pic_id": "", 19 | "is_verified": false, 20 | "has_anonymous_profile_picture": false, 21 | "mutual_followers_count": 0, 22 | "account_badges": [], 23 | "social_context": "Following", 24 | "search_social_context": "Following", 25 | "friendship_status": { 26 | "following": true, 27 | "is_private": false, 28 | "incoming_request": false, 29 | "outgoing_request": false, 30 | "is_bestie": false, 31 | "is_restricted": false 32 | }, 33 | "latest_reel_media": 0 34 | } 35 | ], 36 | "has_more": false, 37 | "rank_token": "1595729245333|a75fdfda41d0680dba5347ad65e0a5399bb2b83cd21f7cb8d95eaa8bb5e0ee03", 38 | "clear_client_cache": false, 39 | "status": "ok" 40 | } 41 | } 42 | ] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | APScheduler==3.9.1 2 | certifi==2022.12.7 3 | charset-normalizer==2.0.12 4 | idna==3.7 5 | imagesize==1.3.0 6 | orjson==3.9.15 7 | pycryptodomex==3.19.1 8 | pytz==2022.1 9 | pytz-deprecation-shim==0.1.0.post0 10 | requests==2.32.2 11 | six==1.16.0 12 | tzdata==2022.1 13 | tzlocal==4.2 14 | urllib3==1.26.9 15 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | from distutils.core import setup 3 | 4 | setup( 5 | name='instauto', 6 | packages=setuptools.find_packages(), 7 | version='2.1.0', 8 | license='MIT', 9 | description='Python wrapper for the private Instagram API', 10 | author='Stan van Rooy', 11 | author_email='stanvanrooy6@gmail.com', 12 | url='https://github.com/stanvanrooy/instauto', 13 | download_url='https://github.com/stanvanrooy/instauto/archive/2.1.0.tar.gz', 14 | keywords=['instagram api', 'private instagram api'], 15 | install_requires=[ 16 | 'requests', 17 | 'apscheduler', 18 | 'pycryptodomex', 19 | 'imagesize', 20 | 'orjson' 21 | ], 22 | classifiers=[ 23 | 'Development Status :: 5 - Production/Stable', 24 | 'Intended Audience :: Developers', 25 | 'Environment :: Console', 26 | 'Topic :: Software Development :: Libraries :: Python Modules', 27 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3) ', 28 | 'Programming Language :: Python :: 3.8', 29 | 'Programming Language :: Python :: 3.7', 30 | 'Programming Language :: Python :: 3.6' 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /test_feed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanvanrooy/instauto/43866223945c9e8a0e8029ba01bb970ee95ca555/test_feed.jpg -------------------------------------------------------------------------------- /test_story.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanvanrooy/instauto/43866223945c9e8a0e8029ba01bb970ee95ca555/test_story.jpg --------------------------------------------------------------------------------