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/giveaway.py
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
"""The `giveaway` group: joining one, checking a code, launching a prepaid one.
|
|
2
|
+
|
|
3
|
+
Giveaways are a first-class surface in the official clients and they are
|
|
4
|
+
almost entirely free to operate from a CLI, which is why they get a noun of
|
|
5
|
+
their own rather than living under `gift`:
|
|
6
|
+
|
|
7
|
+
* **joining** spends a boost slot I already own, not money;
|
|
8
|
+
* **redeeming a code** activates a subscription somebody else paid for;
|
|
9
|
+
* **launching a prepaid giveaway** spends nothing either — the giveaway was
|
|
10
|
+
bought earlier, and `payments.launchPrepaidGiveaway` only starts it.
|
|
11
|
+
|
|
12
|
+
Buying a *new* giveaway is a payment and is therefore absent, like every
|
|
13
|
+
other purchase in tlgr.
|
|
14
|
+
|
|
15
|
+
`giveaway get` answers the question the public message cannot: am I eligible,
|
|
16
|
+
did I win, and which code did I win. `messageMediaGiveaway` carries the public
|
|
17
|
+
half; `payments.getGiveawayInfo` carries the personal half, and one command
|
|
18
|
+
reports both.
|
|
19
|
+
|
|
20
|
+
Telethon is imported inside functions, never at module scope (§2.2).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Annotated, Any
|
|
26
|
+
|
|
27
|
+
from tlgr.core.errors import UsageError
|
|
28
|
+
from tlgr.core.pagination import PageKind, build_page
|
|
29
|
+
from tlgr.core.timefmt import fmt_dt, parse_dt, to_unix
|
|
30
|
+
from tlgr.models.admin import BoostApplied
|
|
31
|
+
from tlgr.models.base import Request
|
|
32
|
+
from tlgr.models.page import Page
|
|
33
|
+
from tlgr.models.peer import PeerRef
|
|
34
|
+
from tlgr.models.premium import (
|
|
35
|
+
GiftCode,
|
|
36
|
+
GiftCodeApplied,
|
|
37
|
+
GiveawayInfo,
|
|
38
|
+
GiveawayLaunched,
|
|
39
|
+
GiveawayWinner,
|
|
40
|
+
PrepaidGiveaway,
|
|
41
|
+
)
|
|
42
|
+
from tlgr.ops import _settings
|
|
43
|
+
from tlgr.ops._common import client, random_id, window
|
|
44
|
+
from tlgr.ops._params import arg, opt
|
|
45
|
+
from tlgr.ops._spec import OpContext, OperationSpec
|
|
46
|
+
|
|
47
|
+
__all__ = [name for name in dir() if name.startswith("SPEC_")]
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# giveaway get
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GetReq(Request):
|
|
55
|
+
chat: Annotated[PeerRef, arg(0, metavar="CHAT", kind="peer", help="The giveaway's channel.")]
|
|
56
|
+
msg_id: Annotated[int, arg(1, metavar="MSG_ID", kind="msg_id", help="The giveaway message.")]
|
|
57
|
+
winners: Annotated[
|
|
58
|
+
bool, opt("--winners", help="Resolve the winner list from the results message.")
|
|
59
|
+
] = False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def get(ctx: OpContext, req: GetReq) -> GiveawayInfo:
|
|
63
|
+
"""Giveaway status and results: am I eligible, did I win, who won.
|
|
64
|
+
|
|
65
|
+
Two sources, one answer. The message media says how many winners there
|
|
66
|
+
are, when it ends and which countries are eligible;
|
|
67
|
+
`payments.getGiveawayInfo` says whether *this* account is in it and, if
|
|
68
|
+
it has finished, which gift code it won.
|
|
69
|
+
"""
|
|
70
|
+
from telethon.tl.functions import payments as fn
|
|
71
|
+
|
|
72
|
+
handle = client(ctx)
|
|
73
|
+
peer = await _settings.resolve(ctx, req.chat)
|
|
74
|
+
chat_id = _settings.peer_of(peer)
|
|
75
|
+
raw = await handle(fn.GetGiveawayInfoRequest(peer=peer, msg_id=int(req.msg_id)))
|
|
76
|
+
finished = type(raw).__name__.endswith("GiveawayInfoResults")
|
|
77
|
+
info = GiveawayInfo(
|
|
78
|
+
chat_id=chat_id,
|
|
79
|
+
msg_id=int(req.msg_id),
|
|
80
|
+
state="finished" if finished else "ongoing",
|
|
81
|
+
start_date=fmt_dt(getattr(raw, "start_date", None)),
|
|
82
|
+
joined=bool(getattr(raw, "participating", False)),
|
|
83
|
+
disallowed_reason=_disallowed_word(raw),
|
|
84
|
+
winner=bool(getattr(raw, "winner", False)),
|
|
85
|
+
refunded=bool(getattr(raw, "refunded", False)),
|
|
86
|
+
gift_code_slug=getattr(raw, "gift_code_slug", None),
|
|
87
|
+
activated_count=getattr(raw, "activated_count", None),
|
|
88
|
+
until_date=fmt_dt(getattr(raw, "finish_date", None)),
|
|
89
|
+
stars=getattr(raw, "stars_prize", None),
|
|
90
|
+
winners_count=getattr(raw, "winners_count", None),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
media = await _giveaway_media(ctx, peer, int(req.msg_id))
|
|
94
|
+
if media is not None:
|
|
95
|
+
info.winners_count = (
|
|
96
|
+
info.winners_count
|
|
97
|
+
or getattr(media, "quantity", None)
|
|
98
|
+
or getattr(media, "winners_count", None)
|
|
99
|
+
)
|
|
100
|
+
info.months = getattr(media, "months", None)
|
|
101
|
+
info.only_new_subscribers = bool(getattr(media, "only_new_subscribers", False))
|
|
102
|
+
info.countries = [str(code) for code in getattr(media, "countries_iso2", None) or []]
|
|
103
|
+
info.prize_description = getattr(media, "prize_description", None)
|
|
104
|
+
if info.until_date is None:
|
|
105
|
+
info.until_date = fmt_dt(getattr(media, "until_date", None))
|
|
106
|
+
|
|
107
|
+
if req.winners:
|
|
108
|
+
# Only the *results* media carries the winner vector; an ongoing
|
|
109
|
+
# giveaway has none, and reporting an empty list for one would read
|
|
110
|
+
# as "nobody won" rather than "not drawn yet".
|
|
111
|
+
info.winners = [
|
|
112
|
+
GiveawayWinner(user_id=int(user_id))
|
|
113
|
+
for user_id in getattr(media, "winners", None) or []
|
|
114
|
+
]
|
|
115
|
+
return info
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _disallowed_word(raw: Any) -> str | None:
|
|
119
|
+
"""Why this account cannot take part, as one word.
|
|
120
|
+
|
|
121
|
+
`giveawayInfo` spells the three refusals as three unrelated optional
|
|
122
|
+
fields — a country code, a chat id and a date — so the mapping happens
|
|
123
|
+
once, here, rather than in whatever reads the answer.
|
|
124
|
+
"""
|
|
125
|
+
if getattr(raw, "disallowed_country", None):
|
|
126
|
+
return "disallowed-country"
|
|
127
|
+
if getattr(raw, "admin_disallowed_chat_id", None):
|
|
128
|
+
return "admin"
|
|
129
|
+
if getattr(raw, "joined_too_early_date", None):
|
|
130
|
+
return "joined-too-early"
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def _giveaway_media(ctx: OpContext, peer: Any, msg_id: int) -> Any:
|
|
135
|
+
"""The `messageMediaGiveaway*` on the giveaway post, or None."""
|
|
136
|
+
from tlgr.ops import _media
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
message = await _media.fetch_message(ctx, peer, msg_id)
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
ctx.warn(f"could not read the giveaway message: {exc}")
|
|
142
|
+
return None
|
|
143
|
+
media = getattr(message, "media", None)
|
|
144
|
+
if type(media).__name__ in ("MessageMediaGiveaway", "MessageMediaGiveawayResults"):
|
|
145
|
+
return media
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
SPEC_GET = OperationSpec(
|
|
150
|
+
id="giveaway.get",
|
|
151
|
+
request=GetReq,
|
|
152
|
+
response=GiveawayInfo,
|
|
153
|
+
impl=get,
|
|
154
|
+
summary="Giveaway status and results: am I eligible, did I win, who won",
|
|
155
|
+
aliases=("giveaway.info",),
|
|
156
|
+
idempotent=True,
|
|
157
|
+
columns=("state", "joined", "winner", "winners_count", "until_date", "gift_code_slug"),
|
|
158
|
+
headers=("State", "Joined", "Won", "Winners", "Until", "Code"),
|
|
159
|
+
example={
|
|
160
|
+
"chat_id": -1001600,
|
|
161
|
+
"msg_id": 42,
|
|
162
|
+
"state": "ongoing",
|
|
163
|
+
"joined": True,
|
|
164
|
+
"winners_count": 10,
|
|
165
|
+
},
|
|
166
|
+
example_args="giveaway get @mychannel 42",
|
|
167
|
+
covers=(
|
|
168
|
+
"giveaway.info",
|
|
169
|
+
"giveaway.results",
|
|
170
|
+
"groups-channels-admin.giveaway-info",
|
|
171
|
+
"premium.giveaway-info",
|
|
172
|
+
),
|
|
173
|
+
tags=frozenset({"agent-safe"}),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# giveaway join
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class JoinReq(Request):
|
|
183
|
+
chat: Annotated[PeerRef, arg(0, metavar="CHAT", kind="peer", help="The channel to boost.")]
|
|
184
|
+
slots: Annotated[
|
|
185
|
+
list[int], opt("--slots", metavar="N", help="Which of my boost slots to use.")
|
|
186
|
+
] = []
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def join(ctx: OpContext, req: JoinReq) -> BoostApplied:
|
|
190
|
+
"""Join a giveaway by boosting the channel.
|
|
191
|
+
|
|
192
|
+
Joining *is* boosting: the giveaway counts participants by boost. The
|
|
193
|
+
slot is occupied for a month, which is why the command is confirmed, and
|
|
194
|
+
the boost side of it is `boost add` — one implementation, reached from
|
|
195
|
+
the two places the GUI reaches it from.
|
|
196
|
+
"""
|
|
197
|
+
from tlgr.ops.chat_stats import BoostAddReq, add_boost
|
|
198
|
+
|
|
199
|
+
return await add_boost(ctx, BoostAddReq(chat=req.chat, slots=list(req.slots)))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
SPEC_JOIN = OperationSpec(
|
|
203
|
+
id="giveaway.join",
|
|
204
|
+
request=JoinReq,
|
|
205
|
+
response=BoostApplied,
|
|
206
|
+
impl=join,
|
|
207
|
+
summary="Join a giveaway by boosting the channel",
|
|
208
|
+
description=(
|
|
209
|
+
"Needs Premium (or gifted boost slots). A slot stays occupied for a "
|
|
210
|
+
"month, so `-y` is required off a TTY."
|
|
211
|
+
),
|
|
212
|
+
mutating=True,
|
|
213
|
+
idempotent=True,
|
|
214
|
+
rate_class="send",
|
|
215
|
+
columns=("chat_id", "level", "boosts", "slots", "already"),
|
|
216
|
+
headers=("Chat", "Level", "Boosts", "Slots", "Already"),
|
|
217
|
+
example={"chat_id": -1001600, "level": 4, "boosts": 15, "slots": [1]},
|
|
218
|
+
example_args="giveaway join @mychannel",
|
|
219
|
+
covers=("giveaway.join-by-boosting",),
|
|
220
|
+
tags=frozenset({"visible-to-others"}),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# giveaway list
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class ListReq(Request):
|
|
230
|
+
chat: Annotated[
|
|
231
|
+
PeerRef | None,
|
|
232
|
+
arg(0, metavar="CHAT", required=False, kind="peer", help="The channel to inspect."),
|
|
233
|
+
] = None
|
|
234
|
+
prepaid: Annotated[
|
|
235
|
+
bool, opt("--prepaid/--no-prepaid", help="Prepaid giveaways bought for this channel.")
|
|
236
|
+
] = True
|
|
237
|
+
codes: Annotated[
|
|
238
|
+
bool, opt("--codes", help="My received giveaway gift codes (the inbox side).")
|
|
239
|
+
] = False
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
async def list_(ctx: OpContext, req: ListReq) -> Page[PrepaidGiveaway]:
|
|
243
|
+
"""Prepaid giveaways on a channel, or the gift codes I have received.
|
|
244
|
+
|
|
245
|
+
`payments.getPrepaidGiveaways` has no request class in Telethon 1.44, but
|
|
246
|
+
`premium.getBoostsStatus` carries the same `prepaid_giveaways` vector —
|
|
247
|
+
so the answer is the server's, from the method this build can send.
|
|
248
|
+
"""
|
|
249
|
+
from telethon.tl.functions import premium as fn
|
|
250
|
+
|
|
251
|
+
handle = client(ctx)
|
|
252
|
+
if req.codes:
|
|
253
|
+
return await _received_codes(ctx)
|
|
254
|
+
|
|
255
|
+
if req.chat is None:
|
|
256
|
+
raise UsageError("name a channel, or pass --codes for my received codes", field="chat")
|
|
257
|
+
peer = await _settings.resolve(ctx, req.chat)
|
|
258
|
+
status = await handle(fn.GetBoostsStatusRequest(peer=peer))
|
|
259
|
+
rows = [
|
|
260
|
+
PrepaidGiveaway(
|
|
261
|
+
id=int(getattr(row, "id", 0) or 0),
|
|
262
|
+
quantity=int(getattr(row, "quantity", 0) or 0),
|
|
263
|
+
months=getattr(row, "months", None),
|
|
264
|
+
stars=getattr(row, "stars", None),
|
|
265
|
+
boosts=getattr(row, "boosts", None),
|
|
266
|
+
date=fmt_dt(getattr(row, "date", None)),
|
|
267
|
+
date_unix=to_unix(getattr(row, "date", None)),
|
|
268
|
+
from_chat=_settings.peer_of(peer),
|
|
269
|
+
)
|
|
270
|
+
for row in getattr(status, "prepaid_giveaways", None) or []
|
|
271
|
+
]
|
|
272
|
+
return Page(items=rows, has_more=False, total=len(rows))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
async def _received_codes(ctx: OpContext) -> Page[PrepaidGiveaway]:
|
|
276
|
+
"""Gift codes that arrived as `messageActionGiftCode` service messages.
|
|
277
|
+
|
|
278
|
+
There is no "my codes" endpoint: the codes are service messages in the
|
|
279
|
+
account's own history, so they are found by scanning and then checked one
|
|
280
|
+
by one, which is exactly what an official client does.
|
|
281
|
+
"""
|
|
282
|
+
from telethon.tl.functions import payments as fn
|
|
283
|
+
|
|
284
|
+
from tlgr.ops.chat import ListReq as ChatListReq
|
|
285
|
+
from tlgr.ops.chat import list_chats
|
|
286
|
+
|
|
287
|
+
handle = client(ctx)
|
|
288
|
+
limit, _ = window(ctx, "giveaway.list", PageKind.LOCAL, default=20)
|
|
289
|
+
rows: list[PrepaidGiveaway] = []
|
|
290
|
+
dialogs = await list_chats(ctx, ChatListReq())
|
|
291
|
+
for dialog in dialogs.items:
|
|
292
|
+
message = getattr(dialog, "last_message", None)
|
|
293
|
+
action = getattr(message, "action", None)
|
|
294
|
+
slug = getattr(action, "slug", None) if action is not None else None
|
|
295
|
+
if not slug:
|
|
296
|
+
continue
|
|
297
|
+
try:
|
|
298
|
+
checked = await handle(fn.CheckGiftCodeRequest(slug=slug))
|
|
299
|
+
except Exception as exc:
|
|
300
|
+
ctx.warn(f"could not check the code {slug}: {exc}")
|
|
301
|
+
continue
|
|
302
|
+
days = getattr(checked, "days", None)
|
|
303
|
+
rows.append(
|
|
304
|
+
PrepaidGiveaway(
|
|
305
|
+
id=0,
|
|
306
|
+
quantity=1,
|
|
307
|
+
months=int(days) // 30 if days else None,
|
|
308
|
+
slug=slug,
|
|
309
|
+
used=getattr(checked, "used_date", None) is not None,
|
|
310
|
+
date=fmt_dt(getattr(checked, "date", None)),
|
|
311
|
+
date_unix=to_unix(getattr(checked, "date", None)),
|
|
312
|
+
from_chat=getattr(dialog.chat, "id", None) if dialog.chat else None,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
if len(rows) >= limit:
|
|
316
|
+
break
|
|
317
|
+
return build_page(
|
|
318
|
+
rows, op="giveaway.list", kind=PageKind.LOCAL, has_more=False, total=len(rows)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
SPEC_LIST = OperationSpec(
|
|
323
|
+
id="giveaway.list",
|
|
324
|
+
request=ListReq,
|
|
325
|
+
response=Page[PrepaidGiveaway],
|
|
326
|
+
impl=list_,
|
|
327
|
+
summary="Prepaid giveaways available on a channel, and the gift codes I received",
|
|
328
|
+
description=(
|
|
329
|
+
"The prepaid list comes from `premium.getBoostsStatus`, which carries "
|
|
330
|
+
"the same vector as the absent `payments.getPrepaidGiveaways`. "
|
|
331
|
+
"`--codes` scans for `messageActionGiftCode` service messages and "
|
|
332
|
+
"checks each slug, because there is no 'my codes' endpoint."
|
|
333
|
+
),
|
|
334
|
+
paginated=PageKind.LOCAL,
|
|
335
|
+
idempotent=True,
|
|
336
|
+
columns=("id", "quantity", "months", "stars", "slug", "used"),
|
|
337
|
+
headers=("Id", "Winners", "Months", "Stars", "Code", "Used"),
|
|
338
|
+
example={
|
|
339
|
+
"items": [{"id": 77, "quantity": 10, "months": 3, "from_chat": -1001600}],
|
|
340
|
+
"has_more": False,
|
|
341
|
+
},
|
|
342
|
+
example_args="giveaway list @mychannel",
|
|
343
|
+
covers=("giveaway.gift-code-received", "giveaway.list-prepaid"),
|
|
344
|
+
tags=frozenset({"agent-safe"}),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
# giveaway start
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class StartReq(Request):
|
|
354
|
+
chat: Annotated[PeerRef, arg(0, metavar="CHAT", kind="peer", help="The channel.")]
|
|
355
|
+
prepaid_id: Annotated[int, arg(1, metavar="PREPAID_ID", help="From `giveaway list <chat>`.")]
|
|
356
|
+
winners: Annotated[int | None, opt("--winners", metavar="N", help="Number of winners.")] = None
|
|
357
|
+
until: Annotated[
|
|
358
|
+
str | None, opt("--until", metavar="WHEN", kind="datetime", help="Draw date.")
|
|
359
|
+
] = None
|
|
360
|
+
only_new: Annotated[
|
|
361
|
+
bool, opt("--only-new", help="Only subscribers who joined after the start.")
|
|
362
|
+
] = False
|
|
363
|
+
public_winners: Annotated[
|
|
364
|
+
bool, opt("--public-winners", help="Show the winner list when it ends.")
|
|
365
|
+
] = False
|
|
366
|
+
countries: Annotated[
|
|
367
|
+
str | None, opt("--countries", metavar="LIST", help="ISO-2 codes, comma separated.")
|
|
368
|
+
] = None
|
|
369
|
+
also_chat: Annotated[
|
|
370
|
+
list[str],
|
|
371
|
+
opt("--also-chat", metavar="CHAT", help="Another channel a participant must join."),
|
|
372
|
+
] = []
|
|
373
|
+
prize: Annotated[str | None, opt("--prize", metavar="TEXT", help="Prize description.")] = None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
async def start(ctx: OpContext, req: StartReq) -> GiveawayLaunched:
|
|
377
|
+
"""Launch a giveaway that was already paid for.
|
|
378
|
+
|
|
379
|
+
Launching a prepaid giveaway is **not** a payment, which is why it is
|
|
380
|
+
here at all: the Stars or the fiat were spent when the giveaway was
|
|
381
|
+
bought, and this only starts it. Creating a new (bought) giveaway is a
|
|
382
|
+
purchase and is absent.
|
|
383
|
+
"""
|
|
384
|
+
from telethon.tl import types
|
|
385
|
+
from telethon.tl.functions import payments as fn
|
|
386
|
+
|
|
387
|
+
handle = client(ctx)
|
|
388
|
+
peer = await _settings.resolve(ctx, req.chat)
|
|
389
|
+
config = await _settings.app_config(ctx)
|
|
390
|
+
countries = [part.strip().upper() for part in (req.countries or "").split(",") if part.strip()]
|
|
391
|
+
max_countries = int(config.get("giveaway_countries_max") or 0)
|
|
392
|
+
if max_countries and len(countries) > max_countries:
|
|
393
|
+
raise UsageError(f"--countries takes at most {max_countries} entries", field="countries")
|
|
394
|
+
extra = [await _settings.resolve(ctx, ref) for ref in req.also_chat]
|
|
395
|
+
max_peers = int(config.get("giveaway_add_peers_max") or 0)
|
|
396
|
+
if max_peers and len(extra) > max_peers:
|
|
397
|
+
raise UsageError(f"--also-chat takes at most {max_peers} channels", field="also_chat")
|
|
398
|
+
|
|
399
|
+
until = parse_dt(req.until) if req.until else None
|
|
400
|
+
purpose = types.InputStorePaymentPremiumGiveaway(
|
|
401
|
+
boost_peer=peer,
|
|
402
|
+
until_date=until,
|
|
403
|
+
currency="XTR",
|
|
404
|
+
amount=0,
|
|
405
|
+
only_new_subscribers=req.only_new or None,
|
|
406
|
+
winners_are_visible=req.public_winners or None,
|
|
407
|
+
additional_peers=extra or None,
|
|
408
|
+
countries_iso2=countries or None,
|
|
409
|
+
prize_description=req.prize,
|
|
410
|
+
random_id=random_id(),
|
|
411
|
+
)
|
|
412
|
+
result = await handle(
|
|
413
|
+
fn.LaunchPrepaidGiveawayRequest(peer=peer, giveaway_id=int(req.prepaid_id), purpose=purpose)
|
|
414
|
+
)
|
|
415
|
+
msg_id = next(
|
|
416
|
+
(
|
|
417
|
+
int(getattr(getattr(update, "message", None), "id", 0) or 0)
|
|
418
|
+
for update in getattr(result, "updates", None) or []
|
|
419
|
+
if getattr(update, "message", None) is not None
|
|
420
|
+
),
|
|
421
|
+
None,
|
|
422
|
+
)
|
|
423
|
+
chat_id = _settings.peer_of(peer)
|
|
424
|
+
ctx.emit("giveaway_started", {"chat_id": chat_id, "prepaid_id": int(req.prepaid_id)})
|
|
425
|
+
return GiveawayLaunched(
|
|
426
|
+
chat_id=chat_id,
|
|
427
|
+
prepaid_id=int(req.prepaid_id),
|
|
428
|
+
msg_id=msg_id,
|
|
429
|
+
winners_count=int(req.winners or 0),
|
|
430
|
+
until_date=fmt_dt(until),
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
SPEC_START = OperationSpec(
|
|
435
|
+
id="giveaway.start",
|
|
436
|
+
request=StartReq,
|
|
437
|
+
response=GiveawayLaunched,
|
|
438
|
+
impl=start,
|
|
439
|
+
summary="Launch a giveaway that was already paid for (prepaid)",
|
|
440
|
+
description=(
|
|
441
|
+
"Not a payment: the giveaway was bought earlier and this only starts "
|
|
442
|
+
"it. Creating a new, bought giveaway is a purchase and is absent."
|
|
443
|
+
),
|
|
444
|
+
aliases=("giveaway.launch",),
|
|
445
|
+
mutating=True,
|
|
446
|
+
rate_class="send",
|
|
447
|
+
columns=("chat_id", "prepaid_id", "msg_id", "winners_count", "until_date"),
|
|
448
|
+
headers=("Chat", "Prepaid", "Post", "Winners", "Until"),
|
|
449
|
+
example={"chat_id": -1001600, "prepaid_id": 77, "msg_id": 42, "winners_count": 10},
|
|
450
|
+
example_args="giveaway start @mychannel 77 --winners 10 --until +7d",
|
|
451
|
+
covers=("groups-channels-admin.giveaway-prepaid-launch", "premium.giveaway-create"),
|
|
452
|
+
tags=frozenset({"visible-to-others"}),
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ---------------------------------------------------------------------------
|
|
457
|
+
# giveaway code check / apply
|
|
458
|
+
# ---------------------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class CodeCheckReq(Request):
|
|
462
|
+
slug: Annotated[str, arg(0, metavar="SLUG", help="The code, or a t.me/giftcode link.")]
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
async def code_check(ctx: OpContext, req: CodeCheckReq) -> GiftCode:
|
|
466
|
+
"""Check a gift code before using it.
|
|
467
|
+
|
|
468
|
+
Also how a giveaway admin identifies a winner: `to_id` is who the code
|
|
469
|
+
was issued to, which the public results message does not always say.
|
|
470
|
+
"""
|
|
471
|
+
from tlgr.ops.premium import GiftcodeGetReq, giftcode_get
|
|
472
|
+
|
|
473
|
+
return await giftcode_get(ctx, GiftcodeGetReq(slug=req.slug))
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
SPEC_CODE_CHECK = OperationSpec(
|
|
477
|
+
id="giveaway.code.check",
|
|
478
|
+
request=CodeCheckReq,
|
|
479
|
+
response=GiftCode,
|
|
480
|
+
impl=code_check,
|
|
481
|
+
summary="Check a gift code / t.me/giftcode link before using it",
|
|
482
|
+
aliases=("giftcode.check",),
|
|
483
|
+
idempotent=True,
|
|
484
|
+
columns=("slug", "from_id", "to_id", "date", "months", "used_date", "via_giveaway"),
|
|
485
|
+
headers=("Slug", "From", "To", "Date", "Months", "Used", "Giveaway"),
|
|
486
|
+
example={"slug": "abcdef", "from_id": -1001600, "to_id": 777123, "months": 3},
|
|
487
|
+
example_args="giveaway code check abcdef",
|
|
488
|
+
covers=("giftcode.check",),
|
|
489
|
+
tags=frozenset({"agent-safe"}),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class CodeApplyReq(Request):
|
|
494
|
+
slug: Annotated[str, arg(0, metavar="SLUG", help="The code, or a t.me/giftcode link.")]
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
async def code_apply(ctx: OpContext, req: CodeApplyReq) -> GiftCodeApplied:
|
|
498
|
+
"""Redeem a gift code, activating the Premium subscription it carries.
|
|
499
|
+
|
|
500
|
+
Costs nothing: the code was paid for by whoever gave it. A code that is
|
|
501
|
+
already used answers `already: true` rather than failing.
|
|
502
|
+
"""
|
|
503
|
+
from telethon.tl.functions import payments as fn
|
|
504
|
+
|
|
505
|
+
handle = client(ctx)
|
|
506
|
+
slug = req.slug.rsplit("/", 1)[-1]
|
|
507
|
+
checked = await handle(fn.CheckGiftCodeRequest(slug=slug))
|
|
508
|
+
if getattr(checked, "used_date", None) is not None:
|
|
509
|
+
mark = getattr(ctx, "mark_already", None)
|
|
510
|
+
if callable(mark):
|
|
511
|
+
mark()
|
|
512
|
+
return GiftCodeApplied(
|
|
513
|
+
slug=slug,
|
|
514
|
+
applied=False,
|
|
515
|
+
months=getattr(checked, "months", None),
|
|
516
|
+
already=True,
|
|
517
|
+
)
|
|
518
|
+
await handle(fn.ApplyGiftCodeRequest(slug=slug))
|
|
519
|
+
ctx.emit("giftcode_applied", {"slug": slug})
|
|
520
|
+
return GiftCodeApplied(slug=slug, applied=True, months=getattr(checked, "months", None))
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
SPEC_CODE_APPLY = OperationSpec(
|
|
524
|
+
id="giveaway.code.apply",
|
|
525
|
+
request=CodeApplyReq,
|
|
526
|
+
response=GiftCodeApplied,
|
|
527
|
+
impl=code_apply,
|
|
528
|
+
summary="Redeem a gift code (activates the Premium subscription it carries)",
|
|
529
|
+
description="Free: the code is already paid for, so this is not a purchase.",
|
|
530
|
+
aliases=("giftcode.apply",),
|
|
531
|
+
mutating=True,
|
|
532
|
+
idempotent=True,
|
|
533
|
+
rate_class="send",
|
|
534
|
+
columns=("slug", "applied", "months", "already"),
|
|
535
|
+
headers=("Slug", "Applied", "Months", "Already"),
|
|
536
|
+
example={"slug": "abcdef", "applied": True, "months": 3},
|
|
537
|
+
example_args="giveaway code apply abcdef",
|
|
538
|
+
covers=("giftcode.apply", "groups-channels-admin.gift-code-redeem"),
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
__all__ = [name for name in dir() if name.startswith("SPEC_")]
|