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/premium.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"""The `premium` group: subscription status, the limit table, boosts, gifts.
|
|
2
|
+
|
|
3
|
+
The genuinely useful part for a CLI is `premium feature list --limits`. The
|
|
4
|
+
caption length, the upload size, the folder count, the pinned-chat count and
|
|
5
|
+
the public-username count all change with Premium, and a script that guesses
|
|
6
|
+
them writes a message the server then refuses. There is no MTProto method for
|
|
7
|
+
the table — it is assembled from `help.getAppConfig` — which is why every row
|
|
8
|
+
carries `source`.
|
|
9
|
+
|
|
10
|
+
Buying is absent, throughout and by policy. `premium status` prints the
|
|
11
|
+
premium bot and the invoice deep link for a human to open; `premium gift
|
|
12
|
+
send` fetches the payment form, reports the price and stops. PR-10 settled
|
|
13
|
+
this for the `payment` group (`ops/payment.py` names the four methods it will
|
|
14
|
+
not call) and this group inherits it rather than opening a second door onto
|
|
15
|
+
the same money.
|
|
16
|
+
|
|
17
|
+
Telethon is imported inside functions, never at module scope (§2.2).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Annotated, Any
|
|
23
|
+
|
|
24
|
+
from tlgr.core.errors import UsageError
|
|
25
|
+
from tlgr.core.pagination import PageKind
|
|
26
|
+
from tlgr.core.timefmt import fmt_dt, to_unix
|
|
27
|
+
from tlgr.models.admin import Boost
|
|
28
|
+
from tlgr.models.base import Request
|
|
29
|
+
from tlgr.models.page import Page
|
|
30
|
+
from tlgr.models.peer import PeerRef
|
|
31
|
+
from tlgr.models.premium import (
|
|
32
|
+
GiftCode,
|
|
33
|
+
PremiumFeatures,
|
|
34
|
+
PremiumGiftOption,
|
|
35
|
+
PremiumGiftQuote,
|
|
36
|
+
PremiumLimit,
|
|
37
|
+
PremiumStatus,
|
|
38
|
+
)
|
|
39
|
+
from tlgr.ops import _settings
|
|
40
|
+
from tlgr.ops._common import client
|
|
41
|
+
from tlgr.ops._params import arg, opt
|
|
42
|
+
from tlgr.ops._spec import OpContext, OperationSpec
|
|
43
|
+
|
|
44
|
+
__all__ = [name for name in dir() if name.startswith("SPEC_")]
|
|
45
|
+
|
|
46
|
+
#: appConfig suffixes that make a `*_limit_default` / `*_limit_premium` pair.
|
|
47
|
+
_LIMIT_DEFAULT = "_limit_default"
|
|
48
|
+
_LIMIT_PREMIUM = "_limit_premium"
|
|
49
|
+
|
|
50
|
+
#: The boost-level keys, which have no method of their own.
|
|
51
|
+
_LEVEL_SUFFIXES = ("_level_min",)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# premium status / feature list
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class StatusReq(Request):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def status(ctx: OpContext, req: StatusReq) -> PremiumStatus:
|
|
64
|
+
"""My Telegram Premium status, and how a human would buy it.
|
|
65
|
+
|
|
66
|
+
Executing a fiat payment from a CLI is prohibited, and store receipts are
|
|
67
|
+
rejected for third-party api_ids anyway — so the useful answer is the
|
|
68
|
+
deep link, not an error.
|
|
69
|
+
"""
|
|
70
|
+
from telethon.tl.functions import help as fn
|
|
71
|
+
|
|
72
|
+
handle = client(ctx)
|
|
73
|
+
me = await handle.get_me()
|
|
74
|
+
config = await _settings.app_config(ctx)
|
|
75
|
+
result = PremiumStatus(
|
|
76
|
+
premium=bool(getattr(me, "premium", False)),
|
|
77
|
+
premium_purchase_blocked=bool(config.get("premium_purchase_blocked", True)),
|
|
78
|
+
premium_bot=str(config.get("premium_bot_username") or "") or None,
|
|
79
|
+
reason=_settings.NO_SPEND,
|
|
80
|
+
)
|
|
81
|
+
try:
|
|
82
|
+
promo = await handle(fn.GetPremiumPromoRequest())
|
|
83
|
+
except Exception as exc: # pragma: no cover - promo is optional
|
|
84
|
+
ctx.warn(f"the Premium promo is unavailable: {exc}")
|
|
85
|
+
return result
|
|
86
|
+
for option in getattr(promo, "period_options", None) or []:
|
|
87
|
+
link = getattr(option, "bot_url", None)
|
|
88
|
+
if link:
|
|
89
|
+
result.invoice_link = str(link)
|
|
90
|
+
break
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
SPEC_STATUS = OperationSpec(
|
|
95
|
+
id="premium.status",
|
|
96
|
+
request=StatusReq,
|
|
97
|
+
response=PremiumStatus,
|
|
98
|
+
impl=status,
|
|
99
|
+
summary="My Telegram Premium status (and how to buy it, which tlgr never does)",
|
|
100
|
+
idempotent=True,
|
|
101
|
+
columns=("premium", "premium_purchase_blocked", "premium_bot"),
|
|
102
|
+
headers=("Premium", "Purchase blocked", "Bot"),
|
|
103
|
+
example={"premium": True, "premium_purchase_blocked": True, "premium_bot": "PremiumBot"},
|
|
104
|
+
example_args="premium status",
|
|
105
|
+
covers=("premium.status",),
|
|
106
|
+
tags=frozenset({"agent-safe"}),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class FeatureListReq(Request):
|
|
111
|
+
limits: Annotated[bool, opt("--limits", help="Only the *_limit_default/_premium table.")] = (
|
|
112
|
+
False
|
|
113
|
+
)
|
|
114
|
+
boost_levels: Annotated[
|
|
115
|
+
bool, opt("--boost-levels", help="The boost-level unlock table from appConfig.")
|
|
116
|
+
] = False
|
|
117
|
+
channel: Annotated[
|
|
118
|
+
PeerRef | None,
|
|
119
|
+
opt("--channel", metavar="CHAT", kind="peer", help="Show this channel's boost level."),
|
|
120
|
+
] = None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def feature_list(ctx: OpContext, req: FeatureListReq) -> PremiumFeatures:
|
|
124
|
+
"""Premium features, the promo text and the limit table.
|
|
125
|
+
|
|
126
|
+
The limit table is the practically useful half: caption length, upload
|
|
127
|
+
size, folder counts, pinned chats, public usernames. There is no MTProto
|
|
128
|
+
method for the boost-level table either — it is `channel_*_level_min` and
|
|
129
|
+
`group_*_level_min` from appConfig, assembled here.
|
|
130
|
+
"""
|
|
131
|
+
from telethon.tl.functions import help as fn
|
|
132
|
+
from telethon.tl.functions import premium as pfn
|
|
133
|
+
|
|
134
|
+
handle = client(ctx)
|
|
135
|
+
config = await _settings.app_config(ctx)
|
|
136
|
+
result = PremiumFeatures()
|
|
137
|
+
|
|
138
|
+
pairs: dict[str, dict[str, int]] = {}
|
|
139
|
+
for key, value in config.items():
|
|
140
|
+
for suffix, side in ((_LIMIT_DEFAULT, "default"), (_LIMIT_PREMIUM, "premium")):
|
|
141
|
+
if key.endswith(suffix):
|
|
142
|
+
try:
|
|
143
|
+
pairs.setdefault(key[: -len(suffix)], {})[side] = int(float(value))
|
|
144
|
+
except (TypeError, ValueError):
|
|
145
|
+
continue
|
|
146
|
+
result.limits = [
|
|
147
|
+
PremiumLimit(name=name, default=sides.get("default", 0), premium=sides.get("premium", 0))
|
|
148
|
+
for name, sides in sorted(pairs.items())
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
if req.boost_levels or not req.limits:
|
|
152
|
+
result.boost_levels = sorted(
|
|
153
|
+
(
|
|
154
|
+
{"key": key, "level": int(float(value))}
|
|
155
|
+
for key, value in config.items()
|
|
156
|
+
if any(key.endswith(suffix) for suffix in _LEVEL_SUFFIXES) and _is_number(value)
|
|
157
|
+
),
|
|
158
|
+
key=lambda row: (int(row["level"]), str(row["key"])),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if req.channel is not None:
|
|
162
|
+
peer = await _settings.resolve(ctx, req.channel)
|
|
163
|
+
boosts = await handle(pfn.GetBoostsStatusRequest(peer=peer))
|
|
164
|
+
result.channel_level = int(getattr(boosts, "level", 0) or 0)
|
|
165
|
+
|
|
166
|
+
if req.limits or req.boost_levels:
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
promo = await handle(fn.GetPremiumPromoRequest())
|
|
170
|
+
result.status_text = str(getattr(promo, "status_text", "") or "")
|
|
171
|
+
result.period_options = [
|
|
172
|
+
{
|
|
173
|
+
"months": int(getattr(option, "months", 0) or 0),
|
|
174
|
+
"currency": str(getattr(option, "currency", "") or ""),
|
|
175
|
+
"amount": int(getattr(option, "amount", 0) or 0),
|
|
176
|
+
"bot_url": getattr(option, "bot_url", None),
|
|
177
|
+
}
|
|
178
|
+
for option in getattr(promo, "period_options", None) or []
|
|
179
|
+
]
|
|
180
|
+
result.video_sections = [str(name) for name in getattr(promo, "video_sections", None) or []]
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _is_number(value: Any) -> bool:
|
|
185
|
+
try:
|
|
186
|
+
float(value)
|
|
187
|
+
except (TypeError, ValueError):
|
|
188
|
+
return False
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
SPEC_FEATURE_LIST = OperationSpec(
|
|
193
|
+
id="premium.feature.list",
|
|
194
|
+
request=FeatureListReq,
|
|
195
|
+
response=PremiumFeatures,
|
|
196
|
+
impl=feature_list,
|
|
197
|
+
summary="Premium features, promo text and the default/premium limit table",
|
|
198
|
+
description=(
|
|
199
|
+
"`--limits` is the part a script needs: it is what decides whether a "
|
|
200
|
+
"caption, an upload or a folder will be accepted before it is sent."
|
|
201
|
+
),
|
|
202
|
+
aliases=("premium.features",),
|
|
203
|
+
idempotent=True,
|
|
204
|
+
columns=("status_text", "channel_level"),
|
|
205
|
+
headers=("Promo", "Channel level"),
|
|
206
|
+
example={
|
|
207
|
+
"limits": [{"name": "caption_length", "default": 1024, "premium": 2048}],
|
|
208
|
+
"boost_levels": [],
|
|
209
|
+
},
|
|
210
|
+
example_args="premium feature list --limits",
|
|
211
|
+
covers=("content.limits", "premium.boost-level-features", "premium.features-list"),
|
|
212
|
+
tags=frozenset({"agent-safe"}),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# premium boost list
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class BoostListReq(Request):
|
|
222
|
+
channel: Annotated[
|
|
223
|
+
PeerRef | None,
|
|
224
|
+
arg(0, metavar="CHANNEL", required=False, kind="peer", help="Omit for my own slots."),
|
|
225
|
+
] = None
|
|
226
|
+
gifts: Annotated[bool, opt("--gifts", help="Only gift and giveaway boosts.")] = False
|
|
227
|
+
of_user: Annotated[
|
|
228
|
+
PeerRef | None,
|
|
229
|
+
opt("--of-user", metavar="USER", kind="user", help="Narrow to one booster."),
|
|
230
|
+
] = None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
async def boost_list(ctx: OpContext, req: BoostListReq) -> Page[Boost]:
|
|
234
|
+
"""My boost slots, or the boosts applied to a channel I administer.
|
|
235
|
+
|
|
236
|
+
The same listing `boost list` answers with, reached from the Premium
|
|
237
|
+
screen: Premium grants `boosts_per_premium` slots and gifting Premium
|
|
238
|
+
adds more, which is a fact about the subscription rather than about a
|
|
239
|
+
channel.
|
|
240
|
+
"""
|
|
241
|
+
from tlgr.ops.chat_stats import BoostListReq as StatsBoostListReq
|
|
242
|
+
from tlgr.ops.chat_stats import list_boosts
|
|
243
|
+
|
|
244
|
+
return await list_boosts(
|
|
245
|
+
ctx,
|
|
246
|
+
StatsBoostListReq(
|
|
247
|
+
chat=req.channel,
|
|
248
|
+
user=req.of_user,
|
|
249
|
+
gifts=req.gifts,
|
|
250
|
+
mine=req.channel is None,
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
SPEC_BOOST_LIST = OperationSpec(
|
|
256
|
+
id="premium.boost.list",
|
|
257
|
+
request=BoostListReq,
|
|
258
|
+
response=Page[Boost],
|
|
259
|
+
impl=boost_list,
|
|
260
|
+
summary="My boost slots, or the boosts applied to a channel I administer",
|
|
261
|
+
aliases=("boost.slots",),
|
|
262
|
+
paginated=PageKind.PARTICIPANTS,
|
|
263
|
+
idempotent=True,
|
|
264
|
+
columns=("slot", "chat_id", "user_id", "expires", "cooldown_until_date"),
|
|
265
|
+
headers=("Slot", "Chat", "User", "Expires", "Cooldown"),
|
|
266
|
+
example={
|
|
267
|
+
"items": [{"slot": 1, "chat_id": -1001600, "expires": "2026-10-01T00:00:00Z"}],
|
|
268
|
+
"has_more": False,
|
|
269
|
+
},
|
|
270
|
+
example_args="premium boost list",
|
|
271
|
+
covers=("premium.channel-boosts-list", "premium.my-boosts", "stories.boost-status"),
|
|
272
|
+
tags=frozenset({"agent-safe"}),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
# premium gift list / send
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class GiftListReq(Request):
|
|
282
|
+
boost_peer: Annotated[
|
|
283
|
+
PeerRef | None,
|
|
284
|
+
opt(
|
|
285
|
+
"--boost-peer", metavar="CHAT", kind="peer", help="Options tied to boosting a channel."
|
|
286
|
+
),
|
|
287
|
+
] = None
|
|
288
|
+
single: Annotated[
|
|
289
|
+
bool, opt("--single/--all-options", help="Only options for a single recipient.")
|
|
290
|
+
] = True
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
async def gift_list(ctx: OpContext, req: GiftListReq) -> Page[PremiumGiftOption]:
|
|
294
|
+
"""Premium gift price options, in Stars and in fiat.
|
|
295
|
+
|
|
296
|
+
A third-party client can only act on the `XTR` (Stars) options; the fiat
|
|
297
|
+
ones exist for the official apps' store flows. Options with `users > 1`
|
|
298
|
+
are giveaway options rather than direct gifts.
|
|
299
|
+
"""
|
|
300
|
+
from telethon.tl.functions import payments as fn
|
|
301
|
+
|
|
302
|
+
peer = await _settings.resolve(ctx, req.boost_peer) if req.boost_peer else None
|
|
303
|
+
result = await client(ctx)(fn.GetPremiumGiftCodeOptionsRequest(boost_peer=peer))
|
|
304
|
+
rows = [
|
|
305
|
+
PremiumGiftOption(
|
|
306
|
+
months=int(getattr(option, "months", 0) or 0),
|
|
307
|
+
users=int(getattr(option, "users", 1) or 1),
|
|
308
|
+
currency=str(getattr(option, "currency", "") or ""),
|
|
309
|
+
amount=int(getattr(option, "amount", 0) or 0),
|
|
310
|
+
store_product=getattr(option, "store_product", None),
|
|
311
|
+
)
|
|
312
|
+
for option in result or []
|
|
313
|
+
]
|
|
314
|
+
if req.single:
|
|
315
|
+
rows = [row for row in rows if row.users == 1]
|
|
316
|
+
return Page(items=rows, has_more=False, total=len(rows))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
SPEC_GIFT_LIST = OperationSpec(
|
|
320
|
+
id="premium.gift.list",
|
|
321
|
+
request=GiftListReq,
|
|
322
|
+
response=Page[PremiumGiftOption],
|
|
323
|
+
impl=gift_list,
|
|
324
|
+
summary="Premium gift price options (Stars and fiat), for a user or a channel giveaway",
|
|
325
|
+
aliases=("premium.gift.options",),
|
|
326
|
+
paginated=PageKind.LOCAL,
|
|
327
|
+
idempotent=True,
|
|
328
|
+
columns=("months", "users", "currency", "amount", "store_product"),
|
|
329
|
+
headers=("Months", "Users", "Currency", "Amount", "Store"),
|
|
330
|
+
example={
|
|
331
|
+
"items": [{"months": 3, "users": 1, "currency": "XTR", "amount": 1000}],
|
|
332
|
+
"has_more": False,
|
|
333
|
+
},
|
|
334
|
+
example_args="premium gift list",
|
|
335
|
+
covers=("premium.gift-options",),
|
|
336
|
+
tags=frozenset({"agent-safe"}),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class GiftSendReq(Request):
|
|
341
|
+
user: Annotated[PeerRef, arg(0, metavar="USER", kind="user", help="Who to gift Premium to.")]
|
|
342
|
+
months: Annotated[int | None, opt("--months", metavar="N", help="Subscription length.")] = None
|
|
343
|
+
message: Annotated[
|
|
344
|
+
str | None, opt("--message", metavar="TEXT", help="Note attached to the gift.")
|
|
345
|
+
] = None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
async def gift_send(ctx: OpContext, req: GiftSendReq) -> PremiumGiftQuote:
|
|
349
|
+
"""Price gifting Premium to a user — and refuse to buy it.
|
|
350
|
+
|
|
351
|
+
The form is fetched so the price is visible and the recipient is
|
|
352
|
+
validated; `payments.sendStarsForm` is deliberately absent from tlgr's
|
|
353
|
+
whole surface, so `ok` is false and `reason` says why.
|
|
354
|
+
"""
|
|
355
|
+
from telethon.tl import types
|
|
356
|
+
from telethon.tl.functions import payments as fn
|
|
357
|
+
|
|
358
|
+
if not req.months:
|
|
359
|
+
raise UsageError(
|
|
360
|
+
"--months is the subscription length; `premium gift list` shows the options",
|
|
361
|
+
field="months",
|
|
362
|
+
)
|
|
363
|
+
user = await _settings.input_user(ctx, req.user, field="user")
|
|
364
|
+
invoice = types.InputInvoicePremiumGiftStars(
|
|
365
|
+
user_id=user,
|
|
366
|
+
months=int(req.months),
|
|
367
|
+
message=(types.TextWithEntities(text=req.message, entities=[]) if req.message else None),
|
|
368
|
+
)
|
|
369
|
+
form = await client(ctx)(fn.GetPaymentFormRequest(invoice=invoice))
|
|
370
|
+
prices = getattr(getattr(form, "invoice", None), "prices", None) or []
|
|
371
|
+
return PremiumGiftQuote(
|
|
372
|
+
user_id=_settings.peer_of(await _settings.resolve(ctx, req.user)),
|
|
373
|
+
months=int(req.months),
|
|
374
|
+
stars=sum(int(getattr(price, "amount", 0) or 0) for price in prices),
|
|
375
|
+
currency=str(getattr(getattr(form, "invoice", None), "currency", "XTR") or "XTR"),
|
|
376
|
+
ok=False,
|
|
377
|
+
reason=_settings.NO_SPEND,
|
|
378
|
+
form_id=getattr(form, "form_id", None),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
SPEC_GIFT_SEND = OperationSpec(
|
|
383
|
+
id="premium.gift.send",
|
|
384
|
+
request=GiftSendReq,
|
|
385
|
+
response=PremiumGiftQuote,
|
|
386
|
+
impl=gift_send,
|
|
387
|
+
summary="Price gifting Telegram Premium to a user (tlgr reads the form, never signs it)",
|
|
388
|
+
idempotent=True,
|
|
389
|
+
columns=("user_id", "months", "stars", "currency", "ok"),
|
|
390
|
+
headers=("User", "Months", "Stars", "Currency", "Sent"),
|
|
391
|
+
example={"user_id": 777123, "months": 3, "stars": 1000, "currency": "XTR", "ok": False},
|
|
392
|
+
example_args="premium gift send @alice --months 3",
|
|
393
|
+
covers_partial=("premium.gift-to-user",),
|
|
394
|
+
coverage_note=(
|
|
395
|
+
"The recipient, the length and the price are reported; signing the "
|
|
396
|
+
"payment form is absent from tlgr's whole surface by policy."
|
|
397
|
+
),
|
|
398
|
+
tags=frozenset({"agent-safe"}),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
# premium giftcode get
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _code_model(slug: str, raw: Any) -> GiftCode:
|
|
408
|
+
"""`payments.checkedGiftCode` as a model.
|
|
409
|
+
|
|
410
|
+
The wire says `days`; every client and every price option says *months*,
|
|
411
|
+
so both are reported and neither is invented — `months` is the whole
|
|
412
|
+
months the day count buys.
|
|
413
|
+
"""
|
|
414
|
+
date = getattr(raw, "date", None)
|
|
415
|
+
days = getattr(raw, "days", None)
|
|
416
|
+
return GiftCode(
|
|
417
|
+
slug=slug,
|
|
418
|
+
link=f"https://t.me/giftcode/{slug}",
|
|
419
|
+
from_id=_peer_id(getattr(raw, "from_id", None)),
|
|
420
|
+
to_id=getattr(raw, "to_id", None),
|
|
421
|
+
date=fmt_dt(date),
|
|
422
|
+
date_unix=to_unix(date),
|
|
423
|
+
months=int(days) // 30 if days else None,
|
|
424
|
+
days=int(days) if days else None,
|
|
425
|
+
used_date=fmt_dt(getattr(raw, "used_date", None)),
|
|
426
|
+
via_giveaway=bool(getattr(raw, "via_giveaway", False)),
|
|
427
|
+
giveaway_msg_id=getattr(raw, "giveaway_msg_id", None),
|
|
428
|
+
used=getattr(raw, "used_date", None) is not None,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _peer_id(peer: Any) -> int | None:
|
|
433
|
+
from tlgr.ops._serialize import peer_id_of
|
|
434
|
+
|
|
435
|
+
return peer_id_of(peer) if peer is not None else None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class GiftcodeGetReq(Request):
|
|
439
|
+
slug: Annotated[str, arg(0, metavar="SLUG", help="The gift code, or a t.me/giftcode link.")]
|
|
440
|
+
redeem: Annotated[bool, opt("--redeem", help="Apply the code to this account (free).")] = False
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
async def giftcode_get(ctx: OpContext, req: GiftcodeGetReq) -> GiftCode:
|
|
444
|
+
"""Check a Premium gift code, and optionally redeem it.
|
|
445
|
+
|
|
446
|
+
Redeeming involves no payment: the code was already bought by whoever
|
|
447
|
+
sent it, so this is one of the few `payments.*` writes tlgr performs.
|
|
448
|
+
"""
|
|
449
|
+
from telethon.tl.functions import payments as fn
|
|
450
|
+
|
|
451
|
+
handle = client(ctx)
|
|
452
|
+
slug = req.slug.rsplit("/", 1)[-1]
|
|
453
|
+
result = await handle(fn.CheckGiftCodeRequest(slug=slug))
|
|
454
|
+
model = _code_model(slug, result)
|
|
455
|
+
if req.redeem:
|
|
456
|
+
if model.used:
|
|
457
|
+
_already(ctx)
|
|
458
|
+
else:
|
|
459
|
+
await handle(fn.ApplyGiftCodeRequest(slug=slug))
|
|
460
|
+
ctx.emit("giftcode_applied", {"slug": slug})
|
|
461
|
+
model.used = True
|
|
462
|
+
return model
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _already(ctx: OpContext) -> None:
|
|
466
|
+
mark = getattr(ctx, "mark_already", None)
|
|
467
|
+
if callable(mark):
|
|
468
|
+
mark()
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
SPEC_GIFTCODE_GET = OperationSpec(
|
|
472
|
+
id="premium.giftcode.get",
|
|
473
|
+
request=GiftcodeGetReq,
|
|
474
|
+
response=GiftCode,
|
|
475
|
+
impl=giftcode_get,
|
|
476
|
+
summary="Check a Premium gift code, and optionally redeem it",
|
|
477
|
+
description="Redeeming costs nothing — the code is already paid for.",
|
|
478
|
+
mutating=True,
|
|
479
|
+
idempotent=True,
|
|
480
|
+
rate_class="send",
|
|
481
|
+
columns=("slug", "from_id", "to_id", "months", "used_date", "via_giveaway"),
|
|
482
|
+
headers=("Slug", "From", "To", "Months", "Used", "Giveaway"),
|
|
483
|
+
example={"slug": "abcdef", "from_id": -1001600, "months": 3, "via_giveaway": True},
|
|
484
|
+
example_args="premium giftcode get abcdef",
|
|
485
|
+
covers=("premium.giftcode-apply", "premium.giftcode-check"),
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
__all__ = [name for name in dir() if name.startswith("SPEC_")]
|