# retoor import asyncio import logging import random import re from devplacepy.services.bot.config import ( NOTIFICATION_CHECK_PROB, SESSION_COMMENT_CAP_MAX, SESSION_COMMENT_CAP_MIN, ) logger = logging.getLogger(__name__) class BotLoopMixin: async def _cycle( self, mood: str = "normal", *, session_posted: bool = False, session_projects: int = 0, session_follows: int = 0, session_gists: int = 0, can_project: bool = False, can_post: bool = False, can_message: bool = False, ) -> tuple[int, bool, int, int, int]: b = self.b curl = await b.url() page_text = (await b.html()).lower() on_post = "/posts/" in curl on_profile = "/profile/" in curl on_project_detail = "/projects/" in curl on_projects_list = bool(re.search(r"/projects(\?|$)", curl)) on_gist_detail = "/gists/" in curl on_gists_list = bool(re.search(r"/gists(\?|$)", curl)) on_news_detail = "/news/" in curl on_news_list = bool(re.search(r"/news(\?|$)", curl)) on_notifications = "/notifications" in curl actions = 0 did_post = session_posted projs = session_projects follows = session_follows gists = session_gists msgs = 0 mood_factor = {"lurking": 0.3, "casual": 0.6, "normal": 1.0, "deep": 1.5} burst = max(1, int(random.gauss(3, 1) * mood_factor[mood])) for _ in range(burst): if mood == "lurking" and not on_post and random.random() < 0.4: await self._scroll_page() await b._idle(0.3, 1.0) actions += 1 curl = await b.url() page_text = (await b.html()).lower() on_post = "/posts/" in curl continue if on_post: page_text = (await b.html()).lower() own_post = await self._is_own_post() mentioned = own_post and f"@{self.state.username.lower()}" in page_text await self._read_like_human(4, 90) if own_post and not mentioned: self._log("Own post, no mention - skipping interactions") await self._random_nav_click() await b._idle(1.0, 2.5) else: comment_p = { "lurking": 0.5, "casual": 0.75, "normal": 0.85, "deep": 0.9, }[mood] if random.random() < comment_p: if await self._comment_on_post(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.5: if await self._reply_to_random_comment(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.7: if await self._vote_on_feed(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.3: if await self._vote_on_comments(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.6: if await self._vote_on_poll(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.6: if await self._react_to_object(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.75: await b.goto(f"{self.base_url}/feed") await b._idle(0.8, 2.0) elif random.random() < 0.3: await self._random_nav_click() await b._idle(1.0, 2.5) elif on_profile: if not follows and random.random() < 0.4: if await self._follow_user(): follows += 1 actions += 1 await b._idle(0.5, 1.5) if can_message and not msgs and random.random() < 0.3: if await self._send_message(): msgs += 1 actions += 1 await b._idle(0.5, 1.5) await self._scroll_page() await b._idle(0.5, 1.0) if random.random() < 0.3: if await self._click_post_link(): actions += 1 await self._random_nav_click() await b._idle(1.0, 2.0) actions += 1 elif on_news_detail: await self._read_like_human(4, 60) comment_p = {"lurking": 0.4, "casual": 0.6, "normal": 0.7, "deep": 0.8}[ mood ] if random.random() < comment_p: if await self._comment_on_post(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.7: await b.goto(f"{self.base_url}/feed") await b._idle(0.8, 1.8) else: await self._random_nav_click() await b._idle(1.0, 2.5) elif on_news_list: await self._scroll_page() await b._idle(0.4, 1.0) if random.random() < 0.7: if await self._open_content("news"): actions += 1 else: await self._random_nav_click() await b._idle(0.8, 1.8) actions += 1 elif on_project_detail: await self._read_like_human(4, 60) if random.random() < 0.6: if await self._vote_on_project(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.5: if await self._react_to_object(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.4: if await self._comment_on_post(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.7: await b.goto(f"{self.base_url}/projects") await b._idle(0.8, 1.8) else: await self._random_nav_click() await b._idle(1.0, 2.5) elif on_projects_list: if random.random() < 0.55: if await self._vote_on_project(): actions += 1 await b._idle(0.5, 1.5) if can_project and projs < 1 and random.random() < 0.3: if await self._click_create_project(): projs += 1 actions += 1 await b._idle(0.5, 1.0) elif random.random() < 0.6: if await self._open_content("project"): actions += 1 else: await self._scroll_page() await b._idle(0.8, 1.8) actions += 1 elif on_gist_detail: await self._read_like_human(4, 60) if random.random() < 0.45: if await self._comment_on_post(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.6: if await self._vote_on_feed(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.5: if await self._react_to_object(): actions += 1 await b._idle(0.5, 1.5) if random.random() < 0.4: await self._random_nav_click() await b._idle(1.0, 2.5) elif on_gists_list: if random.random() < 0.6: if await self._vote_on_gist(): actions += 1 await b._idle(0.5, 1.5) if gists < 1 and random.random() < 0.25: if await self._create_gist(): gists += 1 actions += 1 await b._idle(0.5, 1.0) elif random.random() < 0.5: if await self._click_gist_link(): actions += 1 else: await self._scroll_page() await b._idle(0.8, 1.8) actions += 1 elif on_notifications: await self._check_notifications() await b._idle(1.0, 2.0) await self._random_nav_click() actions += 1 else: clicked = False if random.random() < 0.3: acted = await self._react_to_object() if not acted: acted = await self._vote_on_poll() if acted: actions += 1 clicked = True if not clicked and random.random() < 0.12: if random.random() < 0.5: await self._search( "feed" if random.random() < 0.7 else "projects" ) else: await self._browse_section() actions += 1 clicked = True if not clicked and random.random() < 0.85: if await self._click_post_link(): clicked = True actions += 1 if not clicked and can_post and not did_post and random.random() < 0.5: if await self._create_post(): did_post = True actions += 1 clicked = True if not clicked and random.random() < 0.2: if await self._click_profile_link(): clicked = True actions += 1 if not clicked and gists < 1 and random.random() < 0.1: if await self._create_gist(): gists += 1 actions += 1 clicked = True if not clicked: if await self._vote_on_feed(): actions += 1 clicked = True if not clicked: if await self._random_nav_click(): clicked = True if not clicked: await self._scroll_page() actions += 1 await b._idle(1.0, 2.5) curl = await b.url() page_text = (await b.html()).lower() on_post = "/posts/" in curl on_profile = "/profile/" in curl on_project_detail = "/projects/" in curl on_projects_list = bool(re.search(r"/projects(\?|$)", curl)) on_gist_detail = "/gists/" in curl on_gists_list = bool(re.search(r"/gists(\?|$)", curl)) on_news_detail = "/news/" in curl on_news_list = bool(re.search(r"/news(\?|$)", curl)) on_notifications = "/notifications" in curl return actions, did_post, projs, follows, gists async def run_forever(self, action_limit: int = 0) -> None: b = self.b self._save() lifetime_actions = 0 crash_streak = 0 session_number = 0 sessions_since_project = 99 sessions_since_post = 99 sessions_since_message = 99 self._bind_monitor() if self._startup_jitter: delay = random.uniform(0, self._startup_jitter) self.b.note(status=f"warming up, first session in {delay / 60:.0f}m") self._log(f"Startup jitter: first session in {delay / 60:.1f}m") try: await asyncio.sleep(delay) except asyncio.CancelledError: self._save() raise while True: session_number += 1 mood, session_len, break_range = await self._session_type() self._session_start_cost = self.llm.total_cost self._session_start_calls = self.llm.total_calls self._session_banner(mood, session_len) self._session_history = [] self._session_energy = 0.5 self._session_stop_after = 0 self._session_comments = 0 self._session_comment_cap = random.randint( SESSION_COMMENT_CAP_MIN, SESSION_COMMENT_CAP_MAX ) self._session_mention_replies = 0 use_ai = False try: await b.launch() except Exception as e: self._log(f"Launch failed: {e}") crash_streak += 1 wait = min(30 * crash_streak, 300) await asyncio.sleep(wait) continue session_ok = False try: if not await self._ensure_auth(): self._log("Auth failed") await b.close() await asyncio.sleep(60) continue if not await self._adopt_account_api_key(): self._log("No own API key yet; ending session to avoid shared-key billing") await b.close() await asyncio.sleep(60) continue self._bind_monitor() if self._ai_decisions: await self._ensure_identity() use_ai = self._ai_decisions and bool(self.state.identity) await self._warm_cache() self._log(f"Session active ({mood}) as {self._identity()}") if not self.state.profile_filled: await self._update_profile() await b.goto(f"{self.base_url}/feed") await b._idle(0.8, 2.0) unread = await self._unread_notification_count() if unread: self._log(f"Unread notifications badge: {unread}") check_prob = NOTIFICATION_CHECK_PROB.get(mood, 1.0) want_notifications = mood in ("normal", "deep") or ( unread > 0 and random.random() < check_prob ) if mood in ("normal", "deep"): await self._check_messages() await b._idle(0.5, 1.5) if want_notifications: await self._check_notifications() await b._idle(1.0, 3.0) if mood in ("normal", "deep"): await self._engage_community() await b._idle(0.8, 2.0) await b.goto(f"{self.base_url}/feed") session_actions = 0 max_actions = session_len + random.randint(0, 5) session_posted = False session_projects = 0 session_follows = 0 session_gists = 0 session_messages_start = self.state.messages_sent sessions_since_project += 1 sessions_since_post += 1 sessions_since_message += 1 can_create_project = ( sessions_since_project >= 3 and random.random() < 0.3 ) can_create_post = sessions_since_post >= 2 and random.random() < 0.5 can_create_message = ( sessions_since_message >= 2 and random.random() < 0.4 ) while session_actions < max_actions: if action_limit and lifetime_actions >= action_limit: self._log(f"Hit max actions ({action_limit}), stopping") self._save() self._cost_summary("final") return ( taken, session_posted, session_projects, session_follows, session_gists, ) = await (self._cycle_ai if use_ai else self._cycle)( mood=mood, session_posted=session_posted, session_projects=session_projects, session_follows=session_follows, session_gists=session_gists, can_project=can_create_project, can_post=can_create_post, can_message=can_create_message, ) if session_posted: sessions_since_post = 0 if session_projects: sessions_since_project = 0 if self.state.messages_sent > session_messages_start: sessions_since_message = 0 session_actions += max(taken, 1) lifetime_actions += max(taken, 1) self._save() crash_streak = 0 if use_ai: if ( self._session_stop_after and session_actions >= self._session_stop_after ): self._log( f"Energy spent (stop_after={self._session_stop_after}): ending session" ) break intra_pause = self._scaled_pause() else: fatigue = await self._fatigue_check(session_actions) if fatigue: self._log(f"Fatigue ({fatigue}): ending session") break intra_pause = random.randint(self._pause_min, self._pause_max) self._log( f"Session pause {intra_pause}s [{session_actions}/{max_actions}] {self._cost_tag()}" ) await asyncio.sleep(intra_pause) session_ok = session_actions > 0 self._session_summary(mood, session_actions) except asyncio.CancelledError: self._save() try: await b.close() except Exception as e: logger.debug("close on cancel failed: %s", e) raise except Exception as e: crash_streak += 1 self._log(f"Session crash ({crash_streak}): {e}") self._save() finally: try: await b.close() except Exception as e: logger.debug("close in finally failed: %s", e) if crash_streak > 3: self._log("Too many consecutive crashes, exiting") self._save() self._cost_summary("final") break if not session_ok and crash_streak > 0: break_s = random.randint(30, 120) self._log(f"Recovery wait {break_s}s") else: break_s = max(5, int(random.randint(*break_range) * self._break_scale)) if use_ai: factor = 1.5 - 1.0 * self._session_energy break_s = max(60, int(break_s * factor)) break_h = break_s / 3600 self._log(f"Session done. Break {break_h:.1f}h") self._save() try: await asyncio.sleep(break_s) except asyncio.CancelledError: self._save() raise self._cost_summary("final") self._log("Bot process exiting")