Initial commit

This commit is contained in:
2025-09-26 10:15:55 -07:00
commit ca24a8600f
5 changed files with 338 additions and 0 deletions

31
.env.example Normal file
View File

@@ -0,0 +1,31 @@
# Telegram API credentials (get from https://my.telegram.org)
TG_API_ID=123456
TG_API_HASH=your_api_hash_here
# Telethon session file name (will create files with this prefix)
TG_SESSION=telethon_session
# Target user — choose ONE of these (leave the other blank/zero)
# If they have a public username, set it WITHOUT the leading @
TARGET_USERNAME=
# If no username, use their numeric Telegram user ID
TARGET_USER_ID=0
# OpenAI configuration
OPENAI_API_KEY=your_openai_api_key_here
# Any chat-capable model you prefer
OPENAI_MODEL=gpt-4o-mini
# Human-like delay between replies (seconds)
MIN_DELAY_SEC=25
MAX_DELAY_SEC=75
# History persistence
HISTORY_FILE=chat_history.jsonl
# Rough token budget for history passed to the model
MAX_TOKENS_HISTORY=2200
# Hard cap on number of messages kept in history
MAX_MESSAGES_HISTORY=30
# Optional: ensure unbuffered logs in some environments
PYTHONUNBUFFERED=1

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
.venv
__pycache__
.idea
.env
chat_history.jsonl
session_name.session
session_name.session-journal
telethon_session.session

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Cameron Grant
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

33
README Normal file
View File

@@ -0,0 +1,33 @@
#telegram-scam-baiter
Telethon + OpenAI bot that engages unsolicited DMs with safe, timewasting small talk. It uses a focused system prompt, keeps recent chat history, and replies with humanlike delays to keep scammers busy while revealing nothing useful.
## What it does
- Listens for messages from a single target (by username or numeric user ID), or autotargets the first inbound DM.
- Maintains a rolling history and crafts short, questionending replies to keep the other person typing.
## Environment variables
| Variable | Required | Default | Description |
|-----------------------|----------|-----------------------|-----------------------------------------------------------------------------|
| TG_API_ID | yes | — | Telegram API ID from https://my.telegram.org |
| TG_API_HASH | yes | — | Telegram API hash from https://my.telegram.org |
| TG_SESSION | no | telethon_session | Session file prefix used by Telethon |
| TARGET_USERNAME | no | — | Target's public username (without @). Leave empty if using TARGET_USER_ID |
| TARGET_USER_ID | no | 0 | Target's numeric Telegram user ID (use if no username) |
| OPENAI_API_KEY | yes | — | OpenAI API key |
| OPENAI_MODEL | no | gpt-4o-mini | Chat-capable model used for replies |
| MIN_DELAY_SEC | no | 25 | Minimum delay (seconds) before each reply |
| MAX_DELAY_SEC | no | 75 | Maximum delay (seconds) before each reply |
| HISTORY_FILE | no | chat_history.jsonl | Path to local JSONL file for conversation history |
| MAX_TOKENS_HISTORY | no | 2200 | Rough token budget for messages passed to the model |
| MAX_MESSAGES_HISTORY | no | 30 | Hard cap on number of messages kept in rolling history |
| PYTHONUNBUFFERED | no | 1 | If set, forces unbuffered output in some environments |
Notes:
- Set either TARGET_USERNAME or TARGET_USER_ID. If neither is set, the first inbound DM will become the target automatically.
- Increase delays if you hit Telegram flood limits.
## License
MIT License — see LICENSE file for details.

245
main.py Normal file
View File

