├── .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 | [](https://github.com/psf/black)
4 | [](https://forthebadge.com)
5 | [](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
--------------------------------------------------------------------------------