├── .flaskenv ├── .gitignore ├── .local.env ├── README.md ├── SECURITY.md ├── app ├── __init__.py ├── config.py ├── constants.py ├── model.py ├── routes │ ├── __init__.py │ └── bot_bp_route.py ├── services │ └── chrome_auto_service.py └── utility.py ├── requirements.txt └── run.py /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | FLASK_ENV=development 3 | FLASK_DEBUG=1 4 | -------------------------------------------------------------------------------- /.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 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the hearting to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # Ruff stuff: 171 | .ruff_cache/ 172 | 173 | # PyPI configuration file 174 | .pypirc 175 | 176 | chrome_profiles/ 177 | typings/ 178 | -------------------------------------------------------------------------------- /.local.env: -------------------------------------------------------------------------------- 1 | BYPASSING_BOT_API_KEY=YOUR_API_KEY 2 | MAX_DELAY=3 3 | MIN_DELAY=10 4 | RETRYABLE_COUNT=3 5 | PORT=5000 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Project Setup Guide 2 | 3 | ## Prerequisites 4 | Ensure you have the hearting installed on your system: 5 | 1. Python 6 | 2. Google Chrome latest version 7 | 3. Ensure Google Chrome Path - C:\Program Files\Google\Chrome\Application\chrome.exe 8 | 4. Bot UI (https://github.com/gold-mouse/tiktok-bot-admin.git) 9 | 10 | ### Install Python 11 | Download and install Python from the [official Python website](https://www.python.org/downloads/). 12 | - During installation, check the option **Add Python to PATH**. 13 | - Verify the installation with: 14 | ```sh 15 | python --version 16 | ``` 17 | 18 | ### Install pip (if not installed) 19 | pip comes bundled with Python, but you can ensure it's updated with: 20 | ```sh 21 | python -m ensurepip --default-pip 22 | python -m pip install --upgrade pip 23 | ``` 24 | 25 | ## Getting Started 26 | 27 | ### 1. Clone the Repository 28 | If the project is hosted on a Git repository, clone it using: 29 | ```sh 30 | git clone https://github.com/gold-mouse/tiktok-python-bot.git 31 | cd tiktok-python-bot 32 | ``` 33 | 34 | ### 2. Install Dependencies 35 | ```sh 36 | pip install -r requirements.txt 37 | ``` 38 | 39 | ### 3. Run the Application 40 | ```sh 41 | python main.py 42 | ``` 43 | 44 | ## Environment Variables 45 | You can get BYPASSING_BOT_API_KEY from https://www.sadcaptcha.com/
46 | Create a `.env` file in the root directory and define them like this: 47 | ```sh 48 | BYPASSING_BOT_API_KEY=YOUR_API_KEY 49 | MIN_DELAY=3 50 | MAX_DELAY=40 51 | RETRYABLE_COUNT=3 52 | PORT=5000 53 | 54 | ``` 55 | 56 | ## Note 57 | If you faced bellow error: 58 | ```sh 59 | ModuleNotFoundError: No module named 'pkg_resources' 60 | ``` 61 | You have to upgrade setuptools to latest version 62 | 63 | ```sh 64 | pip install --upgrade setuptools 65 | ``` 66 | 67 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_cors import CORS 3 | from app.config import Config 4 | from flask_caching import Cache 5 | 6 | from app.routes import register_routes 7 | 8 | def create_app(config_class=Config): 9 | app = Flask(__name__, static_folder="views", static_url_path="") 10 | app.config.from_object(config_class) 11 | CORS(app) 12 | 13 | register_routes(app) 14 | cache = Cache(app) 15 | with app.app_context(): 16 | """Clear the cache when the server starts.""" 17 | cache.clear() 18 | 19 | 20 | return app 21 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | class Config: 2 | CACHE_TYPE = 'simple' 3 | -------------------------------------------------------------------------------- /app/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | load_dotenv() 5 | 6 | MAX_DELAY: int = int(os.getenv('MAX_DELAY', 0)) 7 | MIN_DELAY: int = int(os.getenv('MIN_DELAY', 0)) 8 | RETRYABLE_COUNT: int = int(os.getenv('RETRYABLE_COUNT', 0)) 9 | PORT: int = int(os.getenv("PORT", 5000)) 10 | BYPASSING_BOT_API_KEY = os.getenv('BYPASSING_BOT_API_KEY', "") 11 | 12 | ELEMENT_CSS = { 13 | "heart": [ 14 | "#main-content-video_detail > div > div:nth-child(2) > div > div > div > div:nth-child(4) > div:nth-child(2) > button", 15 | "#main-content-video_detail > div > div:nth-child(2) > div > div > div > div:nth-child(5) > div:nth-child(2) > button", 16 | "article > div > section:nth-child(2) > button:nth-child(2)" 17 | ], 18 | "favorite": [ 19 | "#main-content-video_detail > div > div:nth-child(2) > div > div > div > div:nth-child(4) > div:nth-child(2) > div > button", 20 | "#main-content-video_detail > div > div:nth-child(2) > div > div > div > div:nth-child(5) > div:nth-child(2) > div > button", 21 | "article > div > section:nth-child(2) > div:nth-child(4)", 22 | ], 23 | "comment-input-field": [ 24 | "div.DraftEditor-editorContainer > div.notranslate.public-DraftEditor-content[aria-describedby^='placeholder']", 25 | ], 26 | "open-comment": [ 27 | "#comments > button" 28 | ], 29 | "post-button": [ 30 | "div[data-e2e='comment-post']" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /app/model.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List 2 | class DriverModel: 3 | def __init__(self): 4 | self.drivers: Dict[str, Any] = {} 5 | self.solvers: Dict[str, Any] = {} 6 | 7 | def get_driver(self, username: str) -> Any: 8 | return self.drivers.get(username, None) 9 | 10 | def set_driver(self, username: str, driver: Any): 11 | # Set driver 12 | self.drivers[username] = driver 13 | 14 | 15 | def remove_driver(self, username: str): 16 | driver = self.drivers.pop(username) 17 | driver.quit() 18 | 19 | def check_driver(self, username: str) -> bool: 20 | return username in self.drivers 21 | 22 | def get_usernames_from_driverkyes(self) -> List[str]: 23 | allkeys = self.drivers.keys() 24 | return list(allkeys) 25 | 26 | driver_model = DriverModel() 27 | -------------------------------------------------------------------------------- /app/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from .bot_bp_route import bot_bp 3 | 4 | def register_routes(app: Flask): 5 | app.register_blueprint(bot_bp, url_prefix='/api') 6 | -------------------------------------------------------------------------------- /app/routes/bot_bp_route.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from flask import Blueprint, jsonify, request 3 | from app.model import driver_model 4 | from app.services.chrome_auto_service import launch_driver, login, search 5 | 6 | bot_bp = Blueprint('bot', __name__) 7 | 8 | @bot_bp.before_request 9 | def before_request_middleware(): 10 | open_chrome_endpoints = [ 11 | "user_login", 12 | "keyword_search", 13 | ] 14 | if request.endpoint in open_chrome_endpoints: 15 | username = request.args.get("username", "") 16 | if request.method == "POST": 17 | body = request.get_json() 18 | username = body.get("username", "") 19 | 20 | if username == "": 21 | return jsonify({ "status": False, "message": "Missing payload" }) 22 | 23 | isExist = driver_model.check_driver(username) 24 | 25 | if not isExist: 26 | driver = launch_driver(username) 27 | driver_model.set_driver(username, driver) 28 | sleep(7) 29 | 30 | @bot_bp.route("/user-login", methods=["POST"]) 31 | def user_login(): 32 | try: 33 | body = request.get_json() 34 | username = body.get("username", "") 35 | password = body.get("password", "") 36 | 37 | if username == "" or password == "": 38 | return jsonify({ "status": False, "message": "Missing payload" }) 39 | 40 | res = login(username, password) 41 | 42 | return jsonify(res), 200 43 | except Exception as e: 44 | print(e) 45 | return jsonify({ "status": False, "message": "Something went wrong" }) 46 | 47 | @bot_bp.route("/get-users", methods=["GET"]) 48 | def get_users(): 49 | return jsonify({ "status": True, "data": [{ "id": i + 1, "username": username } for i, username in enumerate(driver_model.get_usernames_from_driverkyes())] }), 200 50 | 51 | @bot_bp.route("/keyword-search", methods=["GET"]) 52 | def keyword_search(): 53 | try: 54 | keyword = request.args.get("keyword", "") 55 | username = request.args.get("username", "") 56 | comment = request.args.get("comment", "Wonderful, I like it") 57 | 58 | if keyword == "" or username == "": 59 | return jsonify({ "status": False, "message": "Missing payload" }) 60 | 61 | res = search(username=username, keyword=keyword, comment=comment) 62 | 63 | return jsonify(res), 200 64 | except Exception as e: 65 | print(e) 66 | return jsonify({ "status": False, "message": "Something went wrong" }) 67 | 68 | @bot_bp.route("/close-driver", methods=["GET"]) 69 | def close_driver(): 70 | username = request.args.get("username", "") 71 | 72 | driver_model.remove_driver(username) 73 | 74 | return jsonify({"status": True, "message": f"Chrome closed for {username}"}) 75 | 76 | @bot_bp.route("/test", methods=["GET"]) 77 | def test(): 78 | username = request.args.get("username", "") 79 | launch_driver(username) 80 | print(username, "-------->>>>good") 81 | return "test", 200 82 | -------------------------------------------------------------------------------- /app/services/chrome_auto_service.py: -------------------------------------------------------------------------------- 1 | from tiktok_captcha_solver import SeleniumSolver # type: ignore 2 | import undetected_chromedriver as uc 3 | from selenium.webdriver.support.ui import WebDriverWait 4 | from selenium.webdriver.support import expected_conditions as EC 5 | from selenium_stealth import stealth # type: ignore 6 | 7 | import os 8 | from time import sleep, time 9 | import random 10 | from halo import Halo # type: ignore 11 | 12 | from selenium.webdriver.common.by import By 13 | from typing import Dict, Any, List, Union, Callable 14 | 15 | from app.model import driver_model 16 | from app.utility import sleep_like_human, update_status 17 | 18 | from app.constants import RETRYABLE_COUNT, BYPASSING_BOT_API_KEY, ELEMENT_CSS 19 | 20 | def bypass_robot(driver: Any): 21 | update_status("Solving Captcha if present...", "info") 22 | sadcaptcha = SeleniumSolver( # type: ignore 23 | driver, 24 | BYPASSING_BOT_API_KEY, 25 | mouse_step_size=1, 26 | mouse_step_delay_ms=10 27 | ) 28 | 29 | sadcaptcha.solve_captcha_if_present() # type: ignore 30 | 31 | sleep_like_human(1, 3) 32 | 33 | def navigate(driver: Any, link: str) -> Any: 34 | update_status(f"Navigating to {link}", "info") 35 | driver.get(link) 36 | sleep_like_human(1, 3) 37 | bypass_robot(driver) 38 | return driver 39 | 40 | def refresh(driver: Any) -> None: 41 | update_status(f"Refreshing page {driver.current_url}", "info") 42 | driver.refresh() 43 | sleep_like_human(1, 3) 44 | bypass_robot(driver) 45 | 46 | def smooth_scroll_for_duration(driver: uc.Chrome, duration: int = 20, scroll_increment: int = 100): 47 | 48 | spinner = Halo(text=f'Scrolling to fetch data for {duration} seconds...', spinner='dots') # type: ignore 49 | spinner.start() # type: ignore 50 | start_time = time() 51 | 52 | while True: 53 | driver.execute_script(f"window.scrollBy(0, {scroll_increment});") # type: ignore 54 | 55 | sleep(random.uniform(*(0.1, 0.3))) 56 | 57 | elapsed_time = time() - start_time 58 | 59 | if elapsed_time >= duration: 60 | break 61 | 62 | spinner.stop() # type: ignore 63 | update_status("Scrolling finished!", "info") 64 | 65 | def human_typing(element: Any, text: str, delay_range: tuple[float, float]=(0.05, 0.2)): 66 | for char in text: 67 | element.send_keys(char) 68 | sleep(random.uniform(*delay_range)) 69 | 70 | def retry_action(driver: Any, selectorStr: Union[str, List[str]], by: str, logStr: str, action: Callable[[str], bool], retry: int) -> bool: 71 | update_status(logStr) 72 | 73 | def try_selectors(selectors: List[str]) -> bool: 74 | for sel in selectors: 75 | if action(sel): 76 | return True 77 | return False 78 | 79 | selectors = selectorStr if isinstance(selectorStr, list) else [selectorStr] 80 | 81 | for i in range(retry): # type: ignore 82 | if try_selectors(selectors): 83 | return True 84 | update_status(f"Failed to process element: {selectorStr}", "error") 85 | if retry == 1: 86 | return False 87 | update_status("retrying...") 88 | refresh(driver) 89 | 90 | return False 91 | 92 | def wait_and_get_element(driver: Any, selectorStr: Union[str, List[str]], by: str, logStr: str, retry: int = RETRYABLE_COUNT, waitTime: int = 10) -> Any: 93 | def action(selector: str): 94 | try: 95 | update_status("Getting Element...") 96 | return WebDriverWait(driver, waitTime).until(EC.presence_of_element_located((by, selector))) 97 | except Exception: # type: ignore 98 | return None 99 | 100 | result = [None] 101 | def wrapped_action(sel: str) -> bool: 102 | res = action(sel) 103 | if res: 104 | result[0] = res # type: ignore 105 | return True 106 | return False 107 | 108 | return result[0] if retry_action(driver, selectorStr, by, logStr, wrapped_action, retry) else None 109 | 110 | def wait_and_click(driver: Any, selectorStr: Union[str, List[str]], by: str, logStr: str, retry: int = RETRYABLE_COUNT, waitTime: int = 10) -> bool: 111 | def action(selector: str) -> bool: 112 | try: 113 | update_status("Getting Element...") 114 | WebDriverWait(driver, waitTime).until(EC.element_to_be_clickable((by, selector))).click() 115 | sleep_like_human() 116 | return True 117 | except Exception: # type: ignore 118 | return False 119 | 120 | return retry_action(driver, selectorStr, by, logStr, action, retry) 121 | 122 | def wait_and_send_keys(driver: Any, selectorStr: Union[str, List[str]], by: str, logStr: str, keys: str, retry: int = RETRYABLE_COUNT, waitTime: int = 10) -> bool: 123 | def action(selector: str) -> bool: 124 | try: 125 | update_status("Getting Element...") 126 | element = WebDriverWait(driver, waitTime).until(EC.element_to_be_clickable((by, selector))) 127 | human_typing(element, keys) 128 | sleep_like_human() 129 | return True 130 | except Exception: # type: ignore 131 | return False 132 | 133 | return retry_action(driver, selectorStr, by, logStr, action, retry) 134 | 135 | def launch_driver(profile: str): 136 | try: 137 | user_data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), f"chrome_profiles/{profile}") 138 | options = uc.ChromeOptions() 139 | options.add_argument("--start-maximized") # type: ignore 140 | options.add_argument("--disable-blink-features=AutomationControlled") # type: ignore 141 | driver = uc.Chrome(user_data_dir=user_data_dir, options=options) 142 | 143 | stealth( 144 | driver, 145 | languages=["en-US", "en"], # Mimic browser language preferences 146 | vendor="Google Inc.", # Set vendor string 147 | platform="Win64", # Define OS platform 148 | webgl_vendor="Intel Inc.", # Mask WebGL vendor details 149 | renderer="Intel Iris OpenGL Engine", # Mask WebGL renderer details 150 | fix_hairline=True # Prevent detection via hairline rendering 151 | ) 152 | 153 | return driver 154 | except Exception as e: 155 | print(e) 156 | return None 157 | 158 | def login(username: str, password: str) -> Dict[str, Any] | None: 159 | driver = driver_model.get_driver(username) 160 | 161 | if driver == None: 162 | return { "status": False, "message": "Can't find profile settings (Open chrome first)" } 163 | 164 | driver = navigate(driver, "https://www.tiktok.com/login/phone-or-email/email") 165 | 166 | isLoggedIn = wait_and_get_element(driver=driver, selectorStr="//form//input[@name='username']", by=By.XPATH, logStr="Checking if logged in...", retry=1) 167 | 168 | if isLoggedIn == None: 169 | return { "status": True, "message": "Already logged in" } 170 | 171 | wait_and_send_keys(driver=driver, selectorStr="//form//input[@name='username']", keys=username, retry=1, by=By.XPATH, logStr="Sending username...") 172 | wait_and_send_keys(driver=driver, selectorStr="//form//input[@type='password']", keys=password, retry=1, by=By.XPATH, logStr="Sending password...") 173 | 174 | wait_and_click(driver=driver, selectorStr="//form//button[@type='submit']", retry=1, by=By.XPATH, logStr="Logging in...") 175 | 176 | sleep(2) 177 | 178 | isSuccess = wait_and_get_element(driver=driver, selectorStr="button[role='searchbox']", by=By.CSS_SELECTOR, logStr="Checking if logged in...", waitTime=30, retry=1) 179 | 180 | if isSuccess == None: 181 | driver_model.remove_driver(username) 182 | return { "status": False, "message": "Login failed" } 183 | 184 | bypass_robot(driver) 185 | 186 | driver_model.set_driver(username, driver) 187 | 188 | return { "status": True, "message": "success" } 189 | 190 | def search(username: str, keyword: str, comment: str) -> Dict[str, Any] | None: 191 | driver = driver_model.get_driver(username) 192 | 193 | if driver == None: 194 | print(username, keyword) 195 | return { "status": False, "message": "Can't find profile settings (Open chrome first)" } 196 | 197 | driver = navigate(driver, f"https://www.tiktok.com/search?q={keyword}") 198 | 199 | smooth_scroll_for_duration(driver) 200 | 201 | searched_links = driver.find_elements(By.CSS_SELECTOR, "#tabs-0-panel-search_top div[mode='search-video-list'] > div > div:nth-child(1) > div > div a") 202 | searched_imgs = driver.find_elements(By.CSS_SELECTOR, "#tabs-0-panel-search_top div[mode='search-video-list'] > div > div:nth-child(1) > div > div img") 203 | 204 | searched_videos: List[Dict[str, Any]] = [] # type: ignore 205 | 206 | for i in range(len(searched_links)): 207 | src = searched_imgs[i].get_attribute("src") 208 | href = searched_links[i].get_attribute("href") 209 | if src.startswith("data:"): 210 | continue 211 | searched_videos.append({ 212 | "link": href, 213 | "img": src, 214 | "id": i, 215 | }) 216 | 217 | update_status(f"Found {len(searched_videos)} videos") 218 | update_status("Processing video...") 219 | 220 | if len(searched_videos) > 0: 221 | searched_videos = searched_videos[:5] # splice first 5 for testing porses 222 | update_status("Only processing first 5 videos for testing purposes", "info") 223 | 224 | 225 | return { 226 | "status": True, 227 | "message": "success", 228 | "data": [ 229 | { 230 | "id": videoInfo["id"], 231 | "link": videoInfo["link"], 232 | "img": videoInfo["img"], 233 | "result": main_action(username, videoInfo["link"], comment) 234 | } for videoInfo in searched_videos 235 | ] 236 | } 237 | 238 | def main_action(username: str, link: str, comment: str) -> Dict[str, Any] | None: 239 | 240 | driver = driver_model.get_driver(username) 241 | 242 | driver.get(link) 243 | sleep_like_human(2, 4) 244 | 245 | videoEl = None 246 | for i in range(RETRYABLE_COUNT): # type: ignore 247 | try: 248 | videoEl = wait_and_get_element(driver=driver, selectorStr="video", by=By.CSS_SELECTOR, logStr="Getting video element...") 249 | break 250 | except Exception as e: # type: ignore 251 | update_status("Not found video element", "error") 252 | bypass_robot(driver) 253 | 254 | if (videoEl == None): 255 | return { "success": False, "message": "Video not found" } 256 | 257 | driver.execute_script("arguments[0].pause()", videoEl) # once video is finished, necessary elements are disappeared. so it must be paused first 258 | 259 | sleep_like_human(2, 4) 260 | 261 | heart_res = wait_and_click(driver=driver, selectorStr=ELEMENT_CSS.get("heart", []), logStr="Clicking heart...", by=By.CSS_SELECTOR) 262 | 263 | favorite_res = wait_and_click(driver=driver, selectorStr=ELEMENT_CSS.get("favorite", []), logStr="Clicking favorite...", by=By.CSS_SELECTOR) 264 | 265 | comment_res = leaveComment(driver=driver, comment=comment) 266 | 267 | update_status("Done", "info") 268 | update_status(f"Link: {link}") 269 | 270 | sleep_like_human() 271 | return { 272 | "success": heart_res and favorite_res and comment_res, 273 | "data": { 274 | "heart": heart_res, 275 | "favorite": favorite_res, 276 | "comment": comment_res 277 | } 278 | } 279 | 280 | def leaveComment(driver: str, comment: str = "Wonderful, I like it") -> bool: 281 | comments = [c.strip() for c in comment.split(",") if c.strip()] 282 | random_comment = random.choice(comments) 283 | 284 | for i in range(2): # check if comment button is opened. If not, open it on second trying 285 | sendKey_res = wait_and_send_keys( 286 | driver=driver, 287 | selectorStr=ELEMENT_CSS.get("comment-input-field", []), 288 | keys=random_comment, 289 | logStr="Sending comment...", 290 | by=By.CSS_SELECTOR, 291 | retry=1 292 | ) 293 | if not sendKey_res: 294 | print("Failed to send comment", "error") 295 | if i == 0: 296 | wait_and_click(driver=driver, selectorStr=ELEMENT_CSS.get("open-comment", []), logStr="Opening comments...", by=By.CSS_SELECTOR, retry=1) 297 | continue 298 | return False 299 | 300 | try: 301 | update_status("Posting comment...") 302 | element = WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, "div[data-e2e='comment-post']"))) # type: ignore 303 | driver.execute_script("arguments[0].click();", element) # type: ignore 304 | return True 305 | except Exception as e: # type: ignore 306 | print("Failed to post comment", "error") 307 | return False 308 | 309 | return True 310 | -------------------------------------------------------------------------------- /app/utility.py: -------------------------------------------------------------------------------- 1 | from halo import Halo # type: ignore 2 | from time import sleep 3 | from datetime import datetime 4 | from colorama import Fore 5 | import random 6 | 7 | from app.constants import MAX_DELAY, MIN_DELAY 8 | 9 | def update_status(msg: str, context: str = "normal"): 10 | now = datetime.now() 11 | current_time = now.strftime("%Y-%m-%d %H:%M:%S") 12 | 13 | # Determine color based on context 14 | if context == "error": 15 | color = Fore.RED 16 | elif context == "warning": 17 | color = Fore.YELLOW 18 | elif context == "info": 19 | color = Fore.BLUE 20 | else: # normal context or any other unspecified context 21 | color = Fore.WHITE 22 | 23 | print(color + str(current_time) + " - " + str(msg)) 24 | 25 | def get_random_sec(a: int, b: int): 26 | if 0 < b < a: 27 | return random.randint(MIN_DELAY, MAX_DELAY) 28 | return random.randint(a if a > 0 else MIN_DELAY, b if b > 0 else MAX_DELAY) 29 | 30 | def sleep_like_human(a: int = 0, b: int = 0): 31 | sec = get_random_sec(a, b) 32 | update_status(f"delay for {sec} seconds") 33 | for remaining in range(sec, 0, -1): 34 | with Halo(text=f"{remaining} seconds remaining...", spinner="dots"): 35 | sleep(0.9) 36 | print("\r", end="") 37 | 38 | return True 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.9.0 3 | attrs==25.3.0 4 | beautifulsoup4==4.12.3 5 | blinker==1.9.0 6 | Brotli==1.1.0 7 | bs4==0.0.2 8 | certifi==2025.1.31 9 | cffi==1.17.1 10 | charset-normalizer==3.4.1 11 | click==8.1.8 12 | colorama==0.4.6 13 | comtypes==1.4.10 14 | cryptography==44.0.2 15 | cssselect==1.3.0 16 | discordwebhook==1.0.3 17 | distro==1.9.0 18 | fake-useragent==2.1.0 19 | Flask==3.1.0 20 | flask-cors==5.0.1 21 | greenlet==3.1.1 22 | h11==0.16.0 23 | h2==4.2.0 24 | halo==0.0.31 25 | hpack==4.1.0 26 | html5lib==1.1 27 | httpcore==1.0.7 28 | httpx==0.28.1 29 | hupper==1.12.1 30 | hyperframe==6.1.0 31 | idna==3.10 32 | iniconfig==2.1.0 33 | itsdangerous==2.2.0 34 | Jinja2==3.1.6 35 | jiter==0.9.0 36 | jmespath==1.0.1 37 | kaitaistruct==0.10 38 | keyboard==0.13.5 39 | log-symbols==0.0.14 40 | lxml==5.3.1 41 | MarkupSafe==3.0.2 42 | MouseInfo==0.1.3 43 | mypy==1.15.0 44 | mypy-extensions==1.0.0 45 | outcome==1.3.0.post0 46 | packaging==24.2 47 | parsel==1.10.0 48 | pillow==10.4.0 49 | playwright==1.51.0 50 | playwright-stealth==1.0.6 51 | pluggy==1.5.0 52 | pyasn1==0.6.1 53 | PyAutoGUI==0.9.54 54 | pycparser==2.22 55 | pycryptodome==3.22.0 56 | pydantic==2.10.6 57 | pydantic_core==2.27.2 58 | pydivert==2.1.0 59 | pyee==12.1.1 60 | PyGetWindow==0.0.9 61 | PyMsgBox==1.0.9 62 | pyOpenSSL==25.0.0 63 | pyparsing==3.2.3 64 | pyperclip==1.9.0 65 | PyRect==0.2.0 66 | PyScreeze==1.0.1 67 | PySocks==1.7.1 68 | pytesseract==0.3.13 69 | pytest==8.3.5 70 | pytest-asyncio==0.26.0 71 | python-dotenv==1.0.1 72 | pytweening==1.2.0 73 | pywin32==310 74 | pywinauto==0.6.9 75 | requests==2.32.3 76 | selenium==4.23.1 77 | selenium-stealth==1.0.6 78 | setuptools==78.1.1 79 | six==1.17.0 80 | sniffio==1.3.1 81 | sortedcontainers==2.4.0 82 | soupsieve==2.6 83 | spinners==0.0.24 84 | termcolor==3.0.0 85 | tiktok-captcha-solver==0.7.4 86 | tqdm==4.66.6 87 | trio==0.29.0 88 | trio-websocket==0.12.2 89 | types-colorama==0.4.15.20240311 90 | types-Flask-Cors==5.0.0.20240902 91 | types-requests==2.32.0.20250306 92 | typing-inspection==0.4.0 93 | typing_extensions==4.12.2 94 | undetected-chromedriver==3.5.5 95 | urllib3==2.3.0 96 | uuid==1.30 97 | w3lib==2.3.1 98 | watchdog==6.0.0 99 | webdriver-manager==4.0.2 100 | webencodings==0.5.1 101 | websocket-client==1.8.0 102 | websockets==15.0.1 103 | Werkzeug==3.1.3 104 | wsproto==1.2.0 105 | zstandard==0.23.0 106 | flask-caching 107 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from app import create_app 2 | 3 | app = create_app() 4 | 5 | if __name__ == '__main__': 6 | app.run() 7 | --------------------------------------------------------------------------------