tlgr-cli 2.0.1__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.
- tlgr/__init__.py +3 -0
- tlgr/__main__.py +6 -0
- tlgr/actions/__init__.py +45 -0
- tlgr/actions/forward.py +74 -0
- tlgr/actions/reply.py +32 -0
- tlgr/cli/__init__.py +259 -0
- tlgr/cli/confirm.py +55 -0
- tlgr/cli/errors.py +84 -0
- tlgr/cli/gen.py +690 -0
- tlgr/cli/globals.py +273 -0
- tlgr/cli/introspect.py +170 -0
- tlgr/cli/params.py +189 -0
- tlgr/cli/render.py +418 -0
- tlgr/core/__init__.py +0 -0
- tlgr/core/accounts.py +384 -0
- tlgr/core/config.py +358 -0
- tlgr/core/custom_tl.py +170 -0
- tlgr/core/errors.py +687 -0
- tlgr/core/eventtypes.py +1170 -0
- tlgr/core/identity.py +127 -0
- tlgr/core/launchd.py +122 -0
- tlgr/core/logging.py +194 -0
- tlgr/core/media.py +134 -0
- tlgr/core/output.py +251 -0
- tlgr/core/pagination.py +227 -0
- tlgr/core/paths.py +360 -0
- tlgr/core/peers.py +427 -0
- tlgr/core/process.py +138 -0
- tlgr/core/signing.py +38 -0
- tlgr/core/systemd.py +96 -0
- tlgr/core/telethon_compat.py +295 -0
- tlgr/core/text.py +211 -0
- tlgr/core/timefmt.py +199 -0
- tlgr/core/tl.py +98 -0
- tlgr/daemon/__init__.py +0 -0
- tlgr/daemon/app.py +869 -0
- tlgr/daemon/dispatch.py +446 -0
- tlgr/daemon/events.py +723 -0
- tlgr/daemon/files.py +431 -0
- tlgr/daemon/idle.py +119 -0
- tlgr/daemon/jobs.py +68 -0
- tlgr/daemon/main.py +161 -0
- tlgr/daemon/peercred.py +75 -0
- tlgr/daemon/policy.py +113 -0
- tlgr/daemon/preauth.py +366 -0
- tlgr/daemon/ratelimit.py +391 -0
- tlgr/daemon/server.py +24 -0
- tlgr/daemon/session.py +648 -0
- tlgr/daemon/sessions.py +274 -0
- tlgr/daemon/singleton.py +114 -0
- tlgr/daemon/stream.py +193 -0
- tlgr/daemon/transfers.py +219 -0
- tlgr/daemon/webhook.py +390 -0
- tlgr/data/catalog_index.json +1 -0
- tlgr/data/parity_waivers.toml +90 -0
- tlgr/filters/__init__.py +42 -0
- tlgr/filters/compose.py +121 -0
- tlgr/filters/content.py +85 -0
- tlgr/filters/context.py +114 -0
- tlgr/filters/message.py +161 -0
- tlgr/filters/temporal.py +87 -0
- tlgr/filters/user.py +36 -0
- tlgr/gateway/__init__.py +1 -0
- tlgr/gateway/config.py +161 -0
- tlgr/gateway/engine.py +215 -0
- tlgr/gateway/event.py +22 -0
- tlgr/jobs/__init__.py +0 -0
- tlgr/jobs/base.py +81 -0
- tlgr/jobs/client.py +37 -0
- tlgr/models/__init__.py +1220 -0
- tlgr/models/admin.py +744 -0
- tlgr/models/auth.py +510 -0
- tlgr/models/base.py +81 -0
- tlgr/models/bot.py +576 -0
- tlgr/models/business.py +265 -0
- tlgr/models/call.py +586 -0
- tlgr/models/config.py +101 -0
- tlgr/models/contact.py +481 -0
- tlgr/models/daemon.py +336 -0
- tlgr/models/dialog.py +626 -0
- tlgr/models/envelope.py +68 -0
- tlgr/models/error.py +30 -0
- tlgr/models/event.py +79 -0
- tlgr/models/export.py +66 -0
- tlgr/models/gift.py +275 -0
- tlgr/models/inline.py +84 -0
- tlgr/models/location.py +115 -0
- tlgr/models/media.py +507 -0
- tlgr/models/message.py +584 -0
- tlgr/models/net.py +232 -0
- tlgr/models/notify.py +105 -0
- tlgr/models/page.py +32 -0
- tlgr/models/payment.py +172 -0
- tlgr/models/peer.py +400 -0
- tlgr/models/poll.py +119 -0
- tlgr/models/premium.py +161 -0
- tlgr/models/privacy.py +93 -0
- tlgr/models/profile.py +217 -0
- tlgr/models/reaction.py +160 -0
- tlgr/models/resolve.py +175 -0
- tlgr/models/settings.py +103 -0
- tlgr/models/stars.py +101 -0
- tlgr/models/sticker.py +243 -0
- tlgr/models/story.py +467 -0
- tlgr/models/sync.py +105 -0
- tlgr/models/todo.py +36 -0
- tlgr/models/webapp.py +89 -0
- tlgr/ops/__init__.py +63 -0
- tlgr/ops/_admin.py +313 -0
- tlgr/ops/_auth.py +599 -0
- tlgr/ops/_bots.py +586 -0
- tlgr/ops/_calls.py +535 -0
- tlgr/ops/_common.py +160 -0
- tlgr/ops/_layer.py +46 -0
- tlgr/ops/_media.py +592 -0
- tlgr/ops/_params.py +212 -0
- tlgr/ops/_rights.py +402 -0
- tlgr/ops/_send.py +593 -0
- tlgr/ops/_serialize.py +667 -0
- tlgr/ops/_settings.py +306 -0
- tlgr/ops/_spec.py +167 -0
- tlgr/ops/_story.py +743 -0
- tlgr/ops/account.py +2604 -0
- tlgr/ops/agent.py +937 -0
- tlgr/ops/auth.py +1282 -0
- tlgr/ops/bot.py +4880 -0
- tlgr/ops/business.py +1520 -0
- tlgr/ops/call.py +1610 -0
- tlgr/ops/chat.py +4025 -0
- tlgr/ops/chat_admin.py +929 -0
- tlgr/ops/chat_extra.py +1061 -0
- tlgr/ops/chat_invite.py +716 -0
- tlgr/ops/chat_manage.py +1691 -0
- tlgr/ops/chat_member.py +1357 -0
- tlgr/ops/chat_stats.py +902 -0
- tlgr/ops/chat_topic.py +905 -0
- tlgr/ops/conference.py +791 -0
- tlgr/ops/config.py +1698 -0
- tlgr/ops/contact.py +2330 -0
- tlgr/ops/daemon.py +1397 -0
- tlgr/ops/draft.py +299 -0
- tlgr/ops/emoji.py +343 -0
- tlgr/ops/events.py +1327 -0
- tlgr/ops/export.py +596 -0
- tlgr/ops/folder.py +1322 -0
- tlgr/ops/gif.py +522 -0
- tlgr/ops/gift.py +1546 -0
- tlgr/ops/giveaway.py +541 -0
- tlgr/ops/inline.py +773 -0
- tlgr/ops/job.py +799 -0
- tlgr/ops/location.py +917 -0
- tlgr/ops/media.py +4495 -0
- tlgr/ops/message.py +3769 -0
- tlgr/ops/net.py +536 -0
- tlgr/ops/notify.py +840 -0
- tlgr/ops/passport.py +464 -0
- tlgr/ops/payment.py +907 -0
- tlgr/ops/poll.py +1078 -0
- tlgr/ops/premium.py +488 -0
- tlgr/ops/privacy.py +794 -0
- tlgr/ops/profile.py +1481 -0
- tlgr/ops/proxy.py +750 -0
- tlgr/ops/reaction.py +1475 -0
- tlgr/ops/resolve.py +1140 -0
- tlgr/ops/search.py +521 -0
- tlgr/ops/settings.py +1066 -0
- tlgr/ops/stars.py +594 -0
- tlgr/ops/sticker.py +1602 -0
- tlgr/ops/story.py +3216 -0
- tlgr/ops/sync.py +788 -0
- tlgr/ops/todo.py +514 -0
- tlgr/ops/user.py +1406 -0
- tlgr/ops/vc.py +2351 -0
- tlgr/ops/webapp.py +717 -0
- tlgr/ops/webhook.py +418 -0
- tlgr/parity.py +386 -0
- tlgr/processors/__init__.py +125 -0
- tlgr/processors/regex.py +26 -0
- tlgr/processors/text.py +56 -0
- tlgr/registry.py +519 -0
- tlgr/schema.py +173 -0
- tlgr/transport/__init__.py +30 -0
- tlgr/transport/autostart.py +293 -0
- tlgr/transport/client.py +805 -0
- tlgr/transport/ndjson.py +44 -0
- tlgr/version.py +31 -0
- tlgr_cli-2.0.1.dist-info/METADATA +957 -0
- tlgr_cli-2.0.1.dist-info/RECORD +192 -0
- tlgr_cli-2.0.1.dist-info/WHEEL +5 -0
- tlgr_cli-2.0.1.dist-info/entry_points.txt +2 -0
- tlgr_cli-2.0.1.dist-info/licenses/LICENSE +21 -0
- tlgr_cli-2.0.1.dist-info/top_level.txt +1 -0
tlgr/core/accounts.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""The alias registry: which accounts exist, which is active, and how each is.
|
|
2
|
+
|
|
3
|
+
Three things changed from v1 and each of them was a bug:
|
|
4
|
+
|
|
5
|
+
* **an alias is validated before it becomes a path** (SEC-02). v1 accepted
|
|
6
|
+
anything `str.isalnum()` liked after stripping `_` and `-`, which passes for
|
|
7
|
+
the empty string and for a 4 KB name, and rejected nothing that mattered.
|
|
8
|
+
* **a read never creates.** `get_account_dir()` called `mkdir(parents=True)`,
|
|
9
|
+
so `tlgr account list` materialised a directory for an alias that did not
|
|
10
|
+
exist, and a typo left litter behind.
|
|
11
|
+
* **health is persisted here.** The daemon writes each account's state
|
|
12
|
+
(`online`/`degraded`/`needs_login`/`frozen`) into this file, so
|
|
13
|
+
`tlgr account list` tells the truth about an account even when the daemon is
|
|
14
|
+
not running — which is exactly when you ask.
|
|
15
|
+
|
|
16
|
+
Order is preserved everywhere: `list_accounts()` and `connect_order()` return
|
|
17
|
+
lists, never a `set`. v1 connected "whichever alias came first out of a set",
|
|
18
|
+
which is COR-02 — a two-account user could send from the wrong identity.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
from dataclasses import asdict, dataclass, field
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from tlgr.core.errors import AccountNotFoundError, TlgrError
|
|
32
|
+
from tlgr.core.paths import TlgrPaths, validate_alias, write_private
|
|
33
|
+
|
|
34
|
+
ACCOUNTS_FILE = "accounts.json"
|
|
35
|
+
|
|
36
|
+
#: The states an `AccountSession` can persist (§6.2). `unknown` is what an
|
|
37
|
+
#: account reads as before a daemon has ever held it.
|
|
38
|
+
ACCOUNT_STATES = (
|
|
39
|
+
"unknown",
|
|
40
|
+
"starting",
|
|
41
|
+
"online",
|
|
42
|
+
"degraded",
|
|
43
|
+
"needs_login",
|
|
44
|
+
"frozen",
|
|
45
|
+
"stopped",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _now() -> str:
|
|
50
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class AccountHealth:
|
|
55
|
+
"""What the daemon last knew about an account."""
|
|
56
|
+
|
|
57
|
+
state: str = "unknown"
|
|
58
|
+
reason: str = ""
|
|
59
|
+
since: str = ""
|
|
60
|
+
user_id: int | None = None
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
return asdict(self)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class AccountInfo:
|
|
68
|
+
alias: str
|
|
69
|
+
phone: str | None = None
|
|
70
|
+
username: str | None = None
|
|
71
|
+
first_name: str | None = None
|
|
72
|
+
user_id: int | None = None
|
|
73
|
+
created_at: str | None = None
|
|
74
|
+
health: AccountHealth = field(default_factory=AccountHealth)
|
|
75
|
+
|
|
76
|
+
def to_dict(self) -> dict[str, Any]:
|
|
77
|
+
data = asdict(self)
|
|
78
|
+
return data
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_dict(cls, data: dict[str, Any]) -> AccountInfo:
|
|
82
|
+
known = {k: v for k, v in data.items() if k in cls.__dataclass_fields__}
|
|
83
|
+
raw_health = known.pop("health", None)
|
|
84
|
+
health = AccountHealth()
|
|
85
|
+
if isinstance(raw_health, dict):
|
|
86
|
+
health = AccountHealth(
|
|
87
|
+
**{k: v for k, v in raw_health.items() if k in AccountHealth.__dataclass_fields__}
|
|
88
|
+
)
|
|
89
|
+
return cls(**known, health=health)
|
|
90
|
+
|
|
91
|
+
def display_name(self) -> str:
|
|
92
|
+
if self.username:
|
|
93
|
+
return f"@{self.username}"
|
|
94
|
+
if self.first_name:
|
|
95
|
+
return self.first_name
|
|
96
|
+
if self.phone:
|
|
97
|
+
return f"{self.phone[:4]}...{self.phone[-2:]}"
|
|
98
|
+
return self.alias
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AccountManager:
|
|
102
|
+
def __init__(self, base_dir: Path | None = None):
|
|
103
|
+
self.paths = TlgrPaths(base_dir)
|
|
104
|
+
self.base_dir = self.paths.base
|
|
105
|
+
self.accounts_file = self.paths.accounts_file
|
|
106
|
+
self._data: dict[str, Any] | None = None
|
|
107
|
+
|
|
108
|
+
# -- persistence -------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def accounts_dir(self) -> Path:
|
|
112
|
+
"""The accounts directory. Reading this never creates it."""
|
|
113
|
+
return self.paths.accounts
|
|
114
|
+
|
|
115
|
+
def _load(self) -> dict[str, Any]:
|
|
116
|
+
if self._data is not None:
|
|
117
|
+
return self._data
|
|
118
|
+
if not self.accounts_file.exists():
|
|
119
|
+
self._data = {"active": None, "accounts": {}}
|
|
120
|
+
return self._data
|
|
121
|
+
try:
|
|
122
|
+
with open(self.accounts_file) as handle:
|
|
123
|
+
loaded = json.load(handle)
|
|
124
|
+
if not isinstance(loaded, dict):
|
|
125
|
+
raise ValueError("accounts.json is not an object")
|
|
126
|
+
loaded.setdefault("accounts", {})
|
|
127
|
+
loaded.setdefault("active", None)
|
|
128
|
+
self._data = loaded
|
|
129
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
130
|
+
self._data = {"active": None, "accounts": {}}
|
|
131
|
+
return self._data
|
|
132
|
+
|
|
133
|
+
def reload(self) -> None:
|
|
134
|
+
"""Drop the cached view so the next read sees another process's write."""
|
|
135
|
+
self._data = None
|
|
136
|
+
|
|
137
|
+
def _save(self) -> None:
|
|
138
|
+
if self._data is None:
|
|
139
|
+
return
|
|
140
|
+
write_private(self.accounts_file, json.dumps(self._data, indent=2, ensure_ascii=False))
|
|
141
|
+
|
|
142
|
+
# -- active alias ------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def get_active(self) -> str | None:
|
|
145
|
+
data = self._load()
|
|
146
|
+
active = data.get("active")
|
|
147
|
+
if active and active in data.get("accounts", {}):
|
|
148
|
+
return str(active)
|
|
149
|
+
accounts = data.get("accounts", {})
|
|
150
|
+
if accounts:
|
|
151
|
+
first = next(iter(accounts))
|
|
152
|
+
self.set_active(first)
|
|
153
|
+
return str(first)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def set_active(self, alias: str) -> bool:
|
|
157
|
+
data = self._load()
|
|
158
|
+
if alias not in data.get("accounts", {}):
|
|
159
|
+
return False
|
|
160
|
+
data["active"] = alias
|
|
161
|
+
self._save()
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
# -- queries -----------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
def list_accounts(self) -> list[AccountInfo]:
|
|
167
|
+
data = self._load()
|
|
168
|
+
return [AccountInfo.from_dict(v) for v in data.get("accounts", {}).values()]
|
|
169
|
+
|
|
170
|
+
def aliases(self) -> list[str]:
|
|
171
|
+
"""Registered aliases, in insertion order."""
|
|
172
|
+
return list(self._load().get("accounts", {}))
|
|
173
|
+
|
|
174
|
+
def get_account(self, alias: str) -> AccountInfo | None:
|
|
175
|
+
data = self._load()
|
|
176
|
+
info = data.get("accounts", {}).get(alias)
|
|
177
|
+
return AccountInfo.from_dict(info) if info else None
|
|
178
|
+
|
|
179
|
+
def require_account(self, alias: str) -> AccountInfo:
|
|
180
|
+
validate_alias(alias)
|
|
181
|
+
info = self.get_account(alias)
|
|
182
|
+
if info is None:
|
|
183
|
+
raise AccountNotFoundError(f"no account named {alias!r}")
|
|
184
|
+
return info
|
|
185
|
+
|
|
186
|
+
def has_accounts(self) -> bool:
|
|
187
|
+
return bool(self._load().get("accounts"))
|
|
188
|
+
|
|
189
|
+
def connect_order(self, config_default: str = "", extra: list[str] | None = None) -> list[str]:
|
|
190
|
+
"""The ordered list of aliases the daemon connects at start (§6.1).
|
|
191
|
+
|
|
192
|
+
A *list*: the union used in v1 was a `set`, so the daemon's connect
|
|
193
|
+
order — and therefore which account answered an under-specified
|
|
194
|
+
request — depended on hash randomisation.
|
|
195
|
+
"""
|
|
196
|
+
ordered: list[str] = []
|
|
197
|
+
known = set(self.aliases())
|
|
198
|
+
|
|
199
|
+
def push(alias: str | None) -> None:
|
|
200
|
+
if alias and alias in known and alias not in ordered:
|
|
201
|
+
ordered.append(alias)
|
|
202
|
+
|
|
203
|
+
for alias in extra or []:
|
|
204
|
+
push(alias)
|
|
205
|
+
push(config_default)
|
|
206
|
+
push(self._load().get("active"))
|
|
207
|
+
return ordered
|
|
208
|
+
|
|
209
|
+
# -- mutation ----------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
def add_account(self, alias: str) -> AccountInfo:
|
|
212
|
+
validate_alias(alias)
|
|
213
|
+
data = self._load()
|
|
214
|
+
if alias in data.get("accounts", {}):
|
|
215
|
+
raise TlgrError(f"Account '{alias}' already exists")
|
|
216
|
+
self.paths.ensure_account_dir(alias)
|
|
217
|
+
account = AccountInfo(alias=alias, created_at=datetime.now().isoformat())
|
|
218
|
+
data.setdefault("accounts", {})[alias] = account.to_dict()
|
|
219
|
+
if not data.get("active"):
|
|
220
|
+
data["active"] = alias
|
|
221
|
+
self._save()
|
|
222
|
+
return account
|
|
223
|
+
|
|
224
|
+
def update_account(
|
|
225
|
+
self,
|
|
226
|
+
alias: str,
|
|
227
|
+
*,
|
|
228
|
+
phone: str | None = None,
|
|
229
|
+
username: str | None = None,
|
|
230
|
+
first_name: str | None = None,
|
|
231
|
+
user_id: int | None = None,
|
|
232
|
+
) -> AccountInfo | None:
|
|
233
|
+
data = self._load()
|
|
234
|
+
if alias not in data.get("accounts", {}):
|
|
235
|
+
return None
|
|
236
|
+
stored = data["accounts"][alias]
|
|
237
|
+
if phone is not None:
|
|
238
|
+
stored["phone"] = phone
|
|
239
|
+
if username is not None:
|
|
240
|
+
stored["username"] = username
|
|
241
|
+
if first_name is not None:
|
|
242
|
+
stored["first_name"] = first_name
|
|
243
|
+
if user_id is not None:
|
|
244
|
+
stored["user_id"] = user_id
|
|
245
|
+
self._save()
|
|
246
|
+
return AccountInfo.from_dict(stored)
|
|
247
|
+
|
|
248
|
+
def set_health(
|
|
249
|
+
self,
|
|
250
|
+
alias: str,
|
|
251
|
+
state: str,
|
|
252
|
+
*,
|
|
253
|
+
reason: str = "",
|
|
254
|
+
user_id: int | None = None,
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Record what the daemon knows about *alias* so the CLI can read it.
|
|
257
|
+
|
|
258
|
+
Written even when the alias is unknown to the registry (an imported
|
|
259
|
+
session that was never `account add`-ed still has a health story), so
|
|
260
|
+
this never raises on the daemon's hot path.
|
|
261
|
+
"""
|
|
262
|
+
validate_alias(alias)
|
|
263
|
+
self.reload()
|
|
264
|
+
data = self._load()
|
|
265
|
+
accounts = data.setdefault("accounts", {})
|
|
266
|
+
stored = accounts.setdefault(alias, AccountInfo(alias=alias).to_dict())
|
|
267
|
+
previous = stored.get("health") or {}
|
|
268
|
+
if previous.get("state") == state and previous.get("reason", "") == reason:
|
|
269
|
+
since = previous.get("since") or _now()
|
|
270
|
+
else:
|
|
271
|
+
since = _now()
|
|
272
|
+
stored["health"] = {
|
|
273
|
+
"state": state,
|
|
274
|
+
"reason": reason,
|
|
275
|
+
"since": since,
|
|
276
|
+
"user_id": user_id if user_id is not None else previous.get("user_id"),
|
|
277
|
+
}
|
|
278
|
+
if user_id is not None:
|
|
279
|
+
stored["user_id"] = user_id
|
|
280
|
+
self._save()
|
|
281
|
+
|
|
282
|
+
def get_health(self, alias: str) -> AccountHealth:
|
|
283
|
+
info = self.get_account(alias)
|
|
284
|
+
return info.health if info else AccountHealth()
|
|
285
|
+
|
|
286
|
+
def remove_account(self, alias: str, delete_data: bool = True) -> bool:
|
|
287
|
+
validate_alias(alias)
|
|
288
|
+
data = self._load()
|
|
289
|
+
if alias not in data.get("accounts", {}):
|
|
290
|
+
return False
|
|
291
|
+
del data["accounts"][alias]
|
|
292
|
+
if data.get("active") == alias:
|
|
293
|
+
remaining = list(data["accounts"])
|
|
294
|
+
data["active"] = remaining[0] if remaining else None
|
|
295
|
+
self._save()
|
|
296
|
+
if delete_data:
|
|
297
|
+
account_dir = self.paths.account_dir(alias)
|
|
298
|
+
if account_dir.exists():
|
|
299
|
+
shutil.rmtree(account_dir)
|
|
300
|
+
return True
|
|
301
|
+
|
|
302
|
+
def rename_account(self, old_alias: str, new_alias: str) -> bool:
|
|
303
|
+
validate_alias(old_alias)
|
|
304
|
+
validate_alias(new_alias)
|
|
305
|
+
data = self._load()
|
|
306
|
+
if old_alias not in data.get("accounts", {}):
|
|
307
|
+
return False
|
|
308
|
+
if new_alias in data.get("accounts", {}):
|
|
309
|
+
raise TlgrError(f"Account '{new_alias}' already exists")
|
|
310
|
+
stored = data["accounts"].pop(old_alias)
|
|
311
|
+
stored["alias"] = new_alias
|
|
312
|
+
data["accounts"][new_alias] = stored
|
|
313
|
+
if data.get("active") == old_alias:
|
|
314
|
+
data["active"] = new_alias
|
|
315
|
+
self._save()
|
|
316
|
+
old_dir = self.paths.account_dir(old_alias)
|
|
317
|
+
new_dir = self.paths.account_dir(new_alias)
|
|
318
|
+
if old_dir.exists():
|
|
319
|
+
old_dir.rename(new_dir)
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
# -- paths -------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
def _resolve_alias(self, alias: str | None) -> str:
|
|
325
|
+
if alias is None:
|
|
326
|
+
alias = self.get_active()
|
|
327
|
+
if not alias:
|
|
328
|
+
raise TlgrError("No account specified and no active account")
|
|
329
|
+
return validate_alias(alias)
|
|
330
|
+
|
|
331
|
+
def get_account_dir(self, alias: str | None = None, *, create: bool = False) -> Path:
|
|
332
|
+
"""The account's directory. Does **not** create it unless asked."""
|
|
333
|
+
resolved = self._resolve_alias(alias)
|
|
334
|
+
if create:
|
|
335
|
+
return self.paths.ensure_account_dir(resolved)
|
|
336
|
+
return self.paths.account_dir(resolved)
|
|
337
|
+
|
|
338
|
+
def ensure_account_dir(self, alias: str | None = None) -> Path:
|
|
339
|
+
return self.get_account_dir(alias, create=True)
|
|
340
|
+
|
|
341
|
+
def get_session_path(self, alias: str | None = None) -> Path:
|
|
342
|
+
return self.paths.session(self._resolve_alias(alias))
|
|
343
|
+
|
|
344
|
+
def get_session_lock_path(self, alias: str | None = None) -> Path:
|
|
345
|
+
return self.paths.session_lock(self._resolve_alias(alias))
|
|
346
|
+
|
|
347
|
+
def get_credentials_path(self, alias: str | None = None) -> Path:
|
|
348
|
+
return self.paths.credentials(self._resolve_alias(alias))
|
|
349
|
+
|
|
350
|
+
# -- credentials -------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
def load_credentials(self, alias: str | None = None) -> tuple[int | None, str | None]:
|
|
353
|
+
cred_path = self.get_credentials_path(alias)
|
|
354
|
+
api_id: int | None = None
|
|
355
|
+
api_hash: str | None = None
|
|
356
|
+
|
|
357
|
+
if cred_path.exists():
|
|
358
|
+
try:
|
|
359
|
+
with open(cred_path) as handle:
|
|
360
|
+
data = json.load(handle)
|
|
361
|
+
api_id = data.get("api_id")
|
|
362
|
+
api_hash = data.get("api_hash")
|
|
363
|
+
except (OSError, json.JSONDecodeError):
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
env_id = os.environ.get("TELEGRAM_API_ID")
|
|
367
|
+
env_hash = os.environ.get("TELEGRAM_API_HASH")
|
|
368
|
+
if env_id:
|
|
369
|
+
try:
|
|
370
|
+
api_id = int(env_id)
|
|
371
|
+
except ValueError:
|
|
372
|
+
pass
|
|
373
|
+
if env_hash:
|
|
374
|
+
api_hash = env_hash
|
|
375
|
+
|
|
376
|
+
return api_id, api_hash
|
|
377
|
+
|
|
378
|
+
def save_credentials(self, api_id: int, api_hash: str, alias: str | None = None) -> None:
|
|
379
|
+
resolved = self._resolve_alias(alias)
|
|
380
|
+
self.paths.ensure_account_dir(resolved)
|
|
381
|
+
write_private(
|
|
382
|
+
self.paths.credentials(resolved),
|
|
383
|
+
json.dumps({"api_id": api_id, "api_hash": api_hash}, indent=2),
|
|
384
|
+
)
|