├── .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 | [](#contributors-)
3 | [](https://github.com/stanvanrooy/instauto/stargazers)
4 | [](https://pypi.python.org/project/instauto/)
5 | [](https://pepy.tech/project/instauto)
6 | [](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 |
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
--------------------------------------------------------------------------------