├── _data ├── profile_logs.json ├── farm_data │ ├── search_users.txt │ ├── search_authors.txt │ ├── search_channels.txt │ ├── emoji_names.txt │ ├── subscribe_to_channels.txt │ ├── subscribe_to_users.txt │ ├── images_for_casts │ │ ├── specific │ │ │ └── example_123.png │ │ └── random │ │ │ └── GPy10K8WYAACe7u.jpg │ └── casts.txt ├── sensitive_data │ └── metamask_passwords.txt ├── profile_ids.py └── config.py ├── requirements.txt ├── start.bat ├── src ├── helpers.py ├── AdspowerProfile.py └── WarpcastProfile.py ├── .gitignore ├── farm_warpcast.py └── README.md /_data/profile_logs.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /_data/farm_data/search_users.txt: -------------------------------------------------------------------------------- 1 | brian 2 | scroll 3 | -------------------------------------------------------------------------------- /_data/farm_data/search_authors.txt: -------------------------------------------------------------------------------- 1 | brian 2 | scroll 3 | -------------------------------------------------------------------------------- /_data/farm_data/search_channels.txt: -------------------------------------------------------------------------------- 1 | brian 2 | scroll 3 | -------------------------------------------------------------------------------- /_data/farm_data/emoji_names.txt: -------------------------------------------------------------------------------- 1 | heart 2 | bank 3 | lemon 4 | banana 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.32.2 2 | selenium==4.7.2 3 | loguru==0.6.0 -------------------------------------------------------------------------------- /_data/farm_data/subscribe_to_channels.txt: -------------------------------------------------------------------------------- 1 | food 2 | product 3 | gray 4 | travel -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | call .\venv\Scripts\activate.bat 2 | python -m farm_warpcast 3 | pause -------------------------------------------------------------------------------- /_data/farm_data/subscribe_to_users.txt: -------------------------------------------------------------------------------- 1 | ccarella.eth 2 | bored 3 | slokh 4 | 0xdesigner -------------------------------------------------------------------------------- /_data/sensitive_data/metamask_passwords.txt: -------------------------------------------------------------------------------- 1 | 1|dsjkakdcasdas 2 | 2|dsakkddsdfvas 3 | 3|dspoakfposakpfoas 4 | 4|fskalpofkasofk[asf 5 | 5|dsaokopwqkopdlsad -------------------------------------------------------------------------------- /_data/profile_ids.py: -------------------------------------------------------------------------------- 1 | profile_ids = { 2 | "1": "safasgasg", 3 | "2": "fassafasf", 4 | "3": "dSAFfsdsaf", 5 | "4": "sOpioiweq", 6 | "5": "OISAPoewd" 7 | } 8 | -------------------------------------------------------------------------------- /_data/farm_data/images_for_casts/specific/example_123.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamigordon/Warpcast-adspower-farm/HEAD/_data/farm_data/images_for_casts/specific/example_123.png -------------------------------------------------------------------------------- /_data/farm_data/images_for_casts/random/GPy10K8WYAACe7u.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamigordon/Warpcast-adspower-farm/HEAD/_data/farm_data/images_for_casts/random/GPy10K8WYAACe7u.jpg -------------------------------------------------------------------------------- /_data/farm_data/casts.txt: -------------------------------------------------------------------------------- 1 | 1|Line one\nLine two\n🚀🚀 2 | 1|ZkSync pidarasi 3 | 1|Dohuja 4 | 1|Nasipet 5 | 2|Podpiska 6 | 3|Lajk 7 | 4|Repost 8 | 5|CKC 9 | 2|Krutit 10 | 1|Mir 11 | -------------------------------------------------------------------------------- /src/helpers.py: -------------------------------------------------------------------------------- 1 | from random import uniform 2 | from os import remove, path 3 | 4 | busher_logo = """ 5 | ██████╗░██╗░░░██╗░██████╗██╗░░██╗███████╗██████╗░ 6 | ██╔══██╗██║░░░██║██╔════╝██║░░██║██╔════╝██╔══██╗ 7 | ██████╦╝██║░░░██║╚█████╗░███████║█████╗░░██████╔╝ 8 | ██╔══██╗██║░░░██║░╚═══██╗██╔══██║██╔══╝░░██╔══██╗ 9 | ██████╦╝╚██████╔╝██████╔╝██║░░██║███████╗██║░░██║ 10 | ╚═════╝░░╚═════╝░╚═════╝░╚═╝░░╚═╝╚══════╝╚═╝░░╚═╝ 11 | """ 12 | 13 | social_links = """ 14 | Telegram channel: @CryptoKiddiesClub 15 | Telegram chat: @CryptoKiddiesChat 16 | Twitter: @CryptoBusher 17 | """ 18 | 19 | 20 | def remove_line(file_path: str, line_to_remove: str): 21 | with open(file_path, "r", encoding='utf-8') as _file: 22 | lines = [i.strip() for i in _file] 23 | 24 | with open(file_path, "w", encoding='utf-8') as _file: 25 | for line in lines: 26 | if line != line_to_remove: 27 | _file.write(line + '\n') 28 | 29 | 30 | def probability_check_is_positive(probability: int | float) -> bool: 31 | return True if uniform(0, 1) < probability else False 32 | 33 | 34 | def remove_files(rel_paths: list) -> None: 35 | for file_path in rel_paths: 36 | if path.exists(file_path): 37 | remove(file_path) 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | 165 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 166 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 167 | 168 | # User-specific stuff 169 | .idea/**/workspace.xml 170 | .idea/**/tasks.xml 171 | .idea/**/usage.statistics.xml 172 | .idea/**/dictionaries 173 | .idea/**/shelf 174 | 175 | # AWS User-specific 176 | .idea/**/aws.xml 177 | 178 | # Generated files 179 | .idea/**/contentModel.xml 180 | 181 | # Sensitive or high-churn files 182 | .idea/**/dataSources/ 183 | .idea/**/dataSources.ids 184 | .idea/**/dataSources.local.xml 185 | .idea/**/sqlDataSources.xml 186 | .idea/**/dynamic.xml 187 | .idea/**/uiDesigner.xml 188 | .idea/**/dbnavigator.xml 189 | 190 | # Gradle 191 | .idea/**/gradle.xml 192 | .idea/**/libraries 193 | 194 | # Gradle and Maven with auto-import 195 | # When using Gradle or Maven with auto-import, you should exclude module files, 196 | # since they will be recreated, and may cause churn. Uncomment if using 197 | # auto-import. 198 | # .idea/artifacts 199 | # .idea/compiler.xml 200 | # .idea/jarRepositories.xml 201 | # .idea/modules.xml 202 | # .idea/*.iml 203 | # .idea/modules 204 | # *.iml 205 | # *.ipr 206 | 207 | # CMake 208 | cmake-build-*/ 209 | 210 | # Mongo Explorer plugin 211 | .idea/**/mongoSettings.xml 212 | 213 | # File-based project format 214 | *.iws 215 | 216 | # IntelliJ 217 | out/ 218 | 219 | # mpeltonen/sbt-idea plugin 220 | .idea_modules/ 221 | 222 | # JIRA plugin 223 | atlassian-ide-plugin.xml 224 | 225 | # Cursive Clojure plugin 226 | .idea/replstate.xml 227 | 228 | # SonarLint plugin 229 | .idea/sonarlint/ 230 | 231 | # Crashlytics plugin (for Android Studio and IntelliJ) 232 | com_crashlytics_export_strings.xml 233 | crashlytics.properties 234 | crashlytics-build.properties 235 | fabric.properties 236 | 237 | # Editor-based Rest Client 238 | .idea/httpRequests 239 | 240 | # Android studio 3.1+ serialized cache file 241 | .idea/caches/build_file_checksums.ser 242 | 243 | .idea 244 | data/ 245 | -------------------------------------------------------------------------------- /farm_warpcast.py: -------------------------------------------------------------------------------- 1 | from sys import stderr 2 | from random import randint, choice, shuffle, uniform 3 | from time import sleep 4 | 5 | from loguru import logger 6 | 7 | from src.helpers import * 8 | from src.WarpcastProfile import WarpcastProfile 9 | from data.profile_ids import profile_ids 10 | from data.config import config 11 | 12 | 13 | logger.remove() 14 | logger_level = "DEBUG" if config['show_debug_logs'] else "INFO" 15 | log_format = "{time:HH:mm:ss} | {level: <8} | {message}" 16 | logger.add(stderr, level=logger_level, format=log_format) 17 | logger.add("data/debug_log.log", level="DEBUG", format=log_format) 18 | 19 | 20 | print(busher_logo) 21 | print(social_links) 22 | 23 | 24 | def start_farm(_account: WarpcastProfile): 25 | actions_list = { 26 | _account.cast_on_homepage: { 27 | "use_module": config['module_switches']['cast_on_homepage'], 28 | "probability": config['cast_on_homepage']['use_module_probability'], 29 | "name": "cast on homepage" 30 | }, 31 | _account.surf_feed: { 32 | "use_module": config['module_switches']['surf_feed'], 33 | "probability": config['surf_feed']['use_module_probability'], 34 | "name": "surf feed" 35 | }, 36 | _account.subscribe_to_users_via_explore: { 37 | "use_module": config['module_switches']['subscribe_to_users_via_explore'], 38 | "probability": config['subscribe_via_explore']['to_users_probability'], 39 | "name": "subscribe to users via explore" 40 | }, 41 | _account.subscribe_to_channels_via_explore: { 42 | "use_module": config['module_switches']['subscribe_to_channels_via_explore'], 43 | "probability": config['subscribe_via_explore']['to_channels_probability'], 44 | "name": "subscribe to channels via explore" 45 | }, 46 | _account.subscribe_to_authors_via_search: { 47 | "use_module": config['module_switches']['subscribe_to_authors_via_search'], 48 | "probability": config['subscribe_to_authors_via_search']['use_module_probability'], 49 | "name": "subscribe to authors via search" 50 | }, 51 | _account.subscribe_to_channels_via_search: { 52 | "use_module": config['module_switches']['subscribe_to_channels_via_search'], 53 | "probability": config['subscribe_to_channels_via_search']['use_module_probability'], 54 | "name": "subscribe to channels via search" 55 | }, 56 | _account.subscribe_to_users_via_search: { 57 | "use_module": config['module_switches']['subscribe_to_users_via_search'], 58 | "probability": config['subscribe_to_users_via_search']['use_module_probability'], 59 | "name": "subscribe to users via search" 60 | }, 61 | _account.subscribe_to_mandatory_users: { 62 | "use_module": config['module_switches']['subscribe_to_mandatory_users'], 63 | "probability": config['subscribe_to_mandatory_users']['use_module_probability'], 64 | "name": "subscribe to mandatory users" 65 | }, 66 | _account.subscribe_to_mandatory_channels: { 67 | "use_module": config['module_switches']['subscribe_to_mandatory_channels'], 68 | "probability": config['subscribe_to_mandatory_channels']['use_module_probability'], 69 | "name": "subscribe to mandatory channels" 70 | }, 71 | _account.connect_metamask: { 72 | "use_module": config['module_switches']['connect_metamask'], 73 | "probability": config['connect_metamask']['use_module_probability'], 74 | "name": "connect metamask" 75 | } 76 | } 77 | 78 | switched_off_modules = [] 79 | for key, value in actions_list.items(): 80 | if not value["use_module"]: 81 | switched_off_modules.append(key) 82 | 83 | for key in switched_off_modules: 84 | del actions_list[key] 85 | 86 | all_actions = list(actions_list.keys()) 87 | shuffle(all_actions) 88 | 89 | logger.info(f'{_account.profile_name} - opening adspower profile') 90 | _account.open_profile(config['headless']) 91 | sleep(5) 92 | _account.driver.maximize_window() 93 | 94 | logger.info(f'{_account.profile_name} - opening warpcast homepage') 95 | _account.visit_warpcast() 96 | _account.random_activity_sleep() 97 | 98 | for action in all_actions: 99 | if probability_check_is_positive(actions_list[action]["probability"]): 100 | logger.info(f'{_account.profile_name} - performing activity "{actions_list[action]["name"]}"') 101 | try: 102 | action() 103 | except Exception as _err: 104 | logger.error(f'{_account.profile_name} - failed to perform activity, reason: {_err}') 105 | finally: 106 | _account.random_activity_sleep() 107 | 108 | if not _account.profile_was_running or config["close_running_profiles"]: 109 | logger.info(f'{_account.profile_name} - closing profile') 110 | try: 111 | _account.close_profile() 112 | except Exception as _err: 113 | logger.error(f'{_account.profile_name} - failed to close profile, reason: {_err}') 114 | else: 115 | logger.info(f'{_account.profile_name} - profile was running before farm, leaving it opened') 116 | 117 | 118 | if __name__ == "__main__": 119 | warpcast_accounts = [] 120 | for i, (profile_name, profile_id) in enumerate(profile_ids.items()): 121 | warpcast_accounts.append(WarpcastProfile(profile_name, profile_id)) 122 | 123 | if config["profiles_to_farm"] > len(warpcast_accounts): 124 | logger.info(f"Amount of profiles to farm > total amount of profiles, adjusted") 125 | config["profiles_to_farm"] = len(warpcast_accounts) 126 | 127 | for i in range(config["profiles_to_farm"]): 128 | account = warpcast_accounts.pop(randint(0, len(warpcast_accounts) - 1)) 129 | 130 | try: 131 | logger.info(f'{account.profile_name} - starting farm') 132 | start_farm(account) 133 | logger.success(f'{account.profile_name} - finished farm') 134 | except Exception as err: 135 | logger.error(f'{account.profile_name} - failed to farm, reason: ${err}') 136 | 137 | idle_time_sec = randint(config["delays"]["min_idle_minutes"] * 60, config["delays"]["max_idle_minutes"] * 60) 138 | logger.info(f'Sleeping {round(idle_time_sec / 60, 1)} minutes') 139 | sleep(idle_time_sec) 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🚀 Warpcast adspower farm 2 | Нахуй я этим делюсь? Этот скрипт поможет сэкономить бешеное количество времени. Он работает с профилями Warpcast, которые импортированны в [AdsPower](https://share.adspower.net/Btc9YYgpiyJxhmW). Заебался с рандомизацией, так что можешь спать спокойно. 3 | 4 | Связь с создателем: https://t.me/CrytoBusher
5 | Если ты больше по Твиттеру: https://twitter.com/CryptoBusher
6 | 7 | Залетай сюда, чтоб не пропускать дропы подобных скриптов: https://t.me/CryptoKiddiesClub
8 | И сюда, чтоб общаться с крутыми ребятами: https://t.me/CryptoKiddiesChat
9 | 10 | ## 🔥 Последние обновления 11 | - [x] 28.05.2024 - Подписка на юзеров из списка через прямую ссылку и поиск 12 | - [x] 28.05.2024 - Подписка на каналы из списка только через прямую ссылку 13 | - [x] 28.05.2024 - [Видео - гайд по настройке](https://t.me/CryptoKiddiesClub/513) 14 | - [x] 29.05.2024 - Свичи модулей в конфиге для удобства 15 | - [x] 29.05.2024 - Фикс мелких багов, добавлен дебаг лог в файл "data/debug_log.log" 16 | - [x] 01.06.2024 - Фикс мелких багов 17 | - [x] 01.06.2024 - Привязка Metamask (см. дополнительную информацию) 18 | - [x] 04.06.2024 - Фикс мелких багов (добавлена очистка поля поиска) 19 | - [x] 07.06.2024 - Многострочные касты, поддержка emoji, указанных в тексте кастов 20 | - [x] 13.06.2024 - Изображения в кастах (указанные явно или рандомные). Необходимо обновить структуру конфига и добвить папки с изображениями в data/ (см. _data/)! 21 | 22 | ## ⌛️ На очереди 23 | - Касты в каналах по заданному сценарию 24 | - Активности в топовых проектах (пока хз в каких что делать) 25 | 26 | ## ⚙️ Функции 27 | 1. Постинг кастов на своей стене 28 | 2. Подписка на рандомных юзеров из рекомендаций (скроллит и подписывается) 29 | 3. Подписка на рандомные каналы из рекомендаций (скроллит и подписывается) 30 | 4. Серфинг ленты, включая: 31 | 1. Скролл ленты 32 | 2. Лайк рандомного каста 33 | 3. Рекаст рандомного каста 34 | 4. Букмарк рандомного каста 35 | 5. Использование поиска и последующая (со скроллом и без): 36 | 1. Рандомная подписка на авторов постов из результатов поиска 37 | 2. Рандомная подписка на каналы из результатов поиска 38 | 3. Рандомная подписка на юзеров из результатов поиска 39 | 6. Подписка на юзеров из списка через прямую ссылку и поиск 40 | 7. Подписка на каналы из списка только через прямую ссылку 41 | 8. Подключение метамаска 42 | 43 | ## 🤔 Преимущества 44 | 1. Максимально - возможная рандомизация действий: 45 | 1. Рандомизация последовательности выполнения модулей 46 | 2. Рандомизация активностей в рамках модуля (количество interactions, скорость выполнения действий) 47 | 3. Рандомизация основанна на вероятностях 48 | 4. Рандомизация координат, по которым производится клик по кнопке 49 | 5. Рандомизация задержек при вводе текста 50 | 6. Рандомизация скролла ленты, настройки скролла 51 | 2. Работа через AdsPower 52 | 3. Headless mode 53 | 4. Возможность работать с уже открытыми профилями AdsPower 54 | 5. Использования меню Emoji при касте 55 | 6. Использование изображений в кастах 56 | 57 | ## 📚 Первый запуск 58 | 1. Устанавливаем Python (я работал на 3.12). 59 | 2. Скачиваем проект, в терминале, находясь в папке проекта, вписываем команду "pip install -r requirements.txt" для установки всех зависимостей. 60 | 3. Переименовываем папку "_data" в "data" 61 | 4. Открываем файл "data/profile_ids.py" и забиваем свои профиля как в примере ("название":"ID из AdsPower"). Название должно мэтчиться с названиями в файле "data/farm_data/casts.txt". Проще всего пронумеровать, как в примере. 62 | 5. Открываем файл "data/config.py" и забиваем настройки, описания даны в самом файле. Можно написать в наш [чат](https://t.me/CryptoKiddiesChat) для уточнения каких - либо моментов. 63 | 6. Открываем файл "data/farm_data/casts.txt" и вбиваем текста для постов, каждый с новой строки в формате "acc_name|cast_text". Для каждого аккаунта надо предоставлять свои текста для постов, можно вбивать много текстов для одного аккаунта, скрипт будет выбирать рандомно текста для акка или по - порядку, в зависимости от настроек. Текст каста может содержать emojis и переносы строк ('\n). 64 | 7. Открываем файл "data/farm_data/emoji_names.txt" и вбиваем туда названия emoji, по которым будет производиться поиск в контекстном меню, если в настройках вы активировали данную функцию. 65 | 8. Открываем файл "data/farm_data/search_authors.txt" и вбиваем туда строки, по которым будет происходить поиск постов для последующей подписки на их авторов. 66 | 9. Открываем файл "data/farm_data/search_channels.txt" и вбиваем туда строки, по которым будет происходить поиск каналов для последующей подписки на них. 67 | 10. Открываем файл "data/farm_data/search_users.txt" и вбиваем туда строки, по которым будет происходить поиск юзеров для последующей подписки них. 68 | 11. Открываем файл "data/farm_data/subscribe_to_users.txt" и вбиваем туда список юзернеймов, на которые обязательно надо подписываться (количество подписок за раз указывается в конфиге) 69 | 12. Открываем файл "data/farm_data/subscribe_to_channels.txt" и вбиваем туда список каналов, на которые обязательно надо подписываться (количество подписок за раз указывается в конфиге) 70 | 13. Открываем папку "data/images_for_casts/random" и закидываем любые картинки с любыми названиями, которые хотим рандомно добавлять к постам согласно вероятности в конфиге. 71 | 14. Открываем папку "data/images_for_casts/specific" и закидываем картинки, которые хотим прикладывать к определенным кастам (указываем ссылку на картинку явно в тексте каста). Чтоб прикрепить эту картинку к касту, надо в тексте каста указать ссылку в формате , например "1|ZkSync pidarasi", можно вставлять ссылку в любом месте текста каста (в любом месте после **|**, например так: "1|ZkSync pidarasi": или так "1|ZkSync pidarasi", но не так: "1|ZkSync pidarasi". Изображения удаляются после использования. Не забывайте указать расширение изображения, оно может отличаться (jpg, png и тд.). Вообще, тут будет работать любой поддерживаемый Варпкастом файл. 72 | 15. Открываем файл "data/sensitive_data/metamask_passwords.txt" и вбиваем туда список паролей от метамасков, каждый с новой строки в формате "acc_name|metamask_password", если хотите использовать модуль подключения метамаска (connect_metamask) 73 | 16. Запускаем AdsPower и логинимся в свой аккаунт 74 | 17. В терминале, находясь в папке проекта, вписываем команду "python3 farm_warpcast.py" и жмем ENTER 75 | 76 | ## 🌵 Дополнительная информация 77 | - Я не несу ответственность за ваши аккаунты (ban, shadowban). Однако данный подход был оттестирован комьюнити на примере Twitter, в данном исполнении я учел все возможные косяки, доработал некоторые алгоритмы. 78 | - Если нашли баги - буду благодарен за обратную связь. 79 | - Для того, чтоб работал модуль подключения метамаска (connect_metamask) работал, у тебя должен быть отключен LavaMoat. Если не знаешь, как это сделать - можешь использовать, например, [эту версию метамаска](https://github.com/MetaMask/metamask-extension/releases/tag/v10.25.0), либо подключай руками. Сидки должны быть уже импортированны в метамаск. 80 | 81 | ## 💴 Донат 82 | Если хочешь поддержать мой канал - можешь мне задонатить, все средства пойдут на развитие сообщества. 83 | 0x77777777323736d17883eac36d822d578d0ecc80 84 | -------------------------------------------------------------------------------- /src/AdspowerProfile.py: -------------------------------------------------------------------------------- 1 | from random import randint, uniform 2 | from time import sleep 3 | from platform import system 4 | import json 5 | 6 | import requests 7 | from selenium import webdriver 8 | from selenium.webdriver.chrome.options import Options 9 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 10 | from selenium.webdriver.common.action_chains import ActionChains 11 | from selenium.webdriver.support.ui import WebDriverWait 12 | from selenium.webdriver.remote.webelement import WebElement 13 | from selenium.webdriver.common.keys import Keys 14 | from loguru import logger 15 | 16 | from data.config import config 17 | 18 | 19 | class AdspowerProfile: 20 | API_ROOT = 'http://local.adspower.com:50325' 21 | 22 | def __init__(self, profile_name: str, profile_id: str): 23 | self.profile_name = profile_name 24 | self.profile_id = profile_id 25 | 26 | self.driver = None 27 | self.action_chain = None 28 | self.wait = None 29 | self.profile_was_running = None 30 | 31 | self.__init_profile_logs() 32 | 33 | def __init_profile_logs(self) -> None: 34 | logger.debug('__init_profile_logs: entered method') 35 | with open('data/profile_logs.json') as file: 36 | profile_logs = json.load(file) 37 | 38 | if self.profile_name not in profile_logs: 39 | profile_logs[self.profile_name] = {} 40 | 41 | if "mandatory_users_subscribes" not in profile_logs[self.profile_name]: 42 | profile_logs[self.profile_name]["mandatory_users_subscribes"] = [] 43 | 44 | if "mandatory_channels_subscribes" not in profile_logs[self.profile_name]: 45 | profile_logs[self.profile_name]["mandatory_channels_subscribes"] = [] 46 | 47 | if "wallet_connected" not in profile_logs[self.profile_name]: 48 | profile_logs[self.profile_name]["wallet_connected"] = False 49 | 50 | with open("data/profile_logs.json", "w") as file: 51 | json.dump(profile_logs, file, indent=4) 52 | 53 | def __init_webdriver(self, driver_path: str, debug_address: str) -> None: 54 | chrome_driver = driver_path 55 | chrome_options = Options() 56 | caps = DesiredCapabilities().CHROME 57 | caps["pageLoadStrategy"] = "eager" 58 | 59 | chrome_options.add_experimental_option("debuggerAddress", debug_address) 60 | driver = webdriver.Chrome(chrome_driver, options=chrome_options, desired_capabilities=caps) 61 | driver.implicitly_wait = config['element_wait_sec'] 62 | self.driver = driver 63 | self.action_chain = ActionChains(self.driver) 64 | self.wait = WebDriverWait(self.driver, config['element_wait_sec']) 65 | 66 | @staticmethod 67 | def random_activity_sleep() -> None: 68 | logger.debug('random_activity_sleep: sleeping') 69 | sleep(randint(config["delays"]["min_activity_sec"], config["delays"]["max_activity_sec"])) 70 | logger.debug('random_activity_sleep: finished sleeping') 71 | 72 | @staticmethod 73 | def random_subactivity_sleep() -> None: 74 | logger.debug('random_subactivity_sleep: sleeping') 75 | sleep(randint(config["delays"]["min_subactivity_sec"], config["delays"]["max_subactivity_sec"])) 76 | logger.debug('random_subactivity_sleep: finished sleeping') 77 | 78 | def human_hover(self, element: WebElement, click: bool = False) -> None: 79 | logger.debug('human_hover: entered method') 80 | size = element.size 81 | 82 | width_deviation_pixels = randint(1, int(size["width"] * config["max_click_width_deviation"])) 83 | height_deviation_pixels = randint(1, int(size["height"] * config["max_click_height_deviation"])) 84 | 85 | positive_width_deviation = randint(0, 1) 86 | positive_height_deviation = randint(0, 1) 87 | 88 | x = width_deviation_pixels if positive_width_deviation else -width_deviation_pixels 89 | y = height_deviation_pixels if positive_height_deviation else -height_deviation_pixels 90 | 91 | if click: 92 | logger.debug(f'human_hover: hover + clicking "{element.text}"') 93 | self.action_chain.move_to_element_with_offset(element, x, y).perform() 94 | sleep(uniform(0.5, 2)) 95 | self.action_chain.click().perform() 96 | else: 97 | logger.debug(f'human_hover: hover only "{element.text}"') 98 | self.action_chain.move_to_element_with_offset(element, x, y).perform() 99 | 100 | def human_scroll(self) -> None: 101 | logger.debug('human_scroll: entered method') 102 | ticks_per_scroll = randint(config['min_ticks_per_scroll'], config['max_ticks_per_scroll']) 103 | logger.debug(f'human_scroll: {ticks_per_scroll} ticks_per_scroll') 104 | for tick in range(ticks_per_scroll): 105 | sleep(uniform(config["min_delay_between_scroll_ticks_sec"], config["max_delay_between_scroll_ticks_sec"])) 106 | self.driver.execute_script(f"window.scrollBy(0, {config['pixels_per_scroll_tick']});") 107 | 108 | def human_type(self, text: str) -> None: 109 | logger.debug('human_type: entered method') 110 | text_lines = text.split(r'\n') 111 | 112 | for i, line in enumerate(text_lines): 113 | for char in line: 114 | sleep(uniform(config["delays"]["min_typing_sec"], config["delays"]["max_typing_sec"])) 115 | self.action_chain.send_keys(char).perform() 116 | 117 | if i != len(text_lines) - 1: 118 | self.action_chain.send_keys(Keys.ENTER).perform() 119 | sleep(uniform(config["delays"]["min_typing_sec"], config["delays"]["max_typing_sec"])) 120 | 121 | def human_clear_selected_input(self) -> None: 122 | logger.debug('human_clear_selected_input: entered method') 123 | key_to_hold = Keys.CONTROL if system() == 'Windows' else Keys.COMMAND 124 | self.action_chain.key_down(key_to_hold).send_keys('a').key_up(key_to_hold).perform() 125 | sleep(uniform(0.1, 1)) 126 | self.action_chain.send_keys(Keys.BACKSPACE).perform() 127 | 128 | def open_profile(self, headless: bool = False) -> None: 129 | url = self.API_ROOT + '/api/v1/browser/active' 130 | params = { 131 | "user_id": self.profile_id, 132 | } 133 | 134 | is_active_response = requests.get(url, params=params).json() 135 | if is_active_response["code"] != 0: 136 | raise Exception('Failed to check profile open status') 137 | 138 | if is_active_response['data']['status'] == 'Active': 139 | self.profile_was_running = True 140 | if not config["farm_running_profiles"]: 141 | raise Exception('Profile is active') 142 | 143 | self.__init_webdriver(is_active_response["data"]["webdriver"], is_active_response["data"]["ws"]["selenium"]) 144 | 145 | else: 146 | self.profile_was_running = False 147 | url = self.API_ROOT + '/api/v1/browser/start' 148 | params = { 149 | "user_id": self.profile_id, 150 | "open_tabs": "0", 151 | "ip_tab": "0", 152 | "headless": "1" if headless else "0", 153 | } 154 | 155 | start_response = requests.get(url, params=params).json() 156 | if start_response["code"] != 0: 157 | raise Exception(f'Failed to open profile, server response: {start_response}') 158 | 159 | self.__init_webdriver(start_response["data"]["webdriver"], start_response["data"]["ws"]["selenium"]) 160 | 161 | def close_profile(self) -> None: 162 | url_check_status = self.API_ROOT + '/api/v1/browser/active' + f'?user_id={self.profile_id}' 163 | url_close_profile = self.API_ROOT + '/api/v1/browser/stop' + f'?user_id={self.profile_id}' 164 | 165 | status_response = requests.get(url_check_status).json() 166 | if status_response['data']['status'] == 'Inactive': 167 | self.driver = None 168 | self.action_chain = None 169 | return 170 | 171 | close_response = requests.get(url_close_profile).json() 172 | if close_response["code"] != 0: 173 | raise Exception('Failed to close profile') 174 | 175 | self.driver = None 176 | 177 | def switch_to_tab(self, url_includes_text: str) -> None: 178 | logger.debug('__switch_to_tab: entered method') 179 | logger.debug(f'__switch_to_tab: looking for tab that includes "{url_includes_text}"') 180 | for tab in self.driver.window_handles: 181 | try: 182 | self.driver.switch_to.window(tab) 183 | if url_includes_text in self.driver.current_url: 184 | logger.debug(f'__switch_to_tab: switched to window "{self.driver.current_url}"') 185 | return 186 | except Exception: 187 | pass 188 | 189 | raise Exception(f'Failed to find tab that includes {url_includes_text} in url') 190 | 191 | def wait_for_new_tab(self, init_tabs: list[str]) -> None: 192 | logger.debug('__wait_for_new_tab: entered method') 193 | for i in range(config["element_wait_sec"]): 194 | if list(set(self.driver.window_handles) - set(init_tabs)): 195 | logger.debug('__wait_for_new_tab: found new tab') 196 | return 197 | else: 198 | sleep(1) 199 | 200 | raise Exception('Failed to locate new tab or extension window') 201 | 202 | def close_all_other_tabs(self) -> None: 203 | initial_tab = self.driver.current_window_handle 204 | tabs_to_close = self.driver.window_handles 205 | tabs_to_close.remove(initial_tab) 206 | 207 | for tab in tabs_to_close: 208 | self.driver.switch_to.window(tab) 209 | self.driver.close() 210 | 211 | self.driver.switch_to.window(initial_tab) 212 | -------------------------------------------------------------------------------- /_data/config.py: -------------------------------------------------------------------------------- 1 | config = { 2 | # Общие настройки 3 | "headless": False, # запускать браузер в скрытом режиме (True / False) 4 | "profiles_to_farm": 10, # сколько профилей прогреть (1+) 5 | "farm_running_profiles": True, # работать с уже открытыми профилями (True / False) 6 | "close_running_profiles": False, # закрывать ли те профиля, которые уже были открыты (True / False) 7 | "close_all_other_tabs": False, # закрывать ли лишние вкладки (True / False) 8 | "show_debug_logs": False, # показывать дебаг лог, обычному юзеру не нужно (True / False) 9 | "element_wait_sec": 60, # как долго ждать подгрузки элемента перед фейлом, секунд (1+) 10 | 11 | # Свичи модулей 12 | "module_switches": { 13 | "cast_on_homepage": True, 14 | "surf_feed": True, 15 | "subscribe_to_users_via_explore": True, 16 | "subscribe_to_channels_via_explore": True, 17 | "subscribe_to_authors_via_search": True, 18 | "subscribe_to_channels_via_search": True, 19 | "subscribe_to_users_via_search": True, 20 | "subscribe_to_mandatory_users": True, 21 | "subscribe_to_mandatory_channels": True, 22 | "connect_metamask": True 23 | }, 24 | 25 | # Настройки модулей 26 | "cast_on_homepage": { 27 | "use_module_probability": 0.5, # вероятность поста каста своей ленте (0 - 1) 28 | "keep_order": True, # сохранять порядок кастов для аккаунта как в текстовике (True / False) 29 | "emojis": { 30 | "use_probability": 0.5, # вероятность добавления эмодзи в посте (0 - 1) 31 | "max_dev_from_result": 1, # максимальное отклонение от первого эмодзи в поиске (0+) 32 | "max_repeat": 2 # максимальное количество кликов по одному эмодзи (1+) 33 | }, 34 | "images": { 35 | "use_from_random_probability": 0.5 # вероятность добавления случайной картинки из папки "all" (0 - 1) 36 | } 37 | }, 38 | "surf_feed": { 39 | "use_module_probability": 0.5, # вероятность серфинга по ленте (0 - 1) 40 | "min_scroll_episodes": 2, # минимальное количество эпизодов прокрутки ленты (1+) 41 | "max_scroll_episodes": 5, # максимальное количество эпизодов прокрутки ленты (1+) 42 | "recast_probability": 0.4, # вероятность репоста в рамках эпизода прокрутки (0 - 1) 43 | "like_probability": 0.7, # вероятность лайка в рамках эпизода прокрутки (0 - 1) 44 | "bookmark_probability": 0.1, # вероятность сохранения поста в рамках эпизода прокрутки (0 - 1) 45 | }, 46 | "subscribe_via_explore": { 47 | "to_users_probability": 0.3, # вероятность подписки на пользователей из рекомендаций (0 - 1) 48 | "to_channels_probability": 0.3, # вероятность подписки на каналы из рекомендаций (0 - 1) 49 | "min_scroll_episodes": 1, # минимальное количество эпизодов прокрутки списка (1+) 50 | "max_scroll_episodes": 4, # максимальное количество эпизодов прокрутки списка (1+) 51 | "min_subscribes_per_episode": 0, # минимальное количество подписок в рамках эпизода прокрутки (0+) 52 | "max_subscribes_per_episode": 2, # максимальное количество подписок в рамках эпизода прокрутки (0+) 53 | }, 54 | "subscribe_to_authors_via_search": { 55 | "use_module_probability": 0.1, # вероятность подписки на авторов постов через поиск по запросу (0 - 1) 56 | "remove_text_from_base": False, # удалить из базы текст, использованный для поиска (True / False) 57 | "use_scrolling_probability": 0.5, # вероятность того, что скрипт будет скроллить ленту (0 - 1) 58 | "keep_order_probability": 0.5, # вероятность соблюдения порядка при подписках без скролла (0 - 1) 59 | "min_subscribes": 1, # минимальное количество подписок без скролла (0+) 60 | "max_subscribes": 2, # максимальное количество подписок без скролла (0+) 61 | "min_scroll_episodes": 1, # минимальное количество эпизодов прокрутки (1+) 62 | "max_scroll_episodes": 2, # максимальное количество эпизодов прокрутки (1+) 63 | "min_subscribes_per_episode": 0, # минимальное количество подписок в рамках эпизода прокрутки (0+) 64 | "max_subscribes_per_episode": 3 # максимальное количество подписок в рамках эпизода прокрутки (0+) 65 | }, 66 | "subscribe_to_channels_via_search": { 67 | "use_module_probability": 0.1, # вероятность подписки на каналы через поиск по запросу (0 - 1) 68 | "remove_text_from_base": False, # удалить из базы текст, использованный для поиска (True / False) 69 | "use_scrolling_probability": 0.5, # вероятность того, что скрипт будет скроллить ленту (0 - 1) 70 | "keep_order_probability": 0.5, # вероятность соблюдения порядка при подписках без скролла (0 - 1) 71 | "min_subscribes": 1, # минимальное количество подписок без скролла (0+) 72 | "max_subscribes": 2, # максимальное количество подписок без скролла (0+) 73 | "min_scroll_episodes": 1, # минимальное количество эпизодов прокрутки (1+) 74 | "max_scroll_episodes": 2, # максимальное количество эпизодов прокрутки (1+) 75 | "min_subscribes_per_episode": 0, # минимальное количество подписок в рамках эпизода прокрутки (0+) 76 | "max_subscribes_per_episode": 3 # максимальное количество подписок в рамках эпизода прокрутки (0+) 77 | }, 78 | "subscribe_to_users_via_search": { 79 | "use_module_probability": 1.0, # вероятность подписки на пользователей через поиск по запросу (0 - 1) 80 | "remove_text_from_base": False, # удалить из базы текст, использованный для поиска (True / False) 81 | "use_scrolling_probability": 0.5, # вероятность того, что скрипт будет скроллить ленту (0 - 1) 82 | "keep_order_probability": 0.5, # вероятность соблюдения порядка при подписках без скролла (0 - 1) 83 | "min_subscribes": 1, # минимальное количество подписок без скролла (0+) 84 | "max_subscribes": 2, # максимальное количество подписок без скролла (0+) 85 | "min_scroll_episodes": 1, # минимальное количество эпизодов прокрутки (1+) 86 | "max_scroll_episodes": 2, # максимальное количество эпизодов прокрутки (1+) 87 | "min_subscribes_per_episode": 0, # минимальное количество подписок в рамках эпизода прокрутки (0+) 88 | "max_subscribes_per_episode": 3 # максимальное количество подписок в рамках эпизода прокрутки (0+) 89 | }, 90 | "subscribe_to_mandatory_users": { 91 | "use_module_probability": 1.0, # вероятность подписки на юзеров из предоставленного списка (0 - 1) 92 | "use_direct_link_probability": 0.0, # вероятность перехода на страницу юзера по ссылке (а не через поиск) (0 - 1) 93 | "min_subscribes_per_run": 1, # минимальное количество подписок в рамках выполнения модуля 94 | "max_subscribes_per_run": 3 # максимальное количество подписок в рамках выполнения модуля 95 | }, 96 | "subscribe_to_mandatory_channels": { 97 | "use_module_probability": 1.0, # вероятность подписки на каналы из предоставленного списка (0 - 1) 98 | "min_subscribes_per_run": 1, # минимальное количество подписок в рамках выполнения модуля 99 | "max_subscribes_per_run": 3 # максимальное количество подписок в рамках выполнения модуля 100 | }, 101 | "connect_metamask": { 102 | "use_module_probability": 0.5 # вероятность подключения метамаска (0 - 1) 103 | }, 104 | 105 | # Задержки 106 | "delays": { 107 | "min_activity_sec": 5, # минимальная задержка между модулями, секунды (0+) 108 | "max_activity_sec": 15, # максимальная задержка между модулями, секунды (0+) 109 | "min_subactivity_sec": 1, # минимальная задержка между действиями внутри модуля, секунды (0+) 110 | "max_subactivity_sec": 4, # максимальная задержка между действиями внутри модуля, секунды (0+) 111 | "min_typing_sec": 0.02, # минимальная пауза между вводами символов при печати, секунды (0+) 112 | "max_typing_sec": 0.2, # максимальная пауза между вводами символов при печати, секунды (0+) 113 | "min_idle_minutes": 3, # минимальная задержка между аккаунтами, минуты (0+) 114 | "max_idle_minutes": 5, # максимальная задержка между аккаунтами, минуты (0+) 115 | }, 116 | 117 | # Настройки скролла 118 | "pixels_per_scroll_tick": 100, # сколько пикселей прокручивает один тик колеса мыши (1+) 119 | "min_ticks_per_scroll": 6, # минимальное количество тиков за эпизод прокрутки (1+) 120 | "max_ticks_per_scroll": 12, # максимальное количество тиков за эпизод прокрутки (1+) 121 | "min_delay_between_scroll_ticks_sec": 0.05, # минимальная задержка между тиками в рамках эпизода прокрутки (0+) 122 | "max_delay_between_scroll_ticks_sec": 0.5, # максимальная задержка между тиками в рамках эпизода прокрутки (0+) 123 | 124 | # Отклонения при кликах 125 | "max_click_height_deviation": 0.1, # максимальное вертикальное отклонение от центра кнопки (0 - 1) 126 | "max_click_width_deviation": 0.1, # максимальное горизонтальное отклонение от центра кнопки (0 - 1) 127 | 128 | # Настройки доджа попапов (всплывающая информация профиля) 129 | "popup_dodge": { 130 | "min_tries": 6, # минимальное количество попыток увернуться от попапа (0+) 131 | "max_tries": 10, # максимальное количество попыток увернуться от попапа (0+) 132 | "min_height_deviation_px": 10, # минимальный сдвиг мыши по вертикали в пикселях (0+) 133 | "max_height_deviation_px": 200, # максимальный сдвиг мыши по вертикали в пикселях (0+) 134 | "min_width_deviation_px": 10, # минимальный сдвиг мыши по горизонтали в пикселях (0+) 135 | "max_width_deviation_px": 200, # максимальный сдвиг мыши по горизонтали в пикселях (0+) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/WarpcastProfile.py: -------------------------------------------------------------------------------- 1 | from random import choice, randint, uniform, shuffle 2 | from time import sleep 3 | import json 4 | from os import listdir, path, getcwd 5 | import re 6 | 7 | from selenium.webdriver.common.by import By 8 | from selenium.webdriver.remote.webelement import WebElement 9 | from selenium.webdriver.common.keys import Keys 10 | from selenium.webdriver.support import expected_conditions as EC 11 | from selenium.common.exceptions import NoSuchElementException, NoSuchWindowException 12 | from loguru import logger 13 | 14 | from data.config import config 15 | from src.AdspowerProfile import AdspowerProfile 16 | from src.helpers import remove_line, probability_check_is_positive, remove_files 17 | 18 | 19 | class WarpcastProfile(AdspowerProfile): 20 | def __get_visible_elements(self, elements: list[WebElement]): 21 | visible_elements = [] 22 | 23 | # need to adjust user visibility by navbar overlap 24 | nav_bar = self.driver.find_element(By.XPATH, '//nav[contains(@class, "sticky") and contains(@class, ' 25 | '"flex-col")]') 26 | nav_bar_height = nav_bar.size.get('height') 27 | 28 | win_upper_bound = self.driver.execute_script('return window.pageYOffset') + nav_bar_height 29 | win_height = self.driver.execute_script('return document.documentElement.clientHeight') - nav_bar_height 30 | win_lower_bound = win_upper_bound + win_height 31 | 32 | for element in elements: 33 | element_top_bound = element.location.get('y') 34 | element_height = element.size.get('height') 35 | element_lower_bound = element_top_bound + element_height 36 | 37 | if win_upper_bound <= element_top_bound and win_lower_bound >= element_lower_bound: 38 | visible_elements.append(element) 39 | 40 | return visible_elements 41 | 42 | def __use_search_input(self, text: str, press_enter: bool = True): 43 | logger.debug('__use_search_input: entered method') 44 | logger.debug('__use_search_input: selecting search input') 45 | search_input = self.wait.until(EC.presence_of_element_located((By.XPATH, '//input[@type="search"]'))) 46 | self.human_hover(search_input, True) 47 | self.random_subactivity_sleep() 48 | 49 | self.human_clear_selected_input() 50 | self.random_subactivity_sleep() 51 | 52 | logger.debug('__use_search_input: typing') 53 | self.human_type(text) 54 | self.random_subactivity_sleep() 55 | 56 | if press_enter: 57 | logger.debug('__use_search_input: pressing ENTER') 58 | self.action_chain.send_keys(Keys.ENTER).perform() 59 | self.random_subactivity_sleep() 60 | 61 | def __pick_cast_emoji(self) -> None: 62 | logger.debug('__pick_cast_emoji: entered method') 63 | self.random_subactivity_sleep() 64 | 65 | logger.debug('__pick_cast_emoji: pressing emoji button') 66 | select_emoji_button = self.driver.find_element(By.XPATH, '//div[@id="modal-root"]//div[@type="button"][2]') 67 | self.human_hover(select_emoji_button, click=True) 68 | self.random_subactivity_sleep() 69 | 70 | emoji_picker_shadow_root = self.driver.find_element(By.XPATH, '//em-emoji-picker').shadow_root 71 | 72 | logger.debug('__pick_cast_emoji: selecting emoji search input') 73 | emoji_search_input = emoji_picker_shadow_root.find_element(By.CSS_SELECTOR, 'input') 74 | self.human_hover(emoji_search_input, click=True) 75 | self.random_subactivity_sleep() 76 | 77 | logger.debug('__pick_cast_emoji: entering emoji name') 78 | with open('data/farm_data/emoji_names.txt', 'r', encoding="utf8") as file: 79 | all_emoji_names = [i.strip() for i in file] 80 | if not len(all_emoji_names): 81 | raise Exception('Missing emoji names, check data folder') 82 | 83 | emoji_name = choice(all_emoji_names) 84 | index_to = len(emoji_name) - 1 - randint(0, int(len(emoji_name) / 3)) \ 85 | if len(emoji_name) > 4 else len(emoji_name) 86 | self.human_type(emoji_name[:index_to]) 87 | self.random_subactivity_sleep() 88 | 89 | emoji_xpath_index = randint(0, config["cast_on_homepage"]["emojis"]["max_dev_from_result"]) 90 | emoji_repeats = randint(1, config["cast_on_homepage"]["emojis"]["max_repeat"]) 91 | emojis_list = emoji_picker_shadow_root.find_elements(By.CSS_SELECTOR, 'div.category button') 92 | 93 | if len(emojis_list): 94 | try: 95 | emoji = emojis_list[emoji_xpath_index] 96 | except IndexError: 97 | emoji = emojis_list[0] 98 | 99 | for i in range(emoji_repeats): 100 | logger.debug('__pick_cast_emoji: selecting emoji') 101 | self.human_hover(emoji, click=True) 102 | self.random_subactivity_sleep() 103 | 104 | area_to_click_to_close_emoji_picker = self.driver.find_element(By.XPATH, '//div[@class="DraftEditor-root"]') 105 | self.human_hover(area_to_click_to_close_emoji_picker, click=True) 106 | self.random_subactivity_sleep() 107 | 108 | @staticmethod 109 | def __remove_img_tags_from_text(cast_text: str) -> (str, list): 110 | pattern = r'<[^<>]+\.[^<>]+>' 111 | matches = re.findall(pattern, cast_text) 112 | if matches: 113 | for match in matches: 114 | cast_text = cast_text.replace(match, '') 115 | 116 | picture_names = [match[1:-1] for match in matches] 117 | return cast_text, picture_names 118 | 119 | def __add_picture_to_cast(self, picture_names: list) -> list: 120 | logger.debug(f'__add_picture_to_cast: entered method') 121 | 122 | def pick_images() -> list: 123 | special_images_folder_path = path.join('data', 'farm_data', 'images_for_casts', 'specific') 124 | special_images_files = listdir(special_images_folder_path) 125 | random_images_folder_path = path.join('data', 'farm_data', 'images_for_casts', 'random') 126 | random_images_files = listdir(random_images_folder_path) 127 | images_to_use_paths_ = [] 128 | 129 | # pick special image for text 130 | if picture_names: 131 | images_to_use_paths_ = [path.join(special_images_folder_path, name) for name in picture_names] 132 | logger.debug(f'__add_picture_to_cast:pick_image: img after special folder check: ${images_to_use_paths_}') 133 | 134 | # pick random image 135 | if not images_to_use_paths_ and random_images_files: 136 | if probability_check_is_positive(config["cast_on_homepage"]["images"]["use_from_random_probability"]): 137 | images_to_use_paths_.append(path.join(random_images_folder_path, choice(random_images_files))) 138 | logger.debug(f'__add_picture_to_cast:pick_image: img after random folder check: ${images_to_use_paths_}') 139 | 140 | return images_to_use_paths_ 141 | 142 | def upload_images(image_to_use_paths_: list) -> None: 143 | for img_path in image_to_use_paths_: 144 | if path.exists(img_path): 145 | file_input = self.driver.find_element(By.XPATH, '//input[@type="file"]') 146 | file_input.send_keys(path.join(getcwd(), img_path)) 147 | logger.debug(f'__add_picture_to_cast:upload_image: sent image to input') 148 | self.wait.until(EC.presence_of_element_located((By.XPATH, '//div[@id="modal-root"]//img[@alt="Cast image embed"]'))) 149 | logger.debug(f'__add_picture_to_cast:upload_image: found uploaded image element, success') 150 | self.random_subactivity_sleep() 151 | 152 | image_to_use_paths = pick_images() 153 | upload_images(image_to_use_paths) 154 | return image_to_use_paths 155 | 156 | def __start_subscribing_with_scroll(self, min_scroll_episodes: int, max_scroll_episodes: int, 157 | min_subs_per_episode: int, max_subs_per_episode: int, buttons_xpath: str, 158 | dodge_popups: bool = False): 159 | logger.debug('__start_subscribing_with_scroll: entered method') 160 | for i in range(randint(min_scroll_episodes, max_scroll_episodes)): 161 | logger.debug('__start_subscribing_with_scroll: scrolling') 162 | self.human_scroll() 163 | self.random_subactivity_sleep() 164 | 165 | subscribe_buttons = self.driver.find_elements(By.XPATH, buttons_xpath) 166 | logger.debug(f'__start_subscribing_with_scroll: {len(subscribe_buttons)} subscribe_buttons') 167 | visible_subscribe_buttons = self.__get_visible_elements(subscribe_buttons) 168 | logger.debug(f'__start_subscribing_with_scroll: {len(visible_subscribe_buttons)} visible_subscribe_buttons') 169 | 170 | if len(visible_subscribe_buttons) == 0: 171 | logger.debug(f'__start_subscribing_with_scroll: no any visible subscribe buttons, continuing') 172 | self.random_subactivity_sleep() 173 | continue 174 | 175 | subscribes_per_episode = randint(min_subs_per_episode, max_subs_per_episode) 176 | if subscribes_per_episode > len(visible_subscribe_buttons): 177 | subscribes_per_episode = len(visible_subscribe_buttons) 178 | logger.debug(f'__start_subscribing_with_scroll: {subscribes_per_episode} subscribes_per_episode') 179 | 180 | shuffle(visible_subscribe_buttons) 181 | for subscribe in range(subscribes_per_episode): 182 | button_to_press = visible_subscribe_buttons.pop(0) 183 | logger.debug(f'__start_subscribing_with_scroll: going to subscribe') 184 | self.human_hover(button_to_press, True) 185 | logger.debug(f'__start_subscribing_with_scroll: subscribed') 186 | self.random_subactivity_sleep() 187 | 188 | if dodge_popups: 189 | self.__dodge_popup() 190 | 191 | def __start_subscribing_without_scroll(self, amount: int, keep_order: bool, buttons_xpath: str, 192 | dodge_popups: bool = False): 193 | logger.debug('__start_subscribing_without_scroll: entered method') 194 | subscribe_buttons = self.driver.find_elements(By.XPATH, buttons_xpath) 195 | logger.debug(f'__start_subscribing_without_scroll: {len(subscribe_buttons)} subscribe_buttons') 196 | visible_subscribe_buttons = self.__get_visible_elements(subscribe_buttons) 197 | logger.debug(f'__start_subscribing_without_scroll: {len(visible_subscribe_buttons)} visible_subscribe_buttons') 198 | if len(visible_subscribe_buttons) == 0: 199 | raise Exception('No any visible subscribe buttons') 200 | 201 | if len(visible_subscribe_buttons) < amount: 202 | amount = len(visible_subscribe_buttons) 203 | 204 | logger.debug(f'__start_subscribing_without_scroll: keep_order {keep_order}') 205 | if not keep_order: 206 | logger.debug('__start_subscribing_without_scroll: not keeping order') 207 | shuffle(visible_subscribe_buttons) 208 | 209 | for subscribe in range(amount): 210 | logger.debug(f'__start_subscribing_without_scroll: going to subscribe') 211 | button_to_press = visible_subscribe_buttons.pop(0) 212 | self.human_hover(button_to_press, True) 213 | logger.debug(f'__start_subscribing_without_scroll: subscribed') 214 | self.random_subactivity_sleep() 215 | 216 | if dodge_popups: 217 | self.__dodge_popup() 218 | 219 | def __dodge_popup(self): 220 | logger.debug('__dodge_popup: entered method') 221 | for i in range(randint(config['popup_dodge']['min_tries'], config['popup_dodge']['max_tries'])): 222 | try: 223 | self.driver.find_element(By.CSS_SELECTOR, '[data-radix-popper-content-wrapper]') 224 | logger.debug('__dodge_popup: popup located, trying to dodge') 225 | 226 | x_offset = randint(config['popup_dodge']['min_width_deviation_px'], 227 | config['popup_dodge']['max_width_deviation_px']) 228 | y_offset = randint(config['popup_dodge']['min_height_deviation_px'], 229 | config['popup_dodge']['max_height_deviation_px']) 230 | 231 | x_is_positive = randint(0, 1) 232 | y_is_positive = randint(0, 1) 233 | 234 | x = x_offset if x_is_positive else -x_offset 235 | y = y_offset if y_is_positive else -y_offset 236 | 237 | logger.debug(f'__dodge_popup: x{x} y{y}') 238 | 239 | self.action_chain.move_by_offset(x, y).perform() 240 | self.random_subactivity_sleep() 241 | except NoSuchElementException: 242 | logger.debug('__dodge_popup: popup is not visible') 243 | return 244 | 245 | logger.debug('__dodge_popup: failed to dodge popup') 246 | 247 | def __go_home(self): 248 | home_button = self.driver.find_element(By.XPATH, '//a[@href="/"]') 249 | self.human_hover(home_button, True) 250 | self.random_subactivity_sleep() 251 | 252 | def visit_warpcast(self): 253 | start_tab = self.driver.current_window_handle 254 | try: 255 | self.switch_to_tab('warpcast.com') 256 | if 'settings' in self.driver.current_url: 257 | self.__go_home() 258 | except: 259 | self.driver.switch_to.window(start_tab) 260 | self.driver.get('https://warpcast.com/') 261 | 262 | if config["close_all_other_tabs"]: 263 | self.close_all_other_tabs() 264 | 265 | def cast_on_homepage(self): 266 | logger.debug('cast_on_homepage: entered method') 267 | with open('data/farm_data/casts.txt', 'r', encoding="utf8") as file: 268 | casts_raw = [i.strip() for i in file] 269 | 270 | casts = {} 271 | for cast_raw in casts_raw: 272 | profile_name, cast_text = cast_raw.split('|', 1) 273 | if profile_name not in casts: 274 | casts[profile_name] = [] 275 | casts[profile_name].append(cast_text) 276 | 277 | try: 278 | casts_for_profile = casts[self.profile_name] 279 | if config['cast_on_homepage']['keep_order']: 280 | cast_text = casts_for_profile[0] 281 | else: 282 | cast_text = choice(casts_for_profile) 283 | 284 | except KeyError: 285 | raise Exception('No any casts provided') 286 | 287 | logger.debug('cast_on_homepage: pressing cast button') 288 | cast_button = self.driver.find_element(By.XPATH, '//main//button[text()="Cast"]') 289 | self.human_hover(cast_button, click=True) 290 | self.random_subactivity_sleep() 291 | 292 | logger.debug('cast_on_homepage: entering cast text') 293 | cast_text_without_img_tags, picture_names = self.__remove_img_tags_from_text(cast_text) 294 | self.human_type(cast_text_without_img_tags) 295 | self.random_subactivity_sleep() 296 | 297 | if probability_check_is_positive(config["cast_on_homepage"]["emojis"]["use_probability"]): 298 | self.__pick_cast_emoji() 299 | 300 | added_images_paths = self.__add_picture_to_cast(picture_names) 301 | if added_images_paths: 302 | sleep(randint(7, 15)) # to avoid misclick because of modal window size change after img upload 303 | 304 | logger.debug('cast_on_homepage: pressing final cast button') 305 | final_cast_button = self.driver.find_element(By.XPATH, '//div[@id="modal-root"]//button[@title="Cast"]') 306 | self.human_hover(final_cast_button, click=True) 307 | remove_line('data/farm_data/casts.txt', f'{self.profile_name}|{cast_text}') 308 | remove_files(added_images_paths) 309 | self.random_subactivity_sleep() 310 | 311 | def subscribe_to_users_via_explore(self): 312 | logger.debug('subscribe_to_users: entered method') 313 | current_url = self.driver.current_url 314 | if current_url != 'https://warpcast.com/~/explore/users': 315 | logger.debug('subscribe_to_users: navigating to users list page') 316 | find_users_button = self.driver.find_element(By.XPATH, '//a[@title="Find Users"]') 317 | self.human_hover(find_users_button, click=True) 318 | self.random_subactivity_sleep() 319 | 320 | self.__start_subscribing_with_scroll( 321 | config['subscribe_via_explore']['min_scroll_episodes'], 322 | config['subscribe_via_explore']['max_scroll_episodes'], 323 | config['subscribe_via_explore']['min_subscribes_per_episode'], 324 | config['subscribe_via_explore']['max_subscribes_per_episode'], 325 | '//main//div[@class=" fade-in"]//button[text()="Follow"]' 326 | ) 327 | 328 | def subscribe_to_channels_via_explore(self): 329 | logger.debug('subscribe_to_channels: entered method') 330 | current_url = self.driver.current_url 331 | if current_url != 'https://warpcast.com/~/explore/channels': 332 | if current_url != 'https://warpcast.com/~/explore/users': 333 | logger.debug('subscribe_to_users: navigating to users list page') 334 | find_users_button = self.driver.find_element(By.XPATH, '//a[@title="Find Users"]') 335 | self.human_hover(find_users_button, click=True) 336 | self.random_subactivity_sleep() 337 | 338 | logger.debug('subscribe_to_users: navigating to channels list page') 339 | find_channels_button = self.driver.find_element(By.XPATH, '//a[@title="Channels for you to follow"]') 340 | self.human_hover(find_channels_button, click=True) 341 | self.random_subactivity_sleep() 342 | 343 | self.__start_subscribing_with_scroll( 344 | config['subscribe_via_explore']['min_scroll_episodes'], 345 | config['subscribe_via_explore']['max_scroll_episodes'], 346 | config['subscribe_via_explore']['min_subscribes_per_episode'], 347 | config['subscribe_via_explore']['max_subscribes_per_episode'], 348 | '//main//div[@class=" fade-in"]//button[text()="Follow"]' 349 | ) 350 | 351 | def surf_feed(self): 352 | def like(interaction_button_div: WebElement): 353 | logger.debug('surf_feed:like: pressing like button') 354 | like_button = interaction_button_div.find_element(By.XPATH, './/div[1]/div[1]/div[3]') 355 | self.human_hover(like_button, click=True) 356 | 357 | def recast(interaction_button_div: WebElement): 358 | logger.debug('surf_feed:recast: pressing recast button') 359 | recast_button = interaction_button_div.find_element(By.XPATH, './/div[1]//div[2]') 360 | self.human_hover(recast_button, click=True) 361 | self.random_subactivity_sleep() 362 | 363 | logger.debug('surf_feed:recast: pressing final recast button') 364 | final_recast_button = self.driver.find_element(By.XPATH, '//span[contains(text(), "Recast")]') 365 | self.human_hover(final_recast_button, click=True) 366 | 367 | def bookmark(interaction_button_div: WebElement): 368 | logger.debug('surf_feed:bookmark: pressing bookmark button') 369 | bookmark_button = interaction_button_div.find_element(By.XPATH, './/div[1]/div[2]/div') 370 | self.human_hover(bookmark_button, click=True) 371 | 372 | current_url = self.driver.current_url 373 | if current_url != 'https://warpcast.com/': 374 | logger.debug('surf_feed: navigating to home page') 375 | home_button = self.driver.find_element(By.XPATH, '//a[@title="Home"]') 376 | self.human_hover(home_button, click=True) 377 | self.random_subactivity_sleep() 378 | 379 | for i in range(randint(config['surf_feed']['min_scroll_episodes'], 380 | config['surf_feed']['max_scroll_episodes'])): 381 | self.human_scroll() 382 | self.random_subactivity_sleep() 383 | 384 | to_recast = True if (uniform(0, 1) < config['surf_feed']['recast_probability']) else False 385 | to_like = True if (uniform(0, 1) < config['surf_feed']['like_probability']) else False 386 | to_bookmark = True if (uniform(0, 1) < config['surf_feed']['bookmark_probability']) else False 387 | logger.debug(f'surf_feed: recast - {to_recast}, like - {to_like}, bookmark - {to_bookmark}') 388 | 389 | if not to_recast + to_like + to_bookmark: 390 | logger.debug(f'surf_feed: to_recast + to_like + to_bookmark = 0') 391 | continue 392 | 393 | all_cast_interactions = [] 394 | if to_recast: 395 | all_cast_interactions.append(recast) 396 | if to_like: 397 | all_cast_interactions.append(like) 398 | if to_bookmark: 399 | all_cast_interactions.append(bookmark) 400 | shuffle(all_cast_interactions) 401 | 402 | all_interaction_button_divs = self.driver.find_elements( 403 | By.XPATH, '//main/div/div/div[2]//div[contains(@class, " items-start")]') 404 | logger.debug(f'surf_feed: {len(all_interaction_button_divs)} all_interaction_button_divs') 405 | visible_interaction_button_divs = self.__get_visible_elements(all_interaction_button_divs) 406 | logger.debug(f'surf_feed: {len(visible_interaction_button_divs)} visible_interaction_button_divs') 407 | if not visible_interaction_button_divs: 408 | logger.debug(f'surf_feed: no any visible interaction button divs, skipping actions') 409 | continue 410 | 411 | for interaction in all_cast_interactions: 412 | try: 413 | interaction(choice(visible_interaction_button_divs)) 414 | self.random_subactivity_sleep() 415 | except Exception: 416 | break 417 | 418 | self.random_subactivity_sleep() 419 | 420 | def subscribe_to_authors_via_search(self): 421 | logger.debug('subscribe_to_authors_via_search: entered method') 422 | with open("data/farm_data/search_authors.txt", "r") as _file: 423 | all_text_lines = [i.strip() for i in _file] 424 | 425 | if not len(all_text_lines): 426 | raise Exception('Missing search text, check data folder') 427 | 428 | text = choice(all_text_lines) 429 | self.__use_search_input(text) 430 | if config['subscribe_to_authors_via_search']['remove_text_from_base']: 431 | remove_line("data/farm_data/search_authors.txt", text) 432 | 433 | if not probability_check_is_positive(config['subscribe_to_authors_via_search']['use_scrolling_probability']): 434 | keep_order = probability_check_is_positive(config['subscribe_to_authors_via_search']['keep_order_probability']) 435 | amount = randint(config['subscribe_to_authors_via_search']['min_subscribes'], 436 | config['subscribe_to_authors_via_search']['max_subscribes']) 437 | self.__start_subscribing_without_scroll( 438 | amount, keep_order, '//div[contains(@title, "Follow")]/*[local-name()="svg"]', True) 439 | else: 440 | self.__start_subscribing_with_scroll( 441 | config['subscribe_to_authors_via_search']['min_scroll_episodes'], 442 | config['subscribe_to_authors_via_search']['max_scroll_episodes'], 443 | config['subscribe_to_authors_via_search']['min_subscribes_per_episode'], 444 | config['subscribe_to_authors_via_search']['max_subscribes_per_episode'], 445 | '//div[contains(@title, "Follow")]/*[local-name()="svg"]', 446 | True 447 | ) 448 | 449 | def subscribe_to_channels_via_search(self): 450 | logger.debug('subscribe_to_channels_via_search: entered method') 451 | with open("data/farm_data/search_channels.txt", "r") as _file: 452 | all_text_lines = [i.strip() for i in _file] 453 | 454 | if not len(all_text_lines): 455 | raise Exception('Missing search text, check data folder') 456 | 457 | text = choice(all_text_lines) 458 | self.__use_search_input(text) 459 | if config['subscribe_to_channels_via_search']['remove_text_from_base']: 460 | remove_line("data/farm_data/search_channels.txt", text) 461 | 462 | find_channels_button = self.driver.find_element(By.XPATH, '//a[@title="Channels found based on your search"]') 463 | self.human_hover(find_channels_button, click=True) 464 | self.random_subactivity_sleep() 465 | 466 | if not probability_check_is_positive(config['subscribe_to_channels_via_search']['use_scrolling_probability']): 467 | keep_order = probability_check_is_positive(config['subscribe_to_channels_via_search']['keep_order_probability']) 468 | amount = randint(config['subscribe_to_channels_via_search']['min_subscribes'], 469 | config['subscribe_to_channels_via_search']['max_subscribes']) 470 | self.__start_subscribing_without_scroll(amount, keep_order, 471 | '//main//div[@class=" fade-in"]//button[text()="Follow"]') 472 | else: 473 | self.__start_subscribing_with_scroll( 474 | config['subscribe_to_channels_via_search']['min_scroll_episodes'], 475 | config['subscribe_to_channels_via_search']['max_scroll_episodes'], 476 | config['subscribe_to_channels_via_search']['min_subscribes_per_episode'], 477 | config['subscribe_to_channels_via_search']['max_subscribes_per_episode'], 478 | '//main//div[@class=" fade-in"]//button[text()="Follow"]' 479 | ) 480 | 481 | def subscribe_to_users_via_search(self): 482 | logger.debug('subscribe_to_users_via_search: entered method') 483 | with open("data/farm_data/search_users.txt", "r") as _file: 484 | all_text_lines = [i.strip() for i in _file] 485 | 486 | if not len(all_text_lines): 487 | raise Exception('Missing search text, check data folder') 488 | 489 | text = choice(all_text_lines) 490 | self.__use_search_input(text) 491 | if config['subscribe_to_users_via_search']['remove_text_from_base']: 492 | remove_line("data/farm_data/search_users.txt", text) 493 | 494 | find_users_button = self.driver.find_element(By.XPATH, '//a[@title="Users found based on your search"]') 495 | self.human_hover(find_users_button, click=True) 496 | self.random_subactivity_sleep() 497 | 498 | if not probability_check_is_positive(config['subscribe_to_users_via_search']['use_scrolling_probability']): 499 | keep_order = probability_check_is_positive(config['subscribe_to_users_via_search']['keep_order_probability']) 500 | amount = randint(config['subscribe_to_users_via_search']['min_subscribes'], 501 | config['subscribe_to_users_via_search']['max_subscribes']) 502 | self.__start_subscribing_without_scroll(amount, keep_order, 503 | '//main//div[@class=" fade-in"]//button[text()="Follow"]') 504 | else: 505 | self.__start_subscribing_with_scroll( 506 | config['subscribe_to_users_via_search']['min_scroll_episodes'], 507 | config['subscribe_to_users_via_search']['max_scroll_episodes'], 508 | config['subscribe_to_users_via_search']['min_subscribes_per_episode'], 509 | config['subscribe_to_users_via_search']['max_subscribes_per_episode'], 510 | '//main//div[@class=" fade-in"]//button[text()="Follow"]' 511 | ) 512 | 513 | def subscribe_to_mandatory_users(self): 514 | self.__mandatory_subscribe(True) 515 | 516 | def subscribe_to_mandatory_channels(self): 517 | self.__mandatory_subscribe(False) 518 | 519 | def __mandatory_subscribe(self, to_users: bool): 520 | def direct_link_subscribe(target_name: str): 521 | url = f'https://warpcast.com/{target_name}' if to_users else f'https://warpcast.com/~/channel/{target_name}' 522 | self.driver.get(url) 523 | self.random_subactivity_sleep() 524 | 525 | subscribe_button = self.driver.find_element(By.XPATH, '//main//button[contains(text(),"ollow")]') 526 | if subscribe_button.text == "Follow": 527 | self.human_hover(subscribe_button, click=True) 528 | logger.info(f'{self.profile_name} - subscribed to {target}') 529 | else: 530 | logger.info(f'{self.profile_name} - already following target {target_name}') 531 | 532 | def subscribe_via_search(target_name: str): 533 | self.__use_search_input(target_name, False) 534 | self.random_subactivity_sleep() 535 | 536 | options_list = self.driver.find_element(By.XPATH, f'//form//div[text()="Users"]/../div[2]') 537 | all_options = options_list.find_elements(By.XPATH, 'div') 538 | 539 | for option in all_options: 540 | try: 541 | username = option.find_element(By.CSS_SELECTOR, 'div[class = "text-muted text-sm"]').text 542 | except NoSuchElementException: 543 | break 544 | 545 | if username.replace('@', '') == target_name: 546 | logger.debug('__mandatory_subscribe:subscribe_via_search: found target, subscribing') 547 | self.human_hover(option, True) 548 | self.random_subactivity_sleep() 549 | 550 | subscribe_button = self.driver.find_element(By.XPATH, '//main//button[contains(text(),"ollow")]') 551 | if subscribe_button.text == "Follow": 552 | self.human_hover(subscribe_button, click=True) 553 | logger.info(f'{self.profile_name} - subscribed to {target}') 554 | else: 555 | logger.info(f'{self.profile_name} - already following target {target_name}') 556 | return 557 | 558 | raise Exception('Failed to find user in dropdown menu') 559 | 560 | with open('data/profile_logs.json') as file: 561 | profile_logs = json.load(file) 562 | 563 | if to_users: 564 | logger.debug('__mandatory_subscribe: subscribing to users') 565 | subscribe_config = config['subscribe_to_mandatory_users'] 566 | already_subscribed = profile_logs[self.profile_name]['mandatory_users_subscribes'] 567 | with open('data/farm_data/subscribe_to_users.txt', 'r', encoding="utf8") as file: 568 | subscribe_targets = [i.strip() for i in file] 569 | else: 570 | logger.debug('__mandatory_subscribe: subscribing to channels') 571 | subscribe_config = config['subscribe_to_mandatory_channels'] 572 | already_subscribed = profile_logs[self.profile_name]['mandatory_channels_subscribes'] 573 | with open('data/farm_data/subscribe_to_channels.txt', 'r', encoding="utf8") as file: 574 | subscribe_targets = [i.strip() for i in file] 575 | 576 | remaining_subscribe_targets = list(set(subscribe_targets) - set(already_subscribed)) 577 | shuffle(remaining_subscribe_targets) 578 | logger.debug(f'__mandatory_subscribe: {len(remaining_subscribe_targets)} remaining_subscribe_targets') 579 | if not remaining_subscribe_targets: 580 | logger.info(f'{self.profile_name} - already following all targets') 581 | return 582 | 583 | subscribes_count = randint(subscribe_config['min_subscribes_per_run'], 584 | subscribe_config['max_subscribes_per_run']) 585 | 586 | if subscribes_count > len(remaining_subscribe_targets): 587 | subscribes_count = len(remaining_subscribe_targets) 588 | logger.debug(f'__mandatory_subscribe: {subscribes_count} subscribes_count') 589 | 590 | for i in range(subscribes_count): 591 | target = remaining_subscribe_targets.pop(0) 592 | if to_users: 593 | use_direct_link = probability_check_is_positive(subscribe_config["use_direct_link_probability"]) 594 | else: 595 | use_direct_link = True 596 | 597 | logger.debug(f'__mandatory_subscribe: {use_direct_link} use_direct_link') 598 | 599 | try: 600 | if use_direct_link: 601 | direct_link_subscribe(target) 602 | else: 603 | subscribe_via_search(target) 604 | 605 | log_update_key = "mandatory_users_subscribes" if to_users else "mandatory_channels_subscribes" 606 | profile_logs[self.profile_name][log_update_key].append(target) 607 | with open("data/profile_logs.json", "w") as file: 608 | json.dump(profile_logs, file, indent=4) 609 | 610 | except Exception as e: 611 | logger.error(f'{self.profile_name} - failed to subscribe to user, reason: {e}') 612 | 613 | finally: 614 | self.random_activity_sleep() 615 | 616 | def connect_metamask(self): 617 | logger.debug('connect_metamask: entered method') 618 | 619 | def get_metamask_password() -> str: 620 | logger.debug('connect_metamask:get_metamask_password: entered method') 621 | with open('data/sensitive_data/metamask_passwords.txt', 'r') as _file: 622 | metamask_passwords_raw = [i.strip() for i in _file] 623 | 624 | _metamask_password = '' 625 | 626 | for line in metamask_passwords_raw: 627 | profile_name, password = line.split('|', 1) 628 | if profile_name == self.profile_name: 629 | _metamask_password = password 630 | break 631 | 632 | if not _metamask_password: 633 | raise Exception('Metamask password is not provided') 634 | 635 | return _metamask_password 636 | 637 | def process_wallet_connection(): 638 | logger.debug('connect_metamask:process_wallet_connection: entered method') 639 | 640 | def unlock(): 641 | logger.debug('connect_metamask:process_wallet_connection:unlock: entered method') 642 | pass_input = self.wait.until(EC.element_to_be_clickable((By.XPATH, '//input[@data-testid="unlock-password"]'))) 643 | pass_input.send_keys(metamask_password) 644 | unlock_button = self.wait.until(EC.element_to_be_clickable((By.XPATH, '//button[@data-testid="unlock-submit"]'))) 645 | unlock_button.click() 646 | logger.debug('connect_metamask:process_wallet_connection:unlock: unlocked wallet') 647 | 648 | def connect(): 649 | try: # if connection is cached there will not be connection request 650 | logger.debug('connect_metamask:process_wallet_connection:connect: entered method') 651 | next_button = self.wait.until( 652 | EC.element_to_be_clickable((By.XPATH, '//button[contains(@class, "btn-primary")]'))) 653 | next_button.click() 654 | connect_button = self.wait.until( 655 | EC.element_to_be_clickable((By.XPATH, '//button[@data-testid="page-container-footer-next"]'))) 656 | connect_button.click() 657 | logger.debug('connect_metamask:process_wallet_connection:connect: connected wallet') 658 | except NoSuchWindowException: # cached connection 659 | logger.debug('connect_metamask:process_wallet_connection:connect: cached connection') 660 | pass 661 | 662 | self.switch_to_tab('chrome-extension') 663 | 664 | if '#unlock' in self.driver.current_url: 665 | logger.debug('connect_metamask:process_wallet_connection: need to unlock') 666 | unlock() 667 | logger.debug('connect_metamask:process_wallet_connection: connecting') 668 | connect() 669 | else: 670 | logger.debug('connect_metamask:process_wallet_connection: connecting') 671 | connect() 672 | 673 | def sign_with_metamask(): 674 | logger.debug('connect_metamask:sign_with_metamask: entered method') 675 | 676 | def verify_origin(origin: str): 677 | logger.debug('connect_metamask:sign_with_metamask:verify_origin: entered method') 678 | current_origin = self.wait.until( 679 | EC.presence_of_element_located((By.XPATH, '//div[@class="signature-request__origin"]//span'))).text 680 | if current_origin != origin: 681 | logger.debug("origin mismatch") 682 | signature_cancel_button = self.wait.until( 683 | EC.element_to_be_clickable((By.XPATH, '//button[@data-testid="signature-cancel-button"]'))) 684 | signature_cancel_button.click() 685 | raise Exception(f'Origin mismatch, current: {current_origin}, required: {origin}') 686 | 687 | def sign(): 688 | logger.debug('connect_metamask:sign_with_metamask:sign: entered method') 689 | verify_origin('https://verify.warpcast.com') 690 | 691 | try: 692 | scroll_button = self.wait.until( 693 | EC.element_to_be_clickable((By.XPATH, '//div[@data-testid="signature-request-scroll-button"]'))) 694 | scroll_button.click() 695 | except: 696 | pass 697 | 698 | sign_button = self.wait.until( 699 | EC.element_to_be_clickable((By.XPATH, '//button[@data-testid="signature-sign-button"]'))) 700 | sign_button.click() 701 | 702 | self.switch_to_tab('chrome-extension') 703 | sign() 704 | 705 | with open('data/profile_logs.json') as file: 706 | profile_logs = json.load(file) 707 | 708 | if profile_logs[self.profile_name]["wallet_connected"]: 709 | logger.info(f'{self.profile_name} - wallet is already connected') 710 | return 711 | 712 | metamask_password = get_metamask_password() 713 | 714 | logger.debug('connect_metamask: pressing profile button') 715 | profile_button = self.wait.until(EC.element_to_be_clickable((By.XPATH, '//a[@title="Profile"]'))) 716 | self.human_hover(profile_button, click=True) 717 | self.random_subactivity_sleep() 718 | 719 | profile_button = self.wait.until(EC.element_to_be_clickable((By.XPATH, '//a[@title="Profile"]'))) 720 | self.human_hover(profile_button, click=True) 721 | self.random_subactivity_sleep() 722 | 723 | try: # to go home before Exception as here will be missing search and cast buttons 724 | edit_profile_button = self.wait.until(EC.element_to_be_clickable((By.XPATH, '//button[text()="Edit Profile"]'))) 725 | self.human_hover(edit_profile_button, click=True) 726 | self.random_subactivity_sleep() 727 | 728 | verified_addresses_button = self.wait.until(EC.element_to_be_clickable((By.XPATH, '//a[@href="/~/settings/verified-addresses"]'))) 729 | self.human_hover(verified_addresses_button, click=True) 730 | self.random_subactivity_sleep() 731 | except Exception as e: 732 | self.__go_home() 733 | raise Exception(e) 734 | 735 | try: # check if EVM wallet is already connected 736 | self.driver.find_element(By.XPATH, '//img[@src="/static/media/ethereumLogoPurple.a6ebba304034873ba05c.webp"]') 737 | logger.info(f'{self.profile_name} - looks like wallet was already connected before, skipping') 738 | 739 | profile_logs[self.profile_name]["wallet_connected"] = True 740 | with open("data/profile_logs.json", "w") as file: 741 | json.dump(profile_logs, file, indent=4) 742 | 743 | self.__go_home() 744 | return 745 | except: 746 | pass 747 | 748 | try: # to go home before Exception as here will be missing search and cast buttons 749 | verify_an_address_button = self.wait.until(EC.element_to_be_clickable((By.XPATH, '//button[text()="Verify an address"]'))) 750 | self.human_hover(verify_an_address_button, click=True) 751 | self.random_subactivity_sleep() 752 | except Exception as e: 753 | self.__go_home() 754 | raise Exception(e) 755 | 756 | main_tab = self.driver.current_window_handle 757 | try: # to go to main tab + go home before Exception as here will be missing search and cast buttons 758 | self.switch_to_tab('verify.warpcast.com') 759 | warpcast_verify_tab = self.driver.current_window_handle 760 | self.random_subactivity_sleep() 761 | 762 | sleep(3) 763 | # if wallet is unlocked and connection is cached - there will not be connection button 764 | if self.driver.find_elements(By.XPATH, '//button[text()="Connect wallet"]'): 765 | connect_wallet_button = self.wait.until(EC.element_to_be_clickable((By.XPATH, '//button[text()="Connect wallet"]'))) 766 | self.human_hover(connect_wallet_button, click=True) 767 | self.random_subactivity_sleep() 768 | 769 | init_tabs = self.driver.window_handles 770 | metamask_button = self.wait.until(EC.element_to_be_clickable((By.XPATH, '//button[@data-testid="rk-wallet-option-metaMask"]'))) # rk-wallet-option-io.metamask 771 | self.human_hover(metamask_button, click=True) 772 | self.random_subactivity_sleep() 773 | 774 | self.wait_for_new_tab(init_tabs) 775 | process_wallet_connection() 776 | self.driver.switch_to.window(warpcast_verify_tab) 777 | self.random_subactivity_sleep() 778 | 779 | init_tabs = self.driver.window_handles 780 | sign_message_button = self.wait.until(EC.element_to_be_clickable((By.XPATH, '//button[text()="Sign message"]'))) 781 | self.human_hover(sign_message_button, click=True) 782 | self.random_subactivity_sleep() 783 | 784 | self.wait_for_new_tab(init_tabs) 785 | sign_with_metamask() 786 | self.driver.switch_to.window(warpcast_verify_tab) 787 | self.random_subactivity_sleep() 788 | 789 | self.wait.until(EC.element_to_be_clickable((By.XPATH, '//button[text()="Return to Warpcast"]'))) 790 | profile_logs[self.profile_name]["wallet_connected"] = True 791 | with open("data/profile_logs.json", "w") as file: 792 | json.dump(profile_logs, file, indent=4) 793 | 794 | self.driver.close() 795 | self.driver.switch_to.window(main_tab) 796 | self.__go_home() 797 | 798 | except Exception as e: 799 | self.driver.switch_to.window(main_tab) 800 | self.random_subactivity_sleep() 801 | self.__go_home() 802 | 803 | raise Exception(e) 804 | --------------------------------------------------------------------------------