13 KiB
name, description, version, source, tags, triggers
| name | description | version | source | tags | triggers | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| telegram-mass-join | Mass join Telegram chats/groups across hundreds of accounts — filtering, distribution, async batch processing, and StringSession deadlock fix. | 1.1 | agent-created |
|
|
Telegram Mass Join
Массовое вступление в Telegram-чаты/группы через сотни аккаунтов. Полный пайплайн: фильтрация чатов → распределение по аккаунтам → асинхронный джойн с таймаутами. Пункты 12-14 pitfall'ов покрывают массовые БОТ-флоу (рефки, чекины, фарм поинтов) тем же пулом сессий.
Quick Start
cd /c/Users/User/Desktop/_PROJECTS/рассылка
# 1. Filter chats (if needed)
python expand_assign.py
# 2. Reset progress and run (single process)
rm -f assigned_join_progress.json
python -u join_assigned.py 2>&1
# 3. OR: parallel on 3 processes (3x speed)
rm -f assigned_join_progress*.json
python -u join_assigned.py --start 0 --end 58 --suffix _1 --timeout 120 --pause 15 &
python -u join_assigned.py --start 58 --end 116 --suffix _2 --timeout 120 --pause 15 &
python -u join_assigned.py --start 116 --end 174 --suffix _3 --timeout 120 --pause 15 &
Architecture
Phase 1: Chat Filtering (expand_assign.py)
- Source:
good_chats_for_mailing.json(8,572 chats from broader filter) - Filter: Two keyword lists —
good_kw(барахолки, объявления, услуги, чаты, работа...) andbad_kw(кликеры, crypto, NSFW, каналы, боты...) - Size: Only 3K–100K participants
- Fallback: Score ≥ 11 passes even without keyword match
- Output:
account_chat_assignments.jsonwithaccount_Nkeys,all_good_usernames.txt
Phase 2: Join Execution (join_assigned.py)
- Loads assignments, OK accounts (deduplicated), StringSessions
- Async batch processing via
asyncio.gather(BATCH_SIZE=5) - Per-account hard timeout: 120s (60s with 50 chats = hard_timeout for all)
- Per-chat timeouts: 10s for
get_entity, 10s forJoinChannelRequest - 15s pause between batches (was 30s, lowered for speed — no rate limit issues)
- Saves progress to
assigned_join_progress{suffix}.jsonevery batch - Saves joined list to
joined_chats{suffix}.jsonevery 10 batches
Critical Pitfalls
1. SQLite Session Deadlock → USE STRING SESSION
THE ROOT CAUSE of all hangs. SQLite sessions deadlock on database is locked when accessed in parallel. DO NOT add retry logic — convert to StringSession instead.
Use session_converter.py:
# Convert all .session files to StringSession
python session_converter.py
Output goes to generated/sessions_strings.json.
2. Account Deduplication
revalidate_all.json can have duplicate entries (381 OK but only 174 unique names). Always deduplicate:
seen = set()
ok_accounts = []
for r in reval["results"]:
if r.get("status") == "OK" and r["name"] not in seen:
seen.add(r["name"])
ok_accounts.append(r)
3. Progress File Poisoning
The assigned_join_progress.json survives between runs. If you change assignments or want a fresh start, DELETE it:
rm -f assigned_join_progress.json
Otherwise join_assigned.py resumes from the saved batch number and skips accounts.
4. Round-Robin Distribution
The assignment uses round-robin: account i gets chats at positions i, i+N, i+2N, ... where N = total_accounts. This ensures top accounts get the best chats (position 0, 1, 2...), but ALL accounts get a mix of quality levels.
5. Index-Based Lookup
Assignment keys are account_N (1-indexed). The join script maps them to sorted OK accounts:
global_idx = i + idx
acc_key = f"account_{global_idx + 1}"
acc_chats = assignments.get(acc_key, [])
6. Path Handling on Windows
- Use
C:\Users\User\...for PythonPath()objects - Use
/c/Users/User/...for bash commands write_filetool may resolve toC:\c\Users\...— usemcp_filesystem_write_filefor Windows paths instead
7. Chat Count vs Timeout Tradeoff
With 120s timeout, 50 chats/account works (~2.5s per join). Some accounts still hit hard_timeout depending on network speed. The sweet spot depends on timeout:
- 60s timeout → 5-10 chats max
- 120s timeout → 30-50 chats works for ~25% of accounts
- timeout_connect (~20% of accounts) — DC/network issue, not retryable
- Remaining 40% hard_timeout — slow connections, try fewer chats
8. Python global Keyword Placement
Declare global BEFORE any reference to the variable in the function scope — including default parameter values in add_argument():
# WRONG — SyntaxError: name 'ACCOUNT_TIMEOUT' is used prior to global declaration
async def main():
parser.add_argument("--timeout", default=ACCOUNT_TIMEOUT) # references before global
global ACCOUNT_TIMEOUT
# RIGHT — global declaration comes first
async def main():
global ACCOUNT_TIMEOUT, BATCH_PAUSE
parser.add_argument("--timeout", default=ACCOUNT_TIMEOUT)
9. Em-Dash (—) in Docstrings
Em-dash — (U+2014) causes SyntaxError: invalid character in some Python environments (git-bash/MSYS). Replace with -- in all docstrings and comments in scripts that run under bash on Windows.
10. Parallel Multi-Process Execution — ⚠️ Race Condition on Shared File
For 3x speedup, split accounts across N parallel processes using --start/--end/--suffix:
# 3 parallel processes on 174 accounts
python -u join_assigned.py --start 0 --end 58 --suffix _1 --timeout 120 --pause 15 &
python -u join_assigned.py --start 58 --end 116 --suffix _2 --timeout 120 --pause 15 &
python -u join_assigned.py --start 116 --end 174 --suffix _3 --timeout 120 --pause 15 &
Each process gets its own progress file AND its own joined_chats file:
assigned_join_progress_1.json+joined_chats_1.jsonassigned_join_progress_2.json+joined_chats_2.jsonassigned_join_progress_3.json+joined_chats_3.json
CRITICAL: DO NOT share joined_chats.json between processes. They overwrite each other's data (47/81 joins lost in one run). After all finish, merge:
python merge_joined.py
11. Reset Progress Before Rerun
If assignments changed or you want a fresh start, delete ALL progress files:
rm -f assigned_join_progress*.json joined_chats_*.json
12. Reply Keyboards vs Inline Buttons in Bot Flows
When the same account pool is used to drive BOTS (not channels), detect button type first: plain reply-keyboard buttons (KeyboardButton, no data/url) are NOT clickable via Telethon — send their exact text as a message (send_message(bot, '📋 Check-in')). Inline buttons with data=None still click via msg.click(text=...). Log type(btn).__name__ during the 1-account probe before writing the mass runner.
13. Probe Before Mass-Run; Don't Trust the Stated Flow
Before running any bot flow over hundreds of accounts, probe 1-3 accounts through the FULL path (start → language → menu → target action → reply texts) and verify each claimed gate actually exists — e.g. a 'subscribe to channel' step the user expects may not exist in the bot at all. Writing join/recheck logic for a non-existent gate wastes the whole run.
15. Bot Gates Appear Mid-Life — Re-Probe Every Re-Run
A flow that had no channel-subscribe gate yesterday can have one today (owner flips it server-side). Re-probe 1 account on every re-run, and treat a cluster of unmatched/None results as a possible NEW gate — read one raw reply before assuming timeouts. Gate recipe (join → Verify → retry, ChannelsTooMuchError slot freeing, URL from callback answer): references/bot-ref-checkin-flow.md.
14. Mass-Runner Shape (session pool from inventory.json)
Pool: C:/Users/User/tmp/tg_session_ops/inventory.json (name → {file, status, id, phone}) covering Desktop/CLEAN_ALIVE_SESSIONS; filter status=='LIVE' + file exists (~360 accounts). Runner: asyncio.Semaphore(8), chunks of ~40 with 2-4s pause, asyncio.wait_for(connect, timeout=22), progress JSON + JSONL log written after EVERY account (resume = if name in prog: return). Expect ~10% unauth and ~5-10% connect timeouts (bad DC) — non-retryable, skip. Second pass: delete progress entries with None result state and re-run to resolve unknowns ('Already Checked In' = counted). ValueError: No user has "X" as username = account can't see the bot — skip, not retryable. Launch: env -u PYTHONPATH C:/Users/User/AppData/Local/Programs/Python/Python311/python.exe -u script.py background + log file; poll progress JSON, not the log tail.
File Reference
| File | Purpose |
|---|---|
good_chats_for_mailing.json |
8,572 filtered chats (source) |
account_chat_assignments.json |
account_N → list of chat objects |
revalidate_all.json |
All accounts with status (OK/DEAD) |
generated/sessions_strings.json |
Phone → StringSession mappings |
assigned_join_progress.json |
Progress: batch_num, total_joined |
joined_chats.json |
Accumulated joined chats with metadata (merge target) |
joined_chats_1..3.json |
Per-process joined chats (merge sources) |
all_good_usernames.txt |
Plain username list for import |
expand_assign.py |
Filtering + assignment generator |
join_assigned.py |
Async join executor |
merge_joined.py |
Deduplicate per-process files into joined_chats.json |
session_converter.py |
SQLite → StringSession converter |
Typical Results
- 8,572 chats → assignments on 174 accounts
- 50 chats/account × 174 accounts = 8,700 slots
- ~177M total audience reach
- Per 58-account process: ~25% ok, ~40% hard_timeout, ~20% timeout_connect, ~15% no_stringsession
- 28-36 new joins per 58-account process with 120s timeout
- ~81 new joins total across 3 parallel processes (but race condition loses ~58% on shared file — use per-process files + merge!)
- 0 flood, 0 bans — safe for mass operations
Addlist Folder Parsing Workflow
When the user asks to add more chats via Telegram folders (t.me/addlist/...), first extract chat usernames from folders before joining or assigning.
Working pattern:
from telethon.tl.functions.chatlists import CheckChatlistInviteRequest
result = await asyncio.wait_for(
client(CheckChatlistInviteRequest(slug=slug)),
timeout=15,
)
usernames = [c.username for c in result.chats if getattr(c, "username", None)]
Critical pitfall: Do not iterate result.peers and call client.get_entity(peer) for every peer. It can hang on the first folder under Telethon/Windows. CheckChatlistInviteRequest already returns result.chats; read usernames directly from that list.
Safe pipeline:
- Parse
addlist.txt/addlist_urls.txtinto slugs. - Use a StringSession account and
CheckChatlistInviteRequest(slug=...)with a hard timeout. - Save raw usernames to
addlist_chats.txt+addlist_chats.json. - Deduplicate against
good_chats_for_mailing.jsonand currentaccount_chat_assignments.json. - Apply a conservative username filter: drop crypto/airdrop/casino/bot/test/flood/spam; keep chat/work/rabota/job/barahol/obyav/uslug/arend/market/ads/etc.
- Backup
account_chat_assignments.jsonbefore appending. - Distribute filtered folder chats round-robin across
account_Nassignment keys. - Verify with
wc -l, JSON counts, min/max per account, and file sizes before saying done.
Known result example: 1121 addlist folders → 1062 OK folders → 28,478 raw unique usernames → 21,807 net-new vs existing → 6,894 filtered high-signal chats appended; assignments grew 8,572 → 15,466 slots, 88–90 chats/account.
Common Error Types
bad_CHANNEL_PRIVATE— chat was deleted or made privatebad_USERNAME_NOT_OCCUPIED— username changed or doesn't existhard_timeout— account took >60s to process (usually connection issue)no_stringsession— account has no StringSession (needs conversion)flood_N— rate limited for N seconds (auto-waits, max 60s)
Reference Docs
references/bot-ref-checkin-flow.md— Mass bot flow (ref + daily check-in) over the CLEAN_ALIVE_SESSIONS pool: probe-first, reply-keyboard driving, runner params, second-pass retryreferences/chat-filtering-pipeline.md— Full keyword lists, filtering logic, results breakdownreferences/stringsession-conversion.md— SQLite deadlock root cause, converter script, pitfallsreferences/join-executor-patterns.md— Async flow, error types, account statuses, performance datareferences/parallel-multi-process-execution.md— CLI args, 3-process launch pattern, timeout tuning, Python pitfalls, race condition + merge fixscripts/merge_joined.py— Deduplicate per-process joined_chats files into one