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/ops/stars.py
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
"""The `stars` group: the balance, the ledger, subscriptions and revenue.
|
|
2
|
+
|
|
3
|
+
Everything here reads. Acquiring Stars, moving them and withdrawing them are
|
|
4
|
+
financial transfers, and tlgr performs none of them — `stars url get` prints
|
|
5
|
+
the Fragment URL and leaves the transfer to a human in a browser, which is
|
|
6
|
+
what "control-only" means in the catalog and what the whole group is shaped
|
|
7
|
+
around.
|
|
8
|
+
|
|
9
|
+
Two details are load-bearing.
|
|
10
|
+
|
|
11
|
+
* **A Stars amount is `(amount, nanos)`.** TON arrives in the same shape with
|
|
12
|
+
nine decimals, and both halves are reported. A ledger that rounds is a
|
|
13
|
+
ledger that cannot be reconciled.
|
|
14
|
+
* **The transactions cursor is an opaque string**, not an integer offset.
|
|
15
|
+
`next_offset` comes back from the server and goes back to it unchanged;
|
|
16
|
+
tlgr signs it into a normal `--cursor` token so it cannot be spliced onto
|
|
17
|
+
another account or another op.
|
|
18
|
+
|
|
19
|
+
Telethon is imported inside functions, never at module scope (§2.2).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Annotated, Any
|
|
25
|
+
|
|
26
|
+
from tlgr.core.errors import UsageError
|
|
27
|
+
from tlgr.core.pagination import PageKind, build_page
|
|
28
|
+
from tlgr.core.timefmt import fmt_dt, to_unix
|
|
29
|
+
from tlgr.models.base import Request
|
|
30
|
+
from tlgr.models.page import Page
|
|
31
|
+
from tlgr.models.payment import StarSubscription
|
|
32
|
+
from tlgr.models.peer import PeerRef
|
|
33
|
+
from tlgr.models.stars import (
|
|
34
|
+
StarsBalance,
|
|
35
|
+
StarsRating,
|
|
36
|
+
StarsRefulfill,
|
|
37
|
+
StarsRevenue,
|
|
38
|
+
StarsTransaction,
|
|
39
|
+
StarsUrl,
|
|
40
|
+
)
|
|
41
|
+
from tlgr.ops import _settings
|
|
42
|
+
from tlgr.ops._common import client, window
|
|
43
|
+
from tlgr.ops._params import arg, opt
|
|
44
|
+
from tlgr.ops._spec import OpContext, OperationSpec
|
|
45
|
+
|
|
46
|
+
__all__ = [name for name in dir() if name.startswith("SPEC_")]
|
|
47
|
+
|
|
48
|
+
_PASSWORD = opt(
|
|
49
|
+
secret=True, envvar="TLGR_2FA_PASSWORD", help="The 2FA cloud password (never in argv)."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
#: `starsTransactionPeer*` → the one word that says who the other side was.
|
|
53
|
+
PEER_KINDS = {
|
|
54
|
+
"StarsTransactionPeer": "peer",
|
|
55
|
+
"StarsTransactionPeerAppStore": "app-store",
|
|
56
|
+
"StarsTransactionPeerPlayMarket": "play-market",
|
|
57
|
+
"StarsTransactionPeerPremiumBot": "premium-bot",
|
|
58
|
+
"StarsTransactionPeerFragment": "fragment",
|
|
59
|
+
"StarsTransactionPeerAds": "ads",
|
|
60
|
+
"StarsTransactionPeerAPI": "api",
|
|
61
|
+
"StarsTransactionPeerUnsupported": "unsupported",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#: The boolean flags a transaction carries, in the order a reader wants them.
|
|
65
|
+
KINDS = (
|
|
66
|
+
"gift",
|
|
67
|
+
"reaction",
|
|
68
|
+
"stargift_upgrade",
|
|
69
|
+
"stargift_resale",
|
|
70
|
+
"stargift_auction_bid",
|
|
71
|
+
"business_transfer",
|
|
72
|
+
"posts_search",
|
|
73
|
+
"offer",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# stars balance get
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class BalanceGetReq(Request):
|
|
83
|
+
ton: Annotated[bool, opt("--ton", help="The TON balance instead (amounts are nanotons).")] = (
|
|
84
|
+
False
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def balance_get(ctx: OpContext, req: BalanceGetReq) -> StarsBalance:
|
|
89
|
+
"""My Telegram Stars balance, or my TON balance.
|
|
90
|
+
|
|
91
|
+
Read-only on purpose: topping up and withdrawing happen on Fragment or in
|
|
92
|
+
an official app, and a CLI that could do either would be a CLI that could
|
|
93
|
+
lose money by accident.
|
|
94
|
+
"""
|
|
95
|
+
from telethon.tl import types
|
|
96
|
+
from telethon.tl.functions import payments as fn
|
|
97
|
+
|
|
98
|
+
result = await client(ctx)(
|
|
99
|
+
fn.GetStarsStatusRequest(peer=types.InputPeerSelf(), ton=req.ton or None)
|
|
100
|
+
)
|
|
101
|
+
amount, nanos = _settings.stars_of(getattr(result, "balance", None))
|
|
102
|
+
missing = [
|
|
103
|
+
row
|
|
104
|
+
for row in getattr(result, "subscriptions", None) or []
|
|
105
|
+
if getattr(row, "missing_balance", False)
|
|
106
|
+
]
|
|
107
|
+
return StarsBalance(
|
|
108
|
+
stars=0 if req.ton else amount,
|
|
109
|
+
nanos=nanos,
|
|
110
|
+
ton=amount if req.ton else None,
|
|
111
|
+
currency="TON" if req.ton else "XTR",
|
|
112
|
+
subscriptions_missing_balance=len(missing) or None,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
SPEC_BALANCE_GET = OperationSpec(
|
|
117
|
+
id="stars.balance.get",
|
|
118
|
+
request=BalanceGetReq,
|
|
119
|
+
response=StarsBalance,
|
|
120
|
+
impl=balance_get,
|
|
121
|
+
summary="My Telegram Stars balance (and the TON balance with --ton)",
|
|
122
|
+
description=(
|
|
123
|
+
"`nanos` is the fractional part the wire carries; TON amounts are "
|
|
124
|
+
"nanotons. Neither is rounded, because a rounded ledger cannot be "
|
|
125
|
+
"reconciled."
|
|
126
|
+
),
|
|
127
|
+
idempotent=True,
|
|
128
|
+
columns=("stars", "nanos", "ton", "subscriptions_missing_balance"),
|
|
129
|
+
headers=("Stars", "Nanos", "TON", "Lapsing subs"),
|
|
130
|
+
example={"stars": 250, "nanos": 0, "currency": "XTR"},
|
|
131
|
+
example_args="stars balance get",
|
|
132
|
+
covers=(
|
|
133
|
+
"bots.bot-stars-balance",
|
|
134
|
+
"bots.stars-topup-deeplink",
|
|
135
|
+
"bots.stars-topup-options",
|
|
136
|
+
"stars.balance",
|
|
137
|
+
"stars.topup-options",
|
|
138
|
+
),
|
|
139
|
+
covers_partial=("stars.ton-balance",),
|
|
140
|
+
coverage_note="The TON ledger itself is `stars transaction list --ton`.",
|
|
141
|
+
tags=frozenset({"agent-safe"}),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# stars transaction list
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TransactionListReq(Request):
|
|
151
|
+
inbound: Annotated[bool, opt("--in", help="Incoming only.")] = False
|
|
152
|
+
outbound: Annotated[bool, opt("--out", help="Outgoing only.")] = False
|
|
153
|
+
ton: Annotated[bool, opt("--ton", help="The TON ledger instead of the Stars one.")] = False
|
|
154
|
+
peer: Annotated[
|
|
155
|
+
PeerRef | None,
|
|
156
|
+
opt("--peer", metavar="CHAT", kind="peer", help="Transactions with one bot or channel."),
|
|
157
|
+
] = None
|
|
158
|
+
subscription: Annotated[
|
|
159
|
+
str | None, opt("--subscription", metavar="ID", help="Only one subscription's charges.")
|
|
160
|
+
] = None
|
|
161
|
+
ascending: Annotated[bool, opt("--ascending", help="Oldest first.")] = False
|
|
162
|
+
id: Annotated[
|
|
163
|
+
str | None, opt("--id", metavar="LIST", help="Fetch specific transactions by id.")
|
|
164
|
+
] = None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def transaction_list(ctx: OpContext, req: TransactionListReq) -> Page[StarsTransaction]:
|
|
168
|
+
"""Star (or TON) transaction history.
|
|
169
|
+
|
|
170
|
+
The cursor here is the server's opaque `next_offset` string rather than a
|
|
171
|
+
number: passing an integer where the API wants a token silently restarts
|
|
172
|
+
the walk from the beginning, which is how a ledger export ends up with
|
|
173
|
+
the first page repeated.
|
|
174
|
+
"""
|
|
175
|
+
from telethon.tl import types
|
|
176
|
+
from telethon.tl.functions import payments as fn
|
|
177
|
+
|
|
178
|
+
handle = client(ctx)
|
|
179
|
+
limit, state = window(ctx, "stars.transaction.list", PageKind.RATE, default=50)
|
|
180
|
+
peer = await _settings.resolve(ctx, req.peer) if req.peer is not None else types.InputPeerSelf()
|
|
181
|
+
|
|
182
|
+
if req.id:
|
|
183
|
+
wanted = [
|
|
184
|
+
types.InputStarsTransaction(id=part.strip())
|
|
185
|
+
for part in req.id.split(",")
|
|
186
|
+
if part.strip()
|
|
187
|
+
]
|
|
188
|
+
result = await handle(
|
|
189
|
+
fn.GetStarsTransactionsByIDRequest(peer=peer, id=wanted, ton=req.ton or None)
|
|
190
|
+
)
|
|
191
|
+
rows = [_transaction(row, result) for row in getattr(result, "history", None) or []]
|
|
192
|
+
return Page(items=rows, has_more=False, total=len(rows))
|
|
193
|
+
|
|
194
|
+
result = await handle(
|
|
195
|
+
fn.GetStarsTransactionsRequest(
|
|
196
|
+
peer=peer,
|
|
197
|
+
offset=str(state.get("offset", "") or ""),
|
|
198
|
+
limit=limit,
|
|
199
|
+
inbound=req.inbound or None,
|
|
200
|
+
outbound=req.outbound or None,
|
|
201
|
+
ascending=req.ascending or None,
|
|
202
|
+
ton=req.ton or None,
|
|
203
|
+
subscription_id=req.subscription,
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
rows = [_transaction(row, result) for row in getattr(result, "history", None) or []]
|
|
207
|
+
next_offset = str(getattr(result, "next_offset", "") or "")
|
|
208
|
+
return build_page(
|
|
209
|
+
rows,
|
|
210
|
+
op="stars.transaction.list",
|
|
211
|
+
kind=PageKind.RATE,
|
|
212
|
+
state={"offset": next_offset},
|
|
213
|
+
account=ctx.account,
|
|
214
|
+
has_more=bool(next_offset),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _transaction(raw: Any, envelope: Any) -> StarsTransaction:
|
|
219
|
+
from tlgr.ops._serialize import peer_id_of
|
|
220
|
+
|
|
221
|
+
amount, nanos = _settings.stars_of(getattr(raw, "amount", None))
|
|
222
|
+
holder = getattr(raw, "peer", None)
|
|
223
|
+
inner = getattr(holder, "peer", None)
|
|
224
|
+
known = _settings.entity_map(envelope)
|
|
225
|
+
peer_id = peer_id_of(inner) if inner is not None else None
|
|
226
|
+
date = getattr(raw, "date", None)
|
|
227
|
+
return StarsTransaction(
|
|
228
|
+
id=str(getattr(raw, "id", "") or ""),
|
|
229
|
+
date=fmt_dt(date),
|
|
230
|
+
date_unix=to_unix(date),
|
|
231
|
+
stars=amount,
|
|
232
|
+
nanos=nanos,
|
|
233
|
+
refund=bool(getattr(raw, "refund", False)),
|
|
234
|
+
pending=bool(getattr(raw, "pending", False)),
|
|
235
|
+
failed=bool(getattr(raw, "failed", False)),
|
|
236
|
+
peer=peer_id,
|
|
237
|
+
peer_kind=PEER_KINDS.get(type(holder).__name__, ""),
|
|
238
|
+
peer_ref=_settings.peer_model(known.get(abs(peer_id)) if peer_id else None),
|
|
239
|
+
title=getattr(raw, "title", None),
|
|
240
|
+
description=getattr(raw, "description", None),
|
|
241
|
+
msg_id=getattr(raw, "msg_id", None),
|
|
242
|
+
subscription_period=getattr(raw, "subscription_period", None),
|
|
243
|
+
transaction_url=getattr(raw, "transaction_url", None),
|
|
244
|
+
kind=next((name for name in KINDS if getattr(raw, name, False)), ""),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
SPEC_TRANSACTION_LIST = OperationSpec(
|
|
249
|
+
id="stars.transaction.list",
|
|
250
|
+
request=TransactionListReq,
|
|
251
|
+
response=Page[StarsTransaction],
|
|
252
|
+
impl=transaction_list,
|
|
253
|
+
summary="Star (or TON) transaction history",
|
|
254
|
+
aliases=("stars.transactions",),
|
|
255
|
+
paginated=PageKind.RATE,
|
|
256
|
+
idempotent=True,
|
|
257
|
+
columns=("id", "date", "stars", "peer", "title", "kind", "refund"),
|
|
258
|
+
headers=("Id", "Date", "Stars", "Peer", "Title", "Kind", "Refund"),
|
|
259
|
+
example={
|
|
260
|
+
"items": [
|
|
261
|
+
{"id": "tx1", "stars": -25, "peer": 5000001, "title": "Sticker pack", "kind": "gift"}
|
|
262
|
+
],
|
|
263
|
+
"has_more": False,
|
|
264
|
+
},
|
|
265
|
+
example_args="stars transaction list --out",
|
|
266
|
+
covers=("stars.ton-balance", "stars.transactions"),
|
|
267
|
+
tags=frozenset({"agent-safe"}),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
# stars subscription list / refulfill
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class SubscriptionListReq(Request):
|
|
277
|
+
missing_balance: Annotated[
|
|
278
|
+
bool, opt("--missing-balance", help="Only the ones about to lapse for want of Stars.")
|
|
279
|
+
] = False
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
async def subscription_list(ctx: OpContext, req: SubscriptionListReq) -> Page[StarSubscription]:
|
|
283
|
+
"""My Star subscriptions.
|
|
284
|
+
|
|
285
|
+
`can_refulfill` means the *server* would allow a re-join; tlgr still
|
|
286
|
+
refuses to make the charge, which is what `stars subscription refulfill`
|
|
287
|
+
reports.
|
|
288
|
+
"""
|
|
289
|
+
from telethon.tl import types
|
|
290
|
+
from telethon.tl.functions import payments as fn
|
|
291
|
+
|
|
292
|
+
from tlgr.ops._serialize import peer_id_of
|
|
293
|
+
|
|
294
|
+
_, state = window(ctx, "stars.subscription.list", PageKind.RATE, default=50)
|
|
295
|
+
result = await client(ctx)(
|
|
296
|
+
fn.GetStarsSubscriptionsRequest(
|
|
297
|
+
peer=types.InputPeerSelf(),
|
|
298
|
+
offset=str(state.get("offset", "") or ""),
|
|
299
|
+
missing_balance=req.missing_balance or None,
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
rows = []
|
|
303
|
+
for raw in getattr(result, "subscriptions", None) or []:
|
|
304
|
+
pricing = getattr(raw, "pricing", None)
|
|
305
|
+
until = getattr(raw, "until_date", None)
|
|
306
|
+
rows.append(
|
|
307
|
+
StarSubscription(
|
|
308
|
+
id=str(getattr(raw, "id", "") or ""),
|
|
309
|
+
peer=peer_id_of(getattr(raw, "peer", None)),
|
|
310
|
+
until_date=fmt_dt(until),
|
|
311
|
+
until_date_unix=to_unix(until),
|
|
312
|
+
pricing=(
|
|
313
|
+
{
|
|
314
|
+
"period": int(getattr(pricing, "period", 0) or 0),
|
|
315
|
+
"amount": int(getattr(pricing, "amount", 0) or 0),
|
|
316
|
+
}
|
|
317
|
+
if pricing is not None
|
|
318
|
+
else None
|
|
319
|
+
),
|
|
320
|
+
cancelled=getattr(raw, "canceled", None),
|
|
321
|
+
can_refulfill=getattr(raw, "can_refulfill", None),
|
|
322
|
+
missing_balance=getattr(raw, "missing_balance", None),
|
|
323
|
+
invoice_slug=getattr(raw, "invoice_slug", None),
|
|
324
|
+
chat_invite_hash=getattr(raw, "chat_invite_hash", None),
|
|
325
|
+
title=getattr(raw, "title", None),
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
next_offset = str(getattr(result, "subscriptions_next_offset", "") or "")
|
|
329
|
+
return build_page(
|
|
330
|
+
rows,
|
|
331
|
+
op="stars.subscription.list",
|
|
332
|
+
kind=PageKind.RATE,
|
|
333
|
+
state={"offset": next_offset},
|
|
334
|
+
account=ctx.account,
|
|
335
|
+
has_more=bool(next_offset),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
SPEC_SUBSCRIPTION_LIST = OperationSpec(
|
|
340
|
+
id="stars.subscription.list",
|
|
341
|
+
request=SubscriptionListReq,
|
|
342
|
+
response=Page[StarSubscription],
|
|
343
|
+
impl=subscription_list,
|
|
344
|
+
summary="My Star subscriptions",
|
|
345
|
+
paginated=PageKind.RATE,
|
|
346
|
+
idempotent=True,
|
|
347
|
+
columns=("id", "peer", "until_date", "cancelled", "missing_balance"),
|
|
348
|
+
headers=("Id", "Peer", "Until", "Cancelled", "Lapsing"),
|
|
349
|
+
example={
|
|
350
|
+
"items": [{"id": "sub1", "peer": -1001600, "until_date": "2026-10-01T00:00:00Z"}],
|
|
351
|
+
"has_more": False,
|
|
352
|
+
},
|
|
353
|
+
example_args="stars subscription list",
|
|
354
|
+
covers=(
|
|
355
|
+
"groups-channels-admin.channel-subscription-manage",
|
|
356
|
+
"stars.subscriptions-list",
|
|
357
|
+
),
|
|
358
|
+
tags=frozenset({"agent-safe"}),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class SubscriptionRefulfillReq(Request):
|
|
363
|
+
id: Annotated[str, arg(0, metavar="ID", help="The subscription id.")]
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
async def subscription_refulfill(ctx: OpContext, req: SubscriptionRefulfillReq) -> StarsRefulfill:
|
|
367
|
+
"""Report whether a lapsed Star subscription could be re-joined — and refuse to.
|
|
368
|
+
|
|
369
|
+
Re-joining debits Stars. `payments.fulfillStarsSubscription` is one of the
|
|
370
|
+
four methods `ops/payment.py` names as deliberately absent from tlgr's
|
|
371
|
+
surface, and this command exists to say so with the subscription's own
|
|
372
|
+
state attached rather than to be a second way in.
|
|
373
|
+
"""
|
|
374
|
+
page = await subscription_list(ctx, SubscriptionListReq())
|
|
375
|
+
for row in page.items:
|
|
376
|
+
if row.id == req.id:
|
|
377
|
+
return StarsRefulfill(
|
|
378
|
+
id=req.id,
|
|
379
|
+
ok=False,
|
|
380
|
+
can_refulfill=row.can_refulfill,
|
|
381
|
+
stars=(row.pricing or {}).get("amount"),
|
|
382
|
+
reason=_settings.NO_SPEND,
|
|
383
|
+
)
|
|
384
|
+
raise UsageError(
|
|
385
|
+
f"no Star subscription with the id {req.id!r}; `stars subscription list` shows them",
|
|
386
|
+
field="id",
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
SPEC_SUBSCRIPTION_REFULFILL = OperationSpec(
|
|
391
|
+
id="stars.subscription.refulfill",
|
|
392
|
+
request=SubscriptionRefulfillReq,
|
|
393
|
+
response=StarsRefulfill,
|
|
394
|
+
impl=subscription_refulfill,
|
|
395
|
+
summary="Report whether a lapsed Star subscription can be re-joined (tlgr does not charge)",
|
|
396
|
+
idempotent=True,
|
|
397
|
+
columns=("id", "ok", "can_refulfill", "stars", "reason"),
|
|
398
|
+
headers=("Id", "Done", "Allowed", "Stars", "Why not"),
|
|
399
|
+
example={"id": "sub1", "ok": False, "can_refulfill": True, "stars": 100},
|
|
400
|
+
example_args="stars subscription refulfill sub1",
|
|
401
|
+
covers_partial=("stars.subscription-refulfill",),
|
|
402
|
+
coverage_note=(
|
|
403
|
+
"Whether the server would allow it, and what it would cost, are "
|
|
404
|
+
"reported; the charge itself is absent from tlgr's surface by policy."
|
|
405
|
+
),
|
|
406
|
+
tags=frozenset({"agent-safe"}),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
# stars rating get / revenue get / url get
|
|
412
|
+
# ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class RatingGetReq(Request):
|
|
416
|
+
user: Annotated[
|
|
417
|
+
PeerRef | None,
|
|
418
|
+
opt("--user", metavar="USER", kind="user", help="Whose rating (default: me)."),
|
|
419
|
+
] = None
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
async def rating_get(ctx: OpContext, req: RatingGetReq) -> StarsRating:
|
|
423
|
+
"""The Star rating badge: level, progress and what is still pending."""
|
|
424
|
+
from telethon.tl import types
|
|
425
|
+
from telethon.tl.functions import users as fn
|
|
426
|
+
|
|
427
|
+
target = (
|
|
428
|
+
await _settings.input_user(ctx, req.user) if req.user is not None else types.InputUserSelf()
|
|
429
|
+
)
|
|
430
|
+
answer = await client(ctx)(fn.GetFullUserRequest(id=target))
|
|
431
|
+
full = getattr(answer, "full_user", None)
|
|
432
|
+
rating = getattr(full, "stars_rating", None)
|
|
433
|
+
pending = getattr(full, "stars_my_pending_rating", None)
|
|
434
|
+
config = await _settings.app_config(ctx)
|
|
435
|
+
return StarsRating(
|
|
436
|
+
level=int(getattr(rating, "level", 0) or 0),
|
|
437
|
+
stars=int(getattr(rating, "stars", 0) or 0),
|
|
438
|
+
current_level_stars=int(getattr(rating, "current_level_stars", 0) or 0),
|
|
439
|
+
next_level_stars=getattr(rating, "next_level_stars", None),
|
|
440
|
+
pending_stars=getattr(pending, "stars", None),
|
|
441
|
+
pending_date=fmt_dt(getattr(full, "stars_my_pending_rating_date", None)),
|
|
442
|
+
learnmore_url=str(config.get("stars_rating_learnmore_url") or "") or None,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
SPEC_RATING_GET = OperationSpec(
|
|
447
|
+
id="stars.rating.get",
|
|
448
|
+
request=RatingGetReq,
|
|
449
|
+
response=StarsRating,
|
|
450
|
+
impl=rating_get,
|
|
451
|
+
summary="Star rating badge (level and progress)",
|
|
452
|
+
idempotent=True,
|
|
453
|
+
columns=("level", "stars", "current_level_stars", "next_level_stars", "pending_stars"),
|
|
454
|
+
headers=("Level", "Stars", "This level", "Next level", "Pending"),
|
|
455
|
+
example={"level": 3, "stars": 1200, "current_level_stars": 1000, "next_level_stars": 2000},
|
|
456
|
+
example_args="stars rating get",
|
|
457
|
+
covers=("stars.rating",),
|
|
458
|
+
tags=frozenset({"agent-safe"}),
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class RevenueGetReq(Request):
|
|
463
|
+
chat: Annotated[PeerRef, arg(0, metavar="CHAT", kind="peer", help="A channel or bot I own.")]
|
|
464
|
+
ton: Annotated[bool, opt("--ton", help="TON revenue instead of Stars.")] = False
|
|
465
|
+
dark: Annotated[bool, opt("--dark", help="Dark-theme graph tokens.")] = False
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
async def revenue_get(ctx: OpContext, req: RevenueGetReq) -> StarsRevenue:
|
|
469
|
+
"""Star (or ad) revenue statistics for a channel or bot I own.
|
|
470
|
+
|
|
471
|
+
The graphs come back as `statsGraphAsync` tokens that need a second load;
|
|
472
|
+
the token is reported rather than resolved, because loading it is the
|
|
473
|
+
stats domain's job and doing it here would double every call.
|
|
474
|
+
"""
|
|
475
|
+
from telethon.tl.functions import payments as fn
|
|
476
|
+
|
|
477
|
+
peer = await _settings.resolve(ctx, req.chat)
|
|
478
|
+
result = await client(ctx)(
|
|
479
|
+
fn.GetStarsRevenueStatsRequest(peer=peer, dark=req.dark or None, ton=req.ton or None)
|
|
480
|
+
)
|
|
481
|
+
status = getattr(result, "status", None)
|
|
482
|
+
current, _ = _settings.stars_of(getattr(status, "current_balance", None))
|
|
483
|
+
available, _ = _settings.stars_of(getattr(status, "available_balance", None))
|
|
484
|
+
overall, _ = _settings.stars_of(getattr(status, "overall_revenue", None))
|
|
485
|
+
return StarsRevenue(
|
|
486
|
+
chat_id=_settings.peer_of(peer),
|
|
487
|
+
current_balance=current,
|
|
488
|
+
available_balance=available,
|
|
489
|
+
overall_revenue=overall,
|
|
490
|
+
withdrawal_enabled=bool(getattr(status, "withdrawal_enabled", False)),
|
|
491
|
+
next_withdrawal_at=fmt_dt(getattr(status, "next_withdrawal_at", None)),
|
|
492
|
+
usd_rate=getattr(result, "usd_rate", None),
|
|
493
|
+
revenue_graph=_graph(getattr(result, "revenue_graph", None)),
|
|
494
|
+
top_hours_graph=_graph(getattr(result, "top_hours_graph", None)),
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _graph(raw: Any) -> dict[str, Any] | None:
|
|
499
|
+
if raw is None:
|
|
500
|
+
return None
|
|
501
|
+
return {
|
|
502
|
+
"kind": type(raw).__name__.removeprefix("StatsGraph").lower() or "graph",
|
|
503
|
+
"token": getattr(raw, "token", None),
|
|
504
|
+
"json": getattr(getattr(raw, "json", None), "data", None),
|
|
505
|
+
"error": getattr(raw, "error", None),
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
SPEC_REVENUE_GET = OperationSpec(
|
|
510
|
+
id="stars.revenue.get",
|
|
511
|
+
request=RevenueGetReq,
|
|
512
|
+
response=StarsRevenue,
|
|
513
|
+
impl=revenue_get,
|
|
514
|
+
summary="Star / ad revenue statistics for a channel or bot I own",
|
|
515
|
+
description="Needs `channelFull.can_view_stars_revenue`; the graphs are async tokens.",
|
|
516
|
+
idempotent=True,
|
|
517
|
+
columns=("chat_id", "current_balance", "available_balance", "withdrawal_enabled"),
|
|
518
|
+
headers=("Chat", "Balance", "Available", "Withdrawable"),
|
|
519
|
+
example={"chat_id": -1001600, "current_balance": 4200, "withdrawal_enabled": True},
|
|
520
|
+
example_args="stars revenue get @mychannel",
|
|
521
|
+
covers=("bots.bot-revenue-stats", "stars.revenue-stats"),
|
|
522
|
+
tags=frozenset({"agent-safe"}),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class UrlGetReq(Request):
|
|
527
|
+
chat: Annotated[PeerRef, arg(0, metavar="CHAT", kind="peer", help="The channel or bot.")]
|
|
528
|
+
password: Annotated[str | None, _PASSWORD] = None
|
|
529
|
+
ads: Annotated[bool, opt("--ads", help="The ads-account URL instead of a withdrawal URL.")] = (
|
|
530
|
+
False
|
|
531
|
+
)
|
|
532
|
+
amount: Annotated[int | None, opt("--amount", metavar="STARS", help="Amount to withdraw.")] = (
|
|
533
|
+
None
|
|
534
|
+
)
|
|
535
|
+
ton: Annotated[bool, opt("--ton", help="Withdraw TON instead of Stars.")] = False
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
async def url_get(ctx: OpContext, req: UrlGetReq) -> StarsUrl:
|
|
539
|
+
"""Print the Fragment URL for withdrawing revenue, or for the ads account.
|
|
540
|
+
|
|
541
|
+
Control-only by design. The withdrawal needs the cloud password as an SRP
|
|
542
|
+
proof, and what comes back is a URL a human opens in a browser — tlgr
|
|
543
|
+
prints it and stops, and does not drive the ad-purchase flow either.
|
|
544
|
+
"""
|
|
545
|
+
from telethon.tl.functions import payments as fn
|
|
546
|
+
|
|
547
|
+
from tlgr.ops import _auth
|
|
548
|
+
|
|
549
|
+
handle = client(ctx)
|
|
550
|
+
peer = await _settings.resolve(ctx, req.chat)
|
|
551
|
+
if req.ads:
|
|
552
|
+
result = await handle(fn.GetStarsRevenueAdsAccountUrlRequest(peer=peer))
|
|
553
|
+
return StarsUrl(
|
|
554
|
+
url=str(getattr(result, "url", "") or ""),
|
|
555
|
+
kind="ads",
|
|
556
|
+
chat_id=_settings.peer_of(peer),
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
result = await _auth.with_password(
|
|
560
|
+
handle,
|
|
561
|
+
lambda srp: fn.GetStarsRevenueWithdrawalUrlRequest(
|
|
562
|
+
peer=peer, password=srp, ton=req.ton or None, amount=req.amount
|
|
563
|
+
),
|
|
564
|
+
req.password,
|
|
565
|
+
)
|
|
566
|
+
return StarsUrl(
|
|
567
|
+
url=str(getattr(result, "url", "") or ""),
|
|
568
|
+
kind="withdrawal",
|
|
569
|
+
chat_id=_settings.peer_of(peer),
|
|
570
|
+
amount=req.amount,
|
|
571
|
+
ton=req.ton,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
SPEC_URL_GET = OperationSpec(
|
|
576
|
+
id="stars.url.get",
|
|
577
|
+
request=UrlGetReq,
|
|
578
|
+
response=StarsUrl,
|
|
579
|
+
impl=url_get,
|
|
580
|
+
summary="Get the Fragment URL for withdrawing revenue, or for buying ads with Stars",
|
|
581
|
+
description=(
|
|
582
|
+
"Control-only: the URL is printed and the human completes the "
|
|
583
|
+
"transfer in a browser. tlgr moves no money."
|
|
584
|
+
),
|
|
585
|
+
idempotent=True,
|
|
586
|
+
rate_class="send",
|
|
587
|
+
columns=("kind", "url", "amount", "ton"),
|
|
588
|
+
headers=("Kind", "URL", "Amount", "TON"),
|
|
589
|
+
example={"kind": "withdrawal", "url": "https://fragment.com/stars/withdraw?…"},
|
|
590
|
+
example_args="stars url get @mychannel --amount 1000",
|
|
591
|
+
covers=("gifts.withdraw-ton", "stars.ads-account", "stars.withdraw"),
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
__all__ = [name for name in dir() if name.startswith("SPEC_")]
|