forked from CameronJGrant/telegram-scam-baiter
Initial commit
This commit is contained in:
31
.env.example
Normal file
31
.env.example
Normal 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
8
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
33
README
Normal file
@@ -0,0 +1,33 @@
|
||||
#telegram-scam-baiter
|
||||
|
||||
Telethon + OpenAI bot that engages unsolicited DMs with safe, time‑wasting small talk. It uses a focused system prompt, keeps recent chat history, and replies with human‑like 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 auto‑targets the first inbound DM.
|
||||
- Maintains a rolling history and crafts short, question‑ending 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
245
main.py
Normal 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 I’m 5,” request pretend paper examples, and keep questioning, never committing.
|
||||
- Keep replies human-sounding and concise (1–4 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; Wi‑Fi/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 (1–4 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. I’m 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 I’m 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.")
|
||||
Reference in New Issue
Block a user