diff --git a/.env.example b/.env.example index c34a108..75dbb11 100644 --- a/.env.example +++ b/.env.example @@ -38,5 +38,14 @@ MAX_MESSAGES_HISTORY=30 AUTO_OPENER_ENABLED=false OPENER_TEXT= +# Typing simulation (show "typing..." before sending) +# Enable/disable typing indicator and tune how long it appears based on message length. +TYPING_SIM_ENABLED=true +# Approximate words per minute used to estimate typing duration (5 chars ≈ 1 word) +TYPING_WPM=22 +# Clamp the simulated typing duration to this range (in seconds) +TYPING_MIN_SEC=2.0 +TYPING_MAX_SEC=18.0 + # Optional: ensure unbuffered logs in some environments PYTHONUNBUFFERED=1 \ No newline at end of file diff --git a/README b/README index 33e0d10..182dbf2 100644 --- a/README +++ b/README @@ -26,6 +26,10 @@ Telethon + OpenAI bot that engages unsolicited DMs with safe, time-wasting small | MAX_MESSAGES_HISTORY | no | 30 | Hard cap on number of messages kept in rolling history | | AUTO_OPENER_ENABLED | no | false | If true, send an initial opener only on fresh start when a target is resolved and no catch-up reply was needed | | OPENER_TEXT | no | — | The opener text to send when AUTO_OPENER_ENABLED=true | +| TYPING_SIM_ENABLED | no | true | Show a "typing…" indicator before sending the message | +| TYPING_WPM | no | 22 | Approximate words per minute to estimate typing duration | +| TYPING_MIN_SEC | no | 2.0 | Minimum typing duration (seconds) | +| TYPING_MAX_SEC | no | 18.0 | Maximum typing duration (seconds) | | PYTHONUNBUFFERED | no | 1 | If set, forces unbuffered output in some environments | Notes: diff --git a/main.py b/main.py index 012c773..bcc5258 100644 --- a/main.py +++ b/main.py @@ -41,6 +41,14 @@ TARGET_CACHE_FILE = Path(os.environ.get("TARGET_CACHE_FILE", "target_id.txt")) AUTO_OPENER_ENABLED = os.environ.get("AUTO_OPENER_ENABLED", "false").lower() == "true" OPENER_TEXT = os.environ.get("OPENER_TEXT", "").strip() +# Typing simulation controls +TYPING_SIM_ENABLED = os.environ.get("TYPING_SIM_ENABLED", "true").lower() == "true" +# Approx words per minute to estimate how long to "type" +TYPING_WPM = int(os.environ.get("TYPING_WPM", "22")) +# Clamp typing duration into [min, max] seconds +TYPING_MIN_SEC = float(os.environ.get("TYPING_MIN_SEC", "2.0")) +TYPING_MAX_SEC = float(os.environ.get("TYPING_MAX_SEC", "18.0")) + # ---------- Validation ---------- def _require(cond: bool, msg: str): if not cond: @@ -299,14 +307,50 @@ async def startup_catchup_if_needed(client: TelegramClient, target_entity) -> bo await safe_send(client, target_entity, reply) return True - - async def human_delay(): await asyncio.sleep(random.randint(MIN_DELAY_SEC, MAX_DELAY_SEC)) +def _estimate_typing_seconds(text: str) -> float: + """ + Estimate a human-like typing duration based on configured WPM. + Roughly assumes 5 characters per word. + """ + if not text: + return TYPING_MIN_SEC + words = max(1, len(text) / 5.0) + seconds = (words / max(1, TYPING_WPM)) * 60.0 + return max(TYPING_MIN_SEC, min(TYPING_MAX_SEC, seconds)) + +async def _simulate_typing(client: TelegramClient, entity, seconds: float): + """ + Sends 'typing...' chat actions periodically so the peer sees the typing indicator. + """ + if seconds <= 0: + return + # Telethon auto-refreshes typing when using the context manager. + # We slice the sleep into small chunks to keep the indicator alive. + slice_len = 3.5 + total = 0.0 + try: + async with client.action(entity, 'typing'): + while total < seconds: + remaining = seconds - total + sl = slice_len if remaining > slice_len else remaining + await asyncio.sleep(sl) + total += sl + except Exception as e: + # Typing is best-effort; fall back silently + print(f"Typing simulation warning: {e}") async def safe_send(client: TelegramClient, entity, text: str): + # Initial human-like pause before reacting at all await human_delay() + + # Show "typing..." before sending the full message + if TYPING_SIM_ENABLED: + seconds = _estimate_typing_seconds(text) + await _simulate_typing(client, entity, seconds) + try: await client.send_message(entity, text) except FloodWaitError as e: