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/todo.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""The `todo` group: checklists.
|
|
2
|
+
|
|
3
|
+
The whole group turns on one invariant: **a task's id is never reused and
|
|
4
|
+
never renumbered.** `messages.editMessage` rewrites the entire `todoList`, and
|
|
5
|
+
completions are keyed by task id — so removing task 2 by renumbering 3 and 4
|
|
6
|
+
down to 2 and 3 would silently move every tick onto the wrong row. Deleting
|
|
7
|
+
keeps the survivors' ids, and appending always uses `max(existing) + 1`
|
|
8
|
+
because reusing an id is `TODO_ITEM_DUPLICATE`.
|
|
9
|
+
|
|
10
|
+
Telethon is imported inside functions, never at module scope (§2.2).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Annotated, Any
|
|
16
|
+
|
|
17
|
+
from tlgr.core.errors import NotFoundError, PermissionError_, UsageError
|
|
18
|
+
from tlgr.core.timefmt import fmt_dt, to_unix
|
|
19
|
+
from tlgr.models.base import Request
|
|
20
|
+
from tlgr.models.peer import PeerRef
|
|
21
|
+
from tlgr.models.todo import Todo, TodoTask
|
|
22
|
+
from tlgr.ops import _send
|
|
23
|
+
from tlgr.ops._common import already, client, only, random_id
|
|
24
|
+
from tlgr.ops._params import arg, opt
|
|
25
|
+
from tlgr.ops._serialize import peer_id_of
|
|
26
|
+
from tlgr.ops._spec import OpContext, OperationSpec
|
|
27
|
+
|
|
28
|
+
__all__ = [name for name in dir() if name.startswith("SPEC_")]
|
|
29
|
+
|
|
30
|
+
_EXAMPLE_TODO: dict[str, Any] = {
|
|
31
|
+
"chat_id": 777123,
|
|
32
|
+
"msg_id": 12345,
|
|
33
|
+
"title": "Release checklist",
|
|
34
|
+
"tasks": [
|
|
35
|
+
{"id": 1, "title": "tag the commit", "done": True, "completed_by": 777},
|
|
36
|
+
{"id": 2, "title": "publish the wheel"},
|
|
37
|
+
],
|
|
38
|
+
"done_count": 1,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Reading a checklist off a message
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _text_with_entities(value: Any) -> tuple[str, list[Any]]:
|
|
48
|
+
from tlgr.ops._serialize import message_entities
|
|
49
|
+
|
|
50
|
+
if value is None:
|
|
51
|
+
return "", []
|
|
52
|
+
|
|
53
|
+
class _Holder:
|
|
54
|
+
entities = list(getattr(value, "entities", None) or [])
|
|
55
|
+
|
|
56
|
+
return str(getattr(value, "text", value) or ""), message_entities(_Holder())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def todo_model(media: Any, *, chat_id: int = 0, msg_id: int = 0) -> Todo:
|
|
60
|
+
"""`MessageMediaToDo` → the one `Todo` shape this group returns."""
|
|
61
|
+
raw = getattr(media, "todo", None)
|
|
62
|
+
completions = {
|
|
63
|
+
int(getattr(item, "id", 0)): item for item in (getattr(media, "completions", None) or [])
|
|
64
|
+
}
|
|
65
|
+
title, entities = _text_with_entities(getattr(raw, "title", None))
|
|
66
|
+
tasks: list[TodoTask] = []
|
|
67
|
+
for item in getattr(raw, "list", None) or []:
|
|
68
|
+
task_id = int(getattr(item, "id", 0))
|
|
69
|
+
text, task_entities = _text_with_entities(getattr(item, "title", None))
|
|
70
|
+
done = completions.get(task_id)
|
|
71
|
+
tasks.append(
|
|
72
|
+
TodoTask(
|
|
73
|
+
id=task_id,
|
|
74
|
+
title=text,
|
|
75
|
+
entities=task_entities,
|
|
76
|
+
done=done is not None,
|
|
77
|
+
completed_by=peer_id_of(getattr(done, "completed_by", None)) if done else None,
|
|
78
|
+
completed_date=fmt_dt(getattr(done, "date", None)) if done else None,
|
|
79
|
+
completed_date_unix=to_unix(getattr(done, "date", None)) if done else None,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
return Todo(
|
|
83
|
+
chat_id=chat_id,
|
|
84
|
+
msg_id=msg_id,
|
|
85
|
+
title=title,
|
|
86
|
+
entities=entities,
|
|
87
|
+
others_can_add=bool(getattr(raw, "others_can_append", False)),
|
|
88
|
+
others_can_complete=bool(getattr(raw, "others_can_complete", False)),
|
|
89
|
+
tasks=tasks,
|
|
90
|
+
done_count=sum(1 for task in tasks if task.done),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def _fetch(ctx: OpContext, peer: Any, chat_id: int, msg_id: int) -> tuple[Any, Todo]:
|
|
95
|
+
message = await client(ctx).get_messages(peer, ids=msg_id)
|
|
96
|
+
media = getattr(message, "media", None) if message is not None else None
|
|
97
|
+
if getattr(media, "todo", None) is None:
|
|
98
|
+
raise NotFoundError(f"message {msg_id} in {chat_id} is not a checklist")
|
|
99
|
+
return message, todo_model(media, chat_id=chat_id, msg_id=msg_id)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def _reread(ctx: OpContext, peer: Any, chat_id: int, msg_id: int, updates: Any) -> Todo:
|
|
103
|
+
for update in getattr(updates, "updates", None) or []:
|
|
104
|
+
media = getattr(getattr(update, "message", None), "media", None)
|
|
105
|
+
if getattr(media, "todo", None) is not None:
|
|
106
|
+
return todo_model(media, chat_id=chat_id, msg_id=msg_id)
|
|
107
|
+
return (await _fetch(ctx, peer, chat_id, msg_id))[1]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _tl_items(entries: list[tuple[int, str]], parse: str | None) -> list[Any]:
|
|
111
|
+
from telethon.tl import types
|
|
112
|
+
|
|
113
|
+
out: list[Any] = []
|
|
114
|
+
for task_id, text in entries:
|
|
115
|
+
plain, entities = _send.body(text, parse=parse)
|
|
116
|
+
out.append(
|
|
117
|
+
types.TodoItem(
|
|
118
|
+
id=task_id,
|
|
119
|
+
title=types.TextWithEntities(
|
|
120
|
+
text=plain, entities=_send.tl_entities(entities) or []
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# todo create
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class CreateReq(_send.SendOptions, kw_only=True):
|
|
133
|
+
chat: Annotated[PeerRef, arg(0, metavar="CHAT", kind="peer", help="Where to post it.")]
|
|
134
|
+
title: Annotated[str, arg(1, metavar="TITLE", help="The list's title.")]
|
|
135
|
+
tasks: Annotated[
|
|
136
|
+
list[str], arg(2, metavar="TASK", variadic=True, help="The tasks, in order.")
|
|
137
|
+
] = []
|
|
138
|
+
others_can_add: Annotated[bool, opt("--others-can-add", help="Anyone may append tasks.")] = (
|
|
139
|
+
False
|
|
140
|
+
)
|
|
141
|
+
others_can_complete: Annotated[
|
|
142
|
+
bool, opt("--others-can-complete", help="Anyone may tick tasks.")
|
|
143
|
+
] = False
|
|
144
|
+
parse: Annotated[
|
|
145
|
+
str | None, opt("--parse", metavar="MODE", help="md|html|none for title and tasks.")
|
|
146
|
+
] = None
|
|
147
|
+
reply_to: Annotated[
|
|
148
|
+
int | None, opt("--reply-to", metavar="ID", kind="msg_id", help="Reply to this message.")
|
|
149
|
+
] = None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def create(ctx: OpContext, req: CreateReq) -> Todo:
|
|
153
|
+
"""Send a checklist.
|
|
154
|
+
|
|
155
|
+
Ids are assigned 1..n here, once, and never touched again: every later
|
|
156
|
+
command in this group addresses a task by the id it was born with.
|
|
157
|
+
"""
|
|
158
|
+
from telethon.tl import types
|
|
159
|
+
from telethon.tl.functions import messages as fn
|
|
160
|
+
|
|
161
|
+
if not req.tasks:
|
|
162
|
+
raise UsageError("a checklist needs at least one task", field="tasks")
|
|
163
|
+
|
|
164
|
+
peer = await _send.resolve(ctx, req.chat)
|
|
165
|
+
chat_id = _send.peer_id_of(peer)
|
|
166
|
+
title, entities = _send.body(req.title, parse=req.parse)
|
|
167
|
+
todo = types.TodoList(
|
|
168
|
+
title=types.TextWithEntities(text=title, entities=_send.tl_entities(entities) or []),
|
|
169
|
+
list=_tl_items(list(enumerate(req.tasks, start=1)), req.parse),
|
|
170
|
+
others_can_append=req.others_can_add or None,
|
|
171
|
+
others_can_complete=req.others_can_complete or None,
|
|
172
|
+
)
|
|
173
|
+
values = {
|
|
174
|
+
"peer": peer,
|
|
175
|
+
"media": types.InputMediaTodo(todo=todo),
|
|
176
|
+
"message": "",
|
|
177
|
+
"random_id": random_id(),
|
|
178
|
+
"silent": req.silent or None,
|
|
179
|
+
"noforwards": req.protect or None,
|
|
180
|
+
"reply_to": await _send.reply_target(ctx, reply_to=req.reply_to, topic=req.topic),
|
|
181
|
+
"schedule_date": _send.schedule_at(req.schedule),
|
|
182
|
+
"send_as": await _send.resolve(ctx, req.send_as) if req.send_as is not None else None,
|
|
183
|
+
"effect": _send.effect_id(req.effect),
|
|
184
|
+
"allow_paid_stars": req.paid_stars,
|
|
185
|
+
}
|
|
186
|
+
result = await client(ctx)(fn.SendMediaRequest(**only(values, fn.SendMediaRequest)))
|
|
187
|
+
sent = _send.message_from_updates(result, chat_id=chat_id)
|
|
188
|
+
checklist = await _reread(ctx, peer, chat_id, sent.id, result)
|
|
189
|
+
checklist.msg_id = sent.id
|
|
190
|
+
ctx.emit("todo_created", {"chat_id": chat_id, "msg_id": sent.id, "title": checklist.title})
|
|
191
|
+
return checklist
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
SPEC_CREATE = OperationSpec(
|
|
195
|
+
id="todo.create",
|
|
196
|
+
request=CreateReq,
|
|
197
|
+
response=Todo,
|
|
198
|
+
impl=create,
|
|
199
|
+
summary="Send a checklist / to-do list",
|
|
200
|
+
description=(
|
|
201
|
+
"Creating a checklist needs Telegram Premium. Task ids are assigned "
|
|
202
|
+
"1..n and are the handle every other `todo` command uses."
|
|
203
|
+
),
|
|
204
|
+
aliases=("message.checklist",),
|
|
205
|
+
mutating=True,
|
|
206
|
+
rate_class="send",
|
|
207
|
+
columns=("chat_id", "msg_id", "title", "done_count"),
|
|
208
|
+
example=_EXAMPLE_TODO,
|
|
209
|
+
example_args="todo create @team 'Release checklist' 'tag the commit' 'publish the wheel'",
|
|
210
|
+
covers=("messages-core.checklist-create", "todo.create"),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# todo get
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class GetReq(Request):
|
|
220
|
+
chat: Annotated[PeerRef, arg(0, metavar="CHAT", kind="peer", help="Chat.")]
|
|
221
|
+
msg_id: Annotated[int, arg(1, metavar="MSG_ID", kind="msg_id", help="Checklist message id.")]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def get(ctx: OpContext, req: GetReq) -> Todo:
|
|
225
|
+
"""Read a checklist: the tasks, and who ticked what and when."""
|
|
226
|
+
peer = await _send.resolve(ctx, req.chat)
|
|
227
|
+
chat_id = _send.peer_id_of(peer)
|
|
228
|
+
return (await _fetch(ctx, peer, chat_id, req.msg_id))[1]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
SPEC_GET = OperationSpec(
|
|
232
|
+
id="todo.get",
|
|
233
|
+
request=GetReq,
|
|
234
|
+
response=Todo,
|
|
235
|
+
impl=get,
|
|
236
|
+
summary="Read a checklist: tasks, who ticked what and when",
|
|
237
|
+
columns=("msg_id", "title", "done_count"),
|
|
238
|
+
example=_EXAMPLE_TODO,
|
|
239
|
+
example_args="todo get @team 12345",
|
|
240
|
+
covers=("todo.view-completions",),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# todo toggle
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class ToggleReq(Request):
|
|
250
|
+
chat: Annotated[PeerRef, arg(0, metavar="CHAT", kind="peer", help="Chat.")]
|
|
251
|
+
msg_id: Annotated[int, arg(1, metavar="MSG_ID", kind="msg_id", help="Checklist message id.")]
|
|
252
|
+
done: Annotated[list[str], opt("--done", metavar="ID", help="Task ids to tick.")] = []
|
|
253
|
+
undone: Annotated[list[str], opt("--undone", metavar="ID", help="Task ids to untick.")] = []
|
|
254
|
+
send_as: Annotated[
|
|
255
|
+
PeerRef | None,
|
|
256
|
+
opt("--send-as", metavar="PEER", kind="peer", help="Tick as a channel or anonymously."),
|
|
257
|
+
] = None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _task_ids(values: list[str], known: set[int], field: str) -> list[int]:
|
|
261
|
+
from tlgr.ops._common import ids as expand
|
|
262
|
+
|
|
263
|
+
out = expand(tuple(values))
|
|
264
|
+
unknown = [task for task in out if task not in known]
|
|
265
|
+
if unknown:
|
|
266
|
+
raise UsageError(f"this checklist has no task {unknown[0]}", field=field)
|
|
267
|
+
return out
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
async def toggle(ctx: OpContext, req: ToggleReq) -> Todo:
|
|
271
|
+
"""Tick and untick tasks in one call.
|
|
272
|
+
|
|
273
|
+
`toggleTodoCompleted` takes both vectors at once, so "move task 2 to done
|
|
274
|
+
and task 3 back" is one request and one service message rather than two
|
|
275
|
+
that could interleave with somebody else's edit.
|
|
276
|
+
"""
|
|
277
|
+
from telethon.tl.functions import messages as fn
|
|
278
|
+
|
|
279
|
+
peer = await _send.resolve(ctx, req.chat)
|
|
280
|
+
chat_id = _send.peer_id_of(peer)
|
|
281
|
+
_, checklist = await _fetch(ctx, peer, chat_id, req.msg_id)
|
|
282
|
+
known = {task.id for task in checklist.tasks}
|
|
283
|
+
completed = _task_ids(req.done, known, "done")
|
|
284
|
+
incompleted = _task_ids(req.undone, known, "undone")
|
|
285
|
+
if not completed and not incompleted:
|
|
286
|
+
raise UsageError("name at least one task with --done or --undone", field="done")
|
|
287
|
+
|
|
288
|
+
state = {task.id: task.done for task in checklist.tasks}
|
|
289
|
+
if all(state.get(task) for task in completed) and not any(
|
|
290
|
+
state.get(task) for task in incompleted
|
|
291
|
+
):
|
|
292
|
+
already(ctx)
|
|
293
|
+
checklist.already = True
|
|
294
|
+
return checklist
|
|
295
|
+
|
|
296
|
+
if req.send_as is not None:
|
|
297
|
+
# Ticking honours the chat's default send-as identity, and there is no
|
|
298
|
+
# per-request field for it; the default is what the server reads.
|
|
299
|
+
await client(ctx)(
|
|
300
|
+
fn.SaveDefaultSendAsRequest(peer=peer, send_as=await _send.resolve(ctx, req.send_as))
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
result = await client(ctx)(
|
|
304
|
+
fn.ToggleTodoCompletedRequest(
|
|
305
|
+
peer=peer, msg_id=req.msg_id, completed=completed, incompleted=incompleted
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
updated = await _reread(ctx, peer, chat_id, req.msg_id, result)
|
|
309
|
+
ctx.emit(
|
|
310
|
+
"todo_toggled",
|
|
311
|
+
{"chat_id": chat_id, "msg_id": req.msg_id, "done": completed, "undone": incompleted},
|
|
312
|
+
)
|
|
313
|
+
return updated
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
SPEC_TOGGLE = OperationSpec(
|
|
317
|
+
id="todo.toggle",
|
|
318
|
+
request=ToggleReq,
|
|
319
|
+
response=Todo,
|
|
320
|
+
impl=toggle,
|
|
321
|
+
summary="Tick and untick checklist tasks in one call",
|
|
322
|
+
description=(
|
|
323
|
+
"Needs to be the author unless the list was created with "
|
|
324
|
+
"`--others-can-complete`. A no-op is `already: true`."
|
|
325
|
+
),
|
|
326
|
+
aliases=("todo.done", "todo.undone"),
|
|
327
|
+
mutating=True,
|
|
328
|
+
idempotent=True,
|
|
329
|
+
rate_class="send",
|
|
330
|
+
columns=("msg_id", "done_count"),
|
|
331
|
+
example=_EXAMPLE_TODO,
|
|
332
|
+
example_args="todo toggle @team 12345 --done 1",
|
|
333
|
+
covers=(
|
|
334
|
+
"messages-core.checklist-toggle-task",
|
|
335
|
+
"todo.send-as",
|
|
336
|
+
"todo.toggle-completed",
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
# todo add
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class AddReq(Request):
|
|
347
|
+
chat: Annotated[PeerRef, arg(0, metavar="CHAT", kind="peer", help="Chat.")]
|
|
348
|
+
msg_id: Annotated[int, arg(1, metavar="MSG_ID", kind="msg_id", help="Checklist message id.")]
|
|
349
|
+
tasks: Annotated[list[str], arg(2, metavar="TASK", variadic=True, help="Tasks to append.")] = []
|
|
350
|
+
parse: Annotated[str | None, opt("--parse", metavar="MODE", help="md|html|none.")] = None
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
async def add(ctx: OpContext, req: AddReq) -> Todo:
|
|
354
|
+
"""Append tasks to an existing checklist.
|
|
355
|
+
|
|
356
|
+
New ids continue from `max(existing) + 1`. Reusing an id the list has
|
|
357
|
+
already seen is `TODO_ITEM_DUPLICATE`, and reusing one it has *forgotten*
|
|
358
|
+
would re-attach an old completion to a new task.
|
|
359
|
+
"""
|
|
360
|
+
from telethon.tl.functions import messages as fn
|
|
361
|
+
|
|
362
|
+
if not req.tasks:
|
|
363
|
+
raise UsageError("name at least one task to append", field="tasks")
|
|
364
|
+
|
|
365
|
+
peer = await _send.resolve(ctx, req.chat)
|
|
366
|
+
chat_id = _send.peer_id_of(peer)
|
|
367
|
+
_, checklist = await _fetch(ctx, peer, chat_id, req.msg_id)
|
|
368
|
+
if not checklist.others_can_add:
|
|
369
|
+
# Non-authors are refused by the server; saying so here costs nothing
|
|
370
|
+
# and turns an RPC error into an answer.
|
|
371
|
+
ctx.warn("this checklist is not open for appending; only its author may add tasks")
|
|
372
|
+
|
|
373
|
+
start = max((task.id for task in checklist.tasks), default=0) + 1
|
|
374
|
+
entries = list(enumerate(req.tasks, start=start))
|
|
375
|
+
result = await client(ctx)(
|
|
376
|
+
fn.AppendTodoListRequest(peer=peer, msg_id=req.msg_id, list=_tl_items(entries, req.parse))
|
|
377
|
+
)
|
|
378
|
+
return await _reread(ctx, peer, chat_id, req.msg_id, result)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
SPEC_ADD = OperationSpec(
|
|
382
|
+
id="todo.add",
|
|
383
|
+
request=AddReq,
|
|
384
|
+
response=Todo,
|
|
385
|
+
impl=add,
|
|
386
|
+
summary="Append tasks to an existing checklist",
|
|
387
|
+
description=(
|
|
388
|
+
"Ids continue from the highest one the list already has; they are "
|
|
389
|
+
"never reused, because a completion is keyed by task id."
|
|
390
|
+
),
|
|
391
|
+
mutating=True,
|
|
392
|
+
rate_class="send",
|
|
393
|
+
columns=("msg_id", "done_count"),
|
|
394
|
+
example=_EXAMPLE_TODO,
|
|
395
|
+
example_args="todo add @team 12345 'sign the release'",
|
|
396
|
+
covers=("messages-core.checklist-append-tasks",),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# ---------------------------------------------------------------------------
|
|
401
|
+
# todo edit
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class EditReq(Request):
|
|
406
|
+
chat: Annotated[PeerRef, arg(0, metavar="CHAT", kind="peer", help="Chat.")]
|
|
407
|
+
msg_id: Annotated[int, arg(1, metavar="MSG_ID", kind="msg_id", help="Checklist message id.")]
|
|
408
|
+
title: Annotated[str | None, opt("--title", metavar="TEXT", help="New title.")] = None
|
|
409
|
+
add_task: Annotated[
|
|
410
|
+
list[str], opt("--add-task", metavar="TEXT", help="Add a task; repeatable.")
|
|
411
|
+
] = []
|
|
412
|
+
remove_task: Annotated[
|
|
413
|
+
list[str], opt("--remove-task", metavar="ID", help="Remove a task id; repeatable.")
|
|
414
|
+
] = []
|
|
415
|
+
rename_task: Annotated[
|
|
416
|
+
list[str], opt("--rename-task", metavar="ID=TEXT", help="Retitle a task; repeatable.")
|
|
417
|
+
] = []
|
|
418
|
+
others_can_add: Annotated[
|
|
419
|
+
str | None, opt("--others-can-add", metavar="on|off", help="Who may append.")
|
|
420
|
+
] = None
|
|
421
|
+
others_can_complete: Annotated[
|
|
422
|
+
str | None, opt("--others-can-complete", metavar="on|off", help="Who may tick.")
|
|
423
|
+
] = None
|
|
424
|
+
parse: Annotated[str | None, opt("--parse", metavar="MODE", help="md|html|none.")] = None
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _switch(value: str | None, current: bool, field: str) -> bool:
|
|
428
|
+
if value is None:
|
|
429
|
+
return current
|
|
430
|
+
if value not in ("on", "off"):
|
|
431
|
+
raise UsageError(f"--{field} is on or off", field=field)
|
|
432
|
+
return value == "on"
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
async def edit(ctx: OpContext, req: EditReq) -> Todo:
|
|
436
|
+
"""Rename a list, add or remove tasks, change who may add or tick.
|
|
437
|
+
|
|
438
|
+
`editMessage` replaces the whole `todoList`, so this is a read-modify-write
|
|
439
|
+
that *keeps every surviving task's id*. Renumbering would be the natural
|
|
440
|
+
thing to do and would move every completion onto the wrong row.
|
|
441
|
+
"""
|
|
442
|
+
from telethon.tl import types
|
|
443
|
+
from telethon.tl.functions import messages as fn
|
|
444
|
+
|
|
445
|
+
peer = await _send.resolve(ctx, req.chat)
|
|
446
|
+
chat_id = _send.peer_id_of(peer)
|
|
447
|
+
message, checklist = await _fetch(ctx, peer, chat_id, req.msg_id)
|
|
448
|
+
|
|
449
|
+
known = {task.id for task in checklist.tasks}
|
|
450
|
+
doomed = set(_task_ids(req.remove_task, known, "remove-task"))
|
|
451
|
+
renames: dict[int, str] = {}
|
|
452
|
+
for pair in req.rename_task:
|
|
453
|
+
raw, sep, text = pair.partition("=")
|
|
454
|
+
if not sep:
|
|
455
|
+
raise UsageError("--rename-task wants ID=TEXT", field="rename-task")
|
|
456
|
+
try:
|
|
457
|
+
task_id = int(raw)
|
|
458
|
+
except ValueError as exc:
|
|
459
|
+
raise UsageError(f"{raw!r} is not a task id", field="rename-task") from exc
|
|
460
|
+
if task_id not in known:
|
|
461
|
+
raise UsageError(f"this checklist has no task {task_id}", field="rename-task")
|
|
462
|
+
renames[task_id] = text
|
|
463
|
+
|
|
464
|
+
entries = [
|
|
465
|
+
(task.id, renames.get(task.id, task.title))
|
|
466
|
+
for task in checklist.tasks
|
|
467
|
+
if task.id not in doomed
|
|
468
|
+
]
|
|
469
|
+
start = max(known, default=0) + 1
|
|
470
|
+
entries += list(enumerate(req.add_task, start=start))
|
|
471
|
+
if not entries:
|
|
472
|
+
raise UsageError(
|
|
473
|
+
"a checklist cannot be emptied; delete the message instead", field="remove-task"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
title, entities = _send.body(
|
|
477
|
+
req.title if req.title is not None else checklist.title, parse=req.parse
|
|
478
|
+
)
|
|
479
|
+
todo = types.TodoList(
|
|
480
|
+
title=types.TextWithEntities(text=title, entities=_send.tl_entities(entities) or []),
|
|
481
|
+
list=_tl_items(entries, req.parse),
|
|
482
|
+
others_can_append=_switch(req.others_can_add, checklist.others_can_add, "others-can-add")
|
|
483
|
+
or None,
|
|
484
|
+
others_can_complete=_switch(
|
|
485
|
+
req.others_can_complete, checklist.others_can_complete, "others-can-complete"
|
|
486
|
+
)
|
|
487
|
+
or None,
|
|
488
|
+
)
|
|
489
|
+
if not getattr(message, "out", False):
|
|
490
|
+
raise PermissionError_("only the author of a checklist may edit it")
|
|
491
|
+
|
|
492
|
+
result = await client(ctx)(
|
|
493
|
+
fn.EditMessageRequest(peer=peer, id=req.msg_id, media=types.InputMediaTodo(todo=todo))
|
|
494
|
+
)
|
|
495
|
+
return await _reread(ctx, peer, chat_id, req.msg_id, result)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
SPEC_EDIT = OperationSpec(
|
|
499
|
+
id="todo.edit",
|
|
500
|
+
request=EditReq,
|
|
501
|
+
response=Todo,
|
|
502
|
+
impl=edit,
|
|
503
|
+
summary="Rename a checklist, remove or add tasks, change who may add or tick",
|
|
504
|
+
description=(
|
|
505
|
+
"Surviving tasks keep their ids: completions are keyed by id, and "
|
|
506
|
+
"renumbering would move every tick onto a different task."
|
|
507
|
+
),
|
|
508
|
+
mutating=True,
|
|
509
|
+
rate_class="send",
|
|
510
|
+
columns=("msg_id", "title", "done_count"),
|
|
511
|
+
example=_EXAMPLE_TODO,
|
|
512
|
+
example_args="todo edit @team 12345 --title 'Release 2.0'",
|
|
513
|
+
covers=("messages-core.checklist-edit", "todo.edit-list", "todo.permissions"),
|
|
514
|
+
)
|