Skip to content

Commit 8299604

Browse files
GeiserXclaude
andcommitted
fix: protect Telegram session files from crash-loop overwrites (v7.1.1)
Back up session files before TelegramClient touches them and restore if auth fails with a smaller file. Add non-interactive auth script for headless environments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8bf86e0 commit 8299604

5 files changed

Lines changed: 130 additions & 4 deletions

File tree

docs/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ For upgrade instructions, see [Upgrading](#upgrading) at the bottom.
66

77
## [Unreleased]
88

9+
## [7.1.1] - 2026-03-05
10+
11+
### Added
12+
13+
- **Non-interactive auth script**`scripts/auth_noninteractive.py` for authenticating Telegram sessions without a TTY (useful for SSH automation, CI pipelines)
14+
15+
### Fixed
16+
17+
- **Session file protection** — Telethon session files are now backed up before each connect attempt. If the container crash-loops (e.g. due to database permission errors), the authenticated session is preserved and restored instead of being overwritten with an empty one
18+
- **Duplicate session_path assignment** in config.py removed
19+
920
## [7.1.0] - 2026-03-05
1021

1122
### Added

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "telegram-archive"
7-
version = "7.1.0"
7+
version = "7.1.1"
88
description = "Automated Telegram backup with Docker. Performs incremental backups of messages and media on a configurable schedule."
99
readme = "README.md"
1010
requires-python = ">=3.14"

scripts/auth_noninteractive.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
Non-interactive Telegram authentication helper.
3+
4+
Use this script when you cannot run the interactive setup_auth
5+
(e.g., from SSH without TTY, CI pipelines, or automation scripts).
6+
7+
Usage:
8+
# Step 1: Send verification code to Telegram app
9+
docker compose run --rm backup python scripts/auth_noninteractive.py send
10+
11+
# Step 2: Enter the code (and 2FA password if enabled)
12+
docker compose run --rm backup python scripts/auth_noninteractive.py verify CODE
13+
docker compose run --rm backup python scripts/auth_noninteractive.py verify CODE 2FA_PASSWORD
14+
15+
Environment variables required (same as normal operation):
16+
TELEGRAM_API_ID, TELEGRAM_API_HASH, TELEGRAM_PHONE, SESSION_NAME, BACKUP_PATH
17+
"""
18+
19+
import asyncio
20+
import os
21+
import sys
22+
23+
from telethon import TelegramClient
24+
25+
26+
def _get_session_path() -> str:
27+
"""Derive session path from environment (same logic as Config)."""
28+
backup_path = os.getenv("BACKUP_PATH", "/data/backups")
29+
session_dir = os.getenv("SESSION_DIR", os.path.join(os.path.dirname(backup_path.rstrip("/\\")), "session"))
30+
session_name = os.getenv("SESSION_NAME", "telegram_backup")
31+
os.makedirs(session_dir, exist_ok=True)
32+
return os.path.join(session_dir, session_name)
33+
34+
35+
async def main():
36+
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help", "help"):
37+
print(__doc__)
38+
sys.exit(0)
39+
40+
action = sys.argv[1]
41+
42+
api_id = int(os.environ["TELEGRAM_API_ID"])
43+
api_hash = os.environ["TELEGRAM_API_HASH"]
44+
phone = os.environ["TELEGRAM_PHONE"]
45+
session_path = _get_session_path()
46+
47+
client = TelegramClient(session_path, api_id, api_hash)
48+
await client.connect()
49+
50+
if await client.is_user_authorized():
51+
me = await client.get_me()
52+
print(f"Already authorized as {me.first_name} (@{me.username})")
53+
await client.disconnect()
54+
return
55+
56+
if action == "send":
57+
result = await client.send_code_request(phone)
58+
print(f"Verification code sent to Telegram app ({result.type.__class__.__name__})")
59+
print(f"Phone code hash: {result.phone_code_hash}")
60+
await client.disconnect()
61+
62+
elif action == "verify":
63+
if len(sys.argv) < 3:
64+
print("Usage: python scripts/auth_noninteractive.py verify CODE [2FA_PASSWORD]")
65+
sys.exit(1)
66+
67+
code = sys.argv[2]
68+
password = sys.argv[3] if len(sys.argv) > 3 else None
69+
70+
# Must send_code_request first to establish the flow
71+
await client.send_code_request(phone)
72+
73+
try:
74+
await client.sign_in(phone, code)
75+
except Exception as e:
76+
error_str = str(e)
77+
if "Two-steps verification" in error_str or "password" in error_str.lower() or "SessionPasswordNeeded" in error_str:
78+
if not password:
79+
print("2FA is enabled. Re-run with: verify CODE 2FA_PASSWORD")
80+
await client.disconnect()
81+
sys.exit(1)
82+
await client.sign_in(password=password)
83+
else:
84+
print(f"Authentication failed: {e}")
85+
await client.disconnect()
86+
sys.exit(1)
87+
88+
me = await client.get_me()
89+
print(f"Authenticated as {me.first_name} (@{me.username})")
90+
await client.disconnect()
91+
92+
else:
93+
print(f"Unknown action: {action}")
94+
print("Usage: send | verify CODE [2FA_PASSWORD]")
95+
sys.exit(1)
96+
97+
98+
if __name__ == "__main__":
99+
asyncio.run(main())

src/config.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,7 @@ def __init__(self):
118118
# If BACKUP_PATH is /data/backups, session goes to /data/session
119119
backup_parent = os.path.dirname(self.backup_path.rstrip("/\\"))
120120
self.session_dir = os.getenv("SESSION_DIR", os.path.join(backup_parent, "session"))
121-
self.session_path = os.path.join(self.session_dir, f"{self.session_name}.session")
122-
123-
self.session_path = os.path.join(self.session_dir, f"{self.session_name}.session")
121+
self.session_path = os.path.join(self.session_dir, self.session_name)
124122

125123
# Database path configuration
126124
# Default: inside backup_path

src/connection.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
import asyncio
1515
import logging
16+
import os
17+
import shutil
1618

1719
from telethon import TelegramClient
1820

@@ -86,6 +88,16 @@ async def connect(self) -> TelegramClient:
8688

8789
logger.info("Connecting to Telegram...")
8890

91+
session_file = self.config.session_path + ".session"
92+
backup_file = self.config.session_path + ".session.bak"
93+
94+
# Protect existing authenticated session: back it up before TelegramClient
95+
# touches it. Telethon overwrites sessions on connect, so if the container
96+
# crash-loops (e.g. DB permission errors) the authenticated session would be
97+
# replaced with an empty one. The backup lets us recover.
98+
if os.path.isfile(session_file) and os.path.getsize(session_file) > 0:
99+
shutil.copy2(session_file, backup_file)
100+
89101
self._client = TelegramClient(self.config.session_path, self.config.api_id, self.config.api_hash)
90102

91103
# Enable WAL mode for session DB to handle concurrent access
@@ -96,10 +108,16 @@ async def connect(self) -> TelegramClient:
96108

97109
# Check authorization
98110
if not await self._client.is_user_authorized():
111+
await self._client.disconnect()
112+
# Restore the backup if the live file was overwritten with an empty session
113+
if os.path.isfile(backup_file) and os.path.getsize(backup_file) > os.path.getsize(session_file):
114+
logger.warning("Session lost during connect — restoring from backup")
115+
shutil.copy2(backup_file, session_file)
99116
logger.error("❌ Session not authorized!")
100117
logger.error("Please run the authentication setup first:")
101118
logger.error(" Docker: ./init_auth.bat (Windows) or ./init_auth.sh (Linux/Mac)")
102119
logger.error(" Local: python -m src.setup_auth")
120+
logger.error(" Non-interactive: python scripts/auth_noninteractive.py send")
103121
raise RuntimeError("Session not authorized. Please run authentication setup.")
104122

105123
self._me = await self._client.get_me()

0 commit comments

Comments
 (0)