CliRemote 1.5.0__py3-none-any.whl → 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cliremote-1.5.0.dist-info → cliremote-1.6.0.dist-info}/METADATA +1 -1
- {cliremote-1.5.0.dist-info → cliremote-1.6.0.dist-info}/RECORD +7 -6
- remote/client_manager.py +98 -124
- remote/utils/sqlite_utils.py +45 -0
- {cliremote-1.5.0.dist-info → cliremote-1.6.0.dist-info}/WHEEL +0 -0
- {cliremote-1.5.0.dist-info → cliremote-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {cliremote-1.5.0.dist-info → cliremote-1.6.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
cliremote-1.
|
1
|
+
cliremote-1.6.0.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
|
2
2
|
remote/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
remote/account_manager.py,sha256=u8yqcyq7Vl8LZ1rNlrEfwlJEbJiBzoSAlL3z2hhTzyc,9829
|
4
4
|
remote/account_viewer.py,sha256=MQoH5lOz24651EjhlzVwi6O5hVUAT7Q5fFU6ZHjBlsM,4243
|
@@ -8,7 +8,7 @@ remote/batch_manager.py,sha256=jVGhYVwHMKJd7f7JxcWjKlwr03dq0RaGD1KdkyYdb00,1051
|
|
8
8
|
remote/block_manager.py,sha256=R7UaQigr-hTRtjxjG3OvJdKhvp0mDpLaESp3Of1AYhs,5692
|
9
9
|
remote/caption_manager.py,sha256=ekgcZ_D1q8C24WP18TXxlM5eWTknJmw-KNXDfqlsnEw,966
|
10
10
|
remote/cleaner.py,sha256=gvPtkosGsf7Eb3zmYyeC44xB4HtCxlzKH3e3qLuVos4,5742
|
11
|
-
remote/client_manager.py,sha256=
|
11
|
+
remote/client_manager.py,sha256=EP2pVasUOUwjr4h-vO-LWWf9_rMCGAsFrzVvnGXJcNI,8250
|
12
12
|
remote/client_picker.py,sha256=3QYi1UQ8pZjqR5YU2oKgQlCYV7-1pSPWknCyRFMcFak,4428
|
13
13
|
remote/config.py,sha256=VK0e96gEINRViKIq99CYYuYyaVZTLtlWlPKKkBd41Cg,2377
|
14
14
|
remote/device_manager.py,sha256=SUCONe1qa5jMHOMqqS27ATtv3CaqAT8cN9jNi7AI_Go,5813
|
@@ -31,7 +31,8 @@ remote/speed_manager.py,sha256=fIWSQAP9qW8AHZtMZq0MrC4_nvxcTFU1SBU75kpRzB8,1115
|
|
31
31
|
remote/stop_manager.py,sha256=UXzKJTblEyQqCjp7fenvQ51Q96Unx05WeOiuFMdj25M,1151
|
32
32
|
remote/text_manager.py,sha256=C2wNSXPSCDu8NSD3RsfbKmUQMWOYd1B5N4tzy-Jsriw,2195
|
33
33
|
remote/username_manager.py,sha256=nMNdke-2FIv86xR1Y6rR-43oUoQu_3Khw8wEo54noXI,3388
|
34
|
-
|
35
|
-
cliremote-1.
|
36
|
-
cliremote-1.
|
37
|
-
cliremote-1.
|
34
|
+
remote/utils/sqlite_utils.py,sha256=5i0oUXsBgKC_8qHZPJ-Gyhp9D1TwqKHVvuZRIhKpS6w,1260
|
35
|
+
cliremote-1.6.0.dist-info/METADATA,sha256=TVNkkckVN52498m8gUoc5SFnEYyZ1cKFE6wDUZh6GTk,1202
|
36
|
+
cliremote-1.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
37
|
+
cliremote-1.6.0.dist-info/top_level.txt,sha256=yBZidJ6zCix_a2ubGlYaewvlzBFXWbckQt20dudxJ1E,7
|
38
|
+
cliremote-1.6.0.dist-info/RECORD,,
|
remote/client_manager.py
CHANGED
@@ -5,253 +5,227 @@ import asyncio
|
|
5
5
|
import logging
|
6
6
|
import random
|
7
7
|
import traceback
|
8
|
-
from typing import Optional, Dict, List, Set
|
8
|
+
from typing import Optional, Dict, List, Set
|
9
9
|
from pyrogram import Client, errors
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
from .utils.sqlite_utils import (
|
12
|
+
ensure_dir, chmod_rw, cleanup_sqlite_sidecars, probe_sqlite
|
13
|
+
)
|
14
|
+
|
15
|
+
# =========================
|
16
|
+
# Logging
|
17
|
+
# =========================
|
14
18
|
os.makedirs("logs", exist_ok=True)
|
15
19
|
LOG_FILE = "logs/client_debug_log.txt"
|
16
20
|
logger = logging.getLogger(__name__)
|
17
21
|
logger.setLevel(logging.DEBUG)
|
18
|
-
|
19
22
|
if not any(isinstance(h, logging.FileHandler) and getattr(h, "baseFilename", "").endswith(LOG_FILE) for h in logger.handlers):
|
20
23
|
fh = logging.FileHandler(LOG_FILE, encoding="utf-8")
|
21
24
|
fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S")
|
22
25
|
fh.setFormatter(fmt)
|
23
26
|
logger.addHandler(fh)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
#
|
28
|
-
#
|
29
|
-
|
30
|
-
ACCOUNTS_FOLDER = "acc"
|
31
|
-
ACCOUNTS_DATA_FOLDER = "acc_data"
|
32
|
-
os.
|
33
|
-
|
27
|
+
logger.info("🧩 client_manager loaded (v1.6.0, DEBUG).")
|
28
|
+
|
29
|
+
# =========================
|
30
|
+
# Paths
|
31
|
+
# =========================
|
32
|
+
BASE_DIR = os.path.abspath(os.getcwd())
|
33
|
+
ACCOUNTS_FOLDER = os.path.join(BASE_DIR, "acc")
|
34
|
+
ACCOUNTS_DATA_FOLDER = os.path.join(BASE_DIR, "acc_data")
|
35
|
+
ACC_TEMP = os.path.join(BASE_DIR, "acc_temp")
|
36
|
+
for p in (ACCOUNTS_FOLDER, ACCOUNTS_DATA_FOLDER, ACC_TEMP):
|
37
|
+
ensure_dir(p)
|
34
38
|
|
35
39
|
client_pool: Dict[str, Client] = {}
|
36
40
|
client_locks: Dict[str, asyncio.Lock] = {}
|
37
41
|
|
38
|
-
|
39
|
-
|
40
|
-
# ============================================================
|
41
|
-
def _ensure_dir(path: str):
|
42
|
-
try:
|
43
|
-
os.makedirs(path, exist_ok=True)
|
44
|
-
except Exception as e:
|
45
|
-
logger.error(f"⚠️ Cannot create directory {path}: {type(e).__name__}: {e}")
|
42
|
+
def _strip_session_ext(x: str) -> str:
|
43
|
+
return x[:-8] if x.endswith(".session") else x
|
46
44
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
return session_base[:-8]
|
51
|
-
return session_base
|
52
|
-
|
53
|
-
# ============================================================
|
54
|
-
# 💾 JSON helpers
|
55
|
-
# ============================================================
|
45
|
+
# =========================
|
46
|
+
# JSON helpers
|
47
|
+
# =========================
|
56
48
|
def get_account_data(phone_number: str) -> Optional[Dict]:
|
57
|
-
|
58
|
-
if not os.path.exists(
|
59
|
-
logger.warning(
|
49
|
+
fp = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
|
50
|
+
if not os.path.exists(fp):
|
51
|
+
logger.warning("%s: account JSON not found: %s", phone_number, fp)
|
60
52
|
return None
|
61
53
|
try:
|
62
|
-
with open(
|
54
|
+
with open(fp, "r", encoding="utf-8") as f:
|
63
55
|
return json.load(f)
|
64
56
|
except Exception as e:
|
65
|
-
logger.error(
|
57
|
+
logger.error("%s: error reading JSON: %s: %s", phone_number, type(e).__name__, e)
|
66
58
|
return None
|
67
59
|
|
68
60
|
def save_account_data(phone_number: str, data: Dict) -> None:
|
69
|
-
|
70
|
-
|
61
|
+
fp = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
|
62
|
+
ensure_dir(ACCOUNTS_DATA_FOLDER)
|
71
63
|
try:
|
72
|
-
with open(
|
64
|
+
with open(fp, "w", encoding="utf-8") as f:
|
73
65
|
json.dump(data, f, ensure_ascii=False, indent=4)
|
74
|
-
logger.info(
|
66
|
+
logger.info("%s: JSON saved: %s", phone_number, fp)
|
75
67
|
except Exception as e:
|
76
|
-
logger.error(
|
68
|
+
logger.error("%s: error saving JSON: %s: %s", phone_number, type(e).__name__, e)
|
77
69
|
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
70
|
+
# =========================
|
71
|
+
# Client builder
|
72
|
+
# =========================
|
81
73
|
def _make_client_from_json(phone_number: str) -> Optional[Client]:
|
82
74
|
try:
|
83
|
-
|
84
|
-
if not
|
85
|
-
logger.error(f"{phone_number}: ⚠️ Account JSON not found → {data_path}")
|
75
|
+
data = get_account_data(phone_number)
|
76
|
+
if not data:
|
86
77
|
return None
|
87
|
-
|
88
|
-
with open(data_path, "r", encoding="utf-8") as f:
|
89
|
-
account_data = json.load(f)
|
90
|
-
|
91
|
-
session_base = account_data.get("session")
|
78
|
+
session_base = data.get("session")
|
92
79
|
if not session_base:
|
93
|
-
logger.error(
|
80
|
+
logger.error("%s: JSON missing 'session'", phone_number)
|
94
81
|
return None
|
95
82
|
|
96
|
-
#
|
83
|
+
# absolute name WITHOUT .session
|
97
84
|
if os.path.isabs(session_base) or os.path.dirname(session_base):
|
98
85
|
session_name = _strip_session_ext(session_base)
|
99
86
|
else:
|
100
87
|
session_name = os.path.join(ACCOUNTS_FOLDER, _strip_session_ext(session_base))
|
101
88
|
|
102
|
-
|
103
|
-
_ensure_dir(os.path.dirname(session_name) or ".")
|
89
|
+
ensure_dir(os.path.dirname(session_name) or BASE_DIR)
|
104
90
|
|
105
|
-
api_id =
|
106
|
-
api_hash =
|
91
|
+
api_id = data.get("api_id")
|
92
|
+
api_hash = data.get("api_hash")
|
107
93
|
if not api_id or not api_hash:
|
108
|
-
logger.error(
|
94
|
+
logger.error("%s: JSON missing api_id/api_hash", phone_number)
|
109
95
|
return None
|
110
96
|
|
111
|
-
|
112
|
-
workdir
|
113
|
-
_ensure_dir(workdir)
|
97
|
+
workdir = os.path.join(ACC_TEMP, phone_number)
|
98
|
+
ensure_dir(workdir)
|
114
99
|
|
115
100
|
cli = Client(
|
116
|
-
name=session_name,
|
101
|
+
name=session_name, # NO .session here
|
117
102
|
api_id=int(api_id),
|
118
103
|
api_hash=str(api_hash),
|
119
104
|
sleep_threshold=30,
|
120
105
|
workdir=workdir,
|
121
106
|
no_updates=True,
|
122
107
|
)
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
logger.debug(f"{phone_number}: Prepared Client(name={session_name})")
|
108
|
+
if data.get("2fa_password"):
|
109
|
+
setattr(cli, "_twofa_password", data["2fa_password"])
|
110
|
+
logger.debug("%s: Prepared Client(name=%s)", phone_number, session_name)
|
128
111
|
return cli
|
129
|
-
|
130
112
|
except Exception as e:
|
131
113
|
tb = traceback.format_exc(limit=3)
|
132
|
-
logger.critical(
|
114
|
+
logger.critical("%s: client build failed: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
|
133
115
|
return None
|
134
116
|
|
135
|
-
#
|
136
|
-
#
|
137
|
-
#
|
117
|
+
# =========================
|
118
|
+
# Start client (safe)
|
119
|
+
# =========================
|
138
120
|
async def get_or_start_client(phone_number: str) -> Optional[Client]:
|
139
121
|
cli = client_pool.get(phone_number)
|
140
122
|
try:
|
141
123
|
if cli is not None and getattr(cli, "is_connected", False):
|
142
|
-
logger.debug(
|
124
|
+
logger.debug("%s: already connected (%s)", phone_number, getattr(cli, "name", "?"))
|
143
125
|
return cli
|
144
126
|
|
145
127
|
cli = _make_client_from_json(phone_number)
|
146
128
|
if cli is None:
|
147
|
-
logger.error(f"{phone_number}: ❌ Could not build client (invalid JSON/session)")
|
148
129
|
return None
|
149
130
|
|
150
|
-
# Compute expected session file path for logging
|
151
131
|
session_file = f"{cli.name}.session"
|
152
|
-
|
132
|
+
parent = os.path.dirname(session_file) or BASE_DIR
|
133
|
+
ensure_dir(parent)
|
134
|
+
|
135
|
+
# sanitize permissions + sidecars
|
153
136
|
if os.path.exists(session_file):
|
137
|
+
chmod_rw(session_file)
|
138
|
+
cleanup_sqlite_sidecars(cli.name)
|
139
|
+
else:
|
154
140
|
try:
|
155
|
-
|
156
|
-
logger.debug(f"{phone_number}: Session file exists ({size} bytes)")
|
141
|
+
os.chmod(parent, 0o777)
|
157
142
|
except Exception:
|
158
|
-
|
159
|
-
|
160
|
-
|
143
|
+
pass
|
144
|
+
|
145
|
+
if os.path.exists(session_file) and not probe_sqlite(session_file):
|
146
|
+
logger.warning("%s: sqlite probe failed; retrying after cleanup", phone_number)
|
147
|
+
chmod_rw(session_file)
|
148
|
+
cleanup_sqlite_sidecars(cli.name)
|
161
149
|
|
162
|
-
# Start with careful handling
|
163
150
|
try:
|
164
151
|
await cli.start()
|
165
|
-
await asyncio.sleep(0.4)
|
166
|
-
logger.info(
|
152
|
+
await asyncio.sleep(0.4)
|
153
|
+
logger.info("%s: client started", phone_number)
|
167
154
|
except errors.SessionPasswordNeeded:
|
168
155
|
twofa = getattr(cli, "_twofa_password", None)
|
169
156
|
if twofa:
|
170
157
|
await cli.check_password(twofa)
|
171
|
-
logger.info(
|
158
|
+
logger.info("%s: 2FA applied", phone_number)
|
172
159
|
else:
|
173
|
-
logger.error(
|
160
|
+
logger.error("%s: 2FA required but missing", phone_number)
|
174
161
|
return None
|
175
162
|
except errors.AuthKeyDuplicated:
|
176
|
-
logger.error(
|
163
|
+
logger.error("%s: AuthKeyDuplicated (invalid session)", phone_number)
|
177
164
|
return None
|
178
165
|
except Exception as e:
|
179
166
|
tb = traceback.format_exc(limit=3)
|
180
|
-
logger.error(
|
167
|
+
logger.error("%s: start failed: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
|
181
168
|
return None
|
182
169
|
|
183
170
|
client_pool[phone_number] = cli
|
184
171
|
client_locks.setdefault(phone_number, asyncio.Lock())
|
185
172
|
return cli
|
186
|
-
|
187
173
|
except Exception as e:
|
188
174
|
tb = traceback.format_exc(limit=3)
|
189
|
-
logger.critical(
|
175
|
+
logger.critical("%s: fatal in get_or_start_client: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
|
190
176
|
return None
|
191
177
|
|
192
|
-
#
|
193
|
-
#
|
194
|
-
#
|
178
|
+
# =========================
|
179
|
+
# Preload
|
180
|
+
# =========================
|
195
181
|
async def preload_clients(limit: Optional[int] = None) -> None:
|
196
182
|
phones = list(get_active_accounts())
|
197
183
|
if limit is not None:
|
198
184
|
phones = phones[:max(0, int(limit))]
|
199
|
-
|
200
185
|
if not phones:
|
201
|
-
logger.info("
|
186
|
+
logger.info("no accounts to preload")
|
202
187
|
return
|
203
188
|
|
204
|
-
logger.info(
|
205
|
-
ok
|
206
|
-
|
189
|
+
logger.info("preloading %d client(s)...", len(phones))
|
190
|
+
ok = bad = 0
|
207
191
|
for idx, phone in enumerate(phones, 1):
|
208
|
-
logger.info(
|
192
|
+
logger.info("[%d/%d] preload %s", idx, len(phones), phone)
|
209
193
|
try:
|
210
194
|
cli = await get_or_start_client(phone)
|
211
195
|
if cli and getattr(cli, "is_connected", False):
|
212
196
|
ok += 1
|
213
|
-
logger.info(f"{phone}: ✅ Connected.")
|
214
197
|
else:
|
215
198
|
bad += 1
|
216
|
-
logger.warning(f"{phone}: ❌ Not connected after start().")
|
217
199
|
except Exception as e:
|
218
200
|
bad += 1
|
219
|
-
|
220
|
-
logger.error(f"{phone}: ❌ Exception during preload - {type(e).__name__}: {e}\n{tb}")
|
221
|
-
|
201
|
+
logger.error("%s: exception during preload: %s: %s", phone, type(e).__name__, e)
|
222
202
|
await asyncio.sleep(0.8 + random.uniform(0.1, 0.3))
|
203
|
+
logger.info("preload done: ok=%d fail=%d", ok, bad)
|
223
204
|
|
224
|
-
|
225
|
-
|
226
|
-
#
|
227
|
-
# 🧹 توقف تمام کلاینتها
|
228
|
-
# ============================================================
|
205
|
+
# =========================
|
206
|
+
# Stop all
|
207
|
+
# =========================
|
229
208
|
async def stop_all_clients() -> None:
|
230
|
-
logger.info("
|
209
|
+
logger.info("stopping all clients...")
|
231
210
|
for phone, cli in list(client_pool.items()):
|
232
211
|
try:
|
233
212
|
await cli.stop()
|
234
|
-
logger.info(
|
213
|
+
logger.info("%s: stopped", phone)
|
235
214
|
except Exception as e:
|
236
|
-
|
237
|
-
logger.warning(f"{phone}: ⚠️ Error stopping client - {type(e).__name__}: {e}\n{tb}")
|
215
|
+
logger.warning("%s: stop error: %s: %s", phone, type(e).__name__, e)
|
238
216
|
finally:
|
239
217
|
client_pool.pop(phone, None)
|
240
218
|
client_locks.pop(phone, None)
|
241
219
|
await asyncio.sleep(0.2)
|
242
|
-
logger.info("
|
220
|
+
logger.info("all clients stopped")
|
243
221
|
|
244
|
-
#
|
245
|
-
#
|
246
|
-
#
|
222
|
+
# =========================
|
223
|
+
# Enumerate accounts
|
224
|
+
# =========================
|
247
225
|
def accounts() -> List[str]:
|
248
|
-
accs: Set[str] = set()
|
249
226
|
if not os.path.isdir(ACCOUNTS_FOLDER):
|
250
227
|
return []
|
251
|
-
for
|
252
|
-
if acc.endswith(".session"):
|
253
|
-
accs.add(acc.split(".")[0])
|
254
|
-
return list(accs)
|
228
|
+
return [f[:-8] for f in os.listdir(ACCOUNTS_FOLDER) if f.endswith(".session")]
|
255
229
|
|
256
230
|
def get_active_accounts() -> Set[str]:
|
257
231
|
return set(accounts())
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
import os
|
3
|
+
import sqlite3
|
4
|
+
import logging
|
5
|
+
|
6
|
+
logger = logging.getLogger(__name__)
|
7
|
+
|
8
|
+
SIDE_SUFFIXES = (".session-wal", ".session-shm", ".session-journal")
|
9
|
+
|
10
|
+
def ensure_dir(path: str):
|
11
|
+
try:
|
12
|
+
os.makedirs(path, exist_ok=True)
|
13
|
+
try:
|
14
|
+
os.chmod(path, 0o777)
|
15
|
+
except Exception:
|
16
|
+
pass
|
17
|
+
except Exception as e:
|
18
|
+
logger.error("Cannot create directory %s: %s: %s", path, type(e).__name__, e)
|
19
|
+
|
20
|
+
def chmod_rw(path: str):
|
21
|
+
try:
|
22
|
+
os.chmod(path, 0o666)
|
23
|
+
except Exception:
|
24
|
+
pass
|
25
|
+
|
26
|
+
def cleanup_sqlite_sidecars(db_without_ext: str):
|
27
|
+
base = f"{db_without_ext}.session"
|
28
|
+
for suf in ("-wal", "-shm", "-journal"):
|
29
|
+
f = f"{base}{suf}"
|
30
|
+
if os.path.exists(f):
|
31
|
+
try:
|
32
|
+
os.remove(f)
|
33
|
+
logger.debug("Removed sqlite sidecar: %s", f)
|
34
|
+
except Exception as e:
|
35
|
+
logger.debug("Cannot remove sidecar %s: %s", f, e)
|
36
|
+
|
37
|
+
def probe_sqlite(db_file: str) -> bool:
|
38
|
+
try:
|
39
|
+
conn = sqlite3.connect(db_file, timeout=1)
|
40
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
41
|
+
conn.close()
|
42
|
+
return True
|
43
|
+
except Exception as e:
|
44
|
+
logger.debug("SQLite probe failed for %s: %s: %s", db_file, type(e).__name__, e)
|
45
|
+
return False
|
File without changes
|
File without changes
|
File without changes
|