├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── tests.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── dependencies.py ├── docker-compose.yml ├── examples ├── htpasswd └── nginx.conf ├── golang ├── .gitignore ├── README.md ├── client.go └── go.mod ├── helpers.py ├── logo.png ├── main.py ├── requirements.txt ├── routers ├── __init__.py ├── album.py ├── auth.py ├── clip.py ├── igtv.py ├── insights.py ├── media.py ├── photo.py ├── story.py ├── user.py └── video.py ├── runtime.txt ├── storages.py ├── swift ├── .gitignore ├── README.md └── client.swift └── tests.py /.dockerignore: -------------------------------------------------------------------------------- 1 | /Dockerfile 2 | /docker-compose.yml 3 | /golang 4 | /swift 5 | /examples 6 | /.git 7 | /.github 8 | /CODE_OF_CONDUCT.md 9 | /LICENSE 10 | /logo.png 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | 7 | --- 8 | 9 | ### Try Instagrapi SaaS with a free trial https://hikerapi.com/p/5GBWznd3 10 | 11 | **Describe the bug** 12 | A clear and concise description of what the bug is. 13 | 14 | **To Reproduce** 15 | Provide a piece of code to reproduce the problem. 16 | 17 | **Traceback** 18 | Show your full traceback so that it is clear where exactly the error occurred. 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Ubuntu 21.04] 28 | - Python version [e.g. 3.8.3] 29 | - instagrapi version [e.g. 1.9.3, not "latest"] 30 | - moveipy version if used 31 | - imagemagick version if used 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | 7 | --- 8 | 9 | ### Try Instagrapi SaaS with a free trial https://hikerapi.com/p/5GBWznd3 10 | 11 | **Is your feature request related to a problem? Please describe.** 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.8 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.8 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install flake8 pytest 27 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 28 | - name: Lint with flake8 29 | run: | 30 | # stop the build if there are Python syntax errors or undefined names 31 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 33 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 34 | - name: Test with pytest 35 | run: | 36 | pytest tests.py::test_media_pk_from_code 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | .idea 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | # vscode user settings 134 | .vscode/ 135 | 136 | # user database 137 | db.json 138 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | https://t.me/instagrapi. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | RUN apt-get update \ 4 | && apt-get install gcc ffmpeg -y \ 5 | && apt-get clean 6 | 7 | EXPOSE 8000 8 | ENV PIP_DISABLE_PIP_VERSION_CHECK=1 9 | ENV PIP_NO_CACHE_DIR=1 10 | ENV PYTHONUNBUFFERED=1 11 | ENV PYTHONPATH=/app 12 | COPY . /app/ 13 | WORKDIR /app 14 | RUN pip install -r requirements.txt 15 | 16 | CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn main:app --host=0.0.0.0 --port=${PORT:-8000} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | If you want to work with Instagrapi (business interests), we strongly advise you to prefer [HikerAPI](https://hikerapi.com/p/7RAo9ACK) project. 2 | However, you won't need to spend weeks or even months setting it up. 3 | The best service available today is [HikerAPI](https://hikerapi.com/p/7RAo9ACK), which handles 4–5 million daily requests, provides support around-the-clock, and offers partners a special rate. 4 | In many instances, our clients tried to save money and preferred instagrapi, but in our experience, they ultimately returned to [HikerAPI](https://hikerapi.com/p/7RAo9ACK) after spending much more time and money. 5 | It will be difficult to find good accounts, good proxies, or resolve challenges, and IG will ban your accounts. 6 | 7 | The instagrapi more suits for testing or research than a working business! 8 | 9 | ✨ [aiograpi - Asynchronous Python library for Instagram Private API](https://github.com/subzeroid/aiograpi) ✨ 10 | 11 | ### We recommend using our services: 12 | 13 | * [LamaTok](https://lamatok.com/p/43zuPqyT) for TikTok API 🔥 14 | * [HikerAPI](https://hikerapi.com/p/7RAo9ACK) for Instagram API ⚡⚡⚡ 15 | * [DataLikers](https://datalikers.com/p/S9Lv5vBy) for Instagram Datasets 🚀 16 | 17 | # RESTful API Service 18 | 19 | Allows you to use the Instagram Private API on any operating system from any programming language (C++, C#, F#, D, [Golang](golang), Erlang, Elixir, Nim, Haskell, Lisp, Closure, Julia, R, Java, Kotlin, Scala, OCaml, JavaScript, Crystal, Ruby, Rust, [Swift](swift), Objective-C, Visual Basic, .NET, Pascal, Perl, Lua, PHP and others) to automate the work of your accounts. 20 | 21 | [Support Chat in Telegram](https://t.me/instagrapi) 22 | ![](https://gist.githubusercontent.com/m8rge/4c2b36369c9f936c02ee883ca8ec89f1/raw/c03fd44ee2b63d7a2a195ff44e9bb071e87b4a40/telegram-single-path-24px.svg) 23 | 24 | # Features 25 | 26 | 1. Authorization: Login, support 2FA and manage settings 27 | 2. Media: info, delete, edit, like, archive and much more else 28 | 3. Video: download, upload to feed and story 29 | 4. Photo: download, upload to feed and story 30 | 5. IGTV: download, upload to feed and story 31 | 6. Clip (Reels): download, upload to feed and story 32 | 7. Album: download, upload to feed and story 33 | 8. Story: info, delete, seen, download and much more else 34 | 9. User: followers/following, info, follow/unfollow, remove_follower and much more else 35 | 10. Insights: media, account 36 | 37 | # Installation 38 | 39 | Install ImageMagick library: 40 | ``` 41 | sudo apt install imagemagick 42 | ``` 43 | 44 | ...and comment the line with strict security policies of ImageMagick in `/etc/ImageMagick-6/policy.xml`: 45 | ``` 46 | 47 | ``` 48 | 49 | Run docker container: 50 | ``` 51 | docker run subzeroid/instagrapi-rest 52 | ``` 53 | 54 | Or clone the repository: 55 | ``` 56 | git clone https://github.com/subzeroid/instagrapi-rest.git 57 | cd instagrapi-rest 58 | ``` 59 | 60 | Build your image and run the container: 61 | ``` 62 | docker build -t instagrapi-rest . 63 | docker run -p 8000:8000 instagrapi-rest 64 | ``` 65 | 66 | Or you can use docker-compose: 67 | ``` 68 | docker-compose up -d 69 | ``` 70 | 71 | Or manual installation and launch: 72 | 73 | ``` 74 | python3 -m venv .venv 75 | . .venv/bin/activate 76 | pip install -U wheel pip -Ur requirements.txt 77 | uvicorn main:app --host 0.0.0.0 --port 8000 --reload 78 | ``` 79 | 80 | # Usage 81 | 82 | Open in browser [http://localhost:8000/docs](http://localhost:8000/docs) and follow the instructions 83 | 84 | ![swagger](https://user-images.githubusercontent.com/546889/126989357-8214aa5c-fe42-4be4-b118-bd3585cd3292.png) 85 | 86 | 87 | Get sessionid: 88 | 89 | ``` 90 | curl -X 'POST' \ 91 | 'http://localhost:8000/auth/login' \ 92 | -H 'accept: application/json' \ 93 | -H 'Content-Type: application/x-www-form-urlencoded' \ 94 | -d 'username=&password=&verification_code=<2FA CODE>' 95 | ``` 96 | 97 | Upload photo: 98 | 99 | ``` 100 | curl -X 'POST' \ 101 | 'http://localhost:8000/photo/upload_to_story' \ 102 | -H 'accept: application/json' \ 103 | -H 'Content-Type: multipart/form-data' \ 104 | -F 'sessionid=' \ 105 | -F 'file=@photo.jpeg;type=image/jpeg' 106 | ``` 107 | 108 | Upload photo by URL: 109 | 110 | ``` 111 | curl -X 'POST' \ 112 | 'https://localhost:8000/photo/upload_to_story/by_url' \ 113 | -H 'accept: application/json' \ 114 | -H 'Content-Type: application/x-www-form-urlencoded' \ 115 | -d 'sessionid=&url=https%3A%2F%2Fapi.telegram.org%2Ffile%2Ftest.jpg' 116 | ``` 117 | 118 | Upload video: 119 | 120 | ``` 121 | curl -X 'POST' \ 122 | 'http://localhost:8000/video/upload_to_story' \ 123 | -H 'accept: application/json' \ 124 | -H 'Content-Type: multipart/form-data' \ 125 | -F 'sessionid=' \ 126 | -F 'file=@video.mp4;type=video/mp4' 127 | ``` 128 | 129 | Upload video by URL: 130 | 131 | ``` 132 | curl -X 'POST' \ 133 | 'https://localhost:8000/video/upload_to_story/by_url' \ 134 | -H 'accept: application/json' \ 135 | -H 'Content-Type: application/x-www-form-urlencoded' \ 136 | -d 'sessionid=&url=https%3A%2F%2Fapi.telegram.org%2Ffile%2Ftest.MP4' 137 | ``` 138 | 139 | # Generating client code 140 | 141 | You can use [this repo](https://www.npmjs.com/package/@openapitools/openapi-generator-cli) to generate client code for this rest api in any language you want to use. 142 | 143 | Exapmle: 144 | `openapi-generator-cli generate -g python -i https://localhost:8000]/openapi.json --skip-validate-spec` 145 | Note `skip-validate-spec` is not necesserily required, when running it on my pc it couldn't validate the spec for some reason. 146 | 147 | # Testing 148 | 149 | Tests can be run like this: 150 | 151 | `docker-compose run api pytest tests.py` 152 | 153 | One test: 154 | 155 | `docker-compose run api pytest tests.py::test_media_pk_from_code` 156 | 157 | or without docker-compose: 158 | 159 | `docker run --rm -v "$(pwd):/app" instagrapi-rest pytest tests.py` 160 | 161 | # Development 162 | 163 | For debugging: 164 | 165 | `docker-compose run --service-ports api` 166 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "instagrapi-rest", 3 | "description": "Deploy a Instagrapi RESTful API service (Instagram Private API)", 4 | "keywords": [ 5 | "instagram", 6 | "instagram-private-api", 7 | "instagram-api", 8 | "instagram-bot", 9 | "instagrapi", 10 | "instagrapi-bot", 11 | "instagrapi-rest", 12 | "python", 13 | "fastapi", 14 | "rest", 15 | "instagrapi", 16 | "private", 17 | "api" 18 | ], 19 | "website": "https://github.com/subzeroid/instagrapi-rest", 20 | "repository": "https://github.com/subzeroid/instagrapi-rest", 21 | "success_url": "/docs", 22 | "logo": "https://github.com/subzeroid/instagrapi-rest/raw/main/logo.png" 23 | } 24 | -------------------------------------------------------------------------------- /dependencies.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | 3 | from storages import ClientStorage 4 | 5 | 6 | def get_clients() -> Generator: 7 | try: 8 | clients = ClientStorage() 9 | yield clients 10 | finally: 11 | clients.close() 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | api: 4 | build: . 5 | stdin_open: true 6 | tty: true 7 | ports: 8 | - "8000:8000" 9 | volumes: 10 | - .:/app 11 | -------------------------------------------------------------------------------- /examples/htpasswd: -------------------------------------------------------------------------------- 1 | example:$apr1$b9Z9NYvE$xdCCl15USWsglOcnckRUh0 -------------------------------------------------------------------------------- /examples/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | server_name example.com; 3 | client_max_body_size 300m; 4 | proxy_connect_timeout 600s; 5 | proxy_read_timeout 600s; 6 | proxy_send_timeout 600s; 7 | 8 | auth_basic "closed site"; 9 | auth_basic_user_file htpasswd; 10 | # create echo ":`openssl passwd -apr1`" > /etc/nginx/htpasswd 11 | 12 | location / { 13 | proxy_pass http://localhost:8000; 14 | } 15 | # listen [::]:443 ssl ipv6only=on; # managed by Certbot 16 | # listen 443 ssl; # managed by Certbot 17 | # ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot 18 | # ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot 19 | # include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 20 | # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 21 | } 22 | # server { 23 | # if ($host = example.com) { 24 | # return 301 https://$host$request_uri; 25 | # } # managed by Certbot 26 | # listen 80 default_server; 27 | # listen [::]:80 default_server; 28 | # server_name example.com; 29 | # return 404; # managed by Certbot 30 | # } -------------------------------------------------------------------------------- /golang/.gitignore: -------------------------------------------------------------------------------- 1 | client 2 | -------------------------------------------------------------------------------- /golang/README.md: -------------------------------------------------------------------------------- 1 | Examples of working with the instagrapi-rest service 2 | 3 | ## Build 4 | 5 | `go build client.go` 6 | 7 | ## Launch 8 | 9 | `./client` 10 | -------------------------------------------------------------------------------- /golang/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/go-resty/resty/v2" 14 | ) 15 | 16 | const BaseUrl = "http://localhost:8000" 17 | 18 | var client *resty.Client 19 | 20 | func init() { 21 | client = resty.New() 22 | client.SetHostURL(BaseUrl) 23 | client.SetContentLength(true) 24 | } 25 | 26 | func getVersion() string { 27 | resp, err := client.R().Get("/version") 28 | if err != nil { 29 | log.Println(err) 30 | } 31 | return resp.String() 32 | } 33 | 34 | func pkFromCode(code string) string { 35 | resp, err := client.R(). 36 | SetQueryParams(map[string]string{ 37 | "code": code, 38 | }). 39 | Get("/media/pk_from_code") 40 | if err != nil { 41 | log.Println(err) 42 | } 43 | return resp.String() 44 | } 45 | 46 | func pkFromUrl(url string) string { 47 | resp, err := client.R(). 48 | SetQueryParams(map[string]string{ 49 | "url": url, 50 | }). 51 | Get("/media/pk_from_url") 52 | if err != nil { 53 | log.Println(err) 54 | } 55 | return resp.String() 56 | } 57 | 58 | func id_from_username(sessionid, username string) string { 59 | resp, err := client.R(). 60 | SetFormData(map[string]string{ 61 | "sessionid": sessionid, 62 | "username": username, 63 | }).Post("/user/id_from_username") 64 | if err != nil { 65 | log.Println(err) 66 | } 67 | return resp.String() 68 | } 69 | 70 | func login(username, password string) string { 71 | resp, err := client.R(). 72 | SetFormData(map[string]string{ 73 | "username": username, 74 | "password": password, 75 | }).Post("/auth/login") 76 | if err != nil { 77 | log.Println(err) 78 | } 79 | if resp.StatusCode() != 200 { 80 | log.Println(resp.String()) 81 | return "" 82 | } 83 | return resp.String() 84 | } 85 | 86 | func relogin(sessionid string) { 87 | resp, err := client.R(). 88 | SetFormData(map[string]string{ 89 | "sessionid": sessionid, 90 | }).Post("/auth/relogin") 91 | if err != nil { 92 | log.Println(err) 93 | } 94 | if resp.StatusCode() != 200 { 95 | log.Println(resp.String()) 96 | } 97 | } 98 | 99 | func photo_download(sessionid, media_pk, folder string) string { 100 | resp, err := client.R(). 101 | SetFormData(map[string]string{ 102 | "sessionid": sessionid, 103 | "media_pk": media_pk, 104 | "folder": folder, 105 | "returnFile": "false", 106 | }).Post("/photo/download") 107 | if err != nil { 108 | log.Println(err) 109 | return "" 110 | } 111 | if resp.StatusCode() != 200 { 112 | log.Println(resp.String()) 113 | return "" 114 | } 115 | return resp.String() 116 | } 117 | 118 | func video_download(sessionid, media_pk, folder string) string { 119 | resp, err := client.R(). 120 | SetFormData(map[string]string{ 121 | "sessionid": sessionid, 122 | "media_pk": media_pk, 123 | "folder": folder, 124 | "returnFile": "false", 125 | }).Post("/video/download") 126 | if err != nil { 127 | log.Println(err) 128 | return "" 129 | } 130 | if resp.StatusCode() != 200 { 131 | log.Println(resp.String()) 132 | return "" 133 | } 134 | return resp.String() 135 | } 136 | 137 | func getSettings(sessionid string) string { 138 | resp, err := client.R(). 139 | SetQueryParams(map[string]string{ 140 | "sessionid": sessionid, 141 | }).Get("/auth/settings/get") 142 | if err != nil { 143 | log.Println(err) 144 | } 145 | if resp.StatusCode() != 200 { 146 | log.Println(resp.String()) 147 | return "{}" 148 | } 149 | return resp.String() 150 | } 151 | 152 | func setSettings(sessionid, settings string) string { 153 | resp, err := client.R(). 154 | SetFormData(map[string]string{ 155 | "sessionid": sessionid, 156 | "settings": settings, 157 | }).Post("/auth/settings/set") 158 | if err != nil { 159 | log.Println(err) 160 | } 161 | if resp.StatusCode() != 200 { 162 | log.Println(resp.String()) 163 | return "{}" 164 | } 165 | return resp.String() 166 | } 167 | 168 | func loadSettings(file string) string { 169 | if _, err := os.Stat(file); err != nil { 170 | if os.IsNotExist(err) { 171 | return "{}" 172 | } 173 | } 174 | content, err := ioutil.ReadFile(file) 175 | if err != nil { 176 | log.Println(err) 177 | return "{}" 178 | } 179 | return string(content) 180 | } 181 | 182 | func saveSettings(file string, settings string) { 183 | fd, err := os.Create(file) 184 | defer fd.Close() 185 | _, err = fd.WriteString(settings) 186 | if err != nil { 187 | log.Println(err) 188 | } 189 | } 190 | 191 | func user_stories(sessionid, id string, amount int) []string { 192 | resp, err := client.R(). 193 | SetFormData(map[string]string{ 194 | "sessionid": sessionid, 195 | "user_id": id, 196 | "amount": strconv.Itoa(amount), 197 | }).Post("/story/user_stories") 198 | if err != nil { 199 | log.Println(err) 200 | return []string{} 201 | } 202 | if resp.StatusCode() != 200 { 203 | log.Println(resp.String()) 204 | return []string{} 205 | } 206 | 207 | log.Println(resp.String()) 208 | 209 | var ( 210 | stories []map[string]interface{} 211 | result []string 212 | ) 213 | 214 | json.Unmarshal([]byte(resp.String()), &stories) 215 | for _, v := range stories { 216 | result = append(result, strings.SplitN(v["id"].(string), "_", 2)[0]) 217 | } 218 | return result 219 | } 220 | 221 | func igtv_download(sessionid, media_pk, folder string) string { 222 | resp, err := client.R(). 223 | SetFormData(map[string]string{ 224 | "sessionid": sessionid, 225 | "media_pk": media_pk, 226 | "folder": folder, 227 | "returnFile": "false", 228 | }).Post("/igtv/download") 229 | if err != nil { 230 | log.Println(err) 231 | return "" 232 | } 233 | if resp.StatusCode() != 200 { 234 | log.Println(resp.String()) 235 | return "" 236 | } 237 | return resp.String() 238 | } 239 | 240 | func story_download(sessionid, story_pk, folder string) string { 241 | resp, err := client.R(). 242 | SetFormData(map[string]string{ 243 | "sessionid": sessionid, 244 | "story_pk": story_pk, 245 | "folder": folder, 246 | "returnFile": "false", 247 | }).Post("/story/download") 248 | if err != nil { 249 | log.Println(err) 250 | return "" 251 | } 252 | if resp.StatusCode() != 200 { 253 | log.Println(resp.String()) 254 | return "" 255 | } 256 | return resp.String() 257 | } 258 | 259 | func album_upload(sessionid string, files []string, caption string) string { 260 | r := client.R() 261 | for _, path := range files { 262 | path = strings.Trim(path, "\" ") 263 | filedata, _ := ioutil.ReadFile(path) 264 | r = r.SetFileReader("files", filepath.Base(path), bytes.NewReader(filedata)) 265 | } 266 | 267 | resp, err := r.SetFormData(map[string]string{ 268 | "sessionid": sessionid, 269 | "caption": caption, 270 | }).Post("/album/upload") 271 | 272 | if err != nil { 273 | log.Println(err) 274 | return "" 275 | } 276 | if resp.StatusCode() != 200 { 277 | log.Println(resp.String()) 278 | return "" 279 | } 280 | return resp.String() 281 | } 282 | 283 | func photo_upload_to_story(sessionid, filephoto string) string { 284 | resp, err := client.R(). 285 | SetFile("file", strings.Trim(filephoto, "\" ")). 286 | SetFormData(map[string]string{ 287 | "sessionid": sessionid, 288 | }).Post("/photo/upload_to_story") 289 | 290 | if err != nil { 291 | log.Println(err) 292 | return "" 293 | } 294 | if resp.StatusCode() != 200 { 295 | log.Println(resp.String()) 296 | return "" 297 | } 298 | return resp.String() 299 | } 300 | 301 | func main() { 302 | log.Println("Version: ", getVersion()) 303 | log.Println("pkFromCode: B1LbfVPlwIA -> ", pkFromCode("B1LbfVPlwIA")) 304 | settings := loadSettings("./settings.json") 305 | sessionid := "" 306 | if settings != "{}" { 307 | sessionid = setSettings("", settings) 308 | } else { 309 | sessionid = login("example", "test") 310 | } 311 | if sessionid != "" { 312 | log.Println("SESSIONID: ", sessionid) 313 | settings = getSettings(sessionid) 314 | if settings != "{}" { 315 | log.Println(settings) 316 | saveSettings("./settings.json", settings) 317 | } 318 | } else { 319 | log.Fatal("Login error!") 320 | } 321 | 322 | photo := photo_download(sessionid, pkFromUrl("https://www.instagram.com/p/COQebHWhRUg/"), "") 323 | log.Println("photo_download:", photo) 324 | 325 | photo_story := photo_upload_to_story(sessionid, photo) 326 | log.Println("photo_upload_to_story:", photo_story) 327 | 328 | video := video_download(sessionid, pkFromUrl("https://www.instagram.com/p/CGgDsi7JQdS/"), "") 329 | log.Println("video_download:", video) 330 | 331 | igtv := igtv_download(sessionid, pkFromUrl("https://www.instagram.com/p/CRHO6N6HLvQ/"), "") 332 | log.Println("igtv_download:", igtv) 333 | 334 | stories := user_stories(sessionid, id_from_username(sessionid, "therock"), 1) 335 | log.Println(stories) 336 | 337 | if len(stories) > 0 { 338 | story := story_download(sessionid, stories[0], "") 339 | log.Println("story_download:", story) 340 | } 341 | 342 | result := album_upload(sessionid, []string{photo, video}, "hello world") 343 | log.Println(result) 344 | 345 | } 346 | -------------------------------------------------------------------------------- /golang/go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.16 4 | 5 | require github.com/go-resty/resty/v2 v2.6.0 // indirect 6 | -------------------------------------------------------------------------------- /helpers.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import os 3 | from instagrapi.story import StoryBuilder 4 | 5 | 6 | async def photo_upload_story_as_video(cl, content, **kwargs): 7 | with tempfile.NamedTemporaryFile(suffix='.jpg') as fp: 8 | fp.write(content) 9 | mentions = kwargs.get('mentions') or [] 10 | caption = kwargs.get('caption') or '' 11 | video = StoryBuilder(fp.name, caption, mentions).photo(15) 12 | return cl.video_upload_to_story(video.path, **kwargs) 13 | 14 | 15 | async def photo_upload_story_as_photo(cl, content, **kwargs): 16 | with tempfile.NamedTemporaryFile(suffix='.jpg') as fp: 17 | fp.write(content) 18 | return cl.photo_upload_to_story(fp.name, **kwargs) 19 | 20 | 21 | async def video_upload_story(cl, content, **kwargs): 22 | with tempfile.NamedTemporaryFile(suffix='.mp4') as fp: 23 | fp.write(content) 24 | mentions = kwargs.get('mentions') or [] 25 | caption = kwargs.get('caption') or '' 26 | video = StoryBuilder(fp.name, caption, mentions).video(15) 27 | return cl.video_upload_to_story(video.path, **kwargs) 28 | 29 | 30 | async def photo_upload_post(cl, content, **kwargs): 31 | with tempfile.NamedTemporaryFile(suffix='.jpg') as fp: 32 | fp.write(content) 33 | return cl.photo_upload(fp.name, **kwargs) 34 | 35 | 36 | async def video_upload_post(cl, content, **kwargs): 37 | with tempfile.NamedTemporaryFile(suffix='.mp4') as fp: 38 | fp.write(content) 39 | return cl.video_upload(fp.name, **kwargs) 40 | 41 | 42 | async def album_upload_post(cl, files, **kwargs): 43 | with tempfile.TemporaryDirectory() as td: 44 | paths = [] 45 | for i in range(len(files)): 46 | filename, ext = os.path.splitext(files[i].filename) 47 | fp = tempfile.NamedTemporaryFile(suffix=ext, delete=False, dir=td) 48 | fp.write(await files[i].read()) 49 | fp.close() 50 | paths.append(fp.name) 51 | return cl.album_upload(paths, **kwargs) 52 | 53 | 54 | async def igtv_upload_post(cl, content, **kwargs): 55 | with tempfile.NamedTemporaryFile(suffix='.mp4') as fp: 56 | fp.write(content) 57 | return cl.igtv_upload(fp.name, **kwargs) 58 | 59 | 60 | async def clip_upload_post(cl, content, **kwargs): 61 | with tempfile.NamedTemporaryFile(suffix='.mp4') as fp: 62 | fp.write(content) 63 | return cl.clip_upload(fp.name, **kwargs) 64 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subzeroid/instagrapi-rest/8fd8c2c6f9d59d70794d86705c543ea3a9ad3f8e/logo.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | from fastapi import FastAPI 4 | from fastapi.openapi.utils import get_openapi 5 | from starlette.responses import RedirectResponse, JSONResponse 6 | from routers import ( 7 | auth, media, video, photo, user, 8 | igtv, clip, album, story, 9 | insights 10 | ) 11 | 12 | app = FastAPI() 13 | app.include_router(auth.router) 14 | app.include_router(media.router) 15 | app.include_router(video.router) 16 | app.include_router(photo.router) 17 | app.include_router(user.router) 18 | app.include_router(igtv.router) 19 | app.include_router(clip.router) 20 | app.include_router(album.router) 21 | app.include_router(story.router) 22 | app.include_router(insights.router) 23 | 24 | 25 | @app.get("/", tags=["system"], summary="Redirect to /docs") 26 | async def root(): 27 | """Redirect to /docs 28 | """ 29 | return RedirectResponse(url="/docs") 30 | 31 | 32 | @app.get("/version", tags=["system"], summary="Get dependency versions") 33 | async def version(): 34 | """Get dependency versions 35 | """ 36 | versions = {} 37 | for name in ('instagrapi', ): 38 | item = pkg_resources.require(name) 39 | if item: 40 | versions[name] = item[0].version 41 | return versions 42 | 43 | 44 | @app.exception_handler(Exception) 45 | async def handle_exception(request, exc: Exception): 46 | return JSONResponse({ 47 | "detail": str(exc), 48 | "exc_type": str(type(exc).__name__) 49 | }, status_code=500) 50 | 51 | 52 | def custom_openapi(): 53 | if app.openapi_schema: 54 | return app.openapi_schema 55 | # for route in app.routes: 56 | # body_field = getattr(route, 'body_field', None) 57 | # if body_field: 58 | # body_field.type_.__name__ = 'name' 59 | openapi_schema = get_openapi( 60 | title="instagrapi-rest", 61 | version="1.0.0", 62 | description="RESTful API Service for instagrapi", 63 | routes=app.routes, 64 | ) 65 | app.openapi_schema = openapi_schema 66 | return app.openapi_schema 67 | 68 | app.openapi = custom_openapi 69 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.65.1 2 | uvicorn==0.11.3 3 | ipython>=7.17.0 4 | instagrapi>=1.16.30 5 | python-multipart==0.0.5 6 | Pillow==8.1.1 7 | moviepy==1.0.3 8 | pytest-asyncio==0.14.0 9 | httpx==0.17.1 10 | pudb==2021.1 11 | ipdb==0.13.9 12 | aiofiles==0.7.0 13 | requests==2.26.0 14 | starlette~=0.14.2 15 | pytest~=6.2.4 16 | tinydb==4.5.1 17 | -------------------------------------------------------------------------------- /routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subzeroid/instagrapi-rest/8fd8c2c6f9d59d70794d86705c543ea3a9ad3f8e/routers/__init__.py -------------------------------------------------------------------------------- /routers/album.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from pathlib import Path 3 | import json 4 | 5 | from fastapi import APIRouter, Depends, File, UploadFile, Form 6 | from instagrapi.types import Media, Location, Usertag 7 | 8 | from dependencies import ClientStorage, get_clients 9 | from helpers import album_upload_post 10 | 11 | 12 | router = APIRouter( 13 | prefix="/album", 14 | tags=["album"], 15 | responses={404: {"description": "Not found"}}, 16 | ) 17 | 18 | 19 | @router.post("/download", response_model=List[Path]) 20 | async def album_download(sessionid: str = Form(...), 21 | media_pk: int = Form(...), 22 | folder: Optional[Path] = Form(""), 23 | clients: ClientStorage = Depends(get_clients)) -> List[Path]: 24 | """Download photo using media pk 25 | """ 26 | cl = clients.get(sessionid) 27 | result = cl.album_download(media_pk, folder) 28 | return result 29 | 30 | 31 | @router.post("/download/by_urls", response_model=List[Path]) 32 | async def album_download_by_urls(sessionid: str = Form(...), 33 | urls: List[str] = Form(...), 34 | folder: Optional[Path] = Form(""), 35 | clients: ClientStorage = Depends(get_clients)) -> List[Path]: 36 | """Download photo using URL 37 | """ 38 | cl = clients.get(sessionid) 39 | result = cl.album_download_by_urls(urls, folder) 40 | return result 41 | 42 | 43 | @router.post("/upload", response_model=Media) 44 | async def album_upload(sessionid: str = Form(...), 45 | files: List[UploadFile] = File(...), 46 | caption: str = Form(...), 47 | usertags: Optional[List[str]] = Form([]), 48 | location: Optional[Location] = Form(None), 49 | clients: ClientStorage = Depends(get_clients) 50 | ) -> Media: 51 | """Upload album to feed 52 | """ 53 | cl = clients.get(sessionid) 54 | 55 | usernames_tags = [] 56 | for usertag in usertags: 57 | usertag_json = json.loads(usertag) 58 | usernames_tags.append(Usertag(user=usertag_json['user'], x=usertag_json['x'], y=usertag_json['y'])) 59 | 60 | return await album_upload_post( 61 | cl, files, caption=caption, 62 | usertags=usernames_tags, 63 | location=location) 64 | -------------------------------------------------------------------------------- /routers/auth.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest.mock import patch 3 | from typing import Optional, Dict 4 | from fastapi import APIRouter, Depends, Form 5 | from dependencies import ClientStorage, get_clients 6 | 7 | router = APIRouter( 8 | prefix="/auth", 9 | tags=["auth"], 10 | responses={404: {"description": "Not found"}} 11 | ) 12 | 13 | 14 | @router.post("/login") 15 | async def auth_login(username: str = Form(...), 16 | password: str = Form(...), 17 | verification_code: Optional[str] = Form(""), 18 | proxy: Optional[str] = Form(""), 19 | locale: Optional[str] = Form(""), 20 | timezone: Optional[str] = Form(""), 21 | clients: ClientStorage = Depends(get_clients)) -> str: 22 | """Login by username and password with 2FA 23 | """ 24 | cl = clients.client() 25 | if proxy != "": 26 | cl.set_proxy(proxy) 27 | 28 | if locale != "": 29 | cl.set_locale(locale) 30 | 31 | if timezone != "": 32 | cl.set_timezone_offset(timezone) 33 | 34 | # We're mocking the input 35 | with patch('builtins.input', return_value=verification_code): 36 | result = cl.login(username, password) 37 | 38 | if result: 39 | clients.set(cl) 40 | return cl.sessionid 41 | return result 42 | 43 | 44 | @router.post("/login_by_sessionid") 45 | async def auth_login_by_sessionid(sessionid: str = Form(...), 46 | clients: ClientStorage = Depends(get_clients)) -> str: 47 | """Login by sessionid 48 | """ 49 | cl = clients.client() 50 | result = cl.login_by_sessionid(sessionid) 51 | if result: 52 | clients.set(cl) 53 | return cl.sessionid 54 | return result 55 | 56 | 57 | @router.post("/relogin") 58 | async def auth_relogin(sessionid: str = Form(...), 59 | clients: ClientStorage = Depends(get_clients)) -> str: 60 | """Relogin by username and password (with clean cookies) 61 | """ 62 | cl = clients.get(sessionid) 63 | result = cl.relogin() 64 | return result 65 | 66 | 67 | @router.get("/settings/get") 68 | async def settings_get(sessionid: str, 69 | clients: ClientStorage = Depends(get_clients)) -> Dict: 70 | """Get client's settings 71 | """ 72 | cl = clients.get(sessionid) 73 | return cl.get_settings() 74 | 75 | 76 | @router.post("/settings/set") 77 | async def settings_set(settings: str = Form(...), 78 | sessionid: Optional[str] = Form(""), 79 | clients: ClientStorage = Depends(get_clients)) -> str: 80 | """Set client's settings 81 | """ 82 | if sessionid != "": 83 | cl = clients.get(sessionid) 84 | else: 85 | cl = clients.client() 86 | cl.set_settings(json.loads(settings)) 87 | cl.expose() 88 | clients.set(cl) 89 | return cl.sessionid 90 | 91 | 92 | @router.get("/timeline_feed") 93 | async def timeline_feed(sessionid: str, 94 | clients: ClientStorage = Depends(get_clients)) -> Dict: 95 | """Get your timeline feed 96 | """ 97 | cl = clients.get(sessionid) 98 | return cl.get_timeline_feed() 99 | -------------------------------------------------------------------------------- /routers/clip.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from pathlib import Path 3 | import requests 4 | import json 5 | from fastapi.responses import FileResponse 6 | from fastapi import APIRouter, Depends, File, UploadFile, Form 7 | from instagrapi.types import Media, Location, Usertag 8 | 9 | from dependencies import ClientStorage, get_clients 10 | from helpers import clip_upload_post 11 | 12 | 13 | router = APIRouter( 14 | prefix="/clip", 15 | tags=["clip"], 16 | responses={404: {"description": "Not found"}}, 17 | ) 18 | 19 | 20 | @router.post("/download") 21 | async def clip_download(sessionid: str = Form(...), 22 | media_pk: int = Form(...), 23 | folder: Optional[Path] = Form(""), 24 | returnFile: Optional[bool] = Form(True), 25 | clients: ClientStorage = Depends(get_clients)): 26 | """Download CLIP video using media pk 27 | """ 28 | cl = clients.get(sessionid) 29 | result = cl.clip_download(media_pk, folder) 30 | if returnFile: 31 | return FileResponse(result) 32 | else: 33 | return result 34 | 35 | 36 | @router.post("/download/by_url") 37 | async def clip_download_by_url(sessionid: str = Form(...), 38 | url: str = Form(...), 39 | filename: Optional[str] = Form(""), 40 | folder: Optional[Path] = Form(""), 41 | returnFile: Optional[bool] = Form(True), 42 | clients: ClientStorage = Depends(get_clients)): 43 | """Download CLIP video using URL 44 | """ 45 | cl = clients.get(sessionid) 46 | result = cl.clip_download_by_url(url, filename, folder) 47 | if returnFile: 48 | return FileResponse(result) 49 | else: 50 | return result 51 | 52 | 53 | @router.post("/upload", response_model=Media) 54 | async def clip_upload(sessionid: str = Form(...), 55 | file: UploadFile = File(...), 56 | caption: str = Form(...), 57 | thumbnail: Optional[UploadFile] = File(None), 58 | usertags: Optional[List[str]] = Form([]), 59 | location: Optional[Location] = Form(None), 60 | clients: ClientStorage = Depends(get_clients) 61 | ) -> Media: 62 | """Upload photo and configure to feed 63 | """ 64 | cl = clients.get(sessionid) 65 | 66 | usernames_tags = [] 67 | for usertag in usertags: 68 | usertag_json = json.loads(usertag) 69 | usernames_tags.append(Usertag(user=usertag_json['user'], x=usertag_json['x'], y=usertag_json['y'])) 70 | 71 | content = await file.read() 72 | if thumbnail is not None: 73 | thumb = await thumbnail.read() 74 | return await clip_upload_post( 75 | cl, content, caption=caption, 76 | thumbnail=thumb, 77 | usertags=usernames_tags, 78 | location=location) 79 | return await clip_upload_post( 80 | cl, content, caption=caption, 81 | usertags=usernames_tags, 82 | location=location) 83 | 84 | @router.post("/upload/by_url", response_model=Media) 85 | async def clip_upload(sessionid: str = Form(...), 86 | url: str = Form(...), 87 | caption: str = Form(...), 88 | thumbnail: Optional[UploadFile] = File(None), 89 | usertags: Optional[List[str]] = Form([]), 90 | location: Optional[Location] = Form(None), 91 | clients: ClientStorage = Depends(get_clients) 92 | ) -> Media: 93 | """Upload photo by URL and configure to feed 94 | """ 95 | cl = clients.get(sessionid) 96 | usernames_tags = [] 97 | for usertag in usertags: 98 | usertag_json = json.loads(usertag) 99 | usernames_tags.append(Usertag(user=usertag_json['user'], x=usertag_json['x'], y=usertag_json['y'])) 100 | 101 | content = requests.get(url).content 102 | if thumbnail is not None: 103 | thumb = await thumbnail.read() 104 | return await clip_upload_post( 105 | cl, content, caption=caption, 106 | thumbnail=thumb, 107 | usertags=usernames_tags, 108 | location=location) 109 | return await clip_upload_post( 110 | cl, content, caption=caption, 111 | usertags=usernames_tags, 112 | location=location) 113 | -------------------------------------------------------------------------------- /routers/igtv.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from pathlib import Path 3 | import requests 4 | import json 5 | from fastapi import APIRouter, Depends, File, UploadFile, Form 6 | from fastapi.responses import FileResponse 7 | from instagrapi.types import Media, Location, Usertag 8 | 9 | from dependencies import ClientStorage, get_clients 10 | from helpers import igtv_upload_post 11 | 12 | router = APIRouter( 13 | prefix="/igtv", 14 | tags=["igtv"], 15 | responses={404: {"description": "Not found"}}, 16 | ) 17 | 18 | 19 | @router.post("/download") 20 | async def igtv_download(sessionid: str = Form(...), 21 | media_pk: int = Form(...), 22 | folder: Optional[Path] = Form(""), 23 | returnFile: Optional[bool] = Form(True), 24 | clients: ClientStorage = Depends(get_clients)): 25 | """Download IGTV video using media pk 26 | """ 27 | cl = clients.get(sessionid) 28 | result = cl.igtv_download(media_pk, folder) 29 | if returnFile: 30 | return FileResponse(result) 31 | else: 32 | return result 33 | 34 | 35 | @router.post("/download/by_url") 36 | async def igtv_download_by_url(sessionid: str = Form(...), 37 | url: str = Form(...), 38 | filename: Optional[str] = Form(""), 39 | folder: Optional[Path] = Form(""), 40 | returnFile: Optional[bool] = Form(True), 41 | clients: ClientStorage = Depends(get_clients)): 42 | """Download IGTV video using URL 43 | """ 44 | cl = clients.get(sessionid) 45 | result = cl.igtv_download_by_url(url, filename, folder) 46 | if returnFile: 47 | return FileResponse(result) 48 | else: 49 | return result 50 | 51 | 52 | @router.post("/upload", response_model=Media) 53 | async def igtv_upload(sessionid: str = Form(...), 54 | file: UploadFile = File(...), 55 | title: str = Form(...), 56 | caption: str = Form(...), 57 | thumbnail: Optional[UploadFile] = File(None), 58 | usertags: Optional[List[str]] = Form([]), 59 | location: Optional[Location] = Form(None), 60 | clients: ClientStorage = Depends(get_clients) 61 | ) -> Media: 62 | """Upload photo and configure to feed 63 | """ 64 | cl = clients.get(sessionid) 65 | 66 | usernames_tags = [] 67 | for usertag in usertags: 68 | usertag_json = json.loads(usertag) 69 | usernames_tags.append(Usertag(user=usertag_json['user'], x=usertag_json['x'], y=usertag_json['y'])) 70 | 71 | content = await file.read() 72 | if thumbnail is not None: 73 | thumb = await thumbnail.read() 74 | return await igtv_upload_post( 75 | cl, content, title=title, 76 | caption=caption, 77 | thumbnail=thumb, 78 | usertags=usernames_tags, 79 | location=location) 80 | return await igtv_upload_post( 81 | cl, content, title=title, 82 | caption=caption, 83 | usertags=usernames_tags, 84 | location=location) 85 | 86 | @router.post("/upload/by_url", response_model=Media) 87 | async def igtv_upload(sessionid: str = Form(...), 88 | url: str = Form(...), 89 | title: str = Form(...), 90 | caption: str = Form(...), 91 | thumbnail: Optional[UploadFile] = File(None), 92 | usertags: Optional[List[str]] = Form([]), 93 | location: Optional[Location] = Form(None), 94 | clients: ClientStorage = Depends(get_clients) 95 | ) -> Media: 96 | """Upload photo by URL and configure to feed 97 | """ 98 | cl = clients.get(sessionid) 99 | 100 | usernames_tags = [] 101 | for usertag in usertags: 102 | usertag_json = json.loads(usertag) 103 | usernames_tags.append(Usertag(user=usertag_json['user'], x=usertag_json['x'], y=usertag_json['y'])) 104 | 105 | content = requests.get(url).content 106 | if thumbnail is not None: 107 | thumb = await thumbnail.read() 108 | return await igtv_upload_post( 109 | cl, content, title=title, 110 | caption=caption, 111 | thumbnail=thumb, 112 | usertags=usernames_tags, 113 | location=location) 114 | return await igtv_upload_post( 115 | cl, content, title=title, 116 | caption=caption, 117 | usertags=usernames_tags, 118 | location=location) 119 | -------------------------------------------------------------------------------- /routers/insights.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Form 2 | 3 | from typing import List, Dict, Optional 4 | 5 | from instagrapi import Client 6 | from instagrapi.mixins.insights import POST_TYPE, TIME_FRAME, DATA_ORDERING 7 | 8 | from dependencies import ClientStorage, get_clients 9 | 10 | 11 | router = APIRouter( 12 | prefix="/insights", 13 | tags=["insights"], 14 | responses={404: {"description": "Not found"}} 15 | ) 16 | 17 | 18 | @router.post("/media_feed_all", response_model=List[Dict]) 19 | async def media_feed_all(sessionid: str = Form(...), 20 | post_type: POST_TYPE = "ALL", 21 | time_frame: TIME_FRAME = "TWO_YEARS", 22 | data_ordering: DATA_ORDERING = "REACH_COUNT", 23 | count: int = 0, 24 | clients: ClientStorage = Depends(get_clients)) -> List[Dict]: 25 | """Return medias with insights 26 | """ 27 | cl = clients.get(sessionid) 28 | return cl.insights_media_feed_all(post_type, time_frame, data_ordering, count, sleep=2) 29 | 30 | 31 | @router.post("/account", response_model=Dict) 32 | async def account(sessionid: str = Form(...), 33 | clients: ClientStorage = Depends(get_clients)) -> Dict: 34 | """Get insights for account 35 | """ 36 | cl = clients.get(sessionid) 37 | return cl.insights_account() 38 | 39 | 40 | @router.post("/media", response_model=Dict) 41 | async def media(sessionid: str = Form(...), 42 | media_pk: int = Form(...), 43 | clients: ClientStorage = Depends(get_clients)) -> Dict: 44 | """Get insights data for media 45 | """ 46 | cl = clients.get(sessionid) 47 | return cl.insights_media(media_pk) 48 | -------------------------------------------------------------------------------- /routers/media.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Form 2 | 3 | from typing import List, Dict, Optional 4 | 5 | from instagrapi import Client 6 | from instagrapi.types import Media, Usertag, Location, UserShort 7 | 8 | from dependencies import ClientStorage, get_clients 9 | 10 | 11 | router = APIRouter( 12 | prefix="/media", 13 | tags=["media"], 14 | responses={404: {"description": "Not found"}} 15 | ) 16 | 17 | 18 | @router.get("/id") 19 | async def media_id(media_pk: int) -> str: 20 | """Get full media id 21 | """ 22 | return Client().media_id(media_pk) 23 | 24 | 25 | @router.get("/pk") 26 | async def media_pk(media_id: str) -> str: 27 | """Get short media id 28 | """ 29 | return str(Client().media_pk(media_id)) 30 | 31 | 32 | @router.get("/pk_from_code") 33 | async def media_pk_from_code(code: str) -> str: 34 | """Get media pk from code 35 | """ 36 | return str(Client().media_pk_from_code(code)) 37 | 38 | 39 | @router.get("/pk_from_url") 40 | async def media_pk_from_url(url: str) -> str: 41 | """Get Media PK from URL 42 | """ 43 | return str(Client().media_pk_from_url(url)) 44 | 45 | 46 | @router.post("/info", response_model=Media) 47 | async def media_info(sessionid: str = Form(...), 48 | pk: int = Form(...), 49 | use_cache: Optional[bool] = Form(True), 50 | clients: ClientStorage = Depends(get_clients)) -> Media: 51 | """Get media info by pk 52 | """ 53 | cl = clients.get(sessionid) 54 | return cl.media_info(pk, use_cache) 55 | 56 | 57 | @router.post("/user_medias", response_model=List[Media]) 58 | async def user_medias(sessionid: str = Form(...), 59 | user_id: int = Form(...), 60 | amount: Optional[int] = Form(50), 61 | clients: ClientStorage = Depends(get_clients)) -> List[Media]: 62 | """Get a user's media 63 | """ 64 | cl = clients.get(sessionid) 65 | return cl.user_medias(user_id, amount) 66 | 67 | 68 | @router.post("/usertag_medias", response_model=List[Media]) 69 | async def usertag_medias(sessionid: str = Form(...), 70 | user_id: int = Form(...), 71 | amount: Optional[int] = Form(50), 72 | clients: ClientStorage = Depends(get_clients)) -> List[Media]: 73 | """Get medias where a user is tagged 74 | """ 75 | cl = clients.get(sessionid) 76 | return cl.usertag_medias(user_id, amount) 77 | 78 | 79 | @router.post("/delete", response_model=bool) 80 | async def media_delete(sessionid: str = Form(...), 81 | media_id: str = Form(...), 82 | clients: ClientStorage = Depends(get_clients)) -> bool: 83 | """Delete media by Media ID 84 | """ 85 | cl = clients.get(sessionid) 86 | return cl.media_delete(media_id) 87 | 88 | 89 | @router.post("/edit", response_model=Dict) 90 | async def media_edit(sessionid: str = Form(...), 91 | media_id: str = Form(...), 92 | caption: str = Form(...), 93 | title: Optional[str] = Form(""), 94 | usertags: Optional[List[Usertag]] = Form([]), 95 | location: Optional[Location] = Form(None), 96 | clients: ClientStorage = Depends(get_clients)) -> Dict: 97 | """Edit caption for media 98 | """ 99 | cl = clients.get(sessionid) 100 | return cl.media_edit(media_id, caption, title, usertags, location) 101 | 102 | 103 | @router.post("/user", response_model=UserShort) 104 | async def media_user(sessionid: str = Form(...), 105 | media_pk: int = Form(...), 106 | clients: ClientStorage = Depends(get_clients)) -> UserShort: 107 | """Get author of the media 108 | """ 109 | cl = clients.get(sessionid) 110 | return cl.media_user(media_pk) 111 | 112 | 113 | @router.post("/oembed", response_model=Dict) 114 | async def media_oembed(sessionid: str = Form(...), 115 | url: str = Form(...), 116 | clients: ClientStorage = Depends(get_clients)) -> Dict: 117 | """Return info about media and user from post URL 118 | """ 119 | cl = clients.get(sessionid) 120 | return cl.media_oembed(url) 121 | 122 | 123 | @router.post("/like", response_model=bool) 124 | async def media_like(sessionid: str = Form(...), 125 | media_id: str = Form(...), 126 | revert: Optional[bool] = Form(False), 127 | clients: ClientStorage = Depends(get_clients)) -> bool: 128 | """Like a media 129 | """ 130 | cl = clients.get(sessionid) 131 | return cl.media_like(media_id, revert) 132 | 133 | 134 | @router.post("/unlike", response_model=bool) 135 | async def media_unlike(sessionid: str = Form(...), 136 | media_id: str = Form(...), 137 | clients: ClientStorage = Depends(get_clients)) -> bool: 138 | """Unlike a media 139 | """ 140 | cl = clients.get(sessionid) 141 | return cl.media_unlike(media_id) 142 | 143 | 144 | @router.post("/seen", response_model=bool) 145 | async def media_seen(sessionid: str = Form(...), 146 | media_ids: List[str] = Form(...), 147 | skipped_media_ids: Optional[List[str]] = Form([]), 148 | clients: ClientStorage = Depends(get_clients)) -> bool: 149 | """Mark a media as seen 150 | """ 151 | cl = clients.get(sessionid) 152 | return cl.media_seen(media_ids, skipped_media_ids) 153 | 154 | 155 | @router.post("/likers", response_model=List[UserShort]) 156 | async def media_likers(sessionid: str = Form(...), 157 | media_id: str = Form(...), 158 | clients: ClientStorage = Depends(get_clients)) -> List[UserShort]: 159 | """Get user's likers 160 | """ 161 | cl = clients.get(sessionid) 162 | return cl.media_likers(media_id) 163 | 164 | 165 | @router.post("/archive", response_model=bool) 166 | async def media_archive(sessionid: str = Form(...), 167 | media_id: str = Form(...), 168 | revert: Optional[bool] = Form(False), 169 | clients: ClientStorage = Depends(get_clients)) -> bool: 170 | """Archive a media 171 | """ 172 | cl = clients.get(sessionid) 173 | return cl.media_archive(media_id, revert) 174 | 175 | 176 | @router.post("/unarchive", response_model=bool) 177 | async def media_unarchive(sessionid: str = Form(...), 178 | media_id: str = Form(...), 179 | clients: ClientStorage = Depends(get_clients)) -> bool: 180 | """Unarchive a media 181 | """ 182 | cl = clients.get(sessionid) 183 | return cl.media_unarchive(media_id) 184 | -------------------------------------------------------------------------------- /routers/photo.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from pathlib import Path 3 | import requests 4 | import json 5 | from pydantic import AnyHttpUrl 6 | from fastapi import APIRouter, Depends, File, UploadFile, Form 7 | from fastapi.responses import FileResponse 8 | from instagrapi.types import ( 9 | Story, StoryHashtag, StoryLink, 10 | StoryLocation, StoryMention, StorySticker, 11 | Media, Location, Usertag 12 | ) 13 | from helpers import photo_upload_story_as_video, photo_upload_story_as_photo, photo_upload_post 14 | from dependencies import ClientStorage, get_clients 15 | 16 | 17 | router = APIRouter( 18 | prefix="/photo", 19 | tags=["photo"], 20 | responses={404: {"description": "Not found"}}, 21 | ) 22 | 23 | 24 | @router.post("/upload_to_story", response_model=Story) 25 | async def photo_upload_to_story(sessionid: str = Form(...), 26 | file: UploadFile = File(...), 27 | as_video: Optional[bool] = Form(False), 28 | caption: Optional[str] = Form(""), 29 | mentions: Optional[List[StoryMention]] = Form([]), 30 | locations: Optional[List[StoryLocation]] = Form([]), 31 | links: Optional[List[StoryLink]] = Form([]), 32 | hashtags: Optional[List[StoryHashtag]] = Form([]), 33 | stickers: Optional[List[StorySticker]] = Form([]), 34 | clients: ClientStorage = Depends(get_clients) 35 | ) -> Story: 36 | """Upload photo to story 37 | """ 38 | cl = clients.get(sessionid) 39 | content = await file.read() 40 | if as_video: 41 | return await photo_upload_story_as_video( 42 | cl, content, caption=caption, 43 | mentions=mentions, 44 | links=links, 45 | hashtags=hashtags, 46 | locations=locations, 47 | stickers=stickers) 48 | else: 49 | return await photo_upload_story_as_photo( 50 | cl, content, caption=caption, 51 | mentions=mentions, 52 | links=links, 53 | hashtags=hashtags, 54 | locations=locations, 55 | stickers=stickers) 56 | 57 | 58 | @router.post("/upload_to_story/by_url", response_model=Story) 59 | async def photo_upload_to_story_by_url(sessionid: str = Form(...), 60 | url: AnyHttpUrl = Form(...), 61 | as_video: Optional[bool] = Form(False), 62 | caption: Optional[str] = Form(""), 63 | mentions: Optional[List[StoryMention]] = Form([]), 64 | locations: Optional[List[StoryLocation]] = Form([]), 65 | links: Optional[List[StoryLink]] = Form([]), 66 | hashtags: Optional[List[StoryHashtag]] = Form([]), 67 | stickers: Optional[List[StorySticker]] = Form([]), 68 | clients: ClientStorage = Depends(get_clients) 69 | ) -> Story: 70 | """Upload photo to story by URL to file 71 | """ 72 | cl = clients.get(sessionid) 73 | content = requests.get(url).content 74 | if as_video: 75 | return await photo_upload_story_as_video( 76 | cl, content, caption=caption, 77 | mentions=mentions, 78 | links=links, 79 | hashtags=hashtags, 80 | locations=locations, 81 | stickers=stickers) 82 | else: 83 | return await photo_upload_story_as_photo( 84 | cl, content, caption=caption, 85 | mentions=mentions, 86 | links=links, 87 | hashtags=hashtags, 88 | locations=locations, 89 | stickers=stickers) 90 | 91 | 92 | 93 | @router.post("/download") 94 | async def photo_download(sessionid: str = Form(...), 95 | media_pk: int = Form(...), 96 | folder: Optional[Path] = Form(""), 97 | returnFile: Optional[bool] = Form(True), 98 | clients: ClientStorage = Depends(get_clients)): 99 | """Download photo using media pk 100 | """ 101 | cl = clients.get(sessionid) 102 | result = cl.photo_download(media_pk, folder) 103 | if returnFile: 104 | return FileResponse(result) 105 | else: 106 | return result 107 | 108 | 109 | @router.post("/download/by_url") 110 | async def photo_download_by_url(sessionid: str = Form(...), 111 | url: str = Form(...), 112 | filename: Optional[str] = Form(""), 113 | folder: Optional[Path] = Form(""), 114 | returnFile: Optional[bool] = Form(True), 115 | clients: ClientStorage = Depends(get_clients)): 116 | """Download photo using URL 117 | """ 118 | cl = clients.get(sessionid) 119 | result = cl.photo_download_by_url(url, filename, folder) 120 | if returnFile: 121 | return FileResponse(result) 122 | else: 123 | return result 124 | 125 | 126 | @router.post("/upload", response_model=Media) 127 | async def photo_upload(sessionid: str = Form(...), 128 | file: UploadFile = File(...), 129 | caption: str = Form(...), 130 | upload_id: Optional[str] = Form(""), 131 | usertags: Optional[List[str]] = Form([]), 132 | location: Optional[Location] = Form(None), 133 | clients: ClientStorage = Depends(get_clients) 134 | ) -> Media: 135 | """Upload photo and configure to feed 136 | """ 137 | cl = clients.get(sessionid) 138 | 139 | usernames_tags = [] 140 | for usertag in usertags: 141 | usertag_json = json.loads(usertag) 142 | usernames_tags.append(Usertag(user=usertag_json['user'], x=usertag_json['x'], y=usertag_json['y'])) 143 | 144 | content = await file.read() 145 | return await photo_upload_post( 146 | cl, content, caption=caption, 147 | upload_id=upload_id, 148 | usertags=usernames_tags, 149 | location=location) 150 | 151 | @router.post("/upload/by_url", response_model=Media) 152 | async def photo_upload(sessionid: str = Form(...), 153 | url: AnyHttpUrl = Form(...), 154 | caption: str = Form(...), 155 | upload_id: Optional[str] = Form(""), 156 | usertags: Optional[List[str]] = Form([]), 157 | location: Optional[Location] = Form(None), 158 | clients: ClientStorage = Depends(get_clients) 159 | ) -> Media: 160 | """Upload photo and configure to feed 161 | """ 162 | cl = clients.get(sessionid) 163 | 164 | usernames_tags = [] 165 | for usertag in usertags: 166 | usertag_json = json.loads(usertag) 167 | usernames_tags.append(Usertag(user=usertag_json['user'], x=usertag_json['x'], y=usertag_json['y'])) 168 | 169 | content = requests.get(url).content 170 | return await photo_upload_post( 171 | cl, content, caption=caption, 172 | upload_id=upload_id, 173 | usertags=usernames_tags, 174 | location=location) 175 | -------------------------------------------------------------------------------- /routers/story.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from pathlib import Path 3 | 4 | from fastapi import APIRouter, Depends, Form 5 | from fastapi.responses import FileResponse 6 | from instagrapi import Client 7 | from instagrapi.types import Story 8 | 9 | from dependencies import ClientStorage, get_clients 10 | 11 | 12 | router = APIRouter( 13 | prefix="/story", 14 | tags=["story"], 15 | responses={404: {"description": "Not found"}}, 16 | ) 17 | 18 | 19 | @router.post("/user_stories", response_model=List[Story]) 20 | async def story_user_stories(sessionid: str = Form(...), 21 | user_id: str = Form(...), 22 | amount: Optional[int] = Form(None), 23 | clients: ClientStorage = Depends(get_clients)) -> List[Story]: 24 | """Get a user's stories 25 | """ 26 | cl = clients.get(sessionid) 27 | return cl.user_stories(user_id, amount) 28 | 29 | 30 | @router.post("/info", response_model=Story) 31 | async def story_info(sessionid: str = Form(...), 32 | story_pk: int = Form(...), 33 | use_cache: Optional[bool] = Form(True), 34 | clients: ClientStorage = Depends(get_clients)) -> Story: 35 | """Get Story by pk or id 36 | """ 37 | cl = clients.get(sessionid) 38 | return cl.story_info(story_pk, use_cache) 39 | 40 | 41 | @router.post("/delete", response_model=bool) 42 | async def story_delete(sessionid: str = Form(...), 43 | story_pk: int = Form(...), 44 | clients: ClientStorage = Depends(get_clients)) -> bool: 45 | """Delete story 46 | """ 47 | cl = clients.get(sessionid) 48 | return cl.story_delete(story_pk) 49 | 50 | 51 | @router.post("/seen", response_model=bool) 52 | async def story_seen(sessionid: str = Form(...), 53 | story_pks: List[int] = Form(...), 54 | skipped_story_pks: Optional[List[int]] = Form([]), 55 | clients: ClientStorage = Depends(get_clients)) -> bool: 56 | """Mark a media as seen 57 | """ 58 | cl = clients.get(sessionid) 59 | return cl.story_seen(story_pks, skipped_story_pks) 60 | 61 | @router.post("/like", response_model=bool) 62 | async def story_like(sessionid: str = Form(...), 63 | story_id: str = Form(...), 64 | revert: Optional[bool] = Form(False), 65 | clients: ClientStorage = Depends(get_clients)) -> bool: 66 | """Like a Story 67 | """ 68 | cl = clients.get(sessionid) 69 | return cl.story_like(story_id, revert) 70 | 71 | @router.post("/unlike", response_model=bool) 72 | async def story_unlike(sessionid: str = Form(...), 73 | story_id: str = Form(...), 74 | clients: ClientStorage = Depends(get_clients)) -> bool: 75 | """Unlike a Story 76 | """ 77 | cl = clients.get(sessionid) 78 | return cl.story_unlike(story_pks) 79 | 80 | 81 | @router.get("/pk_from_url") 82 | async def story_pk_from_url(url: str) -> int: 83 | """Get Story (media) PK from URL 84 | """ 85 | return Client().story_pk_from_url(url) 86 | 87 | 88 | @router.post("/download") 89 | async def story_download(sessionid: str = Form(...), 90 | story_pk: int = Form(...), 91 | filename: Optional[str] = Form(""), 92 | folder: Optional[Path] = Form(""), 93 | returnFile: Optional[bool] = Form(True), 94 | clients: ClientStorage = Depends(get_clients)): 95 | """Download story media by media_type 96 | """ 97 | cl = clients.get(sessionid) 98 | result = cl.story_download(story_pk, filename, folder) 99 | if returnFile: 100 | return FileResponse(result) 101 | else: 102 | return result 103 | 104 | 105 | @router.post("/download/by_url") 106 | async def story_download_by_url(sessionid: str = Form(...), 107 | url: str = Form(...), 108 | filename: Optional[str] = Form(""), 109 | folder: Optional[Path] = Form(""), 110 | returnFile: Optional[bool] = Form(True), 111 | clients: ClientStorage = Depends(get_clients)): 112 | """Download story media using URL 113 | """ 114 | cl = clients.get(sessionid) 115 | result = cl.story_download_by_url(url, filename, folder) 116 | if returnFile: 117 | return FileResponse(result) 118 | else: 119 | return result 120 | -------------------------------------------------------------------------------- /routers/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict 2 | 3 | from fastapi import APIRouter, Depends, Form 4 | from instagrapi.types import ( 5 | User, UserShort 6 | ) 7 | 8 | from dependencies import ClientStorage, get_clients 9 | 10 | 11 | router = APIRouter( 12 | prefix="/user", 13 | tags=["user"], 14 | responses={404: {"description": "Not found"}}, 15 | ) 16 | 17 | 18 | @router.post("/followers", response_model=Dict[int, UserShort]) 19 | async def user_followers(sessionid: str = Form(...), 20 | user_id: str = Form(...), 21 | use_cache: Optional[bool] = Form(True), 22 | amount: Optional[int] = Form(0), 23 | clients: ClientStorage = Depends(get_clients)) -> Dict[int, UserShort]: 24 | """Get user's followers 25 | """ 26 | cl = clients.get(sessionid) 27 | return cl.user_followers(user_id, use_cache, amount) 28 | 29 | 30 | @router.post("/following", response_model=Dict[int, UserShort]) 31 | async def user_following(sessionid: str = Form(...), 32 | user_id: str = Form(...), 33 | use_cache: Optional[bool] = Form(True), 34 | amount: Optional[int] = Form(0), 35 | clients: ClientStorage = Depends(get_clients)) -> Dict[int, UserShort]: 36 | """Get user's followers information 37 | """ 38 | cl = clients.get(sessionid) 39 | return cl.user_following(user_id, use_cache, amount) 40 | 41 | 42 | @router.post("/info", response_model=User) 43 | async def user_info(sessionid: str = Form(...), 44 | user_id: str = Form(...), 45 | use_cache: Optional[bool] = Form(True), 46 | clients: ClientStorage = Depends(get_clients)) -> User: 47 | """Get user object from user id 48 | """ 49 | cl = clients.get(sessionid) 50 | return cl.user_info(user_id, use_cache) 51 | 52 | 53 | @router.post("/info_by_username", response_model=User) 54 | async def user_info_by_username(sessionid: str = Form(...), 55 | username: str = Form(...), 56 | use_cache: Optional[bool] = Form(True), 57 | clients: ClientStorage = Depends(get_clients)) -> User: 58 | """Get user object from username 59 | """ 60 | cl = clients.get(sessionid) 61 | return cl.user_info_by_username(username, use_cache) 62 | 63 | 64 | @router.post("/follow", response_model=bool) 65 | async def user_follow(sessionid: str = Form(...), 66 | user_id: int = Form(...), 67 | clients: ClientStorage = Depends(get_clients)) -> bool: 68 | """Follow a user 69 | """ 70 | cl = clients.get(sessionid) 71 | return cl.user_follow(user_id) 72 | 73 | 74 | @router.post("/unfollow", response_model=bool) 75 | async def user_unfollow(sessionid: str = Form(...), 76 | user_id: int = Form(...), 77 | clients: ClientStorage = Depends(get_clients)) -> bool: 78 | """Unfollow a user 79 | """ 80 | cl = clients.get(sessionid) 81 | return cl.user_unfollow(user_id) 82 | 83 | 84 | @router.post("/id_from_username", response_model=int) 85 | async def user_id_from_username(sessionid: str = Form(...), 86 | username: str = Form(...), 87 | clients: ClientStorage = Depends(get_clients)) -> int: 88 | """Get user id from username 89 | """ 90 | cl = clients.get(sessionid) 91 | return cl.user_id_from_username(username) 92 | 93 | 94 | @router.post("/username_from_id", response_model=str) 95 | async def username_from_user_id(sessionid: str = Form(...), 96 | user_id: int = Form(...), 97 | clients: ClientStorage = Depends(get_clients)) -> str: 98 | """Get username from user id 99 | """ 100 | cl = clients.get(sessionid) 101 | return cl.username_from_user_id(user_id) 102 | 103 | 104 | @router.post("/remove_follower", response_model=bool) 105 | async def user_remove_follower(sessionid: str = Form(...), 106 | user_id: int = Form(...), 107 | clients: ClientStorage = Depends(get_clients)) -> bool: 108 | """Remove a follower 109 | """ 110 | cl = clients.get(sessionid) 111 | return cl.user_remove_follower(user_id) 112 | 113 | 114 | @router.post("/mute_posts_from_follow", response_model=bool) 115 | async def mute_posts_from_follow(sessionid: str = Form(...), 116 | user_id: int = Form(...), 117 | revert: Optional[bool] = Form(False), 118 | clients: ClientStorage = Depends(get_clients)) -> bool: 119 | """Mute posts from following user 120 | """ 121 | cl = clients.get(sessionid) 122 | return cl.mute_posts_from_follow(user_id, revert) 123 | 124 | 125 | @router.post("/unmute_posts_from_follow", response_model=bool) 126 | async def unmute_posts_from_follow(sessionid: str = Form(...), 127 | user_id: int = Form(...), 128 | clients: ClientStorage = Depends(get_clients)) -> bool: 129 | """Unmute posts from following user 130 | """ 131 | cl = clients.get(sessionid) 132 | return cl.unmute_posts_from_follow(user_id) 133 | 134 | 135 | @router.post("/mute_stories_from_follow", response_model=bool) 136 | async def mute_stories_from_follow(sessionid: str = Form(...), 137 | user_id: int = Form(...), 138 | revert: Optional[bool] = Form(False), 139 | clients: ClientStorage = Depends(get_clients)) -> bool: 140 | """Mute stories from following user 141 | """ 142 | cl = clients.get(sessionid) 143 | return cl.mute_stories_from_follow(user_id, revert) 144 | 145 | 146 | @router.post("/unmute_stories_from_follow", response_model=bool) 147 | async def unmute_stories_from_follow(sessionid: str = Form(...), 148 | user_id: int = Form(...), 149 | clients: ClientStorage = Depends(get_clients)) -> bool: 150 | """Unmute stories from following user 151 | """ 152 | cl = clients.get(sessionid) 153 | return cl.unmute_stories_from_follow(user_id) 154 | -------------------------------------------------------------------------------- /routers/video.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from pathlib import Path 3 | import requests 4 | import json 5 | from pydantic import AnyHttpUrl 6 | from fastapi.responses import FileResponse 7 | from fastapi import APIRouter, Depends, File, UploadFile, Form 8 | from instagrapi.types import ( 9 | Story, StoryHashtag, StoryLink, 10 | StoryLocation, StoryMention, StorySticker, 11 | Media, Usertag, Location 12 | ) 13 | 14 | from helpers import video_upload_story, video_upload_post 15 | from dependencies import ClientStorage, get_clients 16 | 17 | 18 | router = APIRouter( 19 | prefix="/video", 20 | tags=["video"], 21 | responses={404: {"description": "Not found"}}, 22 | ) 23 | 24 | 25 | @router.post("/upload_to_story", response_model=Story) 26 | async def video_upload_to_story(sessionid: str = Form(...), 27 | file: UploadFile = File(...), 28 | caption: Optional[str] = Form(''), 29 | mentions: List[StoryMention] = [], 30 | locations: List[StoryLocation] = [], 31 | links: List[StoryLink] = [], 32 | hashtags: List[StoryHashtag] = [], 33 | stickers: List[StorySticker] = [], 34 | clients: ClientStorage = Depends(get_clients) 35 | ) -> Story: 36 | """Upload video to story 37 | """ 38 | cl = clients.get(sessionid) 39 | content = await file.read() 40 | return await video_upload_story( 41 | cl, content, caption=caption, 42 | mentions=mentions, 43 | links=links, 44 | hashtags=hashtags, 45 | locations=locations, 46 | stickers=stickers 47 | ) 48 | 49 | 50 | @router.post("/upload_to_story/by_url", response_model=Story) 51 | async def video_upload_to_story_by_url(sessionid: str = Form(...), 52 | url: AnyHttpUrl = Form(...), 53 | caption: Optional[str] = Form(''), 54 | mentions: List[StoryMention] = [], 55 | locations: List[StoryLocation] = [], 56 | links: List[StoryLink] = [], 57 | hashtags: List[StoryHashtag] = [], 58 | stickers: List[StorySticker] = [], 59 | clients: ClientStorage = Depends(get_clients) 60 | ) -> Story: 61 | """Upload video to story by URL to file 62 | """ 63 | cl = clients.get(sessionid) 64 | content = requests.get(url).content 65 | return await video_upload_story( 66 | cl, content, caption=caption, 67 | mentions=mentions, 68 | links=links, 69 | hashtags=hashtags, 70 | locations=locations, 71 | stickers=stickers 72 | ) 73 | 74 | 75 | @router.post("/download") 76 | async def video_download(sessionid: str = Form(...), 77 | media_pk: int = Form(...), 78 | folder: Optional[Path] = Form(""), 79 | returnFile: Optional[bool] = Form(True), 80 | clients: ClientStorage = Depends(get_clients)): 81 | """Download video using media pk 82 | """ 83 | cl = clients.get(sessionid) 84 | result = cl.video_download(media_pk, folder) 85 | if returnFile: 86 | return FileResponse(result) 87 | else: 88 | return result 89 | 90 | 91 | @router.post("/download/by_url") 92 | async def video_download_by_url(sessionid: str = Form(...), 93 | url: str = Form(...), 94 | filename: Optional[str] = Form(""), 95 | folder: Optional[Path] = Form(""), 96 | returnFile: Optional[bool] = Form(True), 97 | clients: ClientStorage = Depends(get_clients)): 98 | """Download video using URL 99 | """ 100 | cl = clients.get(sessionid) 101 | result = cl.video_download_by_url(url, filename, folder) 102 | if returnFile: 103 | return FileResponse(result) 104 | else: 105 | return result 106 | 107 | 108 | @router.post("/upload", response_model=Media) 109 | async def video_upload(sessionid: str = Form(...), 110 | file: UploadFile = File(...), 111 | caption: str = Form(...), 112 | thumbnail: Optional[UploadFile] = File(None), 113 | usertags: Optional[List[str]] = Form([]), 114 | location: Optional[Location] = Form(None), 115 | clients: ClientStorage = Depends(get_clients) 116 | ) -> Media: 117 | """Upload photo and configure to feed 118 | """ 119 | cl = clients.get(sessionid) 120 | 121 | usernames_tags = [] 122 | for usertag in usertags: 123 | usertag_json = json.loads(usertag) 124 | usernames_tags.append(Usertag(user=usertag_json['user'], x=usertag_json['x'], y=usertag_json['y'])) 125 | 126 | content = await file.read() 127 | if thumbnail is not None: 128 | thumb = await thumbnail.read() 129 | return await video_upload_post( 130 | cl, content, caption=caption, 131 | thumbnail=thumb, 132 | usertags=usernames_tags, 133 | location=location) 134 | return await video_upload_post( 135 | cl, content, caption=caption, 136 | usertags=usernames_tags, 137 | location=location) 138 | 139 | @router.post("/upload/by_url", response_model=Media) 140 | async def video_upload(sessionid: str = Form(...), 141 | url: str = Form(...), 142 | caption: str = Form(...), 143 | thumbnail: Optional[UploadFile] = File(None), 144 | usertags: Optional[List[str]] = Form([]), 145 | location: Optional[Location] = Form(None), 146 | clients: ClientStorage = Depends(get_clients) 147 | ) -> Media: 148 | """Upload photo by URL and configure to feed 149 | """ 150 | cl = clients.get(sessionid) 151 | 152 | usernames_tags = [] 153 | for usertag in usertags: 154 | usertag_json = json.loads(usertag) 155 | usernames_tags.append(Usertag(user=usertag_json['user'], x=usertag_json['x'], y=usertag_json['y'])) 156 | 157 | content = requests.get(url).content 158 | if thumbnail is not None: 159 | thumb = await thumbnail.read() 160 | return await video_upload_post( 161 | cl, content, caption=caption, 162 | thumbnail=thumb, 163 | usertags=usernames_tags, 164 | location=location) 165 | return await video_upload_post( 166 | cl, content, caption=caption, 167 | usertags=usernames_tags, 168 | location=location) 169 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.16 2 | -------------------------------------------------------------------------------- /storages.py: -------------------------------------------------------------------------------- 1 | from urllib import parse 2 | from instagrapi import Client 3 | from tinydb import TinyDB, Query 4 | import json 5 | 6 | class ClientStorage: 7 | db = TinyDB('./db.json') 8 | 9 | def client(self): 10 | """Get new client (helper) 11 | """ 12 | cl = Client() 13 | cl.request_timeout = 0.1 14 | return cl 15 | 16 | def get(self, sessionid: str) -> Client: 17 | """Get client settings 18 | """ 19 | key = parse.unquote(sessionid.strip(" \"")) 20 | try: 21 | settings = json.loads(self.db.search(Query().sessionid == key)[0]['settings']) 22 | cl = Client() 23 | cl.set_settings(settings) 24 | cl.get_timeline_feed() 25 | return cl 26 | except IndexError: 27 | raise Exception('Session not found (e.g. after reload process), please relogin') 28 | 29 | def set(self, cl: Client) -> bool: 30 | """Set client settings 31 | """ 32 | key = parse.unquote(cl.sessionid.strip(" \"")) 33 | self.db.insert({'sessionid': key, 'settings': json.dumps(cl.get_settings())}) 34 | return True 35 | 36 | def close(self): 37 | pass 38 | -------------------------------------------------------------------------------- /swift/.gitignore: -------------------------------------------------------------------------------- 1 | client 2 | -------------------------------------------------------------------------------- /swift/README.md: -------------------------------------------------------------------------------- 1 | ## Launch as script 2 | 3 | `./client.swift` 4 | 5 | ## Compile 6 | 7 | `swiftc main.swift -o kraken` 8 | 9 | or `xcrun -sdk macosx swiftc client.swift -o client` 10 | 11 | ## Launch compiled version 12 | 13 | `./client` 14 | -------------------------------------------------------------------------------- /swift/client.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/swift 2 | // 3 | // client.swift 4 | // for instagrapi-rest 5 | // 6 | // 7 | 8 | import Foundation 9 | 10 | let BaseUrl = "http://localhost:8000" 11 | 12 | func getVersion() { 13 | let url = URL(string: "\(BaseUrl)/version")! 14 | let sem = DispatchSemaphore(value: 0) 15 | let task = URLSession.shared.dataTask(with: url) {(data, response, error) in 16 | guard let data = data else { return } 17 | print("\nVersion:", String(data: data, encoding: .utf8)!) 18 | sem.signal() 19 | } 20 | task.resume() 21 | sem.wait() 22 | } 23 | 24 | //func pkFromCode(code: String) { 25 | // // let url = URL(string: "\(BaseUrl)/media/pk_from_code")! 26 | // let queryItems = URLQueryItem(name: "code", value: code) 27 | // var urlComps = URLComponents(string: "\(BaseUrl)/media/pk_from_code") 28 | // urlComps.queryItems = queryItems 29 | // let url = urlComps.url! 30 | // let sem = DispatchSemaphore(value: 0) 31 | // let task = URLSession.shared.dataTask(with: url) {(data, response, error) in 32 | // guard let data = data else { return } 33 | // print("\npkFromCode: \(code) ->", String(data: data, encoding: .utf8)!) 34 | // sem.signal() 35 | // } 36 | // task.resume() 37 | // sem.wait() 38 | //} 39 | 40 | getVersion() 41 | //pkFromCode(code: "B1LbfVPlwIA") 42 | // dump(Process.arguments) 43 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from httpx import AsyncClient 3 | 4 | from main import app 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_media_pk_from_code() -> None: 9 | async with AsyncClient(app=app, base_url="http://test") as ac: 10 | response = await ac.get( 11 | app.url_path_for("media_pk_from_code"), 12 | params={"code": "B1LbfVPlwIA"} 13 | ) 14 | assert response.status_code == 200 15 | assert response.text == "\"2110901750722920960\"" 16 | 17 | 18 | @pytest.mark.asyncio 19 | async def test_media_info() -> None: 20 | async with AsyncClient(app=app, base_url="http://test") as ac: 21 | response = await ac.get( 22 | app.url_path_for("media_info"), 23 | params={"pk": 2110901750722920960} 24 | ) 25 | media = response.json() 26 | assert response.status_code == 200 27 | assert media["pk"] == 2110901750722920960 28 | assert media["id"] == "2110901750722920960_8572539084" 29 | assert media["code"] == "B1LbfVPlwIA" 30 | assert media["media_type"] == 1 31 | --------------------------------------------------------------------------------