├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── nox.yml ├── .gitignore ├── Dockerfile ├── README.md ├── black_badge.svg ├── config_example.json ├── docs └── CONTRIBUTING.md ├── env2config.py ├── image.jpg ├── main.py ├── noxfile.py ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── src ├── __init__.py ├── mappings.py ├── proxy.py └── utils.py ├── start.bat ├── start.sh ├── startverbose.bat ├── startverbose.sh └── tor ├── Data └── Tor │ ├── geoip │ └── geoip6 └── Tor ├── libcrypto-1_1.dll ├── libevent-2-1-7.dll ├── libevent_core-2-1-7.dll ├── libevent_extra-2-1-7.dll ├── libgcc_s_dw2-1.dll ├── libssl-1_1.dll ├── libssp-0.dll ├── libwinpthread-1.dll ├── tor-gencert.exe ├── tor.exe ├── torrc └── zlib1.dll /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | select = BLK,E,F,W 3 | max-line-length = 88 4 | extend-ignore = E203, E501, W503 5 | -------------------------------------------------------------------------------- /.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 | assignees: '' 7 | 8 | --- 9 | 10 | # Bug Report Form 11 | 12 | ## Checklist 13 | 14 | - [ ] I am using the latest version of the code. 15 | - [ ] I am absolutely sure I read the README in its entirety. 16 | - [ ] I have made sure this is not a duplicate issue. 17 | - [ ] I have filled out this form correctly. 18 | 19 | ## Describe the bug 20 | 21 | Replace this text with a description of the bug. 22 | 23 | ## To Reproduce 24 | 25 | Steps to trigger the bug: 26 | 27 | 1. Go to '...' 28 | 2. Click on '....' 29 | 3. Scroll down to '....' 30 | 4. See error 31 | 32 | ## Expected behavior 33 | 34 | Replace this text with what _should_ have happened if no bug occurred. 35 | 36 | ## Additional context 37 | 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/nox.yml: -------------------------------------------------------------------------------- 1 | name: Nox 2 | on: 3 | # Triggers the workflow on push or pull request events but only for the main branch 4 | push: 5 | pull_request: 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: randomblock1/setup-nox@patch-4 15 | - run: nox 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | test.py 9 | *.old 10 | config.json 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # IPython Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | new_image.jpg 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | ### VirtualEnv template 99 | # Virtualenv 100 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 101 | .Python 102 | [Bb]in 103 | [Ii]nclude 104 | [Ll]ib 105 | [Ll]ib64 106 | [Ll]ocal 107 | [Ss]cripts 108 | pyvenv.cfg 109 | .venv 110 | pip-selfcheck.json 111 | ### JetBrains template 112 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 113 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 114 | 115 | # User-specific stuff: 116 | .idea/workspace.xml 117 | .idea/tasks.xml 118 | .idea/dictionaries 119 | .idea/vcs.xml 120 | .idea/jsLibraryMappings.xml 121 | 122 | # Sensitive or high-churn files: 123 | .idea/dataSources.ids 124 | .idea/dataSources.xml 125 | .idea/dataSources.local.xml 126 | .idea/sqlDataSources.xml 127 | .idea/dynamic.xml 128 | .idea/uiDesigner.xml 129 | 130 | # Gradle: 131 | .idea/gradle.xml 132 | .idea/libraries 133 | 134 | # Mongo Explorer plugin: 135 | .idea/mongoSettings.xml 136 | 137 | .idea/ 138 | 139 | ## File-based project format: 140 | *.iws 141 | 142 | ## Plugin-specific files: 143 | 144 | # IntelliJ 145 | /out/ 146 | 147 | # mpeltonen/sbt-idea plugin 148 | .idea_modules/ 149 | 150 | # JIRA plugin 151 | atlassian-ide-plugin.xml 152 | 153 | # Crashlytics plugin (for Android Studio and IntelliJ) 154 | com_crashlytics_export_strings.xml 155 | crashlytics.properties 156 | crashlytics-build.properties 157 | fabric.properties 158 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.4-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | RUN apk add python3-dev py3-setuptools gcc linux-headers libc-dev 7 | RUN apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ 8 | libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ 9 | libxcb-dev libpng-dev 10 | 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | COPY . . 14 | 15 | CMD ["python", "./main.py"] 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reddit Place Script 2022 2 | 3 | [![Code style: black](./black_badge.svg)](https://github.com/psf/black) 4 | [![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)](https://forthebadge.com) 5 | [![forthebadge](https://forthebadge.com/images/badges/60-percent-of-the-time-works-every-time.svg)](https://forthebadge.com) 6 | 7 | # Thanks to everyone who contributed! r/place is now over! 8 | 9 | 10 | 11 | 12 | ## About 13 | 14 | This is a script to draw an image onto r/place (). 15 | 16 | ## Features 17 | 18 | - Support for multiple accounts. 19 | - Determines the cooldown time remaining for each account. 20 | - Detects existing matching pixels on the r/place map and skips them. 21 | - Automatically converts colors to the r/place color palette. 22 | - Easy(ish) to read output with colors. 23 | - SOCKS proxy support. 24 | - No client id and secret needed. 25 | - Proxies from "proxies.txt" file. 26 | - Tor support. 27 | 28 | ## Requirements 29 | 30 | - [Latest Version of Python 3](https://www.python.org/downloads/) 31 | 32 | ## macOS 33 | 34 | If you want to use tor on macOS. you'll need to provide your own tor binary or install it via [Homebrew](https://brew.sh) using ``brew install tor``, and start it manually. 35 | 36 | Make sure to deactivate the "use_builtin tor" 37 | option in the config and configure your tor to use the correct ports and password. 38 | 39 | *Please note that socks proxy connection to tor doesn't work for the time being, so the config value is for an httpTunnel port* 40 | 41 | ## Get Started 42 | 43 | Move the file 'config_example.json' to 'config.json' 44 | 45 | Edit the values to replace with actual credentials and values 46 | 47 | Note: Please use https://jsonlint.com/ to check that your JSON file is correctly formatted 48 | 49 | ```json 50 | { 51 | //Where the image's path is 52 | "image_path": "image.png", 53 | // [x,y] where you want the top left pixel of the local image to be drawn on canvas 54 | "image_start_coords": [741, 610], 55 | // delay between starting threads (can be 0) 56 | "thread_delay": 2, 57 | // array of accounts to use 58 | "workers": { 59 | // username of account 1 60 | "worker1username": { 61 | // password of account 1 62 | "password": "password", 63 | // which pixel of the image to draw first 64 | "start_coords": [0, 0] 65 | }, 66 | // username of account 2 67 | "worker1username": { 68 | // password of account 2 69 | "password": "password", 70 | // which pixel of the image to draw first 71 | "start_coords": [0, 0] 72 | } 73 | // etc... add as many accounts as you want (but reddit may detect you the more you add) 74 | } 75 | } 76 | ``` 77 | 78 | ### Notes 79 | 80 | - Use `.png` if you wish to make use of transparency or non rectangular images 81 | - If you use 2 factor authentication (2FA) in your account, then change `password` to `password:XXXXXX` where `XXXXXX` is your 2FA code. 82 | 83 | ## Run the Script 84 | 85 | ### Windows 86 | 87 | ```shell 88 | start.bat or startverbose.bat 89 | ``` 90 | 91 | ### Unix-like (Linux, macOS etc.) 92 | 93 | ```shell 94 | chmod +x start.sh startverbose.sh 95 | ./start.sh or ./startverbose.sh 96 | ``` 97 | 98 | **You can get more logs (`DEBUG`) by running the script with `-d` flag:** 99 | 100 | `python3 main.py -d` or `python3 main.py --debug` 101 | 102 | ## Multiple Workers 103 | Just create multiple child arrays to "workers" in the .json file: 104 | 105 | ```json 106 | { 107 | "image_path": "image.png", 108 | "image_start_coords": [741, 610], 109 | "thread_delay": 2, 110 | 111 | "workers": { 112 | "worker1username": { 113 | "password": "password", 114 | "start_coords": [0, 0] 115 | }, 116 | "worker2username": { 117 | "password": "password", 118 | "start_coords": [0, 50] 119 | } 120 | } 121 | } 122 | ``` 123 | 124 | In this case, the first worker will start drawing from (0, 0) and the second worker will start drawing from (0, 50) from the input image.jpg file. 125 | 126 | This is useful if you want different threads drawing different parts of the image with different accounts. 127 | 128 | ## Other Settings 129 | 130 | If any JSON decoders errors are found, the `config.json` needs to be fixed. Make sure to add the below 2 lines in the file. 131 | 132 | ```json 133 | { 134 | "thread_delay": 2, 135 | "unverified_place_frequency": false, 136 | "proxies": ["1.1.1.1:8080", "2.2.2.2:1234"], 137 | "compact_logging": true 138 | } 139 | ``` 140 | 141 | - thread_delay - Adds a delay between starting a new thread. Can be used to avoid ratelimiting. 142 | - unverified_place_frequency - Sets the pixel place frequency to the unverified account limit. 143 | - proxies - Sets proxies to use for sending requests to reddit. The proxy used is randomly selected for each request. Can be used to avoid ratelimiting. 144 | - compact_logging - Disables timer text until next pixel. 145 | - Transparency can be achieved by using the RGB value (69, 42, 0) in any part of your image. 146 | - If you'd like, you can enable Verbose Mode by adding `--verbose` to "python main.py". This will output a lot more information, and not neccessarily in the right order, but it is useful for development and debugging. 147 | - You can also setup proxies by creating a "proxies" and have a new line for each proxies. 148 | 149 | # Tor 150 | Tor can be used as an alternative to normal proxies. Note that currently, you cannot use normal proxies and tor at the same time. 151 | 152 | ```json 153 | "using_tor": false, 154 | "tor_ip": "127.0.0.1", 155 | "tor_port": 1881, 156 | "tor_control_port": 9051, 157 | "tor_password": "Passwort", 158 | "tor_delay": 5, 159 | "use_builtin_tor": true 160 | ``` 161 | 162 | The config values are as follows: 163 | - Deactivates or activates tor. 164 | - Sets the ip/hostname of the tor proxy to use 165 | - Sets the httptunnel port that should be used. 166 | - Sets the tor control port. 167 | - Sets the password (leave it as "Passwort" if you want to use the default binaries. 168 | - The delay that tor should receive to process a new connection. 169 | - Whether the included tor binary should be used. It is preconfigured. If you want to use your own binary, make sure you configure it properly. 170 | 171 | Note that when using the included binaries, only the tunnel port is explicitly set while starting tor. 172 | 173 |

If you want to use your own binaries, follow these steps:

174 | 175 | - Get tor standalone for your platform [here](https://www.torproject.org/download/tor/). For Windows just use the expert bundle. For macOS, you can use [Homebrew](https://brew.sh) to install tor: ``brew install tor``. 176 | - In your tor folder, create a file named ``torrc``. Copy [this](https://github.com/torproject/tor/blob/main/src/config/torrc.sample.in) into it. 177 | - Search for ``ControlPort`` in your torrc file and uncomment it. Change the port number to your desired control port. 178 | - Decide on the password you want to use. Run ``tor --hash-password PASSWORD`` from a terminal in the folder with your tor executable, with "PASSWORD" being your desired password. Copy the resulting hash. 179 | - Search for ``HashedControlPassword`` and uncomment it. Paste the hash value you copied after it. 180 | - Decide on a port for your httptunnel. The default for this script is 1881. 181 | - Fill in your password, your httptunnel port and your control port in this script's ``config.json`` and enable tor with ``using_tor = true``. 182 | - To start tor, run ``tor --defaults-torrc PATHTOTORRC --HttpTunnelPort TUNNELPORT``, with PATHTOTORRC being your path to the torrc file you created and TUNNELPORT being your httptunnel port. 183 | - Now run the script and (hopefully) everything should work. 184 | 185 | License for the included tor binary: 186 | 187 | > Tor is distributed under the "3-clause BSD" license, a commonly used 188 | software license that means Tor is both free software and open source: 189 | Copyright (c) 2001-2004, Roger Dingledine 190 | Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson 191 | Copyright (c) 2007-2019, The Tor Project, Inc. 192 | Redistribution and use in source and binary forms, with or without 193 | modification, are permitted provided that the following conditions are 194 | met: 195 | >- Redistributions of source code must retain the above copyright 196 | notice, this list of conditions and the following disclaimer. 197 | >- Redistributions in binary form must reproduce the above 198 | copyright notice, this list of conditions and the following disclaimer 199 | in the documentation and/or other materials provided with the 200 | distribution. 201 | >- Neither the names of the copyright owners nor the names of its 202 | contributors may be used to endorse or promote products derived from 203 | this software without specific prior written permission. 204 | > 205 | >THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 206 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 207 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 208 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 209 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 210 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 211 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 212 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 213 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 214 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 215 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 216 | 217 | ## Docker 218 | 219 | A dockerfile is provided. Instructions on installing docker are outside the scope of this guide. 220 | 221 | To build: After editing the `config.json` file, run `docker build . -t place-bot`. and wait for the image to build. 222 | 223 | You can now run it with `docker run place-bot` 224 | 225 | ## Contributing 226 | 227 | See the [Contributing Guide](docs/CONTRIBUTING.md). 228 | -------------------------------------------------------------------------------- /black_badge.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "image_path": "image.jpg", 3 | "image_start_coords": [741, 610], 4 | "legacy_transparency": true, 5 | "thread_delay": 2, 6 | "unverified_place_frequency": false, 7 | "compact_logging": true, 8 | "using_tor": false, 9 | "tor_ip": "127.0.0.1", 10 | "tor_port": 1881, 11 | "tor_control_port": 9051, 12 | "tor_password": "Passwort", 13 | "tor_delay": 5, 14 | "use_builtin_tor": true, 15 | "workers": { 16 | "worker1username": { 17 | "password": "password", 18 | "start_coords": [0, 0] 19 | }, 20 | "worker2username": { 21 | "password": "password", 22 | "start_coords": [0, 0] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Pre-requisite 2 | 3 | * __Nox__ can be installed following these instructions: https://nox.thea.codes/en/stable/ 4 | 5 | # Developing 6 | 7 | 1. Add any new source file (extension `.py`) to the list of files to be checked in `noxfile.py` 8 | 2. Run [`black`](https://black.readthedocs.io/en/stable/) on the repo with `nox -rs black` to format the code 9 | 3. Run `nox` on the root of the repo 10 | 11 | ## Debugging 12 | 13 | You should be able to have a more descriptive trace in the code by using the `@logger.catch` decorator (see [documentation](https://loguru.readthedocs.io/en/stable/overview.html#exceptions-catching-within-threads-or-main)) 14 | 15 | # Rules 16 | 17 | `nox` will run the following: 18 | * `flake8` to check for formatting issues: https://flake8.pycqa.org/en/latest/ 19 | * `flake8-black` to check for style issues: 20 | -------------------------------------------------------------------------------- /env2config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv() 6 | 7 | toJSON = { 8 | "image_path": "image.jpg", 9 | "image_start_coords": [ 10 | int(json.loads(os.getenv("ENV_DRAW_X_START"))), 11 | int(json.loads(os.getenv("ENV_DRAW_Y_START"))), 12 | ], 13 | "thread_delay": 3, 14 | "unverified_place_frequency": False, 15 | "proxies": None, 16 | "workers": {}, 17 | } 18 | 19 | for i in range(len(json.loads(os.getenv("ENV_PLACE_USERNAME")))): 20 | toJSON["workers"][json.loads(os.getenv("ENV_PLACE_USERNAME"))[i]] = { 21 | "password": json.loads(os.getenv("ENV_PLACE_PASSWORD"))[i], 22 | "client_id": json.loads(os.getenv("ENV_PLACE_APP_CLIENT_ID"))[i], 23 | "client_secret": json.loads(os.getenv("ENV_PLACE_SECRET_KEY"))[i], 24 | "start_coords": [ 25 | int(json.loads(os.getenv("ENV_R_START"))[i]), 26 | int(json.loads(os.getenv("ENV_C_START"))[i]), 27 | ], 28 | } 29 | 30 | with open("config.json", "w") as outfile: 31 | json.dump(toJSON, outfile, sort_keys=True, indent=4) 32 | -------------------------------------------------------------------------------- /image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/image.jpg -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import requests 4 | import json 5 | import time 6 | import threading 7 | import sys 8 | from io import BytesIO 9 | from http import HTTPStatus 10 | from websocket import create_connection 11 | from websocket._exceptions import WebSocketConnectionClosedException 12 | from PIL import Image 13 | 14 | from loguru import logger 15 | import click 16 | from bs4 import BeautifulSoup 17 | 18 | 19 | from src.mappings import ColorMapper 20 | import src.proxy as proxy 21 | import src.utils as utils 22 | 23 | 24 | class PlaceClient: 25 | def __init__(self, config_path): 26 | self.logger = logger 27 | 28 | # Data 29 | self.json_data = utils.get_json_data(self, config_path) 30 | self.pixel_x_start: int = self.json_data["image_start_coords"][0] 31 | self.pixel_y_start: int = self.json_data["image_start_coords"][1] 32 | 33 | # In seconds 34 | self.delay_between_launches = ( 35 | self.json_data["thread_delay"] 36 | if "thread_delay" in self.json_data 37 | and self.json_data["thread_delay"] is not None 38 | else 3 39 | ) 40 | self.unverified_place_frequency = ( 41 | self.json_data["unverified_place_frequency"] 42 | if "unverified_place_frequency" in self.json_data 43 | and self.json_data["unverified_place_frequency"] is not None 44 | else False 45 | ) 46 | 47 | self.legacy_transparency = ( 48 | self.json_data["legacy_transparency"] 49 | if "legacy_transparency" in self.json_data 50 | and self.json_data["legacy_transparency"] is not None 51 | else True 52 | ) 53 | proxy.Init(self) 54 | 55 | # Color palette 56 | self.rgb_colors_array = ColorMapper.generate_rgb_colors_array() 57 | 58 | # Auth 59 | self.access_tokens = {} 60 | self.access_token_expires_at_timestamp = {} 61 | 62 | # Image information 63 | self.pix = None 64 | self.image_size = None 65 | self.image_path = ( 66 | self.json_data["image_path"] 67 | if "image_path" in self.json_data 68 | else "image.jpg" 69 | ) 70 | self.first_run_counter = 0 71 | 72 | # Initialize-functions 73 | utils.load_image(self) 74 | 75 | self.waiting_thread_index = -1 76 | 77 | """ Main """ 78 | # Draw a pixel at an x, y coordinate in r/place with a specific color 79 | 80 | def set_pixel_and_check_ratelimit( 81 | self, 82 | access_token_in, 83 | x, 84 | y, 85 | name, 86 | color_index_in=18, 87 | canvas_index=0, 88 | thread_index=-1, 89 | ): 90 | # canvas structure: 91 | # 0 | 1 92 | # 2 | 3 93 | logger.warning( 94 | "Thread #{} - {}: Attempting to place {} pixel at {}, {}", 95 | thread_index, 96 | name, 97 | ColorMapper.color_id_to_name(color_index_in), 98 | x + (1000 * (canvas_index % 2)), 99 | y + (1000 * (canvas_index // 2)), 100 | ) 101 | 102 | url = "https://gql-realtime-2.reddit.com/query" 103 | 104 | payload = json.dumps( 105 | { 106 | "operationName": "setPixel", 107 | "variables": { 108 | "input": { 109 | "actionName": "r/replace:set_pixel", 110 | "PixelMessageData": { 111 | "coordinate": {"x": x, "y": y}, 112 | "colorIndex": color_index_in, 113 | "canvasIndex": canvas_index, 114 | }, 115 | } 116 | }, 117 | "query": "mutation setPixel($input: ActInput!) {\n act(input: $input) {\n data {\n ... on BasicMessage {\n id\n data {\n ... on GetUserCooldownResponseMessageData {\n nextAvailablePixelTimestamp\n __typename\n }\n ... on SetPixelResponseMessageData {\n timestamp\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n", 118 | } 119 | ) 120 | headers = { 121 | "origin": "https://hot-potato.reddit.com", 122 | "referer": "https://hot-potato.reddit.com/", 123 | "apollographql-client-name": "mona-lisa", 124 | "Authorization": "Bearer " + access_token_in, 125 | "Content-Type": "application/json", 126 | } 127 | 128 | response = requests.request( 129 | "POST", 130 | url, 131 | headers=headers, 132 | data=payload, 133 | proxies=proxy.get_random_proxy(self, name=None), 134 | ) 135 | logger.debug( 136 | "Thread #{} - {}: Received response: {}", thread_index, name, response.text 137 | ) 138 | 139 | self.waiting_thread_index = -1 140 | 141 | # There are 2 different JSON keys for responses to get the next timestamp. 142 | # If we don't get data, it means we've been rate limited. 143 | # If we do, a pixel has been successfully placed. 144 | if response.json()["data"] is None: 145 | logger.debug(response.json().get("errors")) 146 | waitTime = math.floor( 147 | response.json()["errors"][0]["extensions"]["nextAvailablePixelTs"] 148 | ) 149 | logger.error( 150 | "Thread #{} - {}: Failed placing pixel: rate limited", 151 | thread_index, 152 | name, 153 | ) 154 | else: 155 | waitTime = math.floor( 156 | response.json()["data"]["act"]["data"][0]["data"][ 157 | "nextAvailablePixelTimestamp" 158 | ] 159 | ) 160 | logger.success( 161 | "Thread #{} - {}: Succeeded placing pixel", thread_index, name 162 | ) 163 | 164 | # THIS COMMENTED CODE LETS YOU DEBUG THREADS FOR TESTING 165 | # Works perfect with one thread. 166 | # With multiple threads, every time you press Enter you move to the next one. 167 | # Move the code anywhere you want, I put it here to inspect the API responses. 168 | 169 | # Reddit returns time in ms and we need seconds, so divide by 1000 170 | return waitTime / 1000 171 | 172 | def get_board(self, access_token_in): 173 | logger.debug("Connecting and obtaining board images") 174 | while True: 175 | try: 176 | ws = create_connection( 177 | "wss://gql-realtime-2.reddit.com/query", 178 | origin="https://hot-potato.reddit.com", 179 | ) 180 | break 181 | except Exception: 182 | logger.error( 183 | "Failed to connect to websocket, trying again in 30 seconds..." 184 | ) 185 | time.sleep(30) 186 | 187 | ws.send( 188 | json.dumps( 189 | { 190 | "type": "connection_init", 191 | "payload": {"Authorization": "Bearer " + access_token_in}, 192 | } 193 | ) 194 | ) 195 | while True: 196 | try: 197 | msg = ws.recv() 198 | except WebSocketConnectionClosedException as e: 199 | logger.error(e) 200 | continue 201 | if msg is None: 202 | logger.error("Reddit failed to acknowledge connection_init") 203 | exit() 204 | if msg.startswith('{"type":"connection_ack"}'): 205 | logger.debug("Connected to WebSocket server") 206 | break 207 | logger.debug("Obtaining Canvas information") 208 | ws.send( 209 | json.dumps( 210 | { 211 | "id": "1", 212 | "type": "start", 213 | "payload": { 214 | "variables": { 215 | "input": { 216 | "channel": { 217 | "teamOwner": "AFD2022", 218 | "category": "CONFIG", 219 | } 220 | } 221 | }, 222 | "extensions": {}, 223 | "operationName": "configuration", 224 | "query": "subscription configuration($input: SubscribeInput!) {\n subscribe(input: $input) {\n id\n ... on BasicMessage {\n data {\n __typename\n ... on ConfigurationMessageData {\n colorPalette {\n colors {\n hex\n index\n __typename\n }\n __typename\n }\n canvasConfigurations {\n index\n dx\n dy\n __typename\n }\n canvasWidth\n canvasHeight\n __typename\n }\n }\n __typename\n }\n __typename\n }\n}\n", 225 | }, 226 | } 227 | ) 228 | ) 229 | 230 | while True: 231 | canvas_payload = json.loads(ws.recv()) 232 | if canvas_payload["type"] == "data": 233 | canvas_details = canvas_payload["payload"]["data"]["subscribe"]["data"] 234 | logger.debug("Canvas config: {}", canvas_payload) 235 | break 236 | 237 | canvas_sockets = [] 238 | 239 | canvas_count = len(canvas_details["canvasConfigurations"]) 240 | 241 | for i in range(0, canvas_count): 242 | canvas_sockets.append(2 + i) 243 | logger.debug("Creating canvas socket {}", canvas_sockets[i]) 244 | 245 | ws.send( 246 | json.dumps( 247 | { 248 | "id": str(2 + i), 249 | "type": "start", 250 | "payload": { 251 | "variables": { 252 | "input": { 253 | "channel": { 254 | "teamOwner": "AFD2022", 255 | "category": "CANVAS", 256 | "tag": str(i), 257 | } 258 | } 259 | }, 260 | "extensions": {}, 261 | "operationName": "replace", 262 | "query": "subscription replace($input: SubscribeInput!) {\n subscribe(input: $input) {\n id\n ... on BasicMessage {\n data {\n __typename\n ... on FullFrameMessageData {\n __typename\n name\n timestamp\n }\n ... on DiffFrameMessageData {\n __typename\n name\n currentTimestamp\n previousTimestamp\n }\n }\n __typename\n }\n __typename\n }\n}\n", 263 | }, 264 | } 265 | ) 266 | ) 267 | 268 | imgs = [] 269 | logger.debug("A total of {} canvas sockets opened", len(canvas_sockets)) 270 | 271 | while len(canvas_sockets) > 0: 272 | temp = json.loads(ws.recv()) 273 | logger.debug("Waiting for WebSocket message") 274 | 275 | if temp["type"] == "data": 276 | logger.debug("Received WebSocket data type message") 277 | msg = temp["payload"]["data"]["subscribe"] 278 | 279 | if msg["data"]["__typename"] == "FullFrameMessageData": 280 | logger.debug("Received full frame message") 281 | img_id = int(temp["id"]) 282 | logger.debug("Image ID: {}", img_id) 283 | 284 | if img_id in canvas_sockets: 285 | logger.debug("Getting image: {}", msg["data"]["name"]) 286 | imgs.append( 287 | [ 288 | img_id, 289 | Image.open( 290 | BytesIO( 291 | requests.get( 292 | msg["data"]["name"], 293 | stream=True, 294 | proxies=proxy.get_random_proxy( 295 | self, name=None 296 | ), 297 | ).content 298 | ) 299 | ), 300 | ] 301 | ) 302 | canvas_sockets.remove(img_id) 303 | logger.debug( 304 | "Canvas sockets remaining: {}", len(canvas_sockets) 305 | ) 306 | 307 | for i in range(0, canvas_count - 1): 308 | ws.send(json.dumps({"id": str(2 + i), "type": "stop"})) 309 | 310 | ws.close() 311 | 312 | new_img_width = ( 313 | max(map(lambda x: x["dx"], canvas_details["canvasConfigurations"])) 314 | + canvas_details["canvasWidth"] 315 | ) 316 | logger.debug("New image width: {}", new_img_width) 317 | 318 | new_img_height = ( 319 | max(map(lambda x: x["dy"], canvas_details["canvasConfigurations"])) 320 | + canvas_details["canvasHeight"] 321 | ) 322 | logger.debug("New image height: {}", new_img_height) 323 | 324 | new_img = Image.new("RGB", (new_img_width, new_img_height)) 325 | 326 | for idx, img in enumerate(sorted(imgs, key=lambda x: x[0])): 327 | logger.debug("Adding image (ID {}): {}", img[0], img[1]) 328 | dx_offset = int(canvas_details["canvasConfigurations"][idx]["dx"]) 329 | dy_offset = int(canvas_details["canvasConfigurations"][idx]["dy"]) 330 | new_img.paste(img[1], (dx_offset, dy_offset)) 331 | 332 | return new_img 333 | 334 | def get_unset_pixel(self, x, y, index): 335 | originalX = x 336 | originalY = y 337 | loopedOnce = False 338 | imgOutdated = True 339 | wasWaiting = False 340 | 341 | while True: 342 | time.sleep(0.05) 343 | if self.waiting_thread_index != -1 and self.waiting_thread_index != index: 344 | x = originalX 345 | y = originalY 346 | loopedOnce = False 347 | imgOutdated = True 348 | wasWaiting = True 349 | continue 350 | 351 | # Stagger reactivation of threads after wait 352 | if wasWaiting: 353 | wasWaiting = False 354 | time.sleep(index * self.delay_between_launches) 355 | 356 | if x >= self.image_size[0]: 357 | y += 1 358 | x = 0 359 | 360 | if y >= self.image_size[1]: 361 | 362 | y = 0 363 | 364 | if x == originalX and y == originalY and loopedOnce: 365 | logger.info( 366 | "Thread #{} : All pixels correct, trying again in 10 seconds... ", 367 | index, 368 | ) 369 | self.waiting_thread_index = index 370 | time.sleep(10) 371 | imgOutdated = True 372 | 373 | if imgOutdated: 374 | boardimg = self.get_board(self.access_tokens[index]) 375 | pix2 = boardimg.convert("RGB").load() 376 | imgOutdated = False 377 | 378 | logger.debug("{}, {}", x + self.pixel_x_start, y + self.pixel_y_start) 379 | logger.debug( 380 | "{}, {}, boardimg, {}, {}", x, y, self.image_size[0], self.image_size[1] 381 | ) 382 | 383 | target_rgb = self.pix[x, y] 384 | 385 | new_rgb = ColorMapper.closest_color( 386 | target_rgb, self.rgb_colors_array, self.legacy_transparency 387 | ) 388 | if pix2[x + self.pixel_x_start, y + self.pixel_y_start] != new_rgb: 389 | logger.debug( 390 | "{}, {}, {}, {}", 391 | pix2[x + self.pixel_x_start, y + self.pixel_y_start], 392 | new_rgb, 393 | new_rgb != (69, 42, 0), 394 | pix2[x, y] != new_rgb, 395 | ) 396 | 397 | # (69, 42, 0) is a special color reserved for transparency. 398 | if new_rgb != (69, 42, 0): 399 | logger.debug( 400 | "Thread #{} : Replacing {} pixel at: {},{} with {} color", 401 | index, 402 | pix2[x + self.pixel_x_start, y + self.pixel_y_start], 403 | x + self.pixel_x_start, 404 | y + self.pixel_y_start, 405 | new_rgb, 406 | ) 407 | break 408 | else: 409 | logger.info( 410 | "Transparent Pixel at {}, {} skipped", 411 | x + self.pixel_x_start, 412 | y + self.pixel_y_start, 413 | ) 414 | x += 1 415 | loopedOnce = True 416 | return x, y, new_rgb 417 | 418 | # Draw the input image 419 | def task(self, index, name, worker): 420 | # Whether image should keep drawing itself 421 | repeat_forever = True 422 | while True: 423 | # last_time_placed_pixel = math.floor(time.time()) 424 | 425 | # note: Reddit limits us to place 1 pixel every 5 minutes, so I am setting it to 426 | # 5 minutes and 30 seconds per pixel 427 | if self.unverified_place_frequency: 428 | pixel_place_frequency = 1230 429 | else: 430 | pixel_place_frequency = 330 431 | 432 | next_pixel_placement_time = math.floor(time.time()) + pixel_place_frequency 433 | 434 | try: 435 | # Current pixel row and pixel column being drawn 436 | current_r = worker["start_coords"][0] 437 | current_c = worker["start_coords"][1] 438 | except Exception: 439 | logger.info("You need to provide start_coords to worker '{}'", name) 440 | exit(1) 441 | 442 | # Time until next pixel is drawn 443 | update_str = "" 444 | 445 | # Refresh auth tokens and / or draw a pixel 446 | while True: 447 | # reduce CPU usage 448 | time.sleep(1) 449 | 450 | # get the current time 451 | current_timestamp = math.floor(time.time()) 452 | 453 | # log next time until drawing 454 | time_until_next_draw = next_pixel_placement_time - current_timestamp 455 | 456 | if time_until_next_draw > 10000: 457 | logger.warning( 458 | "Thread #{} - {} :: CANCELLED :: Rate-Limit Banned", index, name 459 | ) 460 | repeat_forever = False 461 | break 462 | 463 | new_update_str = ( 464 | f"{time_until_next_draw} seconds until next pixel is drawn" 465 | ) 466 | 467 | if update_str != new_update_str and time_until_next_draw % 10 == 0: 468 | update_str = new_update_str 469 | else: 470 | update_str = "" 471 | 472 | if len(update_str) > 0: 473 | if not self.compactlogging: 474 | logger.info("Thread #{} - {}: {}", index, name, update_str) 475 | 476 | # refresh access token if necessary 477 | if ( 478 | len(self.access_tokens) == 0 479 | or len(self.access_token_expires_at_timestamp) == 0 480 | or 481 | # index in self.access_tokens 482 | index not in self.access_token_expires_at_timestamp 483 | or ( 484 | self.access_token_expires_at_timestamp.get(index) 485 | and current_timestamp 486 | >= self.access_token_expires_at_timestamp.get(index) 487 | ) 488 | ): 489 | if not self.compactlogging: 490 | logger.info( 491 | "Thread #{} - {}: Refreshing access token", index, name 492 | ) 493 | 494 | # developer's reddit username and password 495 | try: 496 | username = name 497 | password = worker["password"] 498 | except Exception: 499 | logger.exception( 500 | "You need to provide all required fields to worker '{}'", 501 | name, 502 | ) 503 | exit(1) 504 | 505 | while True: 506 | try: 507 | client = requests.Session() 508 | client.proxies = proxy.get_random_proxy(self, name) 509 | client.headers.update( 510 | { 511 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36" 512 | } 513 | ) 514 | 515 | r = client.get( 516 | "https://www.reddit.com/login", 517 | proxies=proxy.get_random_proxy(self, name), 518 | ) 519 | login_get_soup = BeautifulSoup(r.content, "html.parser") 520 | csrf_token = login_get_soup.find( 521 | "input", {"name": "csrf_token"} 522 | )["value"] 523 | data = { 524 | "username": username, 525 | "password": password, 526 | "dest": "https://new.reddit.com/", 527 | "csrf_token": csrf_token, 528 | } 529 | 530 | r = client.post( 531 | "https://www.reddit.com/login", 532 | data=data, 533 | proxies=proxy.get_random_proxy(self, name), 534 | ) 535 | break 536 | except Exception: 537 | logger.error( 538 | "Failed to connect to websocket, trying again in 30 seconds..." 539 | ) 540 | time.sleep(30) 541 | 542 | if r.status_code != HTTPStatus.OK.value: 543 | # password is probably invalid 544 | logger.exception("{} - Authorization failed!", username) 545 | logger.debug("response: {} - {}", r.status_code, r.text) 546 | return 547 | else: 548 | logger.success("{} - Authorization successful!", username) 549 | logger.info("Obtaining access token...") 550 | r = client.get( 551 | "https://new.reddit.com/", 552 | proxies=proxy.get_random_proxy(self, name), 553 | ) 554 | data_str = ( 555 | BeautifulSoup(r.content, features="html.parser") 556 | .find("script", {"id": "data"}) 557 | .contents[0][len("window.__r = ") : -1] 558 | ) 559 | data = json.loads(data_str) 560 | response_data = data["user"]["session"] 561 | 562 | if "error" in response_data: 563 | logger.info( 564 | "An error occured. Make sure you have the correct credentials. Response data: {}", 565 | response_data, 566 | ) 567 | exit(1) 568 | 569 | self.access_tokens[index] = response_data["accessToken"] 570 | # access_token_type = data["user"]["session"]["accessToken"] # this is just "bearer" 571 | access_token_expires_in_seconds = response_data[ 572 | "expiresIn" 573 | ] # this is usually "3600" 574 | # access_token_scope = response_data["scope"] # this is usually "*" 575 | 576 | # ts stores the time in seconds 577 | self.access_token_expires_at_timestamp[ 578 | index 579 | ] = current_timestamp + int(access_token_expires_in_seconds) 580 | if not self.compactlogging: 581 | logger.info( 582 | "Received new access token: {}************", 583 | self.access_tokens.get(index)[:5], 584 | ) 585 | 586 | # draw pixel onto screen 587 | if self.access_tokens.get(index) is not None and ( 588 | current_timestamp >= next_pixel_placement_time 589 | or self.first_run_counter <= index 590 | ): 591 | 592 | # place pixel immediately 593 | # first_run = False 594 | self.first_run_counter += 1 595 | 596 | # get target color 597 | # target_rgb = pix[current_r, current_c] 598 | 599 | # get current pixel position from input image and replacement color 600 | current_r, current_c, new_rgb = self.get_unset_pixel( 601 | current_r, 602 | current_c, 603 | index, 604 | ) 605 | 606 | # get converted color 607 | new_rgb_hex = ColorMapper.rgb_to_hex(new_rgb) 608 | pixel_color_index = ColorMapper.COLOR_MAP[new_rgb_hex] 609 | 610 | logger.info("\nAccount Placing: ", name, "\n") 611 | 612 | # draw the pixel onto r/place 613 | # There's a better way to do this 614 | canvas = 0 615 | pixel_x_start = self.pixel_x_start + current_r 616 | pixel_y_start = self.pixel_y_start + current_c 617 | while pixel_x_start > 999: 618 | pixel_x_start -= 1000 619 | canvas += 1 620 | while pixel_y_start > 999: 621 | pixel_y_start -= 1000 622 | canvas += 2 623 | 624 | # draw the pixel onto r/place 625 | next_pixel_placement_time = self.set_pixel_and_check_ratelimit( 626 | self.access_tokens[index], 627 | pixel_x_start, 628 | pixel_y_start, 629 | name, 630 | pixel_color_index, 631 | canvas, 632 | index, 633 | ) 634 | 635 | current_r += 1 636 | 637 | # go back to first column when reached end of a row while drawing 638 | if current_r >= self.image_size[0]: 639 | current_r = 0 640 | current_c += 1 641 | 642 | # exit when all pixels drawn 643 | if current_c >= self.image_size[1]: 644 | logger.info("Thread #{} :: image completed", index) 645 | break 646 | 647 | if not repeat_forever: 648 | break 649 | 650 | def start(self): 651 | for index, worker in enumerate(self.json_data["workers"]): 652 | threading.Thread( 653 | target=self.task, 654 | args=[index, worker, self.json_data["workers"][worker]], 655 | ).start() 656 | # exit(1) 657 | time.sleep(self.delay_between_launches) 658 | 659 | 660 | @click.command() 661 | @click.option( 662 | "-d", 663 | "--debug", 664 | is_flag=True, 665 | help="Enable debug mode. Prints debug messages to the console.", 666 | ) 667 | @click.option( 668 | "-c", 669 | "--config", 670 | default="config.json", 671 | help="Location of config.json", 672 | ) 673 | def main(debug: bool, config: str): 674 | 675 | if not debug: 676 | # default loguru level is DEBUG 677 | logger.remove() 678 | logger.add(sys.stderr, level="INFO") 679 | 680 | client = PlaceClient(config_path=config) 681 | # Start everything 682 | client.start() 683 | 684 | 685 | if __name__ == "__main__": 686 | main() 687 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | locations = "main.py", "noxfile.py", "src/mappings.py", "src/proxy.py", "src/utils.py" 4 | 5 | 6 | # This is not run automatically 7 | @nox.session 8 | def black(session): 9 | args = session.posargs or locations 10 | session.install("black") 11 | session.run("black", *args) 12 | 13 | 14 | @nox.session 15 | def lint(session): 16 | args = session.posargs or locations 17 | session.install("flake8", "flake8-black") 18 | session.run("flake8", *args) 19 | 20 | 21 | nox.options.sessions = ["lint"] 22 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "beautifulsoup4" 3 | version = "4.10.0" 4 | description = "Screen-scraping library" 5 | category = "main" 6 | optional = false 7 | python-versions = ">3.0.0" 8 | 9 | [package.dependencies] 10 | soupsieve = ">1.2" 11 | 12 | [package.extras] 13 | html5lib = ["html5lib"] 14 | lxml = ["lxml"] 15 | 16 | [[package]] 17 | name = "certifi" 18 | version = "2021.10.8" 19 | description = "Python package for providing Mozilla's CA Bundle." 20 | category = "main" 21 | optional = false 22 | python-versions = "*" 23 | 24 | [[package]] 25 | name = "cffi" 26 | version = "1.15.0" 27 | description = "Foreign Function Interface for Python calling C code." 28 | category = "main" 29 | optional = false 30 | python-versions = "*" 31 | 32 | [package.dependencies] 33 | pycparser = "*" 34 | 35 | [[package]] 36 | name = "charset-normalizer" 37 | version = "2.0.12" 38 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 39 | category = "main" 40 | optional = false 41 | python-versions = ">=3.5.0" 42 | 43 | [package.extras] 44 | unicode_backport = ["unicodedata2"] 45 | 46 | [[package]] 47 | name = "click" 48 | version = "8.1.2" 49 | description = "Composable command line interface toolkit" 50 | category = "main" 51 | optional = false 52 | python-versions = ">=3.7" 53 | 54 | [package.dependencies] 55 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 56 | 57 | [[package]] 58 | name = "colorama" 59 | version = "0.4.4" 60 | description = "Cross-platform colored terminal text." 61 | category = "main" 62 | optional = false 63 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 64 | 65 | [[package]] 66 | name = "gevent" 67 | version = "21.12.0" 68 | description = "Coroutine-based network library" 69 | category = "main" 70 | optional = false 71 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5" 72 | 73 | [package.dependencies] 74 | cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} 75 | greenlet = {version = ">=1.1.0,<2.0", markers = "platform_python_implementation == \"CPython\""} 76 | "zope.event" = "*" 77 | "zope.interface" = "*" 78 | 79 | [package.extras] 80 | dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] 81 | docs = ["repoze.sphinx.autointerface", "sphinxcontrib-programoutput", "zope.schema"] 82 | monitor = ["psutil (>=5.7.0)"] 83 | recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "selectors2", "backports.socketpair", "psutil (>=5.7.0)"] 84 | test = ["requests", "objgraph", "cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "selectors2", "futures", "mock", "backports.socketpair", "contextvars (==2.4)", "coverage (>=5.0)", "coveralls (>=1.7.0)", "psutil (>=5.7.0)"] 85 | 86 | [[package]] 87 | name = "greenlet" 88 | version = "1.1.2" 89 | description = "Lightweight in-process concurrent programming" 90 | category = "main" 91 | optional = false 92 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" 93 | 94 | [package.extras] 95 | docs = ["sphinx"] 96 | 97 | [[package]] 98 | name = "idna" 99 | version = "3.3" 100 | description = "Internationalized Domain Names in Applications (IDNA)" 101 | category = "main" 102 | optional = false 103 | python-versions = ">=3.5" 104 | 105 | [[package]] 106 | name = "loguru" 107 | version = "0.6.0" 108 | description = "Python logging made (stupidly) simple" 109 | category = "main" 110 | optional = false 111 | python-versions = ">=3.5" 112 | 113 | [package.dependencies] 114 | colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} 115 | win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} 116 | 117 | [package.extras] 118 | dev = ["colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "black (>=19.10b0)", "isort (>=5.1.1)", "Sphinx (>=4.1.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)"] 119 | 120 | [[package]] 121 | name = "pillow" 122 | version = "9.1.0" 123 | description = "Python Imaging Library (Fork)" 124 | category = "main" 125 | optional = false 126 | python-versions = ">=3.7" 127 | 128 | [package.extras] 129 | docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] 130 | tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] 131 | 132 | [[package]] 133 | name = "pycparser" 134 | version = "2.21" 135 | description = "C parser in Python" 136 | category = "main" 137 | optional = false 138 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 139 | 140 | [[package]] 141 | name = "python-dotenv" 142 | version = "0.20.0" 143 | description = "Read key-value pairs from a .env file and set them as environment variables" 144 | category = "main" 145 | optional = false 146 | python-versions = ">=3.5" 147 | 148 | [package.extras] 149 | cli = ["click (>=5.0)"] 150 | 151 | [[package]] 152 | name = "requests" 153 | version = "2.27.1" 154 | description = "Python HTTP for Humans." 155 | category = "main" 156 | optional = false 157 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 158 | 159 | [package.dependencies] 160 | certifi = ">=2017.4.17" 161 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 162 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 163 | urllib3 = ">=1.21.1,<1.27" 164 | 165 | [package.extras] 166 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 167 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 168 | 169 | [[package]] 170 | name = "soupsieve" 171 | version = "2.3.1" 172 | description = "A modern CSS selector implementation for Beautiful Soup." 173 | category = "main" 174 | optional = false 175 | python-versions = ">=3.6" 176 | 177 | [[package]] 178 | name = "urllib3" 179 | version = "1.26.9" 180 | description = "HTTP library with thread-safe connection pooling, file post, and more." 181 | category = "main" 182 | optional = false 183 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 184 | 185 | [package.extras] 186 | brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] 187 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 188 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 189 | 190 | [[package]] 191 | name = "websocket" 192 | version = "0.2.1" 193 | description = "Websocket implementation for gevent" 194 | category = "main" 195 | optional = false 196 | python-versions = "*" 197 | 198 | [package.dependencies] 199 | gevent = "*" 200 | greenlet = "*" 201 | 202 | [[package]] 203 | name = "websocket-client" 204 | version = "1.3.2" 205 | description = "WebSocket client for Python with low level API options" 206 | category = "main" 207 | optional = false 208 | python-versions = ">=3.7" 209 | 210 | [package.extras] 211 | docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] 212 | optional = ["python-socks", "wsaccel"] 213 | test = ["websockets"] 214 | 215 | [[package]] 216 | name = "win32-setctime" 217 | version = "1.1.0" 218 | description = "A small Python utility to set file creation time on Windows" 219 | category = "main" 220 | optional = false 221 | python-versions = ">=3.5" 222 | 223 | [package.extras] 224 | dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] 225 | 226 | [[package]] 227 | name = "zope.event" 228 | version = "4.5.0" 229 | description = "Very basic event publishing system" 230 | category = "main" 231 | optional = false 232 | python-versions = "*" 233 | 234 | [package.extras] 235 | docs = ["sphinx"] 236 | test = ["zope.testrunner"] 237 | 238 | [[package]] 239 | name = "zope.interface" 240 | version = "5.4.0" 241 | description = "Interfaces for Python" 242 | category = "main" 243 | optional = false 244 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 245 | 246 | [package.extras] 247 | docs = ["sphinx", "repoze.sphinx.autointerface"] 248 | test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] 249 | testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] 250 | 251 | [metadata] 252 | lock-version = "1.1" 253 | python-versions = "^3.8" 254 | content-hash = "0abd8786060c11499da33ee30d01eb78cc7ad0115c21bf7bbb0f8d551030c388" 255 | 256 | [metadata.files] 257 | beautifulsoup4 = [ 258 | {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"}, 259 | {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, 260 | ] 261 | certifi = [ 262 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 263 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 264 | ] 265 | cffi = [ 266 | {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, 267 | {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, 268 | {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, 269 | {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, 270 | {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, 271 | {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, 272 | {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, 273 | {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, 274 | {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, 275 | {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, 276 | {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, 277 | {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, 278 | {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, 279 | {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, 280 | {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, 281 | {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, 282 | {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, 283 | {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, 284 | {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, 285 | {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, 286 | {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, 287 | {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, 288 | {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, 289 | {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, 290 | {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, 291 | {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, 292 | {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, 293 | {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, 294 | {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, 295 | {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, 296 | {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, 297 | {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, 298 | {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, 299 | {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, 300 | {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, 301 | {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, 302 | {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, 303 | {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, 304 | {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, 305 | {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, 306 | {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, 307 | {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, 308 | {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, 309 | {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, 310 | {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, 311 | {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, 312 | {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, 313 | {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, 314 | {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, 315 | {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, 316 | ] 317 | charset-normalizer = [ 318 | {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, 319 | {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, 320 | ] 321 | click = [ 322 | {file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"}, 323 | {file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"}, 324 | ] 325 | colorama = [ 326 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 327 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 328 | ] 329 | gevent = [ 330 | {file = "gevent-21.12.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:2afa3f3ad528155433f6ac8bd64fa5cc303855b97004416ec719a6b1ca179481"}, 331 | {file = "gevent-21.12.0-cp27-cp27m-win32.whl", hash = "sha256:177f93a3a90f46a5009e0841fef561601e5c637ba4332ab8572edd96af650101"}, 332 | {file = "gevent-21.12.0-cp27-cp27m-win_amd64.whl", hash = "sha256:a5ad4ed8afa0a71e1927623589f06a9b5e8b5e77810be3125cb4d93050d3fd1f"}, 333 | {file = "gevent-21.12.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:eae3c46f9484eaacd67ffcdf4eaf6ca830f587edd543613b0f5c4eb3c11d052d"}, 334 | {file = "gevent-21.12.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1899b921219fc8959ff9afb94dae36be82e0769ed13d330a393594d478a0b3a"}, 335 | {file = "gevent-21.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21cb5c9f4e14d75b3fe0b143ec875d7dbd1495fad6d49704b00e57e781ee0f"}, 336 | {file = "gevent-21.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:542ae891e2aa217d2cf6d8446538fcd2f3263a40eec123b970b899bac391c47a"}, 337 | {file = "gevent-21.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:0082d8a5d23c35812ce0e716a91ede597f6dd2c5ff508a02a998f73598c59397"}, 338 | {file = "gevent-21.12.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da8d2d51a49b2a5beb02ad619ca9ddbef806ef4870ba04e5ac7b8b41a5b61db3"}, 339 | {file = "gevent-21.12.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfff82f05f14b7f5d9ed53ccb7a609ae8604df522bb05c971bca78ec9d8b2b9"}, 340 | {file = "gevent-21.12.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7909780f0cf18a1fc32aafd8c8e130cdd93c6e285b11263f7f2d1a0f3678bc50"}, 341 | {file = "gevent-21.12.0-cp36-cp36m-win32.whl", hash = "sha256:bb5cb8db753469c7a9a0b8a972d2660fe851aa06eee699a1ca42988afb0aaa02"}, 342 | {file = "gevent-21.12.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c43f081cbca41d27fd8fef9c6a32cf83cb979345b20abc07bf68df165cdadb24"}, 343 | {file = "gevent-21.12.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:74fc1ef16b86616cfddcc74f7292642b0f72dde4dd95aebf4c45bb236744be54"}, 344 | {file = "gevent-21.12.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc2fef0f98ee180704cf95ec84f2bc2d86c6c3711bb6b6740d74e0afe708b62c"}, 345 | {file = "gevent-21.12.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08b4c17064e28f4eb85604486abc89f442c7407d2aed249cf54544ce5c9baee6"}, 346 | {file = "gevent-21.12.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:973749bacb7bc4f4181a8fb2a7e0e2ff44038de56d08e856dd54a5ac1d7331b4"}, 347 | {file = "gevent-21.12.0-cp37-cp37m-win32.whl", hash = "sha256:6a02a88723ed3f0fd92cbf1df3c4cd2fbd87d82b0a4bac3e36a8875923115214"}, 348 | {file = "gevent-21.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f289fae643a3f1c3b909d6b033e6921b05234a4907e9c9c8c3f1fe403e6ac452"}, 349 | {file = "gevent-21.12.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:3baeeccc4791ba3f8db27179dff11855a8f9210ddd754f6c9b48e0d2561c2aea"}, 350 | {file = "gevent-21.12.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05c5e8a50cd6868dd36536c92fb4468d18090e801bd63611593c0717bab63692"}, 351 | {file = "gevent-21.12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d86438ede1cbe0fde6ef4cc3f72bf2f1ecc9630d8b633ff344a3aeeca272cdd"}, 352 | {file = "gevent-21.12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01928770972181ad8866ee37ea3504f1824587b188fcab782ef1619ce7538766"}, 353 | {file = "gevent-21.12.0-cp38-cp38-win32.whl", hash = "sha256:3c012c73e6c61f13c75e3a4869dbe6a2ffa025f103421a6de9c85e627e7477b1"}, 354 | {file = "gevent-21.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:b7709c64afa8bb3000c28bb91ec42c79594a7cb0f322e20427d57f9762366a5b"}, 355 | {file = "gevent-21.12.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ec21f9eaaa6a7b1e62da786132d6788675b314f25f98d9541f1bf00584ed4749"}, 356 | {file = "gevent-21.12.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22ce1f38fdfe2149ffe8ec2131ca45281791c1e464db34b3b4321ae9d8d2efbb"}, 357 | {file = "gevent-21.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ccffcf708094564e442ac6fde46f0ae9e40015cb69d995f4b39cc29a7643881"}, 358 | {file = "gevent-21.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24d3550fbaeef5fddd794819c2853bca45a86c3d64a056a2c268d981518220d1"}, 359 | {file = "gevent-21.12.0-cp39-cp39-win32.whl", hash = "sha256:2bcec9f80196c751fdcf389ca9f7141e7b0db960d8465ed79be5e685bfcad682"}, 360 | {file = "gevent-21.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:3dad62f55fad839d498c801e139481348991cee6e1c7706041b5fe096cb6a279"}, 361 | {file = "gevent-21.12.0-pp27-pypy_73-win_amd64.whl", hash = "sha256:9f9652d1e4062d4b5b5a0a49ff679fa890430b5f76969d35dccb2df114c55e0f"}, 362 | {file = "gevent-21.12.0.tar.gz", hash = "sha256:f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e"}, 363 | ] 364 | greenlet = [ 365 | {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, 366 | {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, 367 | {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, 368 | {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, 369 | {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, 370 | {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, 371 | {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, 372 | {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, 373 | {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, 374 | {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, 375 | {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, 376 | {file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"}, 377 | {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, 378 | {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, 379 | {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, 380 | {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, 381 | {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, 382 | {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, 383 | {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, 384 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, 385 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, 386 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, 387 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, 388 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, 389 | {file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"}, 390 | {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, 391 | {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, 392 | {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, 393 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, 394 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, 395 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, 396 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, 397 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, 398 | {file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"}, 399 | {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, 400 | {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, 401 | {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, 402 | {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, 403 | {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, 404 | {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, 405 | {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, 406 | {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, 407 | {file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"}, 408 | {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, 409 | {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, 410 | {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, 411 | {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, 412 | {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, 413 | {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, 414 | {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, 415 | {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, 416 | {file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"}, 417 | {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, 418 | {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, 419 | {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, 420 | ] 421 | idna = [ 422 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 423 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 424 | ] 425 | loguru = [ 426 | {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, 427 | {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, 428 | ] 429 | pillow = [ 430 | {file = "Pillow-9.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea"}, 431 | {file = "Pillow-9.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652"}, 432 | {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a"}, 433 | {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7"}, 434 | {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e"}, 435 | {file = "Pillow-9.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3"}, 436 | {file = "Pillow-9.1.0-cp310-cp310-win32.whl", hash = "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160"}, 437 | {file = "Pillow-9.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033"}, 438 | {file = "Pillow-9.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c"}, 439 | {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165"}, 440 | {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a"}, 441 | {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2"}, 442 | {file = "Pillow-9.1.0-cp37-cp37m-win32.whl", hash = "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244"}, 443 | {file = "Pillow-9.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef"}, 444 | {file = "Pillow-9.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512"}, 445 | {file = "Pillow-9.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477"}, 446 | {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b"}, 447 | {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378"}, 448 | {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e"}, 449 | {file = "Pillow-9.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5"}, 450 | {file = "Pillow-9.1.0-cp38-cp38-win32.whl", hash = "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a"}, 451 | {file = "Pillow-9.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34"}, 452 | {file = "Pillow-9.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"}, 453 | {file = "Pillow-9.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581"}, 454 | {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6"}, 455 | {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4"}, 456 | {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331"}, 457 | {file = "Pillow-9.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8"}, 458 | {file = "Pillow-9.1.0-cp39-cp39-win32.whl", hash = "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58"}, 459 | {file = "Pillow-9.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595"}, 460 | {file = "Pillow-9.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481"}, 461 | {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0"}, 462 | {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a"}, 463 | {file = "Pillow-9.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7"}, 464 | {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3"}, 465 | {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8"}, 466 | {file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"}, 467 | {file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"}, 468 | ] 469 | pycparser = [ 470 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 471 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 472 | ] 473 | python-dotenv = [ 474 | {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, 475 | {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, 476 | ] 477 | requests = [ 478 | {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, 479 | {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, 480 | ] 481 | soupsieve = [ 482 | {file = "soupsieve-2.3.1-py3-none-any.whl", hash = "sha256:1a3cca2617c6b38c0343ed661b1fa5de5637f257d4fe22bd9f1338010a1efefb"}, 483 | {file = "soupsieve-2.3.1.tar.gz", hash = "sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9"}, 484 | ] 485 | urllib3 = [ 486 | {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, 487 | {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, 488 | ] 489 | websocket = [ 490 | {file = "websocket-0.2.1.tar.gz", hash = "sha256:42b506fae914ac5ed654e23ba9742e6a342b1a1c3eb92632b6166c65256469a4"}, 491 | ] 492 | websocket-client = [ 493 | {file = "websocket-client-1.3.2.tar.gz", hash = "sha256:50b21db0058f7a953d67cc0445be4b948d7fc196ecbeb8083d68d94628e4abf6"}, 494 | {file = "websocket_client-1.3.2-py3-none-any.whl", hash = "sha256:722b171be00f2b90e1d4fb2f2b53146a536ca38db1da8ff49c972a4e1365d0ef"}, 495 | ] 496 | win32-setctime = [ 497 | {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, 498 | {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, 499 | ] 500 | "zope.event" = [ 501 | {file = "zope.event-4.5.0-py2.py3-none-any.whl", hash = "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42"}, 502 | {file = "zope.event-4.5.0.tar.gz", hash = "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330"}, 503 | ] 504 | "zope.interface" = [ 505 | {file = "zope.interface-5.4.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7"}, 506 | {file = "zope.interface-5.4.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021"}, 507 | {file = "zope.interface-5.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192"}, 508 | {file = "zope.interface-5.4.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a"}, 509 | {file = "zope.interface-5.4.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531"}, 510 | {file = "zope.interface-5.4.0-cp27-cp27m-win32.whl", hash = "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325"}, 511 | {file = "zope.interface-5.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155"}, 512 | {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"}, 513 | {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959"}, 514 | {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e"}, 515 | {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c"}, 516 | {file = "zope.interface-5.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702"}, 517 | {file = "zope.interface-5.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f"}, 518 | {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05"}, 519 | {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004"}, 520 | {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117"}, 521 | {file = "zope.interface-5.4.0-cp35-cp35m-win32.whl", hash = "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8"}, 522 | {file = "zope.interface-5.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63"}, 523 | {file = "zope.interface-5.4.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f"}, 524 | {file = "zope.interface-5.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920"}, 525 | {file = "zope.interface-5.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46"}, 526 | {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc"}, 527 | {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9"}, 528 | {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2"}, 529 | {file = "zope.interface-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78"}, 530 | {file = "zope.interface-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1"}, 531 | {file = "zope.interface-5.4.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e"}, 532 | {file = "zope.interface-5.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b"}, 533 | {file = "zope.interface-5.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f"}, 534 | {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d"}, 535 | {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8"}, 536 | {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf"}, 537 | {file = "zope.interface-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7"}, 538 | {file = "zope.interface-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94"}, 539 | {file = "zope.interface-5.4.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3"}, 540 | {file = "zope.interface-5.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e"}, 541 | {file = "zope.interface-5.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7"}, 542 | {file = "zope.interface-5.4.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120"}, 543 | {file = "zope.interface-5.4.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48"}, 544 | {file = "zope.interface-5.4.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4"}, 545 | {file = "zope.interface-5.4.0-cp38-cp38-win32.whl", hash = "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb"}, 546 | {file = "zope.interface-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54"}, 547 | {file = "zope.interface-5.4.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4"}, 548 | {file = "zope.interface-5.4.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d"}, 549 | {file = "zope.interface-5.4.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83"}, 550 | {file = "zope.interface-5.4.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25"}, 551 | {file = "zope.interface-5.4.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1"}, 552 | {file = "zope.interface-5.4.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c"}, 553 | {file = "zope.interface-5.4.0-cp39-cp39-win32.whl", hash = "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e"}, 554 | {file = "zope.interface-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09"}, 555 | {file = "zope.interface-5.4.0.tar.gz", hash = "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e"}, 556 | ] 557 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "reddit-place-script-2022" 3 | version = "1.0.0" 4 | description = "A python bot for automatically placing images in r/place" 5 | authors = ["Your Name "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.8" 10 | requests = "^2.27.1" 11 | colorama = "^0.4.4" 12 | Pillow = "^9.1.0" 13 | urllib3 = "^1.26.9" 14 | certifi = "^2021.10.8" 15 | charset-normalizer = "^2.0.12" 16 | idna = "^3.3" 17 | python-dotenv = "^0.20.0" 18 | websocket-client = "^1.3.2" 19 | loguru = "^0.6.0" 20 | click = "^8.1.2" 21 | beautifulsoup4 = "^4.10.0" 22 | websocket = "^0.2.1" 23 | 24 | 25 | 26 | [build-system] 27 | requires = ["poetry-core"] 28 | build-backend = "poetry.core.masonry.api" 29 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2021.10.8 2 | charset-normalizer==2.0.12 3 | idna==3.3 4 | python-dotenv==0.20.0 5 | requests==2.27.1 6 | urllib3==1.26.9 7 | Pillow~=9.1.0 8 | websocket-client~=1.3.2 9 | click==8.1.2 10 | loguru==0.6.0 11 | beautifulsoup4~=4.10.0 12 | stem~=1.8.0 -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/src/__init__.py -------------------------------------------------------------------------------- /src/mappings.py: -------------------------------------------------------------------------------- 1 | import math 2 | from PIL import ImageColor 3 | 4 | 5 | class ColorMapper: 6 | COLOR_MAP = { 7 | "#6D001A": 0, # darkest red 8 | "#BE0039": 1, # dark red 9 | "#FF4500": 2, # red 10 | "#FFA800": 3, # orange 11 | "#FFD635": 4, # yellow 12 | "#FFF8B8": 5, # pale yellow 13 | "#00A368": 6, # dark green 14 | "#00CC78": 7, # green 15 | "#7EED56": 8, # light green 16 | "#00756F": 9, # dark teal 17 | "#009EAA": 10, # teal 18 | "#00CCC0": 11, # light teal 19 | "#2450A4": 12, # dark blue 20 | "#3690EA": 13, # blue 21 | "#51E9F4": 14, # light blue 22 | "#493AC1": 15, # indigo 23 | "#6A5CFF": 16, # periwinkle 24 | "#94B3FF": 17, # lavender 25 | "#811E9F": 18, # dark purple 26 | "#B44AC0": 19, # purple 27 | "#E4ABFF": 20, # pale purple 28 | "#DE107F": 21, # magenta 29 | "#FF3881": 22, # pink 30 | "#FF99AA": 23, # light pink 31 | "#6D482F": 24, # dark brown 32 | "#9C6926": 25, # brown 33 | "#FFB470": 26, # beige 34 | "#000000": 27, # black 35 | "#515252": 28, # dark gray 36 | "#898D90": 29, # gray 37 | "#D4D7D9": 30, # light gray 38 | "#FFFFFF": 31, # white 39 | } 40 | 41 | # map of pixel color ids to verbose name (for debugging) 42 | NAME_MAP = { 43 | 0: "Darkest Red", 44 | 1: "Dark Red", 45 | 2: "Bright Red", 46 | 3: "Orange", 47 | 4: "Yellow", 48 | 5: "Pale yellow", 49 | 6: "Dark Green", 50 | 7: "Green", 51 | 8: "Light Green", 52 | 9: "Dark Teal", 53 | 10: "Teal", 54 | 11: "Light Teal", 55 | 12: "Dark Blue", 56 | 13: "Blue", 57 | 14: "Light Blue", 58 | 15: "Indigo", 59 | 16: "Periwinkle", 60 | 17: "Lavender", 61 | 18: "Dark Purple", 62 | 19: "Purple", 63 | 20: "pale purple", 64 | 21: "magenta", 65 | 22: "Pink", 66 | 23: "Light Pink", 67 | 24: "Dark Brown", 68 | 25: "Brown", 69 | 26: "beige", 70 | 27: "Black", 71 | 28: "dark gray", 72 | 29: "Gray", 73 | 30: "Light Gray", 74 | 31: "White", 75 | } 76 | 77 | @staticmethod 78 | def rgb_to_hex(rgb: tuple): 79 | """Convert rgb tuple to hexadecimal string.""" 80 | return ("#%02x%02x%02x" % rgb).upper() 81 | 82 | @staticmethod 83 | def color_id_to_name(color_id: int): 84 | """More verbose color indicator from a pixel color id.""" 85 | if color_id in ColorMapper.NAME_MAP.keys(): 86 | return "{} ({})".format(ColorMapper.NAME_MAP[color_id], str(color_id)) 87 | return "Invalid Color ({})".format(str(color_id)) 88 | 89 | @staticmethod 90 | def closest_color( 91 | target_rgb: tuple, rgb_colors_array: list, legacy_transparency: bool 92 | ): 93 | """Find the closest rgb color from palette to a target rgb color, as well as handling transparency""" 94 | 95 | # first check is for the alpha channel transparency in ex. png 96 | if target_rgb[3] == 0: 97 | return (69, 42, 0) 98 | # second check is for the legacy method of transparency using hex #452A00. 99 | if target_rgb[:3] == (69, 42, 0) and legacy_transparency: 100 | return (69, 42, 0) 101 | 102 | r, g, b = target_rgb[:3] 103 | color_diffs = [] 104 | for color in rgb_colors_array: 105 | cr, cg, cb = color 106 | color_diff = math.sqrt((r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2) 107 | color_diffs.append((color_diff, color)) 108 | return min(color_diffs)[1] 109 | 110 | @staticmethod 111 | def generate_rgb_colors_array(): 112 | """Generate array of available rgb colors to be used""" 113 | return [ 114 | ImageColor.getcolor(color_hex, "RGB") 115 | for color_hex in list(ColorMapper.COLOR_MAP.keys()) 116 | ] 117 | -------------------------------------------------------------------------------- /src/proxy.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import subprocess 4 | import time 5 | from stem import Signal, InvalidArguments, SocketError, ProtocolError 6 | from stem.control import Controller 7 | 8 | 9 | def Init(self): 10 | self.proxies = ( 11 | get_proxies(self, self.json_data["proxies"]) 12 | if "proxies" in self.json_data and self.json_data["proxies"] is not None 13 | else None 14 | ) 15 | if self.proxies is None and os.path.exists( 16 | os.path.join(os.getcwd(), "proxies.txt") 17 | ): 18 | self.proxies = get_proxies_text(self) 19 | self.compactlogging = ( 20 | self.json_data["compact_logging"] 21 | if "compact_logging" in self.json_data 22 | and self.json_data["compact_logging"] is not None 23 | else True 24 | ) 25 | self.using_tor = ( 26 | self.json_data["using_tor"] 27 | if "using_tor" in self.json_data and self.json_data["using_tor"] is not None 28 | else False 29 | ) 30 | self.tor_password = ( 31 | self.json_data["tor_password"] 32 | if "tor_password" in self.json_data 33 | and self.json_data["tor_password"] is not None 34 | else "Passwort" # this is intentional, as I don't really want to mess around with the torrc again 35 | ) 36 | self.tor_delay = ( 37 | self.json_data["tor_delay"] 38 | if "tor_delay" in self.json_data and self.json_data["tor_delay"] is not None 39 | else 10 40 | ) 41 | self.use_builtin_tor = ( 42 | self.json_data["use_builtin_tor"] 43 | if "use_builtin_tor" in self.json_data 44 | and self.json_data["use_builtin_tor"] is not None 45 | else True 46 | ) 47 | self.tor_port = ( 48 | self.json_data["tor_port"] 49 | if "tor_port" in self.json_data and self.json_data["tor_port"] is not None 50 | else 1881 51 | ) 52 | self.tor_control_port = ( 53 | self.json_data["tor_control_port"] 54 | if "tor_port" in self.json_data 55 | and self.json_data["tor_control_port"] is not None 56 | else 9051 57 | ) 58 | self.tor_ip = ( 59 | self.json_data["tor_ip"] 60 | if "tor_ip" in self.json_data and self.json_data["tor_ip"] is not None 61 | else "127.0.0.1" 62 | ) 63 | 64 | print(self.tor_ip) 65 | 66 | # tor connection 67 | if self.using_tor: 68 | self.proxies = get_proxies(self, [self.tor_ip + ":" + str(self.tor_port)]) 69 | if self.use_builtin_tor: 70 | subprocess.Popen( 71 | '"' 72 | + os.path.join(os.getcwd(), "./tor/Tor/tor.exe") 73 | + '"' 74 | + " --defaults-torrc " 75 | + '"' 76 | + os.path.join(os.getcwd(), "./Tor/Tor/torrc") 77 | + '"' 78 | + " --HTTPTunnelPort " 79 | + str(self.tor_port), 80 | shell=True, 81 | ) 82 | try: 83 | self.tor_controller = Controller.from_port(port=self.tor_control_port) 84 | self.tor_controller.authenticate(self.tor_password) 85 | self.logger.info("successfully connected to tor!") 86 | except (ValueError, SocketError): 87 | self.logger.error("connection to tor failed, disabling tor") 88 | self.using_tor = False 89 | 90 | 91 | def get_proxies_text(self): 92 | path_proxies = os.path.join(os.getcwd(), "proxies.txt") 93 | f = open(path_proxies) 94 | file = f.read() 95 | f.close() 96 | proxies_list = file.splitlines() 97 | self.proxies = [] 98 | for i in proxies_list: 99 | self.proxies.append({"https": i, "http": i}) 100 | self.logger.debug("loaded proxies {} from file {}", i, path_proxies) 101 | 102 | 103 | def get_proxies(self, proxies): 104 | proxies_list = [] 105 | for i in proxies: 106 | proxies_list.append({"https": i, "http": i}) 107 | 108 | self.logger.debug("Loaded proxies: {}", str(proxies_list)) 109 | return proxies_list 110 | return proxies_list 111 | 112 | 113 | # name is the username of the worker and is used for personal proxies 114 | def get_random_proxy(self, name=None): 115 | if not self.using_tor: 116 | random_proxy = None 117 | if name is not None: 118 | if ( 119 | "personal_proxy" in self.json_data["workers"][name] 120 | and self.json_data["workers"][name]["personal_proxy"] is not None 121 | ): 122 | proxy = self.json_data["workers"][name]["personal_proxy"] 123 | return {"https": proxy, "http": proxy} 124 | if self.proxies is not None: 125 | random_proxy = self.proxies[random.randint(0, len(self.proxies) - 1)] 126 | self.logger.debug("Using proxy: {}", str(random_proxy)) 127 | return random_proxy 128 | 129 | return random_proxy 130 | else: 131 | tor_reconnect(self) 132 | self.logger.debug("Using Tor. Selecting first proxy: {}.", str(self.proxies[0])) 133 | return self.proxies[0] 134 | 135 | 136 | def tor_reconnect(self): 137 | if self.using_tor: 138 | try: 139 | self.tor_controller.signal(Signal.NEWNYM) 140 | self.logger.info("New Tor connection processing") 141 | time.sleep(self.tor_delay) 142 | except (InvalidArguments, ProtocolError): 143 | self.logger.error("couldn't establish new tor connection, disabling tor") 144 | self.using_tor = False 145 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from PIL import Image, UnidentifiedImageError 4 | 5 | 6 | def get_json_data(self, config_path): 7 | configFilePath = os.path.join(os.getcwd(), config_path) 8 | 9 | if not os.path.exists(configFilePath): 10 | exit("No config.json file found. Read the README") 11 | 12 | # To not keep file open whole execution time 13 | f = open(configFilePath) 14 | json_data = json.load(f) 15 | f.close() 16 | 17 | return json_data 18 | 19 | # Read the input image.jpg file 20 | 21 | 22 | def load_image(self): 23 | # Read and load the image to draw and get its dimensions 24 | try: 25 | im = Image.open(self.image_path) 26 | except FileNotFoundError: 27 | self.logger.exception("Failed to load image") 28 | exit() 29 | except UnidentifiedImageError: 30 | self.logger.exception("File found, but couldn't identify image format") 31 | 32 | # Convert all images to RGBA - Transparency should only be supported with PNG 33 | if im.mode != "RGBA": 34 | im = im.convert("RGBA") 35 | self.logger.info("Converted to rgba") 36 | self.pix = im.load() 37 | 38 | self.logger.info("Loaded image size: {}", im.size) 39 | 40 | self.image_size = im.size 41 | -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | pip install -r requirements.txt 3 | python main.py -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | pip3 install -r requirements.txt 2 | python3 main.py 3 | -------------------------------------------------------------------------------- /startverbose.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | pip install -r requirements.txt 3 | python main.py --debug -------------------------------------------------------------------------------- /startverbose.sh: -------------------------------------------------------------------------------- 1 | pip3 install -r requirements.txt 2 | python3 main.py --debug 3 | -------------------------------------------------------------------------------- /tor/Tor/libcrypto-1_1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/tor/Tor/libcrypto-1_1.dll -------------------------------------------------------------------------------- /tor/Tor/libevent-2-1-7.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/tor/Tor/libevent-2-1-7.dll -------------------------------------------------------------------------------- /tor/Tor/libevent_core-2-1-7.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/tor/Tor/libevent_core-2-1-7.dll -------------------------------------------------------------------------------- /tor/Tor/libevent_extra-2-1-7.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/tor/Tor/libevent_extra-2-1-7.dll -------------------------------------------------------------------------------- /tor/Tor/libgcc_s_dw2-1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/tor/Tor/libgcc_s_dw2-1.dll -------------------------------------------------------------------------------- /tor/Tor/libssl-1_1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/tor/Tor/libssl-1_1.dll -------------------------------------------------------------------------------- /tor/Tor/libssp-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/tor/Tor/libssp-0.dll -------------------------------------------------------------------------------- /tor/Tor/libwinpthread-1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/tor/Tor/libwinpthread-1.dll -------------------------------------------------------------------------------- /tor/Tor/tor-gencert.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/tor/Tor/tor-gencert.exe -------------------------------------------------------------------------------- /tor/Tor/tor.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/tor/Tor/tor.exe -------------------------------------------------------------------------------- /tor/Tor/torrc: -------------------------------------------------------------------------------- 1 | ## Configuration file for a typical Tor user 2 | ## Last updated 28 February 2019 for Tor 0.3.5.1-alpha. 3 | ## (may or may not work for much older or much newer versions of Tor.) 4 | ## 5 | ## Lines that begin with "## " try to explain what's going on. Lines 6 | ## that begin with just "#" are disabled commands: you can enable them 7 | ## by removing the "#" symbol. 8 | ## 9 | ## See 'man tor', or https://www.torproject.org/docs/tor-manual.html, 10 | ## for more options you can use in this file. 11 | ## 12 | ## Tor will look for this file in various places based on your platform: 13 | ## https://www.torproject.org/docs/faq#torrc 14 | 15 | ## Tor opens a SOCKS proxy on port 9050 by default -- even if you don't 16 | ## configure one below. Set "SOCKSPort 0" if you plan to run Tor only 17 | ## as a relay, and not make any local application connections yourself. 18 | #SOCKSPort 9050 # Default: Bind to localhost:9050 for local connections. 19 | #SOCKSPort 192.168.0.1:9100 # Bind to this address:port too. 20 | 21 | ## Entry policies to allow/deny SOCKS requests based on IP address. 22 | ## First entry that matches wins. If no SOCKSPolicy is set, we accept 23 | ## all (and only) requests that reach a SOCKSPort. Untrusted users who 24 | ## can access your SOCKSPort may be able to learn about the connections 25 | ## you make. 26 | #SOCKSPolicy accept 192.168.0.0/16 27 | #SOCKSPolicy accept6 FC00::/7 28 | #SOCKSPolicy reject * 29 | 30 | ## Logs go to stdout at level "notice" unless redirected by something 31 | ## else, like one of the below lines. You can have as many Log lines as 32 | ## you want. 33 | ## 34 | ## We advise using "notice" in most cases, since anything more verbose 35 | ## may provide sensitive information to an attacker who obtains the logs. 36 | ## 37 | ## Send all messages of level 'notice' or higher to @LOCALSTATEDIR@/log/tor/notices.log 38 | #Log notice file @LOCALSTATEDIR@/log/tor/notices.log 39 | ## Send every possible message to @LOCALSTATEDIR@/log/tor/debug.log 40 | #Log debug file @LOCALSTATEDIR@/log/tor/debug.log 41 | ## Use the system log instead of Tor's logfiles 42 | #Log notice syslog 43 | ## To send all messages to stderr: 44 | #Log debug stderr 45 | 46 | ## Uncomment this to start the process in the background... or use 47 | ## --runasdaemon 1 on the command line. This is ignored on Windows; 48 | ## see the FAQ entry if you want Tor to run as an NT service. 49 | #RunAsDaemon 1 50 | 51 | ## The directory for keeping all the keys/etc. By default, we store 52 | ## things in $HOME/.tor on Unix, and in Application Data\tor on Windows. 53 | #DataDirectory @LOCALSTATEDIR@/lib/tor 54 | 55 | ## The port on which Tor will listen for local connections from Tor 56 | ## controller applications, as documented in control-spec.txt. 57 | ControlPort 9051 58 | ## If you enable the controlport, be sure to enable one of these 59 | ## authentication methods, to prevent attackers from accessing it. 60 | HashedControlPassword 16:89C1D9014810C25E607140215BF484E12EB1B575F6E9DB91302C0F599A 61 | #CookieAuthentication 1 62 | 63 | ############### This section is just for location-hidden services ### 64 | 65 | ## Once you have configured a hidden service, you can look at the 66 | ## contents of the file ".../hidden_service/hostname" for the address 67 | ## to tell people. 68 | ## 69 | ## HiddenServicePort x y:z says to redirect requests on port x to the 70 | ## address y:z. 71 | 72 | #HiddenServiceDir @LOCALSTATEDIR@/lib/tor/hidden_service/ 73 | #HiddenServicePort 80 127.0.0.1:80 74 | 75 | #HiddenServiceDir @LOCALSTATEDIR@/lib/tor/other_hidden_service/ 76 | #HiddenServicePort 80 127.0.0.1:80 77 | #HiddenServicePort 22 127.0.0.1:22 78 | 79 | ################ This section is just for relays ##################### 80 | # 81 | ## See https://www.torproject.org/docs/tor-doc-relay for details. 82 | 83 | ## Required: what port to advertise for incoming Tor connections. 84 | #ORPort 9001 85 | ## If you want to listen on a port other than the one advertised in 86 | ## ORPort (e.g. to advertise 443 but bind to 9090), you can do it as 87 | ## follows. You'll need to do ipchains or other port forwarding 88 | ## yourself to make this work. 89 | #ORPort 443 NoListen 90 | #ORPort 127.0.0.1:9090 NoAdvertise 91 | ## If you want to listen on IPv6 your numeric address must be explicitly 92 | ## between square brackets as follows. You must also listen on IPv4. 93 | #ORPort [2001:DB8::1]:9050 94 | 95 | ## The IP address or full DNS name for incoming connections to your 96 | ## relay. Leave commented out and Tor will guess. 97 | #Address noname.example.com 98 | 99 | ## If you have multiple network interfaces, you can specify one for 100 | ## outgoing traffic to use. 101 | ## OutboundBindAddressExit will be used for all exit traffic, while 102 | ## OutboundBindAddressOR will be used for all OR and Dir connections 103 | ## (DNS connections ignore OutboundBindAddress). 104 | ## If you do not wish to differentiate, use OutboundBindAddress to 105 | ## specify the same address for both in a single line. 106 | #OutboundBindAddressExit 10.0.0.4 107 | #OutboundBindAddressOR 10.0.0.5 108 | 109 | ## A handle for your relay, so people don't have to refer to it by key. 110 | ## Nicknames must be between 1 and 19 characters inclusive, and must 111 | ## contain only the characters [a-zA-Z0-9]. 112 | ## If not set, "Unnamed" will be used. 113 | #Nickname ididnteditheconfig 114 | 115 | ## Define these to limit how much relayed traffic you will allow. Your 116 | ## own traffic is still unthrottled. Note that RelayBandwidthRate must 117 | ## be at least 75 kilobytes per second. 118 | ## Note that units for these config options are bytes (per second), not 119 | ## bits (per second), and that prefixes are binary prefixes, i.e. 2^10, 120 | ## 2^20, etc. 121 | #RelayBandwidthRate 100 KBytes # Throttle traffic to 100KB/s (800Kbps) 122 | #RelayBandwidthBurst 200 KBytes # But allow bursts up to 200KB (1600Kb) 123 | 124 | ## Use these to restrict the maximum traffic per day, week, or month. 125 | ## Note that this threshold applies separately to sent and received bytes, 126 | ## not to their sum: setting "40 GB" may allow up to 80 GB total before 127 | ## hibernating. 128 | ## 129 | ## Set a maximum of 40 gigabytes each way per period. 130 | #AccountingMax 40 GBytes 131 | ## Each period starts daily at midnight (AccountingMax is per day) 132 | #AccountingStart day 00:00 133 | ## Each period starts on the 3rd of the month at 15:00 (AccountingMax 134 | ## is per month) 135 | #AccountingStart month 3 15:00 136 | 137 | ## Administrative contact information for this relay or bridge. This line 138 | ## can be used to contact you if your relay or bridge is misconfigured or 139 | ## something else goes wrong. Note that we archive and publish all 140 | ## descriptors containing these lines and that Google indexes them, so 141 | ## spammers might also collect them. You may want to obscure the fact that 142 | ## it's an email address and/or generate a new address for this purpose. 143 | ## 144 | ## If you are running multiple relays, you MUST set this option. 145 | ## 146 | #ContactInfo Random Person 147 | ## You might also include your PGP or GPG fingerprint if you have one: 148 | #ContactInfo 0xFFFFFFFF Random Person 149 | 150 | ## Uncomment this to mirror directory information for others. Please do 151 | ## if you have enough bandwidth. 152 | #DirPort 9030 # what port to advertise for directory connections 153 | ## If you want to listen on a port other than the one advertised in 154 | ## DirPort (e.g. to advertise 80 but bind to 9091), you can do it as 155 | ## follows. below too. You'll need to do ipchains or other port 156 | ## forwarding yourself to make this work. 157 | #DirPort 80 NoListen 158 | #DirPort 127.0.0.1:9091 NoAdvertise 159 | ## Uncomment to return an arbitrary blob of html on your DirPort. Now you 160 | ## can explain what Tor is if anybody wonders why your IP address is 161 | ## contacting them. See contrib/tor-exit-notice.html in Tor's source 162 | ## distribution for a sample. 163 | #DirPortFrontPage @CONFDIR@/tor-exit-notice.html 164 | 165 | ## Uncomment this if you run more than one Tor relay, and add the identity 166 | ## key fingerprint of each Tor relay you control, even if they're on 167 | ## different networks. You declare it here so Tor clients can avoid 168 | ## using more than one of your relays in a single circuit. See 169 | ## https://www.torproject.org/docs/faq#MultipleRelays 170 | ## However, you should never include a bridge's fingerprint here, as it would 171 | ## break its concealability and potentially reveal its IP/TCP address. 172 | ## 173 | ## If you are running multiple relays, you MUST set this option. 174 | ## 175 | ## Note: do not use MyFamily on bridge relays. 176 | #MyFamily $keyid,$keyid,... 177 | 178 | ## Uncomment this if you want your relay to be an exit, with the default 179 | ## exit policy (or whatever exit policy you set below). 180 | ## (If ReducedExitPolicy, ExitPolicy, or IPv6Exit are set, relays are exits. 181 | ## If none of these options are set, relays are non-exits.) 182 | #ExitRelay 1 183 | 184 | ## Uncomment this if you want your relay to allow IPv6 exit traffic. 185 | ## (Relays do not allow any exit traffic by default.) 186 | #IPv6Exit 1 187 | 188 | ## Uncomment this if you want your relay to be an exit, with a reduced set 189 | ## of exit ports. 190 | #ReducedExitPolicy 1 191 | 192 | ## Uncomment these lines if you want your relay to be an exit, with the 193 | ## specified set of exit IPs and ports. 194 | ## 195 | ## A comma-separated list of exit policies. They're considered first 196 | ## to last, and the first match wins. 197 | ## 198 | ## If you want to allow the same ports on IPv4 and IPv6, write your rules 199 | ## using accept/reject *. If you want to allow different ports on IPv4 and 200 | ## IPv6, write your IPv6 rules using accept6/reject6 *6, and your IPv4 rules 201 | ## using accept/reject *4. 202 | ## 203 | ## If you want to _replace_ the default exit policy, end this with either a 204 | ## reject *:* or an accept *:*. Otherwise, you're _augmenting_ (prepending to) 205 | ## the default exit policy. Leave commented to just use the default, which is 206 | ## described in the man page or at 207 | ## https://www.torproject.org/documentation.html 208 | ## 209 | ## Look at https://www.torproject.org/faq-abuse.html#TypicalAbuses 210 | ## for issues you might encounter if you use the default exit policy. 211 | ## 212 | ## If certain IPs and ports are blocked externally, e.g. by your firewall, 213 | ## you should update your exit policy to reflect this -- otherwise Tor 214 | ## users will be told that those destinations are down. 215 | ## 216 | ## For security, by default Tor rejects connections to private (local) 217 | ## networks, including to the configured primary public IPv4 and IPv6 addresses, 218 | ## and any public IPv4 and IPv6 addresses on any interface on the relay. 219 | ## See the man page entry for ExitPolicyRejectPrivate if you want to allow 220 | ## "exit enclaving". 221 | ## 222 | #ExitPolicy accept *:6660-6667,reject *:* # allow irc ports on IPv4 and IPv6 but no more 223 | #ExitPolicy accept *:119 # accept nntp ports on IPv4 and IPv6 as well as default exit policy 224 | #ExitPolicy accept *4:119 # accept nntp ports on IPv4 only as well as default exit policy 225 | #ExitPolicy accept6 *6:119 # accept nntp ports on IPv6 only as well as default exit policy 226 | #ExitPolicy reject *:* # no exits allowed 227 | 228 | ## Bridge relays (or "bridges") are Tor relays that aren't listed in the 229 | ## main directory. Since there is no complete public list of them, even an 230 | ## ISP that filters connections to all the known Tor relays probably 231 | ## won't be able to block all the bridges. Also, websites won't treat you 232 | ## differently because they won't know you're running Tor. If you can 233 | ## be a real relay, please do; but if not, be a bridge! 234 | ## 235 | ## Warning: when running your Tor as a bridge, make sure than MyFamily is 236 | ## NOT configured. 237 | #BridgeRelay 1 238 | ## By default, Tor will advertise your bridge to users through various 239 | ## mechanisms like https://bridges.torproject.org/. If you want to run 240 | ## a private bridge, for example because you'll give out your bridge 241 | ## address manually to your friends, uncomment this line: 242 | #BridgeDistribution none 243 | 244 | ## Configuration options can be imported from files or folders using the %include 245 | ## option with the value being a path. This path can have wildcards. Wildcards are 246 | ## expanded first, using lexical order. Then, for each matching file or folder, the following 247 | ## rules are followed: if the path is a file, the options from the file will be parsed as if 248 | ## they were written where the %include option is. If the path is a folder, all files on that 249 | ## folder will be parsed following lexical order. Files starting with a dot are ignored. Files 250 | ## on subfolders are ignored. 251 | ## The %include option can be used recursively. 252 | #%include /etc/torrc.d/*.conf 253 | 254 | -------------------------------------------------------------------------------- /tor/Tor/zlib1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdeepak2002/reddit-place-script-2022/b7ecc48c3f8eea1f56522a0a97141f848b1ba310/tor/Tor/zlib1.dll --------------------------------------------------------------------------------