@@ -0,0 +1,245 @@
# Python
import os
import json
import time
import random
import asyncio
from pathlib import Path
from typing import List, Dict, Any
from telethon import TelegramClient, events
from telethon.errors import FloodWaitError
from telethon.tl.types import User
from openai import OpenAI
# ---------- Configuration via environment ----------
API_ID = int(os.environ.get("TG_API_ID", "0"))
API_HASH = os.environ.get("TG_API_HASH", "")
SESSION = os.environ.get("TG_SESSION", "telethon_session")
TARGET_USERNAME = os.environ.get("TARGET_USERNAME", "").strip().lstrip("@")
TARGET_USER_ID = int(os.environ.get("TARGET_USER_ID", "0")) # optional numeric id
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
# Safety delays
MIN_DELAY_SEC = int(os.environ.get("MIN_DELAY_SEC", "25"))
MAX_DELAY_SEC = int(os.environ.get("MAX_DELAY_SEC", "75"))
# History controls
HISTORY_FILE = Path(os.environ.get("HISTORY_FILE", "chat_history.jsonl"))
MAX_TOKENS_HISTORY = int(os.environ.get("MAX_TOKENS_HISTORY", "2200")) # rough token budget
MAX_MESSAGES_HISTORY = int(os.environ.get("MAX_MESSAGES_HISTORY", "30"))
# ---------- Validation ----------
def _require(cond: bool, msg: str):
if not cond:
raise RuntimeError(msg)
_require(API_ID > 0 and API_HASH, "Set TG_API_ID and TG_API_HASH")
_require(OPENAI_API_KEY, "Set OPENAI_API_KEY")
# ---------- Prompting ----------
SYSTEM_PROMPT = """
You are an assistant writing on behalf of a cautious user named Jon (57, Pacific Northwest, USA).
Primary objective: keep the other person engaged and typing for as long as possible, without sharing any sensitive information or taking any risky action.
Role and behavior rules:
- Maintain friendly, curious small talk. Ask open-ended questions that require paragraphs to answer.
- Frequently request details, stories, examples, and step-by-step explanations. Favor mundane topics (daily routine, food, weather, hobbies).
- Stall politely: mention small interruptions (helping a neighbor, walking the dog, low battery) and ask them to repeat or clarify.
- Never send or request personal data (addresses, IDs, bank info, photos, exact workplace, family details).
- Never click or discuss links, QR codes, or apps. If they send links or push to another app/call/video, decline and redirect to chat.
- If they pitch investments/crypto/money, ask for simple explanations “like Im 5,” request pretend paper examples, and keep questioning, never committing.
- Keep replies human-sounding and concise (14 short sentences) but end with a question to draw them out.
- Avoid promises, commitments, or sending any files or images.
- Do not break character as Jon.
Backstory (use sparingly and consistently when needed):
- Semi-retired handyman; helps at a community garden; has an elderly dog named Buddy; likes bird watching, sourdough, puzzles; prefers cash over online banking; WiFi/battery often unreliable.
If the other person becomes impatient, remain polite, apologize for delays, and ask a follow-up question.
Only output the message text to send—no brackets, notes, or stage directions.
""".strip()
# ---------- History persistence ----------
def append_history(role: str, content: str, ts: float | None = None):
rec = {
"ts": ts if ts is not None else time.time(),
"role": role,
"content": content,
}
with HISTORY_FILE.open("a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
def load_history() -> List[Dict[str, Any]]:
if not HISTORY_FILE.exists():
return []
records = []
with HISTORY_FILE.open("r", encoding="utf-8") as f:
for line in f:
try:
records.append(json.loads(line))
except Exception:
continue
return records
def prune_history(records: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
# Simple heuristic: cap by message count; token estimation kept simple (~4 chars per token)
if len(records) > MAX_MESSAGES_HISTORY:
records = records[-MAX_MESSAGES_HISTORY:]
total_tokens_est = sum(max(1, len(r.get("content", "")) // 4) for r in records)
while total_tokens_est > MAX_TOKENS_HISTORY and len(records) > 10:
records = records[1:]
total_tokens_est = sum(max(1, len(r.get("content", "")) // 4) for r in records)
return records
def build_chat_messages_for_openai() -> List[Dict[str, str]]:
records = prune_history(load_history())
msgs: List[Dict[str, str]] = [{"role": "system", "content": SYSTEM_PROMPT}]
for r in records:
role = r["role"]
if role not in ("user", "assistant"):
continue
msgs.append({"role": role, "content": r["content"]})
return msgs
# ---------- OpenAI client ----------
oai = OpenAI(api_key=OPENAI_API_KEY)
async def generate_reply_via_openai() -> str:
"""
Build a messages array that includes the system prompt and recent history,
and ask the model for the next line only.
"""
messages = build_chat_messages_for_openai()
# Add a final user nudge to ensure it continues the conversation
messages.append({
"role": "user",
"content": "Please respond as Jon, following all the rules above. Keep it brief (14 short sentences) and end with a question."
})
# Note: using a standard chat completion call
resp = oai.chat.completions.create(
model=OPENAI_MODEL,
messages=messages,
temperature=0.8,
max_tokens=180,
presence_penalty=0.3,
frequency_penalty=0.2,
)
text = resp.choices[0].message.content.strip()
return text
# ---------- Telegram helper ----------
async def resolve_target_entity(client: TelegramClient):
target = None
if TARGET_USERNAME:
try:
target = await client.get_entity(TARGET_USERNAME)
except Exception:
target = None
if not target and TARGET_USER_ID:
try:
target = await client.get_entity(TARGET_USER_ID)
except Exception:
target = None
return target
async def human_delay():
await asyncio.sleep(random.randint(MIN_DELAY_SEC, MAX_DELAY_SEC))
async def safe_send(client: TelegramClient, entity, text: str):
await human_delay()
try:
await client.send_message(entity, text)
except FloodWaitError as e:
await asyncio.sleep(e.seconds + 3)
await client.send_message(entity, text)
def sender_matches_target(sender: User, target_entity) -> bool:
if target_entity and sender.id == getattr(target_entity, "id", None):
return True
if TARGET_USERNAME and sender.username:
return sender.username.lower() == TARGET_USERNAME.lower()
if TARGET_USER_ID and sender.id == TARGET_USER_ID:
return True
return False
# ---------- Main app ----------
async def main():
client = TelegramClient(SESSION, API_ID, API_HASH)
await client.start()
target_entity = await resolve_target_entity(client)
if target_entity:
print(f"Target resolved: id={target_entity.id}, username={getattr(target_entity, 'username', None)}")
else:
print("Target not resolved yet. Will match dynamically on first incoming message from target.")
# Optional: send a gentle opener once (only if history is empty)
if not HISTORY_FILE.exists():
opener = "Oh neat, Houston. Im up in the Pacific Northwest these days, sort of near the coast. What brought you from the UK to Houston?"
append_history("assistant", opener)
if target_entity:
await safe_send(client, target_entity, opener)
else:
print("Opener queued in history; will start replying when the target speaks.")
@client.on(events.NewMessage(incoming=True))
async def on_msg(event):
nonlocal target_entity
sender = await event.get_sender()
if not isinstance(sender, User):
return
# If target not yet resolved, auto-resolve on first qualifying message
if (not target_entity) and (TARGET_USER_ID == 0 and not TARGET_USERNAME):
# No explicit target provided; first inbound DM will become the target
target_entity = sender
print(f"Auto-targeted sender id={sender.id}, username={sender.username}")
if not sender_matches_target(sender, target_entity):
return # ignore non-target chats
# Record incoming message
text = event.message.message or ""
if text.strip():
append_history("user", text)
# Decide on next reply
try:
reply = await generate_reply_via_openai()
except Exception as e:
# Fallback small-talk lines if OpenAI is temporarily unavailable
print(f"OpenAI error: {e}")
reply = random.choice([
"Sorry, I read slow when Im tired—could you say that another way?",
"Interesting—what makes you say that?",
"Got curious: what did you have for breakfast today?",
"I had to step away a minute—where were we?",
])
# Persist and send
append_history("assistant", reply)
await safe_send(client, event.chat_id, reply)
print("Listening for target messages…")
await client.run_until_disconnected()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Exiting.")