| import asyncio |
| import traceback |
|
|
| from utils.logger import setup_logger |
| from utils.config import reload_config, reload_userData |
| from core.msg_builder import build_message |
| from core.browser import get_browser |
|
|
|
|
| complates = {} |
| logger = setup_logger(level="DEBUG") |
|
|
| CHAT_INPUT_SELECTORS = [ |
| "xpath=//div[contains(@class, 'chat-input-') and @contenteditable='true']", |
| "xpath=//div[contains(@class, 'chat-input-')]", |
| "css=div[class*='chat-input-'][contenteditable='true']", |
| "css=div[contenteditable='true'][role='textbox']", |
| "css=div[contenteditable='true']", |
| ] |
|
|
|
|
| async def retry_operation(name, operation, retries=3, delay=2, *args, **kwargs): |
| for attempt in range(retries): |
| try: |
| return await operation(*args, **kwargs) |
| except Exception as exc: |
| if attempt < retries - 1: |
| logger.warning(f"{name} ???????? {attempt + 1} ?????{exc}") |
| await asyncio.sleep(delay) |
| else: |
| logger.error(f"{name} ????????????????{exc}") |
| raise |
|
|
|
|
| async def scroll_and_select_user(page, username, targets): |
| friends_tab_selector = 'xpath=//*[@id="sub-app"]/div/div/div[1]/div[2]' |
| target_selector = 'xpath=//*[@id="sub-app"]/div/div[1]/div[2]/div[2]//div[contains(@class, "semi-list-item-body semi-list-item-body-flex-start")]' |
| scrollable_friends_selector = 'xpath=//*[@id="sub-app"]/div/div[1]/div[2]/div[2]/div/div/div[3]/div/div/div/ul/div' |
| no_more_selector = 'xpath=//div[contains(@class, "no-more-tip-")]' |
| loading_selector = 'xpath=//div[contains(@class, "semi-spin")]' |
|
|
| logger.debug(f"?? {username} ??????????") |
| logger.debug(f"?? {username} ??????: {targets}") |
|
|
| await page.wait_for_selector(friends_tab_selector) |
| await page.locator(friends_tab_selector).click() |
| logger.debug(f"?? {username} ????????") |
|
|
| first_friend_selector = 'xpath=//*[@id="sub-app"]/div/div/div[2]/div[2]/div/div/div[1]/div/div/div/ul/div/div/div[1]/li/div' |
| await page.wait_for_selector(first_friend_selector) |
| await page.locator(first_friend_selector).click() |
| logger.debug(f"?? {username} ??????????????????") |
|
|
| await asyncio.sleep(2) |
|
|
| found_usernames = set() |
| remaining_targets = set(targets) |
| empty_scroll_count = 0 |
| max_empty_scrolls = 10 |
|
|
| while True: |
| target_elements = await page.locator(target_selector).all() |
| prev_found_count = len(found_usernames) |
|
|
| for element in target_elements: |
| try: |
| span = element.locator("xpath=.//span[contains(@class, 'item-header-name-')]") |
| target_name = (await span.inner_text()).strip() |
|
|
| if target_name in found_usernames: |
| continue |
| found_usernames.add(target_name) |
| logger.debug(f"?? {username} ???? {target_name}") |
|
|
| if target_name in targets: |
| await element.click() |
| logger.info(f"?? {username} ?????? {target_name}???????") |
| yield target_name |
|
|
| if target_name in remaining_targets: |
| remaining_targets.remove(target_name) |
| if not remaining_targets: |
| logger.info(f"?? {username} ???????????????") |
| return |
| break |
| except Exception: |
| traceback.print_exc() |
| else: |
| new_found = len(found_usernames) > prev_found_count |
| if new_found: |
| empty_scroll_count = 0 |
| else: |
| empty_scroll_count += 1 |
|
|
| if await page.locator(no_more_selector).count() > 0: |
| logger.info(f"?? {username} ??????????????????") |
| if remaining_targets: |
| logger.warning(f"?? {username} ??????????????: {remaining_targets}") |
| break |
|
|
| if empty_scroll_count >= max_empty_scrolls: |
| logger.warning( |
| f"?? {username} ?? {max_empty_scrolls} ?????????????????" |
| ) |
| if remaining_targets: |
| logger.warning(f"?? {username} ??????????????: {remaining_targets}") |
| break |
|
|
| if await page.locator(loading_selector).count() > 0: |
| logger.debug(f"?? {username} ??????? (Loading)...") |
| await asyncio.sleep(1.5) |
|
|
| scrollable_element = await page.locator(scrollable_friends_selector).element_handle() |
| if not scrollable_element: |
| logger.error(f"?? {username} ??????????") |
| break |
|
|
| scroll_top_before = await page.evaluate("(element) => element.scrollTop", scrollable_element) |
| await page.evaluate("(element) => element.scrollTop += 800", scrollable_element) |
| await asyncio.sleep(0.3) |
| scroll_top_after = await page.evaluate("(element) => element.scrollTop", scrollable_element) |
|
|
| if scroll_top_before == scroll_top_after: |
| empty_scroll_count += 2 |
| logger.debug( |
| f"?? {username} scrollTop ??? ({scroll_top_before})?????? " |
| f"(?????: {empty_scroll_count}/{max_empty_scrolls})" |
| ) |
| else: |
| logger.debug( |
| f"?? {username} ????????????? " |
| f"(scrollTop: {scroll_top_before} -> {scroll_top_after})" |
| ) |
|
|
| await asyncio.sleep(1.5) |
|
|
|
|
| async def _wait_for_chat_input(page): |
| last_error = None |
| for selector in CHAT_INPUT_SELECTORS: |
| locator = page.locator(selector).first |
| try: |
| await locator.wait_for(state="visible", timeout=30000) |
| return locator, selector |
| except Exception as exc: |
| last_error = exc |
|
|
| raise RuntimeError( |
| "?????????" |
| f" ??????: {CHAT_INPUT_SELECTORS}; ????: {last_error}" |
| ) |
|
|
|
|
| async def _type_message(chat_input, message: str): |
| await chat_input.click() |
| try: |
| await chat_input.press("Control+A") |
| await chat_input.press("Backspace") |
| except Exception: |
| pass |
|
|
| lines = message.split("\n") if message else [""] |
| for index, line in enumerate(lines): |
| if line: |
| await chat_input.type(line) |
| if index < len(lines) - 1: |
| await chat_input.press("Shift+Enter") |
|
|
|
|
| async def do_user_task(browser, username, cookies, targets, semaphore, config): |
| async with semaphore: |
| context = await browser.new_context() |
| context.set_default_navigation_timeout(120000) |
| context.set_default_timeout(120000) |
|
|
| try: |
| page = await context.new_page() |
| await retry_operation( |
| "?????????", |
| page.goto, |
| retries=3, |
| delay=5, |
| url="https://creator.douyin.com/", |
| ) |
| await context.add_cookies(cookies) |
| await retry_operation( |
| "???????", |
| page.goto, |
| retries=3, |
| delay=5, |
| url="https://creator.douyin.com/creator-micro/data/following/chat", |
| ) |
|
|
| logger.info(f"Account {username} start sending messages") |
| async for target_name in scroll_and_select_user(page, username, targets): |
| logger.info(f"Account {username} selected target {target_name}, preparing message") |
| await asyncio.sleep(0.5) |
|
|
| chat_input, selector = await _wait_for_chat_input(page) |
| logger.debug(f"Account {username} resolved chat input selector={selector}") |
|
|
| message = build_message(config=config) |
| await _type_message(chat_input, message) |
|
|
| logger.debug(f"Account {username} ready to send message to {target_name}:\n\t{message}") |
| await chat_input.press("Enter") |
| logger.info(f"Account {username} sent message to {target_name}") |
| await asyncio.sleep(2) |
| finally: |
| await context.close() |
|
|
|
|
| async def runTasks(config=None, userData=None): |
| active_config = config if config is not None else reload_config() |
| active_user_data = userData if userData is not None else reload_userData() |
| playwright, browser = await get_browser() |
| try: |
| logger.info("Task run started with current config") |
| multi_task = bool(active_config.get("multiTask", True)) |
| task_count = int(active_config.get("taskCount", 1) or 1) |
| logger.info(f"multiTask={multi_task}, taskCount={task_count}") |
| logger.info(f"messageTemplate={active_config.get('messageTemplate', '')}") |
| logger.info(f"hitokotoTypes={active_config.get('hitokotoTypes', [])}") |
| for user in active_user_data: |
| logger.info( |
| f"user={user.get('username', 'unknown')}, targets={user.get('targets', [])}" |
| ) |
|
|
| semaphore = asyncio.Semaphore(task_count if multi_task else 1) |
| tasks = [] |
| for user in active_user_data: |
| cookies = user.get("cookies", []) |
| targets = user.get("targets", []) |
| unique_id = user.get("unique_id", "") |
| username = user.get("username", "????") |
|
|
| if not cookies: |
| logger.warning(f"User {username} has no cookies, skipping") |
| continue |
| if not targets: |
| logger.warning(f"User {username} has no targets, skipping") |
| continue |
|
|
| complates[unique_id] = [] |
| tasks.append(do_user_task(browser, username, cookies, targets, semaphore, active_config)) |
|
|
| if tasks: |
| await asyncio.gather(*tasks) |
| else: |
| logger.warning("No runnable tasks found: empty data, missing cookies, or no targets") |
| finally: |
| await browser.close() |
| await playwright.stop() |
|
|