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/gift.py
ADDED
|
@@ -0,0 +1,1546 @@
|
|
|
1
|
+
"""The `gift` group: the catalogue, the gifts a profile holds, collectibles.
|
|
2
|
+
|
|
3
|
+
A gift is addressed by a **reference**, and `ref` is the same string
|
|
4
|
+
everywhere here: `msg:<id>` for one received in a private chat,
|
|
5
|
+
`<peer>:<saved_id>` for one a channel holds, or a bare collectible slug (a
|
|
6
|
+
`t.me/nft/<slug>` link is accepted). One spelling, so a `ref` read out of
|
|
7
|
+
`gift list` can be handed straight to `gift set`, `gift convert`, `gift
|
|
8
|
+
transfer` or `gift upgrade` without a lookup table.
|
|
9
|
+
|
|
10
|
+
The dividing line in this group is **cost**, not danger:
|
|
11
|
+
|
|
12
|
+
* free operations are performed — displaying, pinning, wearing, converting a
|
|
13
|
+
gift back into Stars, a free transfer, a prepaid upgrade, listing a
|
|
14
|
+
collectible for sale, declining an offer, crafting;
|
|
15
|
+
* anything whose completion needs a payment form signed is priced and
|
|
16
|
+
refused, with `refused_reason` saying so. `payments.sendStarsForm` is
|
|
17
|
+
absent from tlgr's whole surface (`ops/payment.py`), and this group does
|
|
18
|
+
not open a second door onto it.
|
|
19
|
+
|
|
20
|
+
`gift craft` is the one free operation that still needs `--yes`: it burns
|
|
21
|
+
every input gift whatever the outcome, which is `rm -rf` semantics without a
|
|
22
|
+
payment anywhere near it.
|
|
23
|
+
|
|
24
|
+
Telethon is imported inside functions, never at module scope (§2.2).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import Annotated, Any
|
|
30
|
+
|
|
31
|
+
from tlgr.core.errors import NotFoundError, UsageError
|
|
32
|
+
from tlgr.core.pagination import PageKind, build_page
|
|
33
|
+
from tlgr.core.timefmt import fmt_dt, fmt_unix, parse_dt, to_unix
|
|
34
|
+
from tlgr.models.base import Request
|
|
35
|
+
from tlgr.models.gift import (
|
|
36
|
+
GiftAttribute,
|
|
37
|
+
GiftAuction,
|
|
38
|
+
GiftAuctionState,
|
|
39
|
+
GiftCollection,
|
|
40
|
+
GiftConverted,
|
|
41
|
+
GiftCrafted,
|
|
42
|
+
GiftDisplay,
|
|
43
|
+
GiftListing,
|
|
44
|
+
GiftOfferResolved,
|
|
45
|
+
GiftTransferred,
|
|
46
|
+
GiftUpgraded,
|
|
47
|
+
GiftVariant,
|
|
48
|
+
OwnedGift,
|
|
49
|
+
ResaleGift,
|
|
50
|
+
StarGift,
|
|
51
|
+
UniqueGift,
|
|
52
|
+
)
|
|
53
|
+
from tlgr.models.page import Page
|
|
54
|
+
from tlgr.models.peer import PeerRef
|
|
55
|
+
from tlgr.ops import _settings
|
|
56
|
+
from tlgr.ops._common import client, window
|
|
57
|
+
from tlgr.ops._params import arg, opt
|
|
58
|
+
from tlgr.ops._spec import OpContext, OperationSpec
|
|
59
|
+
|
|
60
|
+
__all__ = [name for name in dir() if name.startswith("SPEC_")]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Shared serialisation
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _attribute(raw: Any) -> GiftAttribute:
|
|
69
|
+
from tlgr.ops._serialize import peer_id_of
|
|
70
|
+
|
|
71
|
+
name = type(raw).__name__.removeprefix("StarGiftAttribute")
|
|
72
|
+
kind = {
|
|
73
|
+
"Model": "model",
|
|
74
|
+
"Pattern": "pattern",
|
|
75
|
+
"Backdrop": "backdrop",
|
|
76
|
+
"OriginalDetails": "original-details",
|
|
77
|
+
}.get(name, name.lower())
|
|
78
|
+
rarity = getattr(raw, "rarity", None)
|
|
79
|
+
document = getattr(raw, "document", None)
|
|
80
|
+
message = getattr(raw, "message", None)
|
|
81
|
+
sender = getattr(raw, "sender_id", None)
|
|
82
|
+
recipient = getattr(raw, "recipient_id", None)
|
|
83
|
+
return GiftAttribute(
|
|
84
|
+
kind=kind,
|
|
85
|
+
name=str(getattr(raw, "name", "") or ""),
|
|
86
|
+
document_id=getattr(document, "id", None),
|
|
87
|
+
rarity_permille=getattr(rarity, "permille", None),
|
|
88
|
+
backdrop_id=getattr(raw, "backdrop_id", None),
|
|
89
|
+
center_color=getattr(raw, "center_color", None),
|
|
90
|
+
edge_color=getattr(raw, "edge_color", None),
|
|
91
|
+
pattern_color=getattr(raw, "pattern_color", None),
|
|
92
|
+
text_color=getattr(raw, "text_color", None),
|
|
93
|
+
crafted=bool(getattr(raw, "crafted", False)),
|
|
94
|
+
sender_id=peer_id_of(sender) if sender is not None else None,
|
|
95
|
+
recipient_id=peer_id_of(recipient) if recipient is not None else None,
|
|
96
|
+
message=getattr(message, "text", None),
|
|
97
|
+
date=fmt_dt(getattr(raw, "date", None)),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _unique(raw: Any, known: dict[int, Any] | None = None) -> UniqueGift:
|
|
102
|
+
from tlgr.ops._serialize import peer_id_of
|
|
103
|
+
|
|
104
|
+
owner = getattr(raw, "owner_id", None)
|
|
105
|
+
owner_id = peer_id_of(owner) if owner is not None else None
|
|
106
|
+
resale = getattr(raw, "resell_amount", None) or []
|
|
107
|
+
stars = next((a for a in resale if type(a).__name__ == "StarsAmount"), None)
|
|
108
|
+
ton = next((a for a in resale if type(a).__name__ == "StarsTonAmount"), None)
|
|
109
|
+
slug = str(getattr(raw, "slug", "") or "")
|
|
110
|
+
return UniqueGift(
|
|
111
|
+
slug=slug,
|
|
112
|
+
gift_id=int(getattr(raw, "gift_id", 0) or 0),
|
|
113
|
+
id=int(getattr(raw, "id", 0) or 0),
|
|
114
|
+
title=str(getattr(raw, "title", "") or ""),
|
|
115
|
+
num=int(getattr(raw, "num", 0) or 0),
|
|
116
|
+
owner_id=owner_id,
|
|
117
|
+
owner=_settings.peer_model((known or {}).get(abs(owner_id)) if owner_id else None),
|
|
118
|
+
owner_name=getattr(raw, "owner_name", None),
|
|
119
|
+
owner_address=getattr(raw, "owner_address", None),
|
|
120
|
+
gift_address=getattr(raw, "gift_address", None),
|
|
121
|
+
availability_issued=getattr(raw, "availability_issued", None),
|
|
122
|
+
availability_total=getattr(raw, "availability_total", None),
|
|
123
|
+
attributes=[_attribute(a) for a in getattr(raw, "attributes", None) or []],
|
|
124
|
+
resell_stars=_settings.stars_of(stars)[0] if stars is not None else None,
|
|
125
|
+
resell_ton=_settings.stars_of(ton)[0] if ton is not None else None,
|
|
126
|
+
resale_ton_only=bool(getattr(raw, "resale_ton_only", False)),
|
|
127
|
+
value_stars=getattr(raw, "value_amount", None),
|
|
128
|
+
value_currency=getattr(raw, "value_currency", None),
|
|
129
|
+
value_usd=getattr(raw, "value_usd_amount", None),
|
|
130
|
+
hosted=getattr(raw, "host_id", None) is not None,
|
|
131
|
+
burned=bool(getattr(raw, "burned", False)),
|
|
132
|
+
crafted=bool(getattr(raw, "crafted", False)),
|
|
133
|
+
theme_available=bool(getattr(raw, "theme_available", False)),
|
|
134
|
+
peer_color_available=getattr(raw, "peer_color", None) is not None,
|
|
135
|
+
offer_min_stars=getattr(raw, "offer_min_stars", None),
|
|
136
|
+
craft_chance_permille=getattr(raw, "craft_chance_permille", None),
|
|
137
|
+
link=f"https://t.me/nft/{slug}" if slug else None,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _catalog_gift(raw: Any) -> StarGift:
|
|
142
|
+
return StarGift(
|
|
143
|
+
gift_id=int(getattr(raw, "id", 0) or 0),
|
|
144
|
+
title=str(getattr(raw, "title", "") or ""),
|
|
145
|
+
stars=int(getattr(raw, "stars", 0) or 0),
|
|
146
|
+
convert_stars=getattr(raw, "convert_stars", None),
|
|
147
|
+
upgrade_stars=getattr(raw, "upgrade_stars", None),
|
|
148
|
+
limited=bool(getattr(raw, "limited", False)),
|
|
149
|
+
sold_out=bool(getattr(raw, "sold_out", False)),
|
|
150
|
+
birthday=bool(getattr(raw, "birthday", False)),
|
|
151
|
+
require_premium=bool(getattr(raw, "require_premium", False)),
|
|
152
|
+
availability_remains=getattr(raw, "availability_remains", None),
|
|
153
|
+
availability_total=getattr(raw, "availability_total", None),
|
|
154
|
+
availability_resale=getattr(raw, "availability_resale", None),
|
|
155
|
+
first_sale_date=fmt_dt(getattr(raw, "first_sale_date", None)),
|
|
156
|
+
last_sale_date=fmt_dt(getattr(raw, "last_sale_date", None)),
|
|
157
|
+
document_id=getattr(getattr(raw, "sticker", None), "id", None),
|
|
158
|
+
resell_min_stars=getattr(raw, "resell_min_stars", None),
|
|
159
|
+
per_user_total=getattr(raw, "per_user_total", None),
|
|
160
|
+
per_user_remains=getattr(raw, "per_user_remains", None),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _owned(raw: Any, known: dict[int, Any]) -> OwnedGift:
|
|
165
|
+
from tlgr.ops._serialize import peer_id_of
|
|
166
|
+
|
|
167
|
+
gift = getattr(raw, "gift", None)
|
|
168
|
+
unique = _unique(gift, known) if type(gift).__name__ == "StarGiftUnique" else None
|
|
169
|
+
sender = getattr(raw, "from_id", None)
|
|
170
|
+
from_id = peer_id_of(sender) if sender is not None else None
|
|
171
|
+
message = getattr(raw, "message", None)
|
|
172
|
+
date = getattr(raw, "date", None)
|
|
173
|
+
return OwnedGift(
|
|
174
|
+
ref=_settings.gift_ref_text(raw),
|
|
175
|
+
kind="collectible" if unique is not None else "gift",
|
|
176
|
+
gift_id=int(getattr(gift, "id", 0) or 0) or None,
|
|
177
|
+
slug=unique.slug if unique is not None else None,
|
|
178
|
+
title=str(getattr(gift, "title", "") or ""),
|
|
179
|
+
num=getattr(raw, "gift_num", None) or (unique.num if unique is not None else None),
|
|
180
|
+
from_id=from_id,
|
|
181
|
+
from_peer=_settings.peer_model(known.get(abs(from_id)) if from_id else None),
|
|
182
|
+
name_hidden=bool(getattr(raw, "name_hidden", False)),
|
|
183
|
+
message=getattr(message, "text", None),
|
|
184
|
+
date=fmt_dt(date),
|
|
185
|
+
date_unix=to_unix(date),
|
|
186
|
+
msg_id=getattr(raw, "msg_id", None),
|
|
187
|
+
saved_id=getattr(raw, "saved_id", None),
|
|
188
|
+
pinned=bool(getattr(raw, "pinned_to_top", False)),
|
|
189
|
+
displayed=not bool(getattr(raw, "unsaved", False)),
|
|
190
|
+
refunded=bool(getattr(raw, "refunded", False)),
|
|
191
|
+
can_upgrade=bool(getattr(raw, "can_upgrade", False)),
|
|
192
|
+
convert_stars=getattr(raw, "convert_stars", None),
|
|
193
|
+
upgrade_stars=getattr(raw, "upgrade_stars", None),
|
|
194
|
+
transfer_stars=getattr(raw, "transfer_stars", None),
|
|
195
|
+
can_export_at=fmt_unix(getattr(raw, "can_export_at", None) or 0) or None,
|
|
196
|
+
can_transfer_at=fmt_unix(getattr(raw, "can_transfer_at", None) or 0) or None,
|
|
197
|
+
can_resell_at=fmt_unix(getattr(raw, "can_resell_at", None) or 0) or None,
|
|
198
|
+
can_craft_at=fmt_unix(getattr(raw, "can_craft_at", None) or 0) or None,
|
|
199
|
+
collection_ids=[int(v) for v in getattr(raw, "collection_id", None) or []],
|
|
200
|
+
hosted=unique.hosted if unique is not None else False,
|
|
201
|
+
attributes=unique.attributes if unique is not None else [],
|
|
202
|
+
unique=unique,
|
|
203
|
+
resell_stars=unique.resell_stars if unique is not None else None,
|
|
204
|
+
resell_ton=unique.resell_ton if unique is not None else None,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
async def _peer_or_self(ctx: OpContext, ref: PeerRef | None) -> Any:
|
|
209
|
+
from telethon.tl import types
|
|
210
|
+
|
|
211
|
+
if ref is None:
|
|
212
|
+
return types.InputPeerSelf()
|
|
213
|
+
return await _settings.resolve(ctx, ref)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# gift catalog
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class CatalogReq(Request):
|
|
222
|
+
until: Annotated[
|
|
223
|
+
PeerRef | None,
|
|
224
|
+
opt("--until", metavar="PEER", kind="peer", help="Annotate each gift with can_send."),
|
|
225
|
+
] = None
|
|
226
|
+
limited: Annotated[bool, opt("--limited", help="Only limited-supply gifts.")] = False
|
|
227
|
+
available: Annotated[bool, opt("--available", help="Hide sold-out gifts.")] = False
|
|
228
|
+
refresh: Annotated[bool, opt("--refresh", help="Ignore the cached hash.")] = False
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
async def catalog(ctx: OpContext, req: CatalogReq) -> Page[StarGift]:
|
|
232
|
+
"""Browse the gifts on sale.
|
|
233
|
+
|
|
234
|
+
Price discovery is free and automatic; *buying* one is not here, like
|
|
235
|
+
every other purchase. `--until` would annotate each row with whether the
|
|
236
|
+
named recipient accepts it, and needs `payments.canSendStarGift`, which
|
|
237
|
+
Telethon 1.44 has no request class for.
|
|
238
|
+
"""
|
|
239
|
+
from telethon.tl.functions import payments as fn
|
|
240
|
+
|
|
241
|
+
if req.until is not None:
|
|
242
|
+
_settings.method_gap("gift catalog --until", "payments.canSendStarGift")
|
|
243
|
+
|
|
244
|
+
limit, state = window(ctx, "gift.catalog", PageKind.LOCAL, default=20)
|
|
245
|
+
result = await client(ctx)(fn.GetStarGiftsRequest(hash=0))
|
|
246
|
+
rows = [_catalog_gift(gift) for gift in getattr(result, "gifts", None) or []]
|
|
247
|
+
if req.limited:
|
|
248
|
+
rows = [row for row in rows if row.limited]
|
|
249
|
+
if req.available:
|
|
250
|
+
rows = [row for row in rows if not row.sold_out]
|
|
251
|
+
offset = int(state.get("offset", 0) or 0)
|
|
252
|
+
window_rows = rows[offset : offset + limit]
|
|
253
|
+
return build_page(
|
|
254
|
+
window_rows,
|
|
255
|
+
op="gift.catalog",
|
|
256
|
+
kind=PageKind.LOCAL,
|
|
257
|
+
state={"offset": offset + len(window_rows)},
|
|
258
|
+
account=ctx.account,
|
|
259
|
+
has_more=offset + len(window_rows) < len(rows),
|
|
260
|
+
total=len(rows),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
SPEC_CATALOG = OperationSpec(
|
|
265
|
+
id="gift.catalog",
|
|
266
|
+
request=CatalogReq,
|
|
267
|
+
response=Page[StarGift],
|
|
268
|
+
impl=catalog,
|
|
269
|
+
summary="Browse the gifts on sale",
|
|
270
|
+
description=(
|
|
271
|
+
"Reading the catalogue is free; buying from it is absent, like every "
|
|
272
|
+
"purchase in tlgr. `--until` needs `payments.canSendStarGift`, which "
|
|
273
|
+
"this Telethon has no request class for, and refuses with exit 13."
|
|
274
|
+
),
|
|
275
|
+
paginated=PageKind.LOCAL,
|
|
276
|
+
idempotent=True,
|
|
277
|
+
columns=("gift_id", "title", "stars", "limited", "sold_out", "availability_remains"),
|
|
278
|
+
headers=("Id", "Title", "Stars", "Limited", "Sold out", "Left"),
|
|
279
|
+
example={
|
|
280
|
+
"items": [{"gift_id": 5100, "title": "Plush Pepe", "stars": 500, "limited": True}],
|
|
281
|
+
"has_more": False,
|
|
282
|
+
},
|
|
283
|
+
example_args="gift catalog --available",
|
|
284
|
+
covers=("gift.catalog", "gifts.catalog"),
|
|
285
|
+
tags=frozenset({"agent-safe"}),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
# gift list / get
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class ListReq(Request):
|
|
295
|
+
chat: Annotated[
|
|
296
|
+
PeerRef | None,
|
|
297
|
+
arg(0, metavar="PEER", required=False, kind="peer", help="Whose profile; default me."),
|
|
298
|
+
] = None
|
|
299
|
+
collection: Annotated[
|
|
300
|
+
int | None, opt("--collection", metavar="ID", help="Only this collection.")
|
|
301
|
+
] = None
|
|
302
|
+
sort: Annotated[str, opt("--sort", metavar="ORDER", help="date | value.")] = "date"
|
|
303
|
+
exclude_unsaved: Annotated[
|
|
304
|
+
bool, opt("--exclude-unsaved", help="Hide gifts not displayed on the profile.")
|
|
305
|
+
] = False
|
|
306
|
+
exclude_unique: Annotated[bool, opt("--exclude-unique", help="Hide collectibles.")] = False
|
|
307
|
+
exclude_hosted: Annotated[
|
|
308
|
+
bool, opt("--exclude-hosted", help="Hide TON-hosted collectibles.")
|
|
309
|
+
] = False
|
|
310
|
+
only_unique: Annotated[bool, opt("--only-unique", help="Only collectibles.")] = False
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def list_(ctx: OpContext, req: ListReq) -> Page[OwnedGift]:
|
|
314
|
+
"""Gifts a profile holds — mine, or somebody else's.
|
|
315
|
+
|
|
316
|
+
Another profile's gifts are only visible when they chose to display them,
|
|
317
|
+
so an empty list is not evidence that they have none.
|
|
318
|
+
"""
|
|
319
|
+
from telethon.tl.functions import payments as fn
|
|
320
|
+
|
|
321
|
+
limit, state = window(ctx, "gift.list", PageKind.RATE, default=20)
|
|
322
|
+
peer = await _peer_or_self(ctx, req.chat)
|
|
323
|
+
result = await client(ctx)(
|
|
324
|
+
fn.GetSavedStarGiftsRequest(
|
|
325
|
+
peer=peer,
|
|
326
|
+
offset=str(state.get("offset", "") or ""),
|
|
327
|
+
limit=limit,
|
|
328
|
+
exclude_unsaved=req.exclude_unsaved or None,
|
|
329
|
+
exclude_unique=req.exclude_unique or None,
|
|
330
|
+
exclude_hosted=req.exclude_hosted or None,
|
|
331
|
+
exclude_unlimited=None,
|
|
332
|
+
sort_by_value=(req.sort == "value") or None,
|
|
333
|
+
collection_id=req.collection,
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
known = _settings.entity_map(result)
|
|
337
|
+
rows = [_owned(row, known) for row in getattr(result, "gifts", None) or []]
|
|
338
|
+
if req.only_unique:
|
|
339
|
+
rows = [row for row in rows if row.kind == "collectible"]
|
|
340
|
+
next_offset = str(getattr(result, "next_offset", "") or "")
|
|
341
|
+
return build_page(
|
|
342
|
+
rows,
|
|
343
|
+
op="gift.list",
|
|
344
|
+
kind=PageKind.RATE,
|
|
345
|
+
state={"offset": next_offset},
|
|
346
|
+
account=ctx.account,
|
|
347
|
+
has_more=bool(next_offset),
|
|
348
|
+
total=getattr(result, "count", None),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
SPEC_LIST = OperationSpec(
|
|
353
|
+
id="gift.list",
|
|
354
|
+
request=ListReq,
|
|
355
|
+
response=Page[OwnedGift],
|
|
356
|
+
impl=list_,
|
|
357
|
+
summary="Gifts received by a profile (mine or someone else's)",
|
|
358
|
+
description=(
|
|
359
|
+
"`ref` is the handle every other gift command takes: `msg:<id>`, "
|
|
360
|
+
"`<peer>:<saved_id>` or a collectible slug."
|
|
361
|
+
),
|
|
362
|
+
paginated=PageKind.RATE,
|
|
363
|
+
idempotent=True,
|
|
364
|
+
columns=("ref", "kind", "title", "num", "from_id", "displayed", "pinned"),
|
|
365
|
+
headers=("Ref", "Kind", "Title", "#", "From", "Shown", "Pinned"),
|
|
366
|
+
example={
|
|
367
|
+
"items": [{"ref": "msg:120", "kind": "gift", "title": "Plush Pepe", "convert_stars": 250}],
|
|
368
|
+
"has_more": False,
|
|
369
|
+
},
|
|
370
|
+
example_args="gift list",
|
|
371
|
+
covers=("gift.hosted", "gift.received-list"),
|
|
372
|
+
tags=frozenset({"agent-safe"}),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class GetReq(Request):
|
|
377
|
+
ref: Annotated[str, arg(0, metavar="REF", help="msg:<id>, <peer>:<saved_id>, or a slug.")]
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
async def get(ctx: OpContext, req: GetReq) -> OwnedGift:
|
|
381
|
+
"""One owned gift, with every time gate the server publishes.
|
|
382
|
+
|
|
383
|
+
"Can I transfer this?" has three answers — yes, not yet (and here is
|
|
384
|
+
when), never — so `can_transfer_at`, `can_resell_at`, `can_export_at`,
|
|
385
|
+
`can_craft_at` and `can_upgrade` are all reported rather than collapsed
|
|
386
|
+
into one boolean.
|
|
387
|
+
"""
|
|
388
|
+
from telethon.tl.functions import payments as fn
|
|
389
|
+
|
|
390
|
+
handle = client(ctx)
|
|
391
|
+
stargift = await _settings.input_gift(ctx, req.ref)
|
|
392
|
+
if type(stargift).__name__ == "InputSavedStarGiftSlug":
|
|
393
|
+
result = await handle(fn.GetUniqueStarGiftRequest(slug=stargift.slug))
|
|
394
|
+
known = _settings.entity_map(result)
|
|
395
|
+
unique = _unique(getattr(result, "gift", None), known)
|
|
396
|
+
return OwnedGift(
|
|
397
|
+
ref=req.ref,
|
|
398
|
+
kind="collectible",
|
|
399
|
+
gift_id=unique.gift_id,
|
|
400
|
+
slug=unique.slug,
|
|
401
|
+
title=unique.title,
|
|
402
|
+
num=unique.num,
|
|
403
|
+
attributes=unique.attributes,
|
|
404
|
+
unique=unique,
|
|
405
|
+
hosted=unique.hosted,
|
|
406
|
+
resell_stars=unique.resell_stars,
|
|
407
|
+
resell_ton=unique.resell_ton,
|
|
408
|
+
)
|
|
409
|
+
result = await handle(fn.GetSavedStarGiftRequest(stargift=[stargift]))
|
|
410
|
+
known = _settings.entity_map(result)
|
|
411
|
+
gifts = getattr(result, "gifts", None) or []
|
|
412
|
+
if not gifts:
|
|
413
|
+
raise NotFoundError(f"no gift matches {req.ref!r}")
|
|
414
|
+
return _owned(gifts[0], known)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
SPEC_GET = OperationSpec(
|
|
418
|
+
id="gift.get",
|
|
419
|
+
request=GetReq,
|
|
420
|
+
response=OwnedGift,
|
|
421
|
+
impl=get,
|
|
422
|
+
summary="Details of one owned gift, including every time gate",
|
|
423
|
+
idempotent=True,
|
|
424
|
+
columns=("ref", "kind", "title", "convert_stars", "can_transfer_at", "can_resell_at"),
|
|
425
|
+
headers=("Ref", "Kind", "Title", "Convert", "Transfer at", "Resell at"),
|
|
426
|
+
example={"ref": "msg:120", "kind": "gift", "title": "Plush Pepe", "convert_stars": 250},
|
|
427
|
+
example_args="gift get msg:120",
|
|
428
|
+
covers=("gift.get-one", "gifts.get-one"),
|
|
429
|
+
tags=frozenset({"agent-safe"}),
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# ---------------------------------------------------------------------------
|
|
434
|
+
# gift set
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class SetReq(Request):
|
|
439
|
+
ref: Annotated[tuple[str, ...], arg(0, metavar="REF", variadic=True, help="Gift references.")]
|
|
440
|
+
peer: Annotated[
|
|
441
|
+
PeerRef | None,
|
|
442
|
+
opt("--peer", metavar="PEER", kind="peer", help="Profile the gift lives on."),
|
|
443
|
+
] = None
|
|
444
|
+
save: Annotated[bool, opt("--save", help="Display it on the profile.")] = False
|
|
445
|
+
unsave: Annotated[bool, opt("--unsave", help="Hide it from the profile.")] = False
|
|
446
|
+
pin: Annotated[bool, opt("--pin", help="Pin it to the top of the profile.")] = False
|
|
447
|
+
unpin: Annotated[bool, opt("--unpin", help="Unpin it.")] = False
|
|
448
|
+
pin_order: Annotated[
|
|
449
|
+
str | None,
|
|
450
|
+
opt("--pin-order", metavar="LIST", help="Replace the whole pinned set, in this order."),
|
|
451
|
+
] = None
|
|
452
|
+
wear: Annotated[bool, opt("--wear", help="Wear the collectible as my emoji status.")] = False
|
|
453
|
+
wear_off: Annotated[bool, opt("--wear-off", help="Stop wearing it.")] = False
|
|
454
|
+
until: Annotated[
|
|
455
|
+
str | None, opt("--until", metavar="WHEN", kind="datetime", help="Wear until this time.")
|
|
456
|
+
] = None
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
async def set_(ctx: OpContext, req: SetReq) -> GiftDisplay:
|
|
460
|
+
"""Show, hide, pin or wear a gift.
|
|
461
|
+
|
|
462
|
+
Pinning is a *set* operation on the server — `toggleStarGiftsPinnedToTop`
|
|
463
|
+
replaces the pinned collection — so `--pin` reads the current set and
|
|
464
|
+
adds to it, and `--pin-order` is the raw replace for when you mean it.
|
|
465
|
+
"""
|
|
466
|
+
from telethon.tl import types
|
|
467
|
+
from telethon.tl.functions import account as afn
|
|
468
|
+
from telethon.tl.functions import payments as fn
|
|
469
|
+
|
|
470
|
+
handle = client(ctx)
|
|
471
|
+
peer = await _peer_or_self(ctx, req.peer)
|
|
472
|
+
result = GiftDisplay(ref=req.ref[0] if req.ref else "", refs=list(req.ref))
|
|
473
|
+
|
|
474
|
+
if req.save or req.unsave:
|
|
475
|
+
for ref in req.ref:
|
|
476
|
+
await handle(
|
|
477
|
+
fn.SaveStarGiftRequest(
|
|
478
|
+
stargift=await _settings.input_gift(ctx, ref), unsave=req.unsave or None
|
|
479
|
+
)
|
|
480
|
+
)
|
|
481
|
+
result.displayed = not req.unsave
|
|
482
|
+
|
|
483
|
+
if req.pin or req.unpin or req.pin_order is not None:
|
|
484
|
+
wanted: list[str]
|
|
485
|
+
if req.pin_order is not None:
|
|
486
|
+
wanted = [part.strip() for part in req.pin_order.split(",") if part.strip()]
|
|
487
|
+
else:
|
|
488
|
+
page = await list_(ctx, ListReq(chat=req.peer))
|
|
489
|
+
pinned = [row.ref for row in page.items if row.pinned]
|
|
490
|
+
wanted = (
|
|
491
|
+
[ref for ref in pinned if ref not in req.ref] + list(req.ref)
|
|
492
|
+
if req.pin
|
|
493
|
+
else [ref for ref in pinned if ref not in req.ref]
|
|
494
|
+
)
|
|
495
|
+
await handle(
|
|
496
|
+
fn.ToggleStarGiftsPinnedToTopRequest(
|
|
497
|
+
peer=peer,
|
|
498
|
+
stargift=[await _settings.input_gift(ctx, ref) for ref in wanted],
|
|
499
|
+
)
|
|
500
|
+
)
|
|
501
|
+
result.pinned = bool(req.pin)
|
|
502
|
+
|
|
503
|
+
if req.wear or req.wear_off:
|
|
504
|
+
if req.wear_off:
|
|
505
|
+
await handle(afn.UpdateEmojiStatusRequest(emoji_status=types.EmojiStatusEmpty()))
|
|
506
|
+
result.worn = False
|
|
507
|
+
else:
|
|
508
|
+
gift = await get(ctx, GetReq(ref=req.ref[0]))
|
|
509
|
+
if gift.unique is None:
|
|
510
|
+
raise UsageError("only a collectible can be worn as an emoji status", field="ref")
|
|
511
|
+
until = parse_dt(req.until) if req.until else None
|
|
512
|
+
await handle(
|
|
513
|
+
afn.UpdateEmojiStatusRequest(
|
|
514
|
+
emoji_status=types.InputEmojiStatusCollectible(
|
|
515
|
+
collectible_id=gift.unique.id, until=until
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
result.worn = True
|
|
520
|
+
result.until = fmt_dt(until)
|
|
521
|
+
|
|
522
|
+
if result.displayed is None and result.pinned is None and result.worn is None:
|
|
523
|
+
raise UsageError(
|
|
524
|
+
"give --save/--unsave, --pin/--unpin/--pin-order or --wear/--wear-off",
|
|
525
|
+
field="save",
|
|
526
|
+
)
|
|
527
|
+
ctx.emit("gift_display", {"refs": list(req.ref)})
|
|
528
|
+
return result
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
SPEC_SET = OperationSpec(
|
|
532
|
+
id="gift.set",
|
|
533
|
+
request=SetReq,
|
|
534
|
+
response=GiftDisplay,
|
|
535
|
+
impl=set_,
|
|
536
|
+
summary="Profile display state of a gift: show/hide, pin, or wear it as my emoji status",
|
|
537
|
+
aliases=(
|
|
538
|
+
"gift.save",
|
|
539
|
+
"gift.unsave",
|
|
540
|
+
"gift.show",
|
|
541
|
+
"gift.hide",
|
|
542
|
+
"gift.pin",
|
|
543
|
+
"gift.unpin",
|
|
544
|
+
"gift.wear",
|
|
545
|
+
),
|
|
546
|
+
mutating=True,
|
|
547
|
+
idempotent=True,
|
|
548
|
+
rate_class="send",
|
|
549
|
+
columns=("ref", "displayed", "pinned", "worn", "until"),
|
|
550
|
+
headers=("Ref", "Shown", "Pinned", "Worn", "Until"),
|
|
551
|
+
example={"ref": "msg:120", "displayed": True},
|
|
552
|
+
example_args="gift set msg:120 --save",
|
|
553
|
+
covers=(
|
|
554
|
+
"gift.as-emoji-status",
|
|
555
|
+
"gift.display-toggle",
|
|
556
|
+
"gift.pin",
|
|
557
|
+
"gifts.drop-original-details",
|
|
558
|
+
"gifts.save-unsave",
|
|
559
|
+
),
|
|
560
|
+
tags=frozenset({"visible-to-others"}),
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
# ---------------------------------------------------------------------------
|
|
565
|
+
# gift convert / upgrade / transfer / craft
|
|
566
|
+
# ---------------------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
class ConvertReq(Request):
|
|
570
|
+
ref: Annotated[str, arg(0, metavar="REF", help="The gift to convert.")]
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
async def convert(ctx: OpContext, req: ConvertReq) -> GiftConverted:
|
|
574
|
+
"""Convert a received gift back into Stars.
|
|
575
|
+
|
|
576
|
+
Destructive but free: the gift is destroyed and Stars are credited, which
|
|
577
|
+
is why it is confirmed like a spend even though it earns.
|
|
578
|
+
"""
|
|
579
|
+
from telethon.tl.functions import payments as fn
|
|
580
|
+
|
|
581
|
+
handle = client(ctx)
|
|
582
|
+
before = await get(ctx, GetReq(ref=req.ref))
|
|
583
|
+
await handle(fn.ConvertStarGiftRequest(stargift=await _settings.input_gift(ctx, req.ref)))
|
|
584
|
+
ctx.emit("gift_converted", {"ref": req.ref})
|
|
585
|
+
balance = None
|
|
586
|
+
try:
|
|
587
|
+
from tlgr.ops.stars import BalanceGetReq, balance_get
|
|
588
|
+
|
|
589
|
+
balance = (await balance_get(ctx, BalanceGetReq())).stars
|
|
590
|
+
except Exception as exc: # pragma: no cover - the balance is a nicety
|
|
591
|
+
ctx.warn(f"could not read the Stars balance afterwards: {exc}")
|
|
592
|
+
return GiftConverted(
|
|
593
|
+
ref=req.ref, stars_received=before.convert_stars or 0, balance_after=balance
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
SPEC_CONVERT = OperationSpec(
|
|
598
|
+
id="gift.convert",
|
|
599
|
+
request=ConvertReq,
|
|
600
|
+
response=GiftConverted,
|
|
601
|
+
impl=convert,
|
|
602
|
+
summary="Convert a received gift back into Stars",
|
|
603
|
+
description="Free, and irreversible: the gift is gone and cannot be un-converted.",
|
|
604
|
+
mutating=True,
|
|
605
|
+
destructive=True,
|
|
606
|
+
rate_class="send",
|
|
607
|
+
columns=("ref", "stars_received", "balance_after"),
|
|
608
|
+
headers=("Ref", "Stars", "Balance"),
|
|
609
|
+
example={"ref": "msg:120", "stars_received": 250, "balance_after": 500},
|
|
610
|
+
example_args="gift convert msg:120",
|
|
611
|
+
covers=("gift.convert-to-stars",),
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class UpgradeReq(Request):
|
|
616
|
+
ref: Annotated[str, arg(0, metavar="REF", help="The gift to upgrade.")]
|
|
617
|
+
keep_original_details: Annotated[
|
|
618
|
+
bool, opt("--keep-original-details", help="Keep the sender and message on it.")
|
|
619
|
+
] = False
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
async def upgrade(ctx: OpContext, req: UpgradeReq) -> GiftUpgraded:
|
|
623
|
+
"""Upgrade a gift into a collectible, on the free or prepaid path.
|
|
624
|
+
|
|
625
|
+
When the upgrade is prepaid — `savedStarGift.upgrade_stars` is set, or
|
|
626
|
+
the service message carried `prepaid_upgrade` — it is a plain method
|
|
627
|
+
call and tlgr makes it. When it would need a payment form signed, tlgr
|
|
628
|
+
prints the price and refuses, because signing forms is absent from the
|
|
629
|
+
whole surface.
|
|
630
|
+
"""
|
|
631
|
+
from telethon.tl.functions import payments as fn
|
|
632
|
+
|
|
633
|
+
handle = client(ctx)
|
|
634
|
+
before = await get(ctx, GetReq(ref=req.ref))
|
|
635
|
+
if before.upgrade_stars is None and not before.can_upgrade:
|
|
636
|
+
return GiftUpgraded(
|
|
637
|
+
ref=req.ref,
|
|
638
|
+
upgraded=False,
|
|
639
|
+
price_stars=before.upgrade_stars,
|
|
640
|
+
refused_reason=(
|
|
641
|
+
"this gift's upgrade is not prepaid, so it needs a payment form. "
|
|
642
|
+
+ _settings.NO_SPEND
|
|
643
|
+
),
|
|
644
|
+
)
|
|
645
|
+
result = await handle(
|
|
646
|
+
fn.UpgradeStarGiftRequest(
|
|
647
|
+
stargift=await _settings.input_gift(ctx, req.ref),
|
|
648
|
+
keep_original_details=req.keep_original_details or None,
|
|
649
|
+
)
|
|
650
|
+
)
|
|
651
|
+
unique = _find_unique(result)
|
|
652
|
+
ctx.emit("gift_upgraded", {"ref": req.ref})
|
|
653
|
+
return GiftUpgraded(
|
|
654
|
+
ref=req.ref,
|
|
655
|
+
slug=unique.slug if unique is not None else None,
|
|
656
|
+
num=unique.num if unique is not None else None,
|
|
657
|
+
attributes=unique.attributes if unique is not None else [],
|
|
658
|
+
upgraded=True,
|
|
659
|
+
price_stars=before.upgrade_stars,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _find_unique(updates: Any) -> UniqueGift | None:
|
|
664
|
+
"""The `starGiftUnique` an `Updates` container carries, if any."""
|
|
665
|
+
for update in getattr(updates, "updates", None) or []:
|
|
666
|
+
for holder in (update, getattr(update, "message", None)):
|
|
667
|
+
action = getattr(holder, "action", None)
|
|
668
|
+
gift = getattr(action, "gift", None) or getattr(holder, "gift", None)
|
|
669
|
+
if type(gift).__name__ == "StarGiftUnique":
|
|
670
|
+
return _unique(gift)
|
|
671
|
+
return None
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
SPEC_UPGRADE = OperationSpec(
|
|
675
|
+
id="gift.upgrade",
|
|
676
|
+
request=UpgradeReq,
|
|
677
|
+
response=GiftUpgraded,
|
|
678
|
+
impl=upgrade,
|
|
679
|
+
summary="Upgrade a gift into a collectible (free/prepaid path only)",
|
|
680
|
+
mutating=True,
|
|
681
|
+
rate_class="send",
|
|
682
|
+
columns=("ref", "slug", "num", "upgraded", "price_stars", "refused_reason"),
|
|
683
|
+
headers=("Ref", "Slug", "#", "Upgraded", "Price", "Why not"),
|
|
684
|
+
example={"ref": "msg:120", "slug": "PlushPepe-42", "num": 42, "upgraded": True},
|
|
685
|
+
example_args="gift upgrade msg:120",
|
|
686
|
+
covers=("gift.upgrade", "gifts.upgrade", "gifts.upgrade-preview"),
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
class TransferReq(Request):
|
|
691
|
+
ref: Annotated[str, arg(0, metavar="REF", help="The collectible to transfer.")]
|
|
692
|
+
chat: Annotated[PeerRef, arg(1, metavar="PEER", kind="peer", help="Who to transfer it to.")]
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
async def transfer(ctx: OpContext, req: TransferReq) -> GiftTransferred:
|
|
696
|
+
"""Transfer a collectible to another peer, when the transfer is free.
|
|
697
|
+
|
|
698
|
+
A paid transfer goes through `inputInvoiceStarGiftTransfer` and a payment
|
|
699
|
+
form; tlgr prints the price and refuses that path, as it does everywhere
|
|
700
|
+
else.
|
|
701
|
+
"""
|
|
702
|
+
from telethon.tl.functions import payments as fn
|
|
703
|
+
|
|
704
|
+
handle = client(ctx)
|
|
705
|
+
before = await get(ctx, GetReq(ref=req.ref))
|
|
706
|
+
to_peer = await _settings.resolve(ctx, req.chat)
|
|
707
|
+
if before.transfer_stars:
|
|
708
|
+
return GiftTransferred(
|
|
709
|
+
ref=req.ref,
|
|
710
|
+
to=_settings.peer_of(to_peer),
|
|
711
|
+
transferred=False,
|
|
712
|
+
price_stars=before.transfer_stars,
|
|
713
|
+
can_transfer_at=before.can_transfer_at,
|
|
714
|
+
refused_reason="this transfer costs Stars. " + _settings.NO_SPEND,
|
|
715
|
+
)
|
|
716
|
+
await handle(
|
|
717
|
+
fn.TransferStarGiftRequest(stargift=await _settings.input_gift(ctx, req.ref), to_id=to_peer)
|
|
718
|
+
)
|
|
719
|
+
ctx.emit("gift_transferred", {"ref": req.ref, "to": _settings.peer_of(to_peer)})
|
|
720
|
+
return GiftTransferred(
|
|
721
|
+
ref=req.ref,
|
|
722
|
+
to=_settings.peer_of(to_peer),
|
|
723
|
+
transferred=True,
|
|
724
|
+
can_transfer_at=before.can_transfer_at,
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
SPEC_TRANSFER = OperationSpec(
|
|
729
|
+
id="gift.transfer",
|
|
730
|
+
request=TransferReq,
|
|
731
|
+
response=GiftTransferred,
|
|
732
|
+
impl=transfer,
|
|
733
|
+
summary="Transfer a collectible gift to another peer (free transfers only)",
|
|
734
|
+
mutating=True,
|
|
735
|
+
destructive=True,
|
|
736
|
+
rate_class="send",
|
|
737
|
+
columns=("ref", "to", "transferred", "price_stars", "refused_reason"),
|
|
738
|
+
headers=("Ref", "To", "Done", "Price", "Why not"),
|
|
739
|
+
example={"ref": "PlushPepe-42", "to": 777123, "transferred": True},
|
|
740
|
+
example_args="gift transfer PlushPepe-42 @alice",
|
|
741
|
+
covers=("gift.transfer",),
|
|
742
|
+
tags=frozenset({"visible-to-others"}),
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
class CraftReq(Request):
|
|
747
|
+
ref: Annotated[
|
|
748
|
+
tuple[str, ...],
|
|
749
|
+
arg(0, metavar="REF", required=False, variadic=True, help="The gifts to melt down."),
|
|
750
|
+
] = ()
|
|
751
|
+
candidates: Annotated[
|
|
752
|
+
int | None,
|
|
753
|
+
opt("--candidates", metavar="GIFT_ID", help="List my gifts usable to craft this one."),
|
|
754
|
+
] = None
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
async def craft(ctx: OpContext, req: CraftReq) -> GiftCrafted:
|
|
758
|
+
"""Craft (combine) collectible gifts.
|
|
759
|
+
|
|
760
|
+
`payments.craftStarGift` burns **every** input gift regardless of the
|
|
761
|
+
outcome, which is why this is destructive and confirmed even though it
|
|
762
|
+
costs nothing: a failed craft is still four gifts gone.
|
|
763
|
+
"""
|
|
764
|
+
from telethon.tl.functions import payments as fn
|
|
765
|
+
|
|
766
|
+
if req.candidates is not None:
|
|
767
|
+
_settings.method_gap("gift craft --candidates", "payments.getStarGiftCraftCandidates")
|
|
768
|
+
if not req.ref:
|
|
769
|
+
raise UsageError("give the gift references to melt down", field="ref")
|
|
770
|
+
|
|
771
|
+
handle = client(ctx)
|
|
772
|
+
inputs = [await _settings.input_gift(ctx, ref) for ref in req.ref]
|
|
773
|
+
result = await handle(fn.CraftStarGiftRequest(stargift=inputs))
|
|
774
|
+
unique = _find_unique(result)
|
|
775
|
+
ctx.emit("gift_crafted", {"burned": list(req.ref)})
|
|
776
|
+
return GiftCrafted(
|
|
777
|
+
ref=f"slug:{unique.slug}" if unique is not None else None,
|
|
778
|
+
slug=unique.slug if unique is not None else None,
|
|
779
|
+
burned=list(req.ref),
|
|
780
|
+
crafted=True,
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
SPEC_CRAFT = OperationSpec(
|
|
785
|
+
id="gift.craft",
|
|
786
|
+
request=CraftReq,
|
|
787
|
+
response=GiftCrafted,
|
|
788
|
+
impl=craft,
|
|
789
|
+
summary="Craft (combine) collectible gifts",
|
|
790
|
+
description=(
|
|
791
|
+
"Every input gift is burned whatever the outcome, so `--yes` is "
|
|
792
|
+
"required and `--dry-run` prints exactly what would be consumed."
|
|
793
|
+
),
|
|
794
|
+
mutating=True,
|
|
795
|
+
destructive=True,
|
|
796
|
+
rate_class="send",
|
|
797
|
+
columns=("slug", "burned", "crafted"),
|
|
798
|
+
headers=("Result", "Burned", "Crafted"),
|
|
799
|
+
example={"slug": "PlushPepe-77", "burned": ["msg:120", "msg:121"], "crafted": True},
|
|
800
|
+
example_args="gift craft msg:120 msg:121",
|
|
801
|
+
covers=("gift.craft", "gifts.craft"),
|
|
802
|
+
covers_partial=("gift.craft-candidates",),
|
|
803
|
+
coverage_note=(
|
|
804
|
+
"Listing the eligible ingredients needs `payments.getStarGiftCraftCandidates`, "
|
|
805
|
+
"which Telethon 1.44 has no request class for; the flag refuses with exit 13."
|
|
806
|
+
),
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
# ---------------------------------------------------------------------------
|
|
811
|
+
# gift resale list / set, offer approve
|
|
812
|
+
# ---------------------------------------------------------------------------
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
class ResaleListReq(Request):
|
|
816
|
+
gift_id: Annotated[int, arg(0, metavar="GIFT_ID", help="The gift type to browse.")]
|
|
817
|
+
sort: Annotated[str, opt("--sort", metavar="ORDER", help="price | num | date.")] = "price"
|
|
818
|
+
ton_only: Annotated[bool, opt("--ton-only", help="Only TON listings.")] = False
|
|
819
|
+
stars_only: Annotated[bool, opt("--stars-only", help="Only Stars listings.")] = False
|
|
820
|
+
attr: Annotated[
|
|
821
|
+
tuple[str, ...],
|
|
822
|
+
opt("--attr", metavar="KIND=ID", help="Filter by model/pattern/backdrop."),
|
|
823
|
+
] = ()
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
async def resale_list(ctx: OpContext, req: ResaleListReq) -> Page[ResaleGift]:
|
|
827
|
+
"""Browse the collectible marketplace for one gift.
|
|
828
|
+
|
|
829
|
+
Reading prices is free; buying from the marketplace is a purchase and is
|
|
830
|
+
absent, so this is where a price check ends.
|
|
831
|
+
"""
|
|
832
|
+
from telethon.tl import types
|
|
833
|
+
from telethon.tl.functions import payments as fn
|
|
834
|
+
|
|
835
|
+
limit, state = window(ctx, "gift.resale.list", PageKind.RATE, default=20)
|
|
836
|
+
attributes = []
|
|
837
|
+
for entry in req.attr:
|
|
838
|
+
kind, _, value = entry.partition("=")
|
|
839
|
+
if not value.isdigit():
|
|
840
|
+
raise UsageError("--attr takes model=<id>, pattern=<id> or backdrop=<id>", field="attr")
|
|
841
|
+
builder = {
|
|
842
|
+
"model": types.StarGiftAttributeIdModel,
|
|
843
|
+
"pattern": types.StarGiftAttributeIdPattern,
|
|
844
|
+
"backdrop": types.StarGiftAttributeIdBackdrop,
|
|
845
|
+
}.get(kind.strip().lower())
|
|
846
|
+
if builder is None:
|
|
847
|
+
raise UsageError("--attr takes model=, pattern= or backdrop=", field="attr")
|
|
848
|
+
attributes.append(
|
|
849
|
+
builder(backdrop_id=int(value))
|
|
850
|
+
if kind.strip().lower() == "backdrop"
|
|
851
|
+
else builder(document_id=int(value))
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
result = await client(ctx)(
|
|
855
|
+
fn.GetResaleStarGiftsRequest(
|
|
856
|
+
gift_id=int(req.gift_id),
|
|
857
|
+
offset=str(state.get("offset", "") or ""),
|
|
858
|
+
limit=limit,
|
|
859
|
+
sort_by_price=(req.sort == "price") or None,
|
|
860
|
+
sort_by_num=(req.sort == "num") or None,
|
|
861
|
+
stars_only=req.stars_only or None,
|
|
862
|
+
attributes=attributes or None,
|
|
863
|
+
)
|
|
864
|
+
)
|
|
865
|
+
known = _settings.entity_map(result)
|
|
866
|
+
rows = []
|
|
867
|
+
for gift in getattr(result, "gifts", None) or []:
|
|
868
|
+
unique = _unique(gift, known)
|
|
869
|
+
if req.ton_only and unique.resell_ton is None:
|
|
870
|
+
continue
|
|
871
|
+
rows.append(
|
|
872
|
+
ResaleGift(
|
|
873
|
+
slug=unique.slug,
|
|
874
|
+
num=unique.num,
|
|
875
|
+
price_stars=unique.resell_stars,
|
|
876
|
+
price_ton=unique.resell_ton,
|
|
877
|
+
seller_id=unique.owner_id,
|
|
878
|
+
attributes=unique.attributes,
|
|
879
|
+
)
|
|
880
|
+
)
|
|
881
|
+
next_offset = str(getattr(result, "next_offset", "") or "")
|
|
882
|
+
return build_page(
|
|
883
|
+
rows,
|
|
884
|
+
op="gift.resale.list",
|
|
885
|
+
kind=PageKind.RATE,
|
|
886
|
+
state={"offset": next_offset},
|
|
887
|
+
account=ctx.account,
|
|
888
|
+
has_more=bool(next_offset),
|
|
889
|
+
total=getattr(result, "count", None),
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
SPEC_RESALE_LIST = OperationSpec(
|
|
894
|
+
id="gift.resale.list",
|
|
895
|
+
request=ResaleListReq,
|
|
896
|
+
response=Page[ResaleGift],
|
|
897
|
+
impl=resale_list,
|
|
898
|
+
summary="Browse the collectible marketplace for one gift",
|
|
899
|
+
paginated=PageKind.RATE,
|
|
900
|
+
idempotent=True,
|
|
901
|
+
columns=("slug", "num", "price_stars", "price_ton", "seller_id"),
|
|
902
|
+
headers=("Slug", "#", "Stars", "TON", "Seller"),
|
|
903
|
+
example={
|
|
904
|
+
"items": [{"slug": "PlushPepe-42", "num": 42, "price_stars": 12000}],
|
|
905
|
+
"has_more": False,
|
|
906
|
+
},
|
|
907
|
+
example_args="gift resale list 5100",
|
|
908
|
+
covers=("gift.resale-browse", "gifts.resale-price"),
|
|
909
|
+
tags=frozenset({"agent-safe"}),
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
class ResaleSetReq(Request):
|
|
914
|
+
ref: Annotated[str, arg(0, metavar="REF", help="My collectible.")]
|
|
915
|
+
stars: Annotated[int | None, opt("--stars", metavar="N", help="Asking price in Stars.")] = None
|
|
916
|
+
ton: Annotated[int | None, opt("--ton", metavar="NANO", help="Asking price in nanotons.")] = (
|
|
917
|
+
None
|
|
918
|
+
)
|
|
919
|
+
unlist: Annotated[bool, opt("--unlist", help="Take it off the market.")] = False
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
async def resale_set(ctx: OpContext, req: ResaleSetReq) -> GiftListing:
|
|
923
|
+
"""Put one of my collectibles up for sale, or take it off the market.
|
|
924
|
+
|
|
925
|
+
Listing is free — it earns rather than spends — and the sale itself
|
|
926
|
+
happens when a buyer pays, which is somebody else's payment form and not
|
|
927
|
+
tlgr's.
|
|
928
|
+
"""
|
|
929
|
+
from telethon.tl import types
|
|
930
|
+
from telethon.tl.functions import payments as fn
|
|
931
|
+
|
|
932
|
+
handle = client(ctx)
|
|
933
|
+
if req.unlist:
|
|
934
|
+
amount: Any = types.StarsAmount(amount=0, nanos=0)
|
|
935
|
+
elif req.ton is not None:
|
|
936
|
+
amount = types.StarsTonAmount(amount=int(req.ton))
|
|
937
|
+
elif req.stars is not None:
|
|
938
|
+
amount = types.StarsAmount(amount=int(req.stars), nanos=0)
|
|
939
|
+
else:
|
|
940
|
+
raise UsageError("give --stars, --ton, or --unlist", field="stars")
|
|
941
|
+
|
|
942
|
+
before = await get(ctx, GetReq(ref=req.ref))
|
|
943
|
+
await handle(
|
|
944
|
+
fn.UpdateStarGiftPriceRequest(
|
|
945
|
+
stargift=await _settings.input_gift(ctx, req.ref), resell_amount=amount
|
|
946
|
+
)
|
|
947
|
+
)
|
|
948
|
+
ctx.emit("gift_listed", {"ref": req.ref, "unlist": req.unlist})
|
|
949
|
+
return GiftListing(
|
|
950
|
+
ref=req.ref,
|
|
951
|
+
listed=not req.unlist,
|
|
952
|
+
price_stars=None if req.unlist else req.stars,
|
|
953
|
+
price_ton=None if req.unlist else req.ton,
|
|
954
|
+
can_resell_at=before.can_resell_at,
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
SPEC_RESALE_SET = OperationSpec(
|
|
959
|
+
id="gift.resale.set",
|
|
960
|
+
request=ResaleSetReq,
|
|
961
|
+
response=GiftListing,
|
|
962
|
+
impl=resale_set,
|
|
963
|
+
summary="Put one of my collectibles up for sale, or take it off the market",
|
|
964
|
+
mutating=True,
|
|
965
|
+
idempotent=True,
|
|
966
|
+
rate_class="send",
|
|
967
|
+
columns=("ref", "listed", "price_stars", "price_ton", "can_resell_at"),
|
|
968
|
+
headers=("Ref", "Listed", "Stars", "TON", "Sellable at"),
|
|
969
|
+
example={"ref": "PlushPepe-42", "listed": True, "price_stars": 12000},
|
|
970
|
+
example_args="gift resale set PlushPepe-42 --stars 12000",
|
|
971
|
+
covers=("gift.resale-list-mine", "gifts.resale-buy"),
|
|
972
|
+
tags=frozenset({"visible-to-others"}),
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
class OfferApproveReq(Request):
|
|
977
|
+
chat: Annotated[PeerRef, arg(0, metavar="CHAT", kind="peer", help="Where the offer arrived.")]
|
|
978
|
+
msg_id: Annotated[int, arg(1, metavar="MSG_ID", kind="msg_id", help="The offer message.")]
|
|
979
|
+
deny: Annotated[bool, opt("--deny", help="Decline the offer (always free).")] = False
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
async def offer_approve(ctx: OpContext, req: OfferApproveReq) -> GiftOfferResolved:
|
|
983
|
+
"""Decline somebody's offer to buy my collectible — or report the price.
|
|
984
|
+
|
|
985
|
+
Declining is free and is performed. Accepting sells the asset for Stars:
|
|
986
|
+
that is a financial transfer, and tlgr reports the offer instead of
|
|
987
|
+
completing it, in the same way it never signs a payment form.
|
|
988
|
+
"""
|
|
989
|
+
from telethon.tl.functions import payments as fn
|
|
990
|
+
|
|
991
|
+
handle = client(ctx)
|
|
992
|
+
await _settings.resolve(ctx, req.chat)
|
|
993
|
+
if not req.deny:
|
|
994
|
+
return GiftOfferResolved(
|
|
995
|
+
msg_id=int(req.msg_id),
|
|
996
|
+
state="refused",
|
|
997
|
+
reason="accepting an offer sells the collectible for Stars. " + _settings.NO_SPEND,
|
|
998
|
+
)
|
|
999
|
+
await handle(fn.ResolveStarGiftOfferRequest(offer_msg_id=int(req.msg_id), decline=True))
|
|
1000
|
+
ctx.emit("gift_offer", {"msg_id": int(req.msg_id), "declined": True})
|
|
1001
|
+
return GiftOfferResolved(msg_id=int(req.msg_id), state="declined")
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
SPEC_OFFER_APPROVE = OperationSpec(
|
|
1005
|
+
id="gift.offer.approve",
|
|
1006
|
+
request=OfferApproveReq,
|
|
1007
|
+
response=GiftOfferResolved,
|
|
1008
|
+
impl=offer_approve,
|
|
1009
|
+
summary="Decline an offer to buy my collectible (accepting sells an asset and is refused)",
|
|
1010
|
+
aliases=("gift.offer.deny", "gift.offer.decline"),
|
|
1011
|
+
mutating=True,
|
|
1012
|
+
destructive=True,
|
|
1013
|
+
rate_class="send",
|
|
1014
|
+
columns=("msg_id", "state", "price_stars", "reason"),
|
|
1015
|
+
headers=("Message", "State", "Stars", "Why"),
|
|
1016
|
+
example={"msg_id": 512, "state": "declined"},
|
|
1017
|
+
example_args="gift offer approve @alice 512 --deny",
|
|
1018
|
+
covers_partial=("gift.offer-resolve", "gifts.purchase-offer"),
|
|
1019
|
+
coverage_note=(
|
|
1020
|
+
"Declining is performed; accepting transfers an asset for money and is "
|
|
1021
|
+
"reported rather than done, like every other value transfer in tlgr."
|
|
1022
|
+
),
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
# ---------------------------------------------------------------------------
|
|
1027
|
+
# gift unique get / variant list
|
|
1028
|
+
# ---------------------------------------------------------------------------
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
class UniqueGetReq(Request):
|
|
1032
|
+
slug: Annotated[str, arg(0, metavar="SLUG", help="A slug or a t.me/nft/ link.")]
|
|
1033
|
+
value: Annotated[bool, opt("--value", help="Include the valuation the gift carries.")] = False
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
async def unique_get(ctx: OpContext, req: UniqueGetReq) -> UniqueGift:
|
|
1037
|
+
"""Look up a collectible by link or slug.
|
|
1038
|
+
|
|
1039
|
+
The full valuation breakdown — floor price, last sale — needs
|
|
1040
|
+
`payments.getStarGiftValueInfo`, which this Telethon has no request class
|
|
1041
|
+
for; the gift itself carries `value_amount`/`value_currency`, and that is
|
|
1042
|
+
what `--value` reports, with a warning saying what is missing.
|
|
1043
|
+
"""
|
|
1044
|
+
from telethon.tl.functions import payments as fn
|
|
1045
|
+
|
|
1046
|
+
result = await client(ctx)(fn.GetUniqueStarGiftRequest(slug=_settings.slug_of(req.slug)))
|
|
1047
|
+
known = _settings.entity_map(result)
|
|
1048
|
+
gift = _unique(getattr(result, "gift", None), known)
|
|
1049
|
+
if req.value:
|
|
1050
|
+
ctx.warn(
|
|
1051
|
+
"the floor price and last sale need payments.getStarGiftValueInfo, which "
|
|
1052
|
+
"Telethon 1.44 has no request class for; the gift's own valuation is reported"
|
|
1053
|
+
)
|
|
1054
|
+
return gift
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
SPEC_UNIQUE_GET = OperationSpec(
|
|
1058
|
+
id="gift.unique.get",
|
|
1059
|
+
request=UniqueGetReq,
|
|
1060
|
+
response=UniqueGift,
|
|
1061
|
+
impl=unique_get,
|
|
1062
|
+
summary="Look up a collectible by link or slug, with the valuation it carries",
|
|
1063
|
+
idempotent=True,
|
|
1064
|
+
columns=("slug", "num", "title", "owner_id", "value_stars", "resell_stars"),
|
|
1065
|
+
headers=("Slug", "#", "Title", "Owner", "Value", "For sale"),
|
|
1066
|
+
example={"slug": "PlushPepe-42", "num": 42, "title": "Plush Pepe", "value_stars": 15000},
|
|
1067
|
+
example_args="gift unique get PlushPepe-42",
|
|
1068
|
+
covers=("gift.unique-info", "gifts.unique-info"),
|
|
1069
|
+
covers_partial=("gift.unique-value",),
|
|
1070
|
+
coverage_note=(
|
|
1071
|
+
"The gift's own `value_amount`/`value_currency` are reported; the floor "
|
|
1072
|
+
"price and last sale need `payments.getStarGiftValueInfo`, absent from "
|
|
1073
|
+
"Telethon 1.44."
|
|
1074
|
+
),
|
|
1075
|
+
tags=frozenset({"agent-safe"}),
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
class VariantListReq(Request):
|
|
1080
|
+
gift_id: Annotated[int, arg(0, metavar="GIFT_ID", help="The gift type.")]
|
|
1081
|
+
preview: Annotated[
|
|
1082
|
+
bool, opt("--preview", help="Sample attributes an upgrade could produce.")
|
|
1083
|
+
] = False
|
|
1084
|
+
craft_only: Annotated[
|
|
1085
|
+
bool, opt("--craft-only", help="Only variants reachable by crafting.")
|
|
1086
|
+
] = False
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
async def variant_list(ctx: OpContext, req: VariantListReq) -> Page[GiftVariant]:
|
|
1090
|
+
"""Possible collectible variants of a gift, and their rarities.
|
|
1091
|
+
|
|
1092
|
+
`payments.getStarGiftUpgradePreview` is what this build can send; the
|
|
1093
|
+
full attribute table (`payments.getStarGiftAttributes`) has no request
|
|
1094
|
+
class here, so `--craft-only`, which only that method can answer,
|
|
1095
|
+
refuses rather than returning a filtered guess.
|
|
1096
|
+
"""
|
|
1097
|
+
from telethon.tl.functions import payments as fn
|
|
1098
|
+
|
|
1099
|
+
if req.craft_only:
|
|
1100
|
+
_settings.method_gap("gift variant list --craft-only", "payments.getStarGiftAttributes")
|
|
1101
|
+
|
|
1102
|
+
limit, state = window(ctx, "gift.variant.list", PageKind.LOCAL, default=20)
|
|
1103
|
+
result = await client(ctx)(fn.GetStarGiftUpgradePreviewRequest(gift_id=int(req.gift_id)))
|
|
1104
|
+
rows = [
|
|
1105
|
+
GiftVariant(
|
|
1106
|
+
kind=attribute.kind,
|
|
1107
|
+
name=attribute.name,
|
|
1108
|
+
document_id=attribute.document_id,
|
|
1109
|
+
rarity_permille=attribute.rarity_permille,
|
|
1110
|
+
sample=True,
|
|
1111
|
+
)
|
|
1112
|
+
for attribute in (
|
|
1113
|
+
_attribute(raw) for raw in getattr(result, "sample_attributes", None) or []
|
|
1114
|
+
)
|
|
1115
|
+
]
|
|
1116
|
+
offset = int(state.get("offset", 0) or 0)
|
|
1117
|
+
page_rows = rows[offset : offset + limit]
|
|
1118
|
+
return build_page(
|
|
1119
|
+
page_rows,
|
|
1120
|
+
op="gift.variant.list",
|
|
1121
|
+
kind=PageKind.LOCAL,
|
|
1122
|
+
state={"offset": offset + len(page_rows)},
|
|
1123
|
+
account=ctx.account,
|
|
1124
|
+
has_more=offset + len(page_rows) < len(rows),
|
|
1125
|
+
total=len(rows),
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
SPEC_VARIANT_LIST = OperationSpec(
|
|
1130
|
+
id="gift.variant.list",
|
|
1131
|
+
request=VariantListReq,
|
|
1132
|
+
response=Page[GiftVariant],
|
|
1133
|
+
impl=variant_list,
|
|
1134
|
+
summary="Possible collectible variants of a gift, and a preview of an upgrade",
|
|
1135
|
+
aliases=("gift.variants",),
|
|
1136
|
+
paginated=PageKind.LOCAL,
|
|
1137
|
+
idempotent=True,
|
|
1138
|
+
columns=("kind", "name", "rarity_permille", "document_id", "sample"),
|
|
1139
|
+
headers=("Kind", "Name", "Rarity ‰", "Document", "Sample"),
|
|
1140
|
+
example={
|
|
1141
|
+
"items": [{"kind": "model", "name": "Golden", "rarity_permille": 5, "sample": True}],
|
|
1142
|
+
"has_more": False,
|
|
1143
|
+
},
|
|
1144
|
+
example_args="gift variant list 5100 --preview",
|
|
1145
|
+
covers=("gift.upgrade-preview",),
|
|
1146
|
+
covers_partial=("gift.upgrade-attributes",),
|
|
1147
|
+
coverage_note=(
|
|
1148
|
+
"The upgrade preview is the sample the server offers; the exhaustive "
|
|
1149
|
+
"attribute table needs `payments.getStarGiftAttributes`, absent from "
|
|
1150
|
+
"Telethon 1.44."
|
|
1151
|
+
),
|
|
1152
|
+
tags=frozenset({"agent-safe"}),
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
# ---------------------------------------------------------------------------
|
|
1157
|
+
# gift collection list / create / edit / delete
|
|
1158
|
+
# ---------------------------------------------------------------------------
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def _collection(raw: Any, order: int) -> GiftCollection:
|
|
1162
|
+
return GiftCollection(
|
|
1163
|
+
id=int(getattr(raw, "collection_id", 0) or 0),
|
|
1164
|
+
title=str(getattr(raw, "title", "") or ""),
|
|
1165
|
+
count=int(getattr(raw, "gifts_count", 0) or 0),
|
|
1166
|
+
icon_document_id=getattr(getattr(raw, "icon", None), "id", None),
|
|
1167
|
+
order=order,
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
class CollectionListReq(Request):
|
|
1172
|
+
chat: Annotated[
|
|
1173
|
+
PeerRef | None,
|
|
1174
|
+
arg(0, metavar="PEER", required=False, kind="peer", help="Whose profile; default me."),
|
|
1175
|
+
] = None
|
|
1176
|
+
refresh: Annotated[bool, opt("--refresh", help="Ignore the cached hash.")] = False
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
async def collection_list(ctx: OpContext, req: CollectionListReq) -> Page[GiftCollection]:
|
|
1180
|
+
"""Gift collections on a profile, in display order."""
|
|
1181
|
+
from telethon.tl.functions import payments as fn
|
|
1182
|
+
|
|
1183
|
+
peer = await _peer_or_self(ctx, req.chat)
|
|
1184
|
+
result = await client(ctx)(fn.GetStarGiftCollectionsRequest(peer=peer, hash=0))
|
|
1185
|
+
rows = [
|
|
1186
|
+
_collection(raw, index)
|
|
1187
|
+
for index, raw in enumerate(getattr(result, "collections", None) or [])
|
|
1188
|
+
]
|
|
1189
|
+
return Page(items=rows, has_more=False, total=len(rows))
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
SPEC_COLLECTION_LIST = OperationSpec(
|
|
1193
|
+
id="gift.collection.list",
|
|
1194
|
+
request=CollectionListReq,
|
|
1195
|
+
response=Page[GiftCollection],
|
|
1196
|
+
impl=collection_list,
|
|
1197
|
+
summary="Gift collections on a profile",
|
|
1198
|
+
paginated=PageKind.LOCAL,
|
|
1199
|
+
idempotent=True,
|
|
1200
|
+
columns=("id", "title", "count", "order"),
|
|
1201
|
+
headers=("Id", "Title", "Gifts", "Order"),
|
|
1202
|
+
example={"items": [{"id": 1, "title": "Favourites", "count": 4}], "has_more": False},
|
|
1203
|
+
example_args="gift collection list",
|
|
1204
|
+
covers=("gift.collections-list", "gifts.collections"),
|
|
1205
|
+
tags=frozenset({"agent-safe"}),
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
class CollectionCreateReq(Request):
|
|
1210
|
+
chat: Annotated[PeerRef, arg(0, metavar="PEER", kind="peer", help="Whose profile.")]
|
|
1211
|
+
title: Annotated[str, arg(1, metavar="TITLE", help="The collection's name.")]
|
|
1212
|
+
refs: Annotated[
|
|
1213
|
+
tuple[str, ...],
|
|
1214
|
+
arg(2, metavar="REF", required=False, variadic=True, help="Gifts to put in it."),
|
|
1215
|
+
] = ()
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
async def collection_create(ctx: OpContext, req: CollectionCreateReq) -> GiftCollection:
|
|
1219
|
+
"""Create a gift collection. `stargifts_collections_max` bounds how many."""
|
|
1220
|
+
from telethon.tl.functions import payments as fn
|
|
1221
|
+
|
|
1222
|
+
peer = await _settings.resolve(ctx, req.chat)
|
|
1223
|
+
result = await client(ctx)(
|
|
1224
|
+
fn.CreateStarGiftCollectionRequest(
|
|
1225
|
+
peer=peer,
|
|
1226
|
+
title=req.title,
|
|
1227
|
+
stargift=[await _settings.input_gift(ctx, ref) for ref in req.refs],
|
|
1228
|
+
)
|
|
1229
|
+
)
|
|
1230
|
+
ctx.emit("gift_collection", {"title": req.title})
|
|
1231
|
+
return _collection(result, 0)
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
SPEC_COLLECTION_CREATE = OperationSpec(
|
|
1235
|
+
id="gift.collection.create",
|
|
1236
|
+
request=CollectionCreateReq,
|
|
1237
|
+
response=GiftCollection,
|
|
1238
|
+
impl=collection_create,
|
|
1239
|
+
summary="Create a gift collection",
|
|
1240
|
+
mutating=True,
|
|
1241
|
+
rate_class="send",
|
|
1242
|
+
columns=("id", "title", "count"),
|
|
1243
|
+
headers=("Id", "Title", "Gifts"),
|
|
1244
|
+
example={"id": 1, "title": "Favourites", "count": 2},
|
|
1245
|
+
example_args="gift collection create me Favourites msg:120 msg:121",
|
|
1246
|
+
covers=("gift.collection-create",),
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
class CollectionEditReq(Request):
|
|
1251
|
+
chat: Annotated[PeerRef, arg(0, metavar="PEER", kind="peer", help="Whose profile.")]
|
|
1252
|
+
id: Annotated[
|
|
1253
|
+
int | None,
|
|
1254
|
+
arg(1, metavar="ID", required=False, help="Collection id; omit with --order-collections."),
|
|
1255
|
+
] = None
|
|
1256
|
+
title: Annotated[str | None, opt("--title", metavar="TEXT", help="New title.")] = None
|
|
1257
|
+
add: Annotated[str | None, opt("--add", metavar="LIST", help="Gifts to add.")] = None
|
|
1258
|
+
remove: Annotated[str | None, opt("--remove", metavar="LIST", help="Gifts to remove.")] = None
|
|
1259
|
+
order: Annotated[
|
|
1260
|
+
str | None, opt("--order", metavar="LIST", help="New order of the gifts inside it.")
|
|
1261
|
+
] = None
|
|
1262
|
+
order_collections: Annotated[
|
|
1263
|
+
str | None,
|
|
1264
|
+
opt("--order-collections", metavar="LIST", help="New order of the collections."),
|
|
1265
|
+
] = None
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
async def collection_edit(ctx: OpContext, req: CollectionEditReq) -> GiftCollection:
|
|
1269
|
+
"""Rename a collection, add or remove gifts, or reorder either level.
|
|
1270
|
+
|
|
1271
|
+
Removing a gift from a collection never deletes the gift — the two are
|
|
1272
|
+
different operations and only `gift convert` destroys anything.
|
|
1273
|
+
"""
|
|
1274
|
+
from telethon.tl.functions import payments as fn
|
|
1275
|
+
|
|
1276
|
+
handle = client(ctx)
|
|
1277
|
+
peer = await _settings.resolve(ctx, req.chat)
|
|
1278
|
+
|
|
1279
|
+
if req.order_collections is not None:
|
|
1280
|
+
order = [int(p) for p in req.order_collections.split(",") if p.strip().isdigit()]
|
|
1281
|
+
if not order:
|
|
1282
|
+
raise UsageError("--order-collections wants every collection id", field="order")
|
|
1283
|
+
await handle(fn.ReorderStarGiftCollectionsRequest(peer=peer, order=order))
|
|
1284
|
+
return GiftCollection(id=order[0], title="", count=0, order=0)
|
|
1285
|
+
|
|
1286
|
+
if req.id is None:
|
|
1287
|
+
raise UsageError("give a collection id, or --order-collections", field="id")
|
|
1288
|
+
|
|
1289
|
+
async def refs(value: str | None) -> list[Any] | None:
|
|
1290
|
+
if value is None:
|
|
1291
|
+
return None
|
|
1292
|
+
return [
|
|
1293
|
+
await _settings.input_gift(ctx, part.strip())
|
|
1294
|
+
for part in value.split(",")
|
|
1295
|
+
if part.strip()
|
|
1296
|
+
]
|
|
1297
|
+
|
|
1298
|
+
result = await handle(
|
|
1299
|
+
fn.UpdateStarGiftCollectionRequest(
|
|
1300
|
+
peer=peer,
|
|
1301
|
+
collection_id=int(req.id),
|
|
1302
|
+
title=req.title,
|
|
1303
|
+
add_stargift=await refs(req.add),
|
|
1304
|
+
delete_stargift=await refs(req.remove),
|
|
1305
|
+
order=await refs(req.order),
|
|
1306
|
+
)
|
|
1307
|
+
)
|
|
1308
|
+
ctx.emit("gift_collection_edit", {"id": int(req.id)})
|
|
1309
|
+
return _collection(result, 0)
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
SPEC_COLLECTION_EDIT = OperationSpec(
|
|
1313
|
+
id="gift.collection.edit",
|
|
1314
|
+
request=CollectionEditReq,
|
|
1315
|
+
response=GiftCollection,
|
|
1316
|
+
impl=collection_edit,
|
|
1317
|
+
summary="Rename a collection, add or remove gifts, or reorder either level",
|
|
1318
|
+
mutating=True,
|
|
1319
|
+
rate_class="send",
|
|
1320
|
+
columns=("id", "title", "count", "order"),
|
|
1321
|
+
headers=("Id", "Title", "Gifts", "Order"),
|
|
1322
|
+
example={"id": 1, "title": "Favourites", "count": 3},
|
|
1323
|
+
example_args="gift collection edit me 1 --add msg:122",
|
|
1324
|
+
covers=("gift.collection-reorder", "gift.collection-update"),
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
class CollectionDeleteReq(Request):
|
|
1329
|
+
chat: Annotated[PeerRef, arg(0, metavar="PEER", kind="peer", help="Whose profile.")]
|
|
1330
|
+
id: Annotated[int, arg(1, metavar="ID", help="The collection to delete.")]
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
async def collection_delete(ctx: OpContext, req: CollectionDeleteReq) -> GiftCollection:
|
|
1334
|
+
"""Delete a gift collection. The gifts themselves are untouched."""
|
|
1335
|
+
from telethon.tl.functions import payments as fn
|
|
1336
|
+
|
|
1337
|
+
peer = await _settings.resolve(ctx, req.chat)
|
|
1338
|
+
await client(ctx)(fn.DeleteStarGiftCollectionRequest(peer=peer, collection_id=int(req.id)))
|
|
1339
|
+
ctx.emit("gift_collection_deleted", {"id": int(req.id)})
|
|
1340
|
+
return GiftCollection(id=int(req.id), title="", count=0)
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
SPEC_COLLECTION_DELETE = OperationSpec(
|
|
1344
|
+
id="gift.collection.delete",
|
|
1345
|
+
request=CollectionDeleteReq,
|
|
1346
|
+
response=GiftCollection,
|
|
1347
|
+
impl=collection_delete,
|
|
1348
|
+
summary="Delete a gift collection (the gifts stay)",
|
|
1349
|
+
mutating=True,
|
|
1350
|
+
destructive=True,
|
|
1351
|
+
rate_class="send",
|
|
1352
|
+
columns=("id", "title"),
|
|
1353
|
+
headers=("Id", "Title"),
|
|
1354
|
+
example={"id": 1, "title": ""},
|
|
1355
|
+
example_args="gift collection delete me 1",
|
|
1356
|
+
covers=("gift.collection-delete",),
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
# ---------------------------------------------------------------------------
|
|
1361
|
+
# gift auction list / get
|
|
1362
|
+
# ---------------------------------------------------------------------------
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
class AuctionListReq(Request):
|
|
1366
|
+
won: Annotated[bool, opt("--won", help="Gifts I acquired in auctions.")] = False
|
|
1367
|
+
gift_id: Annotated[
|
|
1368
|
+
int | None, opt("--gift-id", metavar="ID", help="With --won: only this gift type.")
|
|
1369
|
+
] = None
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
async def auction_list(ctx: OpContext, req: AuctionListReq) -> Page[GiftAuction]:
|
|
1373
|
+
"""Gift auctions I am bidding in, or gifts I won in one.
|
|
1374
|
+
|
|
1375
|
+
Read-only, and not because of an API gap: a bid cannot be retracted, so
|
|
1376
|
+
tlgr never places one.
|
|
1377
|
+
"""
|
|
1378
|
+
from telethon.tl.functions import payments as fn
|
|
1379
|
+
|
|
1380
|
+
handle = client(ctx)
|
|
1381
|
+
if req.won:
|
|
1382
|
+
if req.gift_id is None:
|
|
1383
|
+
raise UsageError("--won needs --gift-id <id>", field="gift_id")
|
|
1384
|
+
result = await handle(fn.GetStarGiftAuctionAcquiredGiftsRequest(gift_id=int(req.gift_id)))
|
|
1385
|
+
rows = [
|
|
1386
|
+
GiftAuction(
|
|
1387
|
+
auction=str(req.gift_id),
|
|
1388
|
+
gift_id=int(req.gift_id),
|
|
1389
|
+
my_bid=int(getattr(row, "bid_amount", 0) or 0) or None,
|
|
1390
|
+
ends_at=fmt_dt(getattr(row, "date", None)),
|
|
1391
|
+
state="won",
|
|
1392
|
+
)
|
|
1393
|
+
for row in getattr(result, "gifts", None) or []
|
|
1394
|
+
]
|
|
1395
|
+
return Page(items=rows, has_more=False, total=len(rows))
|
|
1396
|
+
|
|
1397
|
+
result = await handle(fn.GetStarGiftActiveAuctionsRequest(hash=0))
|
|
1398
|
+
rows = []
|
|
1399
|
+
for raw in getattr(result, "auctions", None) or []:
|
|
1400
|
+
gift = getattr(raw, "gift", None)
|
|
1401
|
+
state = getattr(raw, "state", None)
|
|
1402
|
+
user = getattr(raw, "user_state", None)
|
|
1403
|
+
gift_id = int(getattr(gift, "gift_id", 0) or getattr(gift, "id", 0) or 0)
|
|
1404
|
+
slug = getattr(gift, "slug", None)
|
|
1405
|
+
rows.append(
|
|
1406
|
+
GiftAuction(
|
|
1407
|
+
auction=slug or str(gift_id),
|
|
1408
|
+
gift_id=gift_id or None,
|
|
1409
|
+
slug=slug,
|
|
1410
|
+
my_bid=getattr(user, "bid_amount", None),
|
|
1411
|
+
min_bid=getattr(state, "min_bid_amount", None),
|
|
1412
|
+
ends_at=fmt_dt(getattr(state, "end_date", None)),
|
|
1413
|
+
state=_state_word(state),
|
|
1414
|
+
)
|
|
1415
|
+
)
|
|
1416
|
+
return Page(items=rows, has_more=False, total=len(rows))
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
def _state_word(state: Any) -> str:
|
|
1420
|
+
"""`starGiftAuctionState*` as one word. `finished` always wins a race."""
|
|
1421
|
+
name = type(state).__name__
|
|
1422
|
+
if name == "StarGiftAuctionStateFinished":
|
|
1423
|
+
return "finished"
|
|
1424
|
+
if name == "StarGiftAuctionStateNotModified":
|
|
1425
|
+
return "not-modified"
|
|
1426
|
+
return "active"
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
SPEC_AUCTION_LIST = OperationSpec(
|
|
1430
|
+
id="gift.auction.list",
|
|
1431
|
+
request=AuctionListReq,
|
|
1432
|
+
response=Page[GiftAuction],
|
|
1433
|
+
impl=auction_list,
|
|
1434
|
+
summary="Gift auctions: the ones I am bidding in, and the gifts I won",
|
|
1435
|
+
description="Read-only on purpose: a bid cannot be retracted, so tlgr never places one.",
|
|
1436
|
+
paginated=PageKind.LOCAL,
|
|
1437
|
+
idempotent=True,
|
|
1438
|
+
columns=("auction", "gift_id", "slug", "my_bid", "min_bid", "ends_at", "state"),
|
|
1439
|
+
headers=("Auction", "Gift", "Slug", "My bid", "Min bid", "Ends", "State"),
|
|
1440
|
+
example={
|
|
1441
|
+
"items": [{"auction": "PlushPepe-42", "my_bid": 5000, "min_bid": 5500}],
|
|
1442
|
+
"has_more": False,
|
|
1443
|
+
},
|
|
1444
|
+
example_args="gift auction list",
|
|
1445
|
+
covers=("auction.acquired-gifts", "auction.active-list", "gifts.auctions"),
|
|
1446
|
+
tags=frozenset({"agent-safe"}),
|
|
1447
|
+
)
|
|
1448
|
+
|
|
1449
|
+
|
|
1450
|
+
class AuctionGetReq(Request):
|
|
1451
|
+
auction: Annotated[str, arg(0, metavar="AUCTION", help="A gift id or a collectible slug.")]
|
|
1452
|
+
with_position: Annotated[
|
|
1453
|
+
bool, opt("--with-position", help="Estimate my position in the ladder.")
|
|
1454
|
+
] = False
|
|
1455
|
+
watch: Annotated[
|
|
1456
|
+
bool, opt("--watch", help="Keep the subscription alive and stream updates.")
|
|
1457
|
+
] = False
|
|
1458
|
+
version: Annotated[int, opt("--version", metavar="N", help="Last seen state version.")] = 0
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
async def auction_get(ctx: OpContext, req: AuctionGetReq) -> Any:
|
|
1462
|
+
"""Auction state, bid ladder and my position — optionally as a stream.
|
|
1463
|
+
|
|
1464
|
+
`getStarGiftAuctionState` doubles as an update subscription that lasts
|
|
1465
|
+
`timeout` seconds, so `--watch` re-invokes it to stay subscribed. A new
|
|
1466
|
+
state is applied only when `version` increases, and a finished state
|
|
1467
|
+
always wins — otherwise a slow reply can overwrite a newer one.
|
|
1468
|
+
"""
|
|
1469
|
+
from telethon.tl import types
|
|
1470
|
+
from telethon.tl.functions import payments as fn
|
|
1471
|
+
|
|
1472
|
+
handle = client(ctx)
|
|
1473
|
+
text = req.auction.strip()
|
|
1474
|
+
auction = (
|
|
1475
|
+
types.InputStarGiftAuction(gift_id=int(text))
|
|
1476
|
+
if text.isdigit()
|
|
1477
|
+
else types.InputStarGiftAuctionSlug(slug=_settings.slug_of(text))
|
|
1478
|
+
)
|
|
1479
|
+
version = int(req.version)
|
|
1480
|
+
seen = -1
|
|
1481
|
+
while True:
|
|
1482
|
+
raw = await handle(fn.GetStarGiftAuctionStateRequest(auction=auction, version=version))
|
|
1483
|
+
state = _auction_state(text, raw, with_position=req.with_position)
|
|
1484
|
+
# A state only counts when its version *increased*; a finished state
|
|
1485
|
+
# always wins, because a slow reply must not overwrite the end.
|
|
1486
|
+
last = not req.watch or state.finished
|
|
1487
|
+
if state.finished or state.version > seen:
|
|
1488
|
+
seen = state.version
|
|
1489
|
+
version = state.version
|
|
1490
|
+
yield Page(items=[state], has_more=not last)
|
|
1491
|
+
if last:
|
|
1492
|
+
return
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
def _auction_state(name: str, raw: Any, *, with_position: bool = False) -> GiftAuctionState:
|
|
1496
|
+
"""`payments.starGiftAuctionState` flattened.
|
|
1497
|
+
|
|
1498
|
+
The server nests three things — the gift, the auction and *my* side of
|
|
1499
|
+
it — and the numbers a bidder wants are spread across all three.
|
|
1500
|
+
"""
|
|
1501
|
+
inner = getattr(raw, "state", None)
|
|
1502
|
+
user = getattr(raw, "user_state", None)
|
|
1503
|
+
word = _state_word(inner)
|
|
1504
|
+
position = None
|
|
1505
|
+
if with_position:
|
|
1506
|
+
bid = getattr(user, "bid_amount", None)
|
|
1507
|
+
if bid is not None:
|
|
1508
|
+
levels = getattr(inner, "bid_levels", None) or []
|
|
1509
|
+
position = 1 + sum(
|
|
1510
|
+
1 for level in levels if int(getattr(level, "amount", 0) or 0) > int(bid)
|
|
1511
|
+
)
|
|
1512
|
+
return GiftAuctionState(
|
|
1513
|
+
auction=name,
|
|
1514
|
+
state=word,
|
|
1515
|
+
version=int(getattr(inner, "version", 0) or 0),
|
|
1516
|
+
min_bid_amount=getattr(inner, "min_bid_amount", None),
|
|
1517
|
+
my_bid=getattr(user, "bid_amount", None),
|
|
1518
|
+
position=position,
|
|
1519
|
+
ends_at=fmt_dt(getattr(inner, "end_date", None)),
|
|
1520
|
+
timeout=getattr(raw, "timeout", None),
|
|
1521
|
+
finished=word == "finished",
|
|
1522
|
+
)
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
SPEC_AUCTION_GET = OperationSpec(
|
|
1526
|
+
id="gift.auction.get",
|
|
1527
|
+
request=AuctionGetReq,
|
|
1528
|
+
response=Page[GiftAuctionState],
|
|
1529
|
+
impl=auction_get,
|
|
1530
|
+
summary="Auction state, bid ladder and my position",
|
|
1531
|
+
stream=True,
|
|
1532
|
+
idempotent=True,
|
|
1533
|
+
columns=("auction", "state", "version", "min_bid_amount", "my_bid", "position", "ends_at"),
|
|
1534
|
+
headers=("Auction", "State", "Version", "Min bid", "My bid", "Place", "Ends"),
|
|
1535
|
+
example={
|
|
1536
|
+
"items": [
|
|
1537
|
+
{"auction": "PlushPepe-42", "state": "active", "version": 3, "min_bid_amount": 5500}
|
|
1538
|
+
],
|
|
1539
|
+
"has_more": False,
|
|
1540
|
+
},
|
|
1541
|
+
example_args="gift auction get PlushPepe-42",
|
|
1542
|
+
covers=("auction.position-estimate", "auction.state"),
|
|
1543
|
+
tags=frozenset({"agent-safe"}),
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1546
|
+
__all__ = [name for name in dir() if name.startswith("SPEC_")]
|