github-actions[bot] commited on
Commit
d55a0af
·
1 Parent(s): 6c3a31d

Automated sync from GitHub

Browse files
.gitattributes CHANGED
@@ -1,3 +1,4 @@
 
1
  *.webp filter=lfs diff=lfs merge=lfs -text
2
  *.mp4 filter=lfs diff=lfs merge=lfs -text
3
  *.mp3 filter=lfs diff=lfs merge=lfs -text
@@ -5,4 +6,3 @@
5
  *.jpeg filter=lfs diff=lfs merge=lfs -text
6
  *.png filter=lfs diff=lfs merge=lfs -text
7
  *.ttf filter=lfs diff=lfs merge=lfs -text
8
- *.otf filter=lfs diff=lfs merge=lfs -text
 
1
+ *.otf filter=lfs diff=lfs merge=lfs -text
2
  *.webp filter=lfs diff=lfs merge=lfs -text
3
  *.mp4 filter=lfs diff=lfs merge=lfs -text
4
  *.mp3 filter=lfs diff=lfs merge=lfs -text
 
6
  *.jpeg filter=lfs diff=lfs merge=lfs -text
7
  *.png filter=lfs diff=lfs merge=lfs -text
8
  *.ttf filter=lfs diff=lfs merge=lfs -text
 
bot/app.py CHANGED
@@ -41,7 +41,7 @@ from .parser import compute_new_name, missing_variables, parse_caption, parse_in
41
  from .pipeline import PROBE_EXTS, Renamer, embedded_thumb, original_name_of, source_ext
42
  from .queue import Job, JobQueue
43
  from .sequence import SeqItem, SequenceState, analyse, format_report
44
- from .settings_ops import CLEAR_WORDS, META_COMMANDS, apply_setting, parse_channel, preview_template
45
  from .telegraph import Telegraph, TelegraphError
46
  from .utils import clip, esc, human_size, human_uptime
47
 
@@ -55,8 +55,6 @@ COMMANDS = [
55
  ("sequence", "Collect files to sort by episode"),
56
  ("sort", "Sort the collected files by episode"),
57
  ("mode", "Choose Filename or Caption mode"),
58
- ("template", "Set the template of your auto format"),
59
- ("caption", "Set the caption of your auto format"),
60
  ("filetype", "Choose how files are sent"),
61
  ("metadata", "Metadata and track settings"),
62
  ("keepaudio", "Keep audio tracks by language or number"),
@@ -66,8 +64,6 @@ COMMANDS = [
66
  ("mediainfo", "Full media details of a file on Telegraph (reply to it)"),
67
  ("ethumb", "Extract the thumbnail of a file or video"),
68
  ("resize", "Resize an image, for thumbnails"),
69
- ("thumb", "View the thumbnail of your auto format"),
70
- ("delthumb", "Delete the thumbnail of your auto format"),
71
  ("queue", "Show your queued files"),
72
  ("cancel", "Cancel your current job"),
73
  ("speed", "Test the bot's internet speed (Ookla)"),
@@ -347,13 +343,11 @@ class App:
347
  def _command_table(self) -> Dict[str, Callable[[Any], Awaitable[None]]]:
348
  table: Dict[str, Callable[[Any], Awaitable[None]]] = {
349
  "start": self.cmd_start, "help": self.cmd_help, "rename": self.cmd_rename, "formats": self.cmd_formats, "format": self.cmd_formats,
350
- "sequence": self.cmd_sequence, "sort": self.cmd_sort, "mode": self.cmd_mode,
351
- "template": self.cmd_template, "caption": self.cmd_caption, "filetype": self.cmd_filetype,
352
  "metadata": self.cmd_metadata, "keepaudio": self._cmd_keep("keep_audio", "Keep Audio", "/keepaudio English Japanese"),
353
  "keepsubtitle": self._cmd_keep("keep_subtitle", "Keep Subtitle", "/keepsubtitle English"),
354
  "tracks": self.cmd_tracks, "check": self.cmd_check, "mediainfo": self.cmd_mediainfo,
355
  "ethumb": self.cmd_ethumb, "resize": self.cmd_resize,
356
- "thumb": self.cmd_thumb, "delthumb": self.cmd_delthumb,
357
  "removewords": self.cmd_remove, "setchannel": self.cmd_setchannel, "queue": self.cmd_queue,
358
  "cancel": self.cmd_cancel, "speed": self.cmd_speed, "status": self.cmd_status, "stats": self.cmd_stats,
359
  "addadmin": self.cmd_addadmin, "deladmin": self.cmd_deladmin, "admins": self.cmd_admins, "smedia": self.cmd_smedia,
@@ -367,15 +361,14 @@ class App:
367
  return (event.pattern_match.group(1) or "").strip() if event.pattern_match else ""
368
 
369
  @staticmethod
370
- def _caption_input(event: Any, start: int = 0) -> str:
371
- """The caption the person sent (the message from ``start`` on). Formatting they put on it in Telegram (bold, italic,
372
- links...) becomes the HTML that is stored, so nobody has to type tags; without any formatting it is taken as typed."""
373
- raw = event.raw_text or ""
374
- typed = raw[start:]
375
  if typed.strip().lower() in CLEAR_WORDS: # "reset" stays a reset even if it was made bold
376
  return typed
377
  entities = getattr(getattr(event, "message", None), "entities", None)
378
- return richtext.from_entities(raw, entities, start) or typed
379
 
380
  async def _reply(self, event: Any, text: str, rows: Optional[List[panels.Row]] = None) -> Any:
381
  buttons = panels.to_buttons(rows) if rows else None
@@ -551,47 +544,6 @@ class App:
551
  """What keeps a setting: the template and the caption are those of the auto format in use, the rest are the user's own."""
552
  return AutoFormatSettings(self.db) if kind in ("template", "caption") else self.db
553
 
554
- async def cmd_template(self, event: Any) -> None:
555
- arg = self._args(event)
556
- uid = event.sender_id
557
- if arg:
558
- _ok, msg = await apply_setting(self._store("template"), uid, "template", arg)
559
- await self._reply(event, msg)
560
- return
561
- auto = await self.db.get_auto_format(uid) or default_auto_format()
562
- await self._reply(
563
- event,
564
- f"<b>Template of your auto format{self._named(auto)}:</b>\n<code>{esc(auto['template'])}</code>\n\n"
565
- f"{preview_template(auto['template'])}\n\n"
566
- f"Variables: {panels.TEMPLATE_VARIABLES}\n"
567
- f"{panels.EPISODE_ZEROS}\n"
568
- "A [bracket] group is dropped when a variable inside it is empty.\n\n"
569
- "Change it: <code>/template [S{season}E{episode}] {title} [{quality}]</code>",
570
- )
571
-
572
- @staticmethod
573
- def _named(auto: Dict[str, Any]) -> str:
574
- """`` “One Piece”`` for a saved auto format, nothing for the built-in default."""
575
- return f" “{esc(clip(auto['name'], 30))}”" if auto.get("name") else ""
576
-
577
- async def cmd_caption(self, event: Any) -> None:
578
- arg = self._args(event)
579
- uid = event.sender_id
580
- if arg:
581
- value = self._caption_input(event, event.pattern_match.start(1))
582
- _ok, msg = await apply_setting(self._store("caption"), uid, "caption", value)
583
- await self._reply(event, msg)
584
- return
585
- auto = await self.db.get_auto_format(uid) or default_auto_format()
586
- await self._reply(
587
- event,
588
- f"<b>Caption of your auto format{self._named(auto)}:</b>\n{richtext.preview(auto['caption'], 600)}\n\n"
589
- f"Variables: {panels.CAPTION_VARIABLES}\n"
590
- "Change it: send <code>/caption</code> and your caption in one message. Format it the way you do in any message "
591
- "(<b>bold</b>, <i>italic</i>, links…) and I'll keep it exactly like that.\n"
592
- "Restore the default: <code>/caption reset</code>",
593
- )
594
-
595
  async def cmd_remove(self, event: Any) -> None:
596
  arg = self._args(event)
597
  uid = event.sender_id
@@ -660,10 +612,10 @@ class App:
660
  await self._reply(event, f"✅ Renamed files will go to <code>{cid}</code>." if cid else "✅ Renamed files will come back to this chat.")
661
 
662
  async def _thumb_format(self, uid: int, fid: Optional[int] = None) -> Optional[Dict[str, Any]]:
663
- """The auto format a thumbnail command or button is about.
664
 
665
  A button names its format (``th:rm:12``), so an old message never reaches the thumbnail of whichever format is in use
666
- now; a command, and the buttons of messages from before that, mean the one in use. ``None``: there is no such format.
667
  """
668
  if fid is None:
669
  return await self.db.get_auto_format(uid)
@@ -682,13 +634,6 @@ class App:
682
  return False
683
  return await self.db.set_format_thumb(uid, auto["id"], None)
684
 
685
- async def cmd_thumb(self, event: Any) -> None:
686
- auto, data = await self._auto_thumb(event.sender_id)
687
- if not data:
688
- await self._reply(event, "Your auto format has no thumbnail. Send me a photo to set one.")
689
- return
690
- await self._send_thumb(event.chat_id, data, auto["id"])
691
-
692
  async def _send_thumb(self, chat_id: int, data: bytes, fid: int) -> None:
693
  bio = io.BytesIO(data)
694
  bio.name = "thumbnail.jpg"
@@ -697,12 +642,6 @@ class App:
697
  buttons=panels.to_buttons([[("🗑 Delete", f"th:rm:{fid}"), ("✖ Close", "x:close")]]),
698
  )
699
 
700
- async def cmd_delthumb(self, event: Any) -> None:
701
- if await self._delete_auto_thumb(event.sender_id):
702
- await self._reply(event, "🗑 Thumbnail deleted.")
703
- else:
704
- await self._reply(event, "Your auto format has no thumbnail.")
705
-
706
  async def on_photo(self, event: Any) -> None:
707
  uid = event.sender_id
708
  p = self.pending.get(uid)
 
41
  from .pipeline import PROBE_EXTS, Renamer, embedded_thumb, original_name_of, source_ext
42
  from .queue import Job, JobQueue
43
  from .sequence import SeqItem, SequenceState, analyse, format_report
44
+ from .settings_ops import CLEAR_WORDS, META_COMMANDS, apply_setting, parse_channel
45
  from .telegraph import Telegraph, TelegraphError
46
  from .utils import clip, esc, human_size, human_uptime
47
 
 
55
  ("sequence", "Collect files to sort by episode"),
56
  ("sort", "Sort the collected files by episode"),
57
  ("mode", "Choose Filename or Caption mode"),
 
 
58
  ("filetype", "Choose how files are sent"),
59
  ("metadata", "Metadata and track settings"),
60
  ("keepaudio", "Keep audio tracks by language or number"),
 
64
  ("mediainfo", "Full media details of a file on Telegraph (reply to it)"),
65
  ("ethumb", "Extract the thumbnail of a file or video"),
66
  ("resize", "Resize an image, for thumbnails"),
 
 
67
  ("queue", "Show your queued files"),
68
  ("cancel", "Cancel your current job"),
69
  ("speed", "Test the bot's internet speed (Ookla)"),
 
343
  def _command_table(self) -> Dict[str, Callable[[Any], Awaitable[None]]]:
344
  table: Dict[str, Callable[[Any], Awaitable[None]]] = {
345
  "start": self.cmd_start, "help": self.cmd_help, "rename": self.cmd_rename, "formats": self.cmd_formats, "format": self.cmd_formats,
346
+ "sequence": self.cmd_sequence, "sort": self.cmd_sort, "mode": self.cmd_mode, "filetype": self.cmd_filetype,
 
347
  "metadata": self.cmd_metadata, "keepaudio": self._cmd_keep("keep_audio", "Keep Audio", "/keepaudio English Japanese"),
348
  "keepsubtitle": self._cmd_keep("keep_subtitle", "Keep Subtitle", "/keepsubtitle English"),
349
  "tracks": self.cmd_tracks, "check": self.cmd_check, "mediainfo": self.cmd_mediainfo,
350
  "ethumb": self.cmd_ethumb, "resize": self.cmd_resize,
 
351
  "removewords": self.cmd_remove, "setchannel": self.cmd_setchannel, "queue": self.cmd_queue,
352
  "cancel": self.cmd_cancel, "speed": self.cmd_speed, "status": self.cmd_status, "stats": self.cmd_stats,
353
  "addadmin": self.cmd_addadmin, "deladmin": self.cmd_deladmin, "admins": self.cmd_admins, "smedia": self.cmd_smedia,
 
361
  return (event.pattern_match.group(1) or "").strip() if event.pattern_match else ""
362
 
363
  @staticmethod
364
+ def _caption_input(event: Any) -> str:
365
+ """The caption the person sent. Formatting they put on it in Telegram (bold, italic, links...) becomes the HTML that is
366
+ stored, so nobody has to type tags; without any formatting it is taken as typed."""
367
+ typed = event.raw_text or ""
 
368
  if typed.strip().lower() in CLEAR_WORDS: # "reset" stays a reset even if it was made bold
369
  return typed
370
  entities = getattr(getattr(event, "message", None), "entities", None)
371
+ return richtext.from_entities(typed, entities) or typed
372
 
373
  async def _reply(self, event: Any, text: str, rows: Optional[List[panels.Row]] = None) -> Any:
374
  buttons = panels.to_buttons(rows) if rows else None
 
544
  """What keeps a setting: the template and the caption are those of the auto format in use, the rest are the user's own."""
545
  return AutoFormatSettings(self.db) if kind in ("template", "caption") else self.db
546
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  async def cmd_remove(self, event: Any) -> None:
548
  arg = self._args(event)
549
  uid = event.sender_id
 
612
  await self._reply(event, f"✅ Renamed files will go to <code>{cid}</code>." if cid else "✅ Renamed files will come back to this chat.")
613
 
614
  async def _thumb_format(self, uid: int, fid: Optional[int] = None) -> Optional[Dict[str, Any]]:
615
+ """The auto format a thumbnail button is about.
616
 
617
  A button names its format (``th:rm:12``), so an old message never reaches the thumbnail of whichever format is in use
618
+ now; the buttons of messages from before that carry no id and mean the one in use. ``None``: there is no such format.
619
  """
620
  if fid is None:
621
  return await self.db.get_auto_format(uid)
 
634
  return False
635
  return await self.db.set_format_thumb(uid, auto["id"], None)
636
 
 
 
 
 
 
 
 
637
  async def _send_thumb(self, chat_id: int, data: bytes, fid: int) -> None:
638
  bio = io.BytesIO(data)
639
  bio.name = "thumbnail.jpg"
 
642
  buttons=panels.to_buttons([[("🗑 Delete", f"th:rm:{fid}"), ("✖ Close", "x:close")]]),
643
  )
644
 
 
 
 
 
 
 
645
  async def on_photo(self, event: Any) -> None:
646
  uid = event.sender_id
647
  p = self.pending.get(uid)
bot/db.py CHANGED
@@ -530,7 +530,7 @@ class Database:
530
  async def ensure_auto_format(self, uid: int) -> Dict[str, Any]:
531
  """The auto format in use; the first time there is none, a format called "Default" is made and used.
532
 
533
- That is what lets ``/template``, ``/caption`` and a thumbnail photo work for someone who never made a format. Several photos
534
  sent at once ask for it at the same moment, so one user is served at a time: the first makes it, the others find it.
535
  """
536
  async with self._default_locks.setdefault(uid, asyncio.Lock()):
 
530
  async def ensure_auto_format(self, uid: int) -> Dict[str, Any]:
531
  """The auto format in use; the first time there is none, a format called "Default" is made and used.
532
 
533
+ That is what lets the Template and Caption buttons and a thumbnail photo work for someone who never made a format. Several photos
534
  sent at once ask for it at the same moment, so one user is served at a time: the first makes it, the others find it.
535
  """
536
  async with self._default_locks.setdefault(uid, asyncio.Lock()):
bot/formats.py CHANGED
@@ -121,7 +121,7 @@ class FormatSettings:
121
 
122
 
123
  class AutoFormatSettings:
124
- """The auto format in use, edited the way a setting is: ``/template``, ``/caption`` and their buttons go through here.
125
 
126
  A user who has no auto format gets one called "Default" the first time they change something.
127
  """
 
121
 
122
 
123
  class AutoFormatSettings:
124
+ """The auto format in use, edited the way a setting is: the Template and Caption buttons go through here.
125
 
126
  A user who has no auto format gets one called "Default" the first time they change something.
127
  """
bot/panels.py CHANGED
@@ -236,7 +236,7 @@ def help_panel() -> Panel:
236
  "1. Send a file — I rename it with your <b>auto format</b> (pick it in the /start menu) and send it back.\n"
237
  "2. /rename — one file with a <b>manual format</b>: pick one, then send the file.\n"
238
  "3. /formats — 12 auto and 12 manual formats (template, caption, thumbnail, metadata): edit, copy between kinds, share.\n"
239
- "4. /template, /caption — change those of your auto format.\n"
240
  "5. /mode — read details from the file name or the caption.\n"
241
  "6. /metadata, /keepaudio, /keepsubtitle — default metadata and tracks (formats can copy it).\n"
242
  "7. /filetype — send as document, video or audio.\n"
 
236
  "1. Send a file — I rename it with your <b>auto format</b> (pick it in the /start menu) and send it back.\n"
237
  "2. /rename — one file with a <b>manual format</b>: pick one, then send the file.\n"
238
  "3. /formats — 12 auto and 12 manual formats (template, caption, thumbnail, metadata): edit, copy between kinds, share.\n"
239
+ "4. /start → Rename, Caption &amp; Thumbnail — edit your auto format.\n"
240
  "5. /mode — read details from the file name or the caption.\n"
241
  "6. /metadata, /keepaudio, /keepsubtitle — default metadata and tracks (formats can copy it).\n"
242
  "7. /filetype — send as document, video or audio.\n"
bot/richtext.py CHANGED
@@ -119,7 +119,7 @@ def _render(body: str, spans: Iterable[Span]) -> str:
119
  def from_entities(text: str, entities: Optional[Iterable[Any]], start: int = 0) -> Optional[str]:
120
  """``text[start:]`` as HTML that carries the formatting in ``entities``, or ``None`` when there is none to keep.
121
 
122
- ``start`` skips what comes before the caption (a ``/caption`` command). A ``{variable}`` is never split: a format that
123
  covers only part of one (double-tapping the word inside the braces does that) is widened to the whole variable, because
124
  the variable is replaced as a whole.
125
  """
 
119
  def from_entities(text: str, entities: Optional[Iterable[Any]], start: int = 0) -> Optional[str]:
120
  """``text[start:]`` as HTML that carries the formatting in ``entities``, or ``None`` when there is none to keep.
121
 
122
+ ``start`` skips what comes before the caption (a command word, say). A ``{variable}`` is never split: a format that
123
  covers only part of one (double-tapping the word inside the braces does that) is widened to the whole variable, because
124
  the variable is replaced as a whole.
125
  """
tests/appkit.py CHANGED
@@ -86,6 +86,12 @@ async def set_auto_thumb(ctx, data, uid=OWNER):
86
  return fmt
87
 
88
 
 
 
 
 
 
 
89
  def jpeg(size=(640, 480)):
90
  buf = io.BytesIO()
91
  Image.new("RGB", size, (200, 30, 30)).save(buf, "JPEG")
 
86
  return fmt
87
 
88
 
89
+ async def edit_auto(ctx, uid=OWNER, **changes):
90
+ """Change the user's auto format (``template=...``, ``caption=...``), making the "Default" one first if they have none."""
91
+ fmt = await ctx.db.ensure_auto_format(uid)
92
+ return await ctx.db.update_format(uid, fmt["id"], **changes)
93
+
94
+
95
  def jpeg(size=(640, 480)):
96
  buf = io.BytesIO()
97
  Image.new("RGB", size, (200, 30, 30)).save(buf, "JPEG")
tests/test_app.py CHANGED
@@ -16,8 +16,8 @@ from bot.queue import JobQueue
16
 
17
  from .conftest import needs_ffmpeg
18
  from .appkit import (
19
- ADMIN_CFG, ADMIN_DB, EPISODE, EPISODE_NEW, LOG, OWNER, STRANGER, auto_thumb, autofmt, click, data_of, env_for, jpeg, prefs, say,
20
- send_doc, set_auto_thumb,
21
  )
22
  from .tgfakes import (
23
  FakeCallback, FakeClient, FakeEvent, FakeMessage, dispatch, document_message, flat_buttons, make_file, variables_line,
@@ -166,25 +166,36 @@ async def test_metadata_command(ctx):
166
  assert "currently: Off" in ev.texts[0]
167
 
168
 
169
- async def test_template_caption_and_removewords_commands(ctx):
170
  assert await autofmt(ctx) is None
171
- assert "Template of your auto format" in (await say(ctx, OWNER, "/template")).texts[0]
172
- assert await autofmt(ctx) is None # only looking: no format is made
173
 
174
- ev = await say(ctx, OWNER, "/template [S{season}E{episode}] {title} [{quality}]")
175
- assert "Template saved" in ev.texts[0]
176
- auto = await autofmt(ctx) # they are the template and caption of the auto format in use, made on the first change
177
  assert auto["name"] == "Default" and auto["kind"] == "auto"
178
  assert auto["template"] == "[S{season}E{episode}] {title} [{quality}]"
179
  assert "template" not in await prefs(ctx) and "caption" not in await prefs(ctx)
180
- text = (await say(ctx, OWNER, "/template")).texts[0]
181
- assert "Template of your auto format" in text and "[S{season}E{episode}] {title} [{quality}]" in text
182
 
183
- await say(ctx, OWNER, "/caption <b>{filename}</b> {size}")
 
184
  assert (await autofmt(ctx))["caption"] == "<b>{filename}</b> {size}"
185
- assert "Caption of your auto format" in (await say(ctx, OWNER, "/caption")).texts[0]
186
  assert await ctx.db.count_formats(OWNER) == 1 # both went into the same format
187
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  await say(ctx, OWNER, "/removewords @Chan, [Group]")
189
  assert (await prefs(ctx))["remove_words"] == ["@Chan", "[Group]"]
190
  assert "@Chan, [Group]" in (await say(ctx, OWNER, "/removewords")).texts[0]
@@ -192,10 +203,10 @@ async def test_template_caption_and_removewords_commands(ctx):
192
  assert (await prefs(ctx))["remove_words"] == []
193
 
194
 
195
- async def test_template_and_caption_commands_list_each_variable_as_its_own_copyable_block(ctx):
196
- line, blocks = variables_line((await say(ctx, OWNER, "/template")).texts[0])
197
  assert blocks == line.split(": ", 1)[1].split() and "{quality}" in blocks and "{3episode}" in blocks and "{4episode}" not in blocks and len(blocks) == 11
198
- line, blocks = variables_line((await say(ctx, OWNER, "/caption")).texts[0])
199
  assert blocks == line.split(": ", 1)[1].split() and "{size}" in blocks and "{3episode}" in blocks and "{4episode}" not in blocks and len(blocks) == 13
200
 
201
 
@@ -260,25 +271,27 @@ async def test_setchannel(ctx):
260
 
261
 
262
  async def test_photo_becomes_the_thumbnail_of_the_auto_format_and_can_be_viewed_and_deleted(ctx):
263
- assert "no thumbnail" in (await say(ctx, OWNER, "/thumb")).texts[0] and await autofmt(ctx) is None
 
264
  ev = FakeEvent(OWNER, message=FakeMessage("", mid=20, photo=True), photo_bytes=jpeg())
265
  await dispatch(ctx.client, ev)
266
  assert "Thumbnail saved" in ev.texts[0] and "auto format" in ev.texts[0]
267
- auto = await autofmt(ctx) # the first photo makes the auto format "Default" (like /template does)
268
  assert auto["name"] == "Default" and auto["has_thumb"]
269
  assert f"th:rm:{auto['id']}" in data_of(ev.out[0][1]) # the button names the format whose thumbnail it deletes
270
  stored = await auto_thumb(ctx)
271
  assert stored[:2] == b"\xff\xd8"
272
  assert await auto_thumb(ctx, ADMIN_CFG) is None and await autofmt(ctx, ADMIN_CFG) is None # per user
273
 
274
- await say(ctx, OWNER, "/thumb")
275
  (chat, file, kw), = ctx.client.files
276
  assert chat == OWNER and file.read() == stored and f"th:rm:{auto['id']}" in data_of(kw["buttons"])
277
 
278
- assert "deleted" in (await say(ctx, OWNER, "/delthumb")).texts[0]
279
  assert await auto_thumb(ctx) is None and (await autofmt(ctx))["id"] == auto["id"] # the format stays
280
- assert "no thumbnail" in (await say(ctx, OWNER, "/delthumb")).texts[0]
281
- assert "no thumbnail" in (await say(ctx, OWNER, "/thumb")).texts[0]
 
282
 
283
 
284
  async def test_a_photo_goes_to_the_auto_format_in_use_and_never_to_a_manual_one(ctx):
@@ -336,7 +349,7 @@ async def test_thumbnail_buttons_name_their_format_so_an_old_message_never_hits_
336
  await ctx.db.set_format_thumb(OWNER, one["id"], b"\xff\xd8one")
337
  await ctx.db.set_format_thumb(OWNER, two["id"], b"\xff\xd8two")
338
  await ctx.db.select_auto_format(OWNER, one["id"])
339
- await say(ctx, OWNER, "/thumb") # the Delete button under it belongs to "One"
340
  (_chat, _file, kw), = ctx.client.files
341
  assert data_of(kw["buttons"])[0] == f"th:rm:{one['id']}"
342
  _text, rows = await ctx.app.panel(OWNER, "cap") # the panel names the format it shows, too
@@ -413,7 +426,7 @@ async def test_remove_words_are_applied_to_the_new_name(ctx):
413
 
414
 
415
  async def test_the_one_piece_style_name_gets_its_episode_number(ctx):
416
- await say(ctx, OWNER, "/template One Piece - E{episode}")
417
  ev = await send_doc(ctx, OWNER, "1147 - One Piece [Dual] 720p @Anime_X_Universe.mkv", mid=21)
418
  (job,) = ctx.app.queue.jobs.values()
419
  assert job.new_name == "One Piece - E1147.mkv" and job.notes == []
@@ -421,7 +434,7 @@ async def test_the_one_piece_style_name_gets_its_episode_number(ctx):
421
 
422
 
423
  async def test_a_template_variable_that_cannot_be_filled_is_flagged(ctx):
424
- await say(ctx, OWNER, "/template One Piece - E{episode}")
425
  ev = await send_doc(ctx, OWNER, "Plain Movie.mkv")
426
  (job,) = ctx.app.queue.jobs.values()
427
  assert job.new_name == "One Piece - E.mkv"
@@ -734,7 +747,7 @@ async def test_toggles_update_settings_and_refresh_the_panel(ctx):
734
 
735
 
736
  async def test_clear_buttons(ctx):
737
- await say(ctx, OWNER, "/caption custom {filename}")
738
  assert (await autofmt(ctx))["caption"] == "custom {filename}"
739
  cb = await click(ctx, OWNER, "clr:caption")
740
  assert (await autofmt(ctx))["caption"] == "<b>{filename}</b>" and "CAPTION" in cb.edits[-1][0]
 
16
 
17
  from .conftest import needs_ffmpeg
18
  from .appkit import (
19
+ ADMIN_CFG, ADMIN_DB, EPISODE, EPISODE_NEW, LOG, OWNER, STRANGER, auto_thumb, autofmt, click, data_of, edit_auto, env_for, jpeg,
20
+ prefs, say, send_doc, set_auto_thumb,
21
  )
22
  from .tgfakes import (
23
  FakeCallback, FakeClient, FakeEvent, FakeMessage, dispatch, document_message, flat_buttons, make_file, variables_line,
 
166
  assert "currently: Off" in ev.texts[0]
167
 
168
 
169
+ async def test_the_template_and_caption_buttons_edit_the_auto_format_and_make_it_when_needed(ctx):
170
  assert await autofmt(ctx) is None
171
+ await click(ctx, OWNER, "in:template", msg_id=500)
172
+ assert await autofmt(ctx) is None # only opening the prompt: no format is made
173
 
174
+ await say(ctx, OWNER, "[S{season}E{episode}] {title} [{quality}]", mid=60)
175
+ assert "Template saved" in ctx.client.edited[-1][1]
176
+ auto = await autofmt(ctx) # it is the template of the auto format in use, made on the first change
177
  assert auto["name"] == "Default" and auto["kind"] == "auto"
178
  assert auto["template"] == "[S{season}E{episode}] {title} [{quality}]"
179
  assert "template" not in await prefs(ctx) and "caption" not in await prefs(ctx)
180
+ assert "[S{season}E{episode}] {title} [{quality}]" in (await click(ctx, OWNER, "m:rename")).edits[-1][0]
 
181
 
182
+ await click(ctx, OWNER, "in:caption", msg_id=501)
183
+ await say(ctx, OWNER, "<b>{filename}</b> {size}", mid=61)
184
  assert (await autofmt(ctx))["caption"] == "<b>{filename}</b> {size}"
 
185
  assert await ctx.db.count_formats(OWNER) == 1 # both went into the same format
186
 
187
+
188
+ @pytest.mark.parametrize("command", ["template", "caption", "thumb", "delthumb"])
189
+ async def test_the_template_caption_and_thumbnail_commands_are_gone(ctx, command):
190
+ assert command not in [name for name, _description in COMMANDS] and command not in ctx.app._command_table()
191
+ await set_auto_thumb(ctx, b"\xff\xd8thumb")
192
+ before = await autofmt(ctx)
193
+ ev = await say(ctx, OWNER, f"/{command} something {{title}}")
194
+ assert ev.out == [] # not a command any more: nothing answers it
195
+ assert await autofmt(ctx) == before and await auto_thumb(ctx) == b"\xff\xd8thumb"
196
+
197
+
198
+ async def test_removewords_command(ctx):
199
  await say(ctx, OWNER, "/removewords @Chan, [Group]")
200
  assert (await prefs(ctx))["remove_words"] == ["@Chan", "[Group]"]
201
  assert "@Chan, [Group]" in (await say(ctx, OWNER, "/removewords")).texts[0]
 
203
  assert (await prefs(ctx))["remove_words"] == []
204
 
205
 
206
+ async def test_template_and_caption_prompts_list_each_variable_as_its_own_copyable_block(ctx):
207
+ line, blocks = variables_line((await click(ctx, OWNER, "in:template")).edits[-1][0])
208
  assert blocks == line.split(": ", 1)[1].split() and "{quality}" in blocks and "{3episode}" in blocks and "{4episode}" not in blocks and len(blocks) == 11
209
+ line, blocks = variables_line((await click(ctx, OWNER, "in:caption")).edits[-1][0])
210
  assert blocks == line.split(": ", 1)[1].split() and "{size}" in blocks and "{3episode}" in blocks and "{4episode}" not in blocks and len(blocks) == 13
211
 
212
 
 
271
 
272
 
273
  async def test_photo_becomes_the_thumbnail_of_the_auto_format_and_can_be_viewed_and_deleted(ctx):
274
+ cb = await click(ctx, OWNER, "th:view")
275
+ assert cb.alerts and "no thumbnail" in cb.alerts[0] and await autofmt(ctx) is None
276
  ev = FakeEvent(OWNER, message=FakeMessage("", mid=20, photo=True), photo_bytes=jpeg())
277
  await dispatch(ctx.client, ev)
278
  assert "Thumbnail saved" in ev.texts[0] and "auto format" in ev.texts[0]
279
+ auto = await autofmt(ctx) # the first photo makes the auto format "Default" (like the first template change does)
280
  assert auto["name"] == "Default" and auto["has_thumb"]
281
  assert f"th:rm:{auto['id']}" in data_of(ev.out[0][1]) # the button names the format whose thumbnail it deletes
282
  stored = await auto_thumb(ctx)
283
  assert stored[:2] == b"\xff\xd8"
284
  assert await auto_thumb(ctx, ADMIN_CFG) is None and await autofmt(ctx, ADMIN_CFG) is None # per user
285
 
286
+ await click(ctx, OWNER, "th:view")
287
  (chat, file, kw), = ctx.client.files
288
  assert chat == OWNER and file.read() == stored and f"th:rm:{auto['id']}" in data_of(kw["buttons"])
289
 
290
+ cb = await click(ctx, OWNER, f"th:del:{auto['id']}") # Delete Thumb in the Caption & Thumbnail panel
291
  assert await auto_thumb(ctx) is None and (await autofmt(ctx))["id"] == auto["id"] # the format stays
292
+ assert "none" in cb.edits[-1][0] and cb.toasts == ["Thumbnail deleted."]
293
+ cb = await click(ctx, OWNER, "th:view")
294
+ assert cb.alerts and "no thumbnail" in cb.alerts[0]
295
 
296
 
297
  async def test_a_photo_goes_to_the_auto_format_in_use_and_never_to_a_manual_one(ctx):
 
349
  await ctx.db.set_format_thumb(OWNER, one["id"], b"\xff\xd8one")
350
  await ctx.db.set_format_thumb(OWNER, two["id"], b"\xff\xd8two")
351
  await ctx.db.select_auto_format(OWNER, one["id"])
352
+ await click(ctx, OWNER, "th:view") # the Delete button under the thumbnail it sends belongs to "One"
353
  (_chat, _file, kw), = ctx.client.files
354
  assert data_of(kw["buttons"])[0] == f"th:rm:{one['id']}"
355
  _text, rows = await ctx.app.panel(OWNER, "cap") # the panel names the format it shows, too
 
426
 
427
 
428
  async def test_the_one_piece_style_name_gets_its_episode_number(ctx):
429
+ await edit_auto(ctx, template="One Piece - E{episode}")
430
  ev = await send_doc(ctx, OWNER, "1147 - One Piece [Dual] 720p @Anime_X_Universe.mkv", mid=21)
431
  (job,) = ctx.app.queue.jobs.values()
432
  assert job.new_name == "One Piece - E1147.mkv" and job.notes == []
 
434
 
435
 
436
  async def test_a_template_variable_that_cannot_be_filled_is_flagged(ctx):
437
+ await edit_auto(ctx, template="One Piece - E{episode}")
438
  ev = await send_doc(ctx, OWNER, "Plain Movie.mkv")
439
  (job,) = ctx.app.queue.jobs.values()
440
  assert job.new_name == "One Piece - E.mkv"
 
747
 
748
 
749
  async def test_clear_buttons(ctx):
750
+ await edit_auto(ctx, caption="custom {filename}")
751
  assert (await autofmt(ctx))["caption"] == "custom {filename}"
752
  cb = await click(ctx, OWNER, "clr:caption")
753
  assert (await autofmt(ctx))["caption"] == "<b>{filename}</b>" and "CAPTION" in cb.edits[-1][0]
tests/test_caption_formatting_app.py CHANGED
@@ -1,108 +1,107 @@
1
  """A caption keeps the formatting it was sent with (bold, italic, links...): nobody has to type HTML.
2
 
3
- /caption and the Set Caption button edit the caption of the auto format in use, so ``autofmt`` is where it is read back."""
4
  from telethon.extensions import html as telegram_html
5
 
6
  from bot.pipeline import make_caption
7
 
8
- from .appkit import OWNER, autofmt, click, data_of, say, say_formatted
9
  from .test_formats_app import make_format
10
 
11
  FORMATTED = "[b]Watch[/b] {title} [i]episode {episode}[/i] on [a=https://t.me/Anime_X]our channel[/a]"
12
  STORED = '<b>Watch</b> {title} <i>episode {episode}</i> on <a href="https://t.me/Anime_X">our channel</a>'
13
 
14
 
 
 
 
 
 
 
 
 
 
 
15
  # ---------------------------------------------------------------------------
16
- # /caption
17
  # ---------------------------------------------------------------------------
18
 
19
 
20
- async def test_the_caption_command_keeps_the_formatting_that_was_applied(ctx):
21
- ev = await say_formatted(ctx, OWNER, "/caption " + FORMATTED)
 
 
 
22
  assert (await autofmt(ctx))["caption"] == STORED
23
- assert "Caption template saved" in ev.texts[0] and "Unknown" not in ev.texts[0]
 
 
 
24
 
25
 
26
- async def test_the_caption_command_shows_the_caption_with_its_formatting(ctx):
27
- await say_formatted(ctx, OWNER, "/caption " + FORMATTED)
28
- text = (await say(ctx, OWNER, "/caption")).texts[0]
29
- assert "Caption of your auto format" in text and STORED in text # shown as the person wrote it, not as tags to read
30
  assert "&lt;b&gt;" not in text and "&lt;a href" not in text
31
 
32
 
33
- async def test_the_command_may_carry_the_bot_name_extra_spaces_and_several_lines(ctx):
34
- await say_formatted(ctx, OWNER, "/caption@RenameBot [b]{filename}[/b]\nsecond [i]line[/i] {size}")
35
  assert (await autofmt(ctx))["caption"] == "<b>{filename}</b>\nsecond <i>line</i> {size}"
36
 
37
 
38
  async def test_emoji_before_the_formatted_words_do_not_shift_it(ctx):
39
- await say_formatted(ctx, OWNER, "/caption 🔥🔥 [b]{filename}[/b] 👨‍👩‍👧 [i]{size}[/i]")
40
  assert (await autofmt(ctx))["caption"] == "🔥🔥 <b>{filename}</b> 👨‍👩‍👧 <i>{size}</i>"
41
 
42
 
43
  async def test_html_typed_by_hand_still_works_when_nothing_is_formatted(ctx):
44
- await say(ctx, OWNER, "/caption <i>{filename}</i> · <a href=\"https://t.me/x\">join</a>")
45
  assert (await autofmt(ctx))["caption"] == '<i>{filename}</i> · <a href="https://t.me/x">join</a>'
46
 
47
 
48
  async def test_what_telegram_finds_by_itself_is_not_formatting(ctx):
49
  """A #hashtag or an @mention is an entity too, but nothing was formatted: typed tags are still taken as typed."""
50
- await say_formatted(ctx, OWNER, "/caption <i>{filename}</i> [tag]#anime[/tag] [at]@channel[/at]")
51
  assert (await autofmt(ctx))["caption"] == "<i>{filename}</i> #anime @channel"
52
 
53
 
54
  async def test_once_words_are_formatted_the_message_is_taken_as_written(ctx):
55
- await say_formatted(ctx, OWNER, "/caption <b>{filename}</b> [i]x[/i]")
56
  assert (await autofmt(ctx))["caption"] == "&lt;b&gt;{filename}&lt;/b&gt; <i>x</i>" # the tags the person typed are just text
57
 
58
 
59
  async def test_a_spoiler_is_not_kept_so_its_words_stay_plain(ctx):
60
- await say_formatted(ctx, OWNER, "/caption [spoiler]secret[/spoiler] {title}")
61
  assert (await autofmt(ctx))["caption"] == "secret {title}"
62
 
63
 
64
  async def test_reset_still_resets_when_it_was_made_bold(ctx):
65
- await say(ctx, OWNER, "/caption custom {filename}")
66
- ev = await say_formatted(ctx, OWNER, "/caption [b]reset[/b]")
67
- assert (await autofmt(ctx))["caption"] == "<b>{filename}</b>" and "Caption template saved" in ev.texts[0]
68
 
69
 
70
  async def test_a_variable_cut_by_the_formatting_is_still_a_whole_variable(ctx):
71
  """Double-tapping {filename} selects the word without its braces: the bold covers the whole variable all the same."""
72
- ev = await say_formatted(ctx, OWNER, "/caption {[b]filename[/b]} · {siz[i]e} left[/i]")
73
  assert (await autofmt(ctx))["caption"] == "<b>{filename}</b> · <i>{size} left</i>"
74
- assert "Unknown" not in ev.texts[0]
75
 
76
 
77
  async def test_unknown_variables_are_still_reported(ctx):
78
- ev = await say_formatted(ctx, OWNER, "/caption [b]{filename}[/b] {nonsense}")
79
- assert "Unknown variables" in ev.texts[0] and "{nonsense}" in ev.texts[0]
80
  assert (await autofmt(ctx))["caption"] == "<b>{filename}</b> {nonsense}"
81
 
82
 
83
  async def test_a_caption_that_is_too_long_with_its_link_addresses_is_refused(ctx):
84
- ev = await say_formatted(ctx, OWNER, f"/caption [a=https://example.com/{'a' * 800}]link[/a] {{filename}}")
85
- assert "too long" in ev.texts[0] and "link addresses" in ev.texts[0]
86
  assert await autofmt(ctx) is None # nothing was saved, so there is no format either
87
 
88
 
89
- # ---------------------------------------------------------------------------
90
- # the Set Caption button, then the caption
91
- # ---------------------------------------------------------------------------
92
-
93
-
94
- async def test_the_set_caption_prompt_keeps_the_formatting(ctx):
95
- cb = await click(ctx, OWNER, "in:caption", msg_id=500)
96
- assert "No HTML needed" in cb.edits[-1][0]
97
- ev = await say_formatted(ctx, OWNER, FORMATTED, mid=60)
98
- assert ev.deleted and OWNER not in ctx.app.pending
99
- assert (await autofmt(ctx))["caption"] == STORED
100
- (chat, text, kw), = ctx.client.edited
101
- assert kw["message"] == 500 and "Caption template saved" in text and "CAPTION" in text
102
- assert STORED in text and "&lt;b&gt;" not in text # the panel shows the caption formatted
103
- assert "in:caption" in data_of(kw["buttons"])
104
-
105
-
106
  async def test_the_set_caption_prompt_still_takes_typed_html_and_reset(ctx):
107
  await click(ctx, OWNER, "in:caption")
108
  await say(ctx, OWNER, "<u>{filename}</u> {size}")
@@ -133,7 +132,7 @@ async def test_a_prompt_that_is_refused_stays_open_for_another_try(ctx):
133
 
134
 
135
  async def test_a_formats_caption_prompt_keeps_the_formatting(ctx):
136
- await say(ctx, OWNER, "/caption <u>{filename}</u>") # the auto format in use has a caption of its own
137
  fmt = await make_format(ctx)
138
  i = fmt["id"]
139
  cb = await click(ctx, OWNER, f"fm:in:{i}:caption", msg_id=710)
@@ -174,7 +173,7 @@ async def test_a_format_caption_reset_works_in_bold_and_other_fields_stay_plain(
174
 
175
 
176
  async def test_a_link_in_a_caption_gets_no_link_preview_on_any_screen_that_shows_it(ctx):
177
- await say_formatted(ctx, OWNER, "/caption " + FORMATTED)
178
  cb = await click(ctx, OWNER, "m:cap")
179
  assert STORED in cb.edits[-1][0] and cb.edit_kwargs[-1]["link_preview"] is False
180
 
@@ -198,7 +197,7 @@ async def test_a_link_in_a_caption_gets_no_link_preview_on_any_screen_that_shows
198
 
199
 
200
  async def test_the_formatting_follows_the_values_that_replace_the_variables(ctx):
201
- await say_formatted(ctx, OWNER, "/caption [b]{filename}[/b] · [i]{size}[/i] · [a=https://t.me/x]{original}[/a]")
202
  caption = make_caption((await autofmt(ctx))["caption"], new_name="One Piece - E5.mkv", orig_name="1147 - One Piece [Dual].mkv",
203
  ext=".mkv", size=1536, duration=0)
204
  text, entities = telegram_html.parse(caption) # what Telegram makes of it when it is sent with parse_mode="html"
@@ -212,7 +211,7 @@ async def test_the_formatting_follows_the_values_that_replace_the_variables(ctx)
212
 
213
 
214
  async def test_a_name_with_html_characters_cannot_break_the_formatting(ctx):
215
- await say_formatted(ctx, OWNER, "/caption [b]{filename}[/b] [i]{original}[/i]")
216
  caption = make_caption((await autofmt(ctx))["caption"], new_name="Tom & <Jerry>.mkv", orig_name="a<b>&c.mkv", ext=".mkv", size=1, duration=0)
217
  text, entities = telegram_html.parse(caption)
218
  assert text == "Tom & <Jerry> a<b>&c.mkv"
 
1
  """A caption keeps the formatting it was sent with (bold, italic, links...): nobody has to type HTML.
2
 
3
+ The Set Caption button edits the caption of the auto format in use, so ``autofmt`` is where it is read back."""
4
  from telethon.extensions import html as telegram_html
5
 
6
  from bot.pipeline import make_caption
7
 
8
+ from .appkit import OWNER, autofmt, click, data_of, edit_auto, say, say_formatted
9
  from .test_formats_app import make_format
10
 
11
  FORMATTED = "[b]Watch[/b] {title} [i]episode {episode}[/i] on [a=https://t.me/Anime_X]our channel[/a]"
12
  STORED = '<b>Watch</b> {title} <i>episode {episode}</i> on <a href="https://t.me/Anime_X">our channel</a>'
13
 
14
 
15
+ async def set_caption(ctx, markup, *, formatted=True):
16
+ """Tap Set Caption and send the caption (``markup`` as ``say_formatted`` takes it, or typed as it is with ``formatted=False``).
17
+
18
+ Returns the message sent and the text the prompt's message turned into once the caption was taken (empty when it was refused)."""
19
+ edited = len(ctx.client.edited)
20
+ await click(ctx, OWNER, "in:caption", msg_id=500)
21
+ ev = await (say_formatted if formatted else say)(ctx, OWNER, markup)
22
+ return ev, (ctx.client.edited[-1][1] if len(ctx.client.edited) > edited else "")
23
+
24
+
25
  # ---------------------------------------------------------------------------
26
+ # the Set Caption button, then the caption
27
  # ---------------------------------------------------------------------------
28
 
29
 
30
+ async def test_the_set_caption_prompt_keeps_the_formatting(ctx):
31
+ cb = await click(ctx, OWNER, "in:caption", msg_id=500)
32
+ assert "No HTML needed" in cb.edits[-1][0]
33
+ ev = await say_formatted(ctx, OWNER, FORMATTED, mid=60)
34
+ assert ev.deleted and OWNER not in ctx.app.pending
35
  assert (await autofmt(ctx))["caption"] == STORED
36
+ (chat, text, kw), = ctx.client.edited
37
+ assert kw["message"] == 500 and "Caption template saved" in text and "CAPTION" in text and "Unknown" not in text
38
+ assert STORED in text and "&lt;b&gt;" not in text # the panel shows the caption formatted
39
+ assert "in:caption" in data_of(kw["buttons"])
40
 
41
 
42
+ async def test_the_caption_screen_shows_the_caption_with_its_formatting(ctx):
43
+ await edit_auto(ctx, caption=STORED)
44
+ text = (await click(ctx, OWNER, "m:cap")).edits[-1][0]
45
+ assert "Of the auto format" in text and STORED in text # shown as the person wrote it, not as tags to read
46
  assert "&lt;b&gt;" not in text and "&lt;a href" not in text
47
 
48
 
49
+ async def test_a_caption_may_have_several_lines(ctx):
50
+ await set_caption(ctx, "[b]{filename}[/b]\nsecond [i]line[/i] {size}")
51
  assert (await autofmt(ctx))["caption"] == "<b>{filename}</b>\nsecond <i>line</i> {size}"
52
 
53
 
54
  async def test_emoji_before_the_formatted_words_do_not_shift_it(ctx):
55
+ await set_caption(ctx, "🔥🔥 [b]{filename}[/b] 👨‍👩‍👧 [i]{size}[/i]")
56
  assert (await autofmt(ctx))["caption"] == "🔥🔥 <b>{filename}</b> 👨‍👩‍👧 <i>{size}</i>"
57
 
58
 
59
  async def test_html_typed_by_hand_still_works_when_nothing_is_formatted(ctx):
60
+ await set_caption(ctx, "<i>{filename}</i> · <a href=\"https://t.me/x\">join</a>", formatted=False)
61
  assert (await autofmt(ctx))["caption"] == '<i>{filename}</i> · <a href="https://t.me/x">join</a>'
62
 
63
 
64
  async def test_what_telegram_finds_by_itself_is_not_formatting(ctx):
65
  """A #hashtag or an @mention is an entity too, but nothing was formatted: typed tags are still taken as typed."""
66
+ await set_caption(ctx, "<i>{filename}</i> [tag]#anime[/tag] [at]@channel[/at]")
67
  assert (await autofmt(ctx))["caption"] == "<i>{filename}</i> #anime @channel"
68
 
69
 
70
  async def test_once_words_are_formatted_the_message_is_taken_as_written(ctx):
71
+ await set_caption(ctx, "<b>{filename}</b> [i]x[/i]")
72
  assert (await autofmt(ctx))["caption"] == "&lt;b&gt;{filename}&lt;/b&gt; <i>x</i>" # the tags the person typed are just text
73
 
74
 
75
  async def test_a_spoiler_is_not_kept_so_its_words_stay_plain(ctx):
76
+ await set_caption(ctx, "[spoiler]secret[/spoiler] {title}")
77
  assert (await autofmt(ctx))["caption"] == "secret {title}"
78
 
79
 
80
  async def test_reset_still_resets_when_it_was_made_bold(ctx):
81
+ await edit_auto(ctx, caption="custom {filename}")
82
+ _ev, text = await set_caption(ctx, "[b]reset[/b]")
83
+ assert (await autofmt(ctx))["caption"] == "<b>{filename}</b>" and "Caption template saved" in text
84
 
85
 
86
  async def test_a_variable_cut_by_the_formatting_is_still_a_whole_variable(ctx):
87
  """Double-tapping {filename} selects the word without its braces: the bold covers the whole variable all the same."""
88
+ _ev, text = await set_caption(ctx, "{[b]filename[/b]} · {siz[i]e} left[/i]")
89
  assert (await autofmt(ctx))["caption"] == "<b>{filename}</b> · <i>{size} left</i>"
90
+ assert "Unknown" not in text
91
 
92
 
93
  async def test_unknown_variables_are_still_reported(ctx):
94
+ _ev, text = await set_caption(ctx, "[b]{filename}[/b] {nonsense}")
95
+ assert "Unknown variables" in text and "{nonsense}" in text
96
  assert (await autofmt(ctx))["caption"] == "<b>{filename}</b> {nonsense}"
97
 
98
 
99
  async def test_a_caption_that_is_too_long_with_its_link_addresses_is_refused(ctx):
100
+ ev, text = await set_caption(ctx, f"[a=https://example.com/{'a' * 800}]link[/a] {{filename}}")
101
+ assert "too long" in ev.texts[0] and "link addresses" in ev.texts[0] and text == ""
102
  assert await autofmt(ctx) is None # nothing was saved, so there is no format either
103
 
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  async def test_the_set_caption_prompt_still_takes_typed_html_and_reset(ctx):
106
  await click(ctx, OWNER, "in:caption")
107
  await say(ctx, OWNER, "<u>{filename}</u> {size}")
 
132
 
133
 
134
  async def test_a_formats_caption_prompt_keeps_the_formatting(ctx):
135
+ await edit_auto(ctx, caption="<u>{filename}</u>") # the auto format in use has a caption of its own
136
  fmt = await make_format(ctx)
137
  i = fmt["id"]
138
  cb = await click(ctx, OWNER, f"fm:in:{i}:caption", msg_id=710)
 
173
 
174
 
175
  async def test_a_link_in_a_caption_gets_no_link_preview_on_any_screen_that_shows_it(ctx):
176
+ await edit_auto(ctx, caption=STORED)
177
  cb = await click(ctx, OWNER, "m:cap")
178
  assert STORED in cb.edits[-1][0] and cb.edit_kwargs[-1]["link_preview"] is False
179
 
 
197
 
198
 
199
  async def test_the_formatting_follows_the_values_that_replace_the_variables(ctx):
200
+ await set_caption(ctx, "[b]{filename}[/b] · [i]{size}[/i] · [a=https://t.me/x]{original}[/a]")
201
  caption = make_caption((await autofmt(ctx))["caption"], new_name="One Piece - E5.mkv", orig_name="1147 - One Piece [Dual].mkv",
202
  ext=".mkv", size=1536, duration=0)
203
  text, entities = telegram_html.parse(caption) # what Telegram makes of it when it is sent with parse_mode="html"
 
211
 
212
 
213
  async def test_a_name_with_html_characters_cannot_break_the_formatting(ctx):
214
+ await set_caption(ctx, "[b]{filename}[/b] [i]{original}[/i]")
215
  caption = make_caption((await autofmt(ctx))["caption"], new_name="Tom & <Jerry>.mkv", orig_name="a<b>&c.mkv", ext=".mkv", size=1, duration=0)
216
  text, entities = telegram_html.parse(caption)
217
  assert text == "Tom & <Jerry> a<b>&c.mkv"
tests/test_check.py CHANGED
@@ -1,5 +1,5 @@
1
  """/check: what the bot can read from a file's name and caption."""
2
- from .appkit import OWNER, say
3
  from .tgfakes import FakeMessage, document_message
4
 
5
  NAME = "[S01 E02] Dragon Ball Super Beerus [1080p].mkv"
@@ -44,7 +44,7 @@ async def test_year_and_codec_show_up_when_the_file_has_them(ctx):
44
 
45
 
46
  async def test_the_current_mode_is_shown_and_decides_the_new_name(ctx):
47
- await say(ctx, OWNER, "/template [S{season}E{episode}] {title} [{quality}]")
48
  reply = document_message("random.mkv", caption="One Piece - S01E1147 [720p]")
49
  ev = await say(ctx, OWNER, "/check", reply=reply)
50
  assert "<code>FILENAME</code>" in ev.texts[0]
@@ -58,7 +58,7 @@ async def test_the_current_mode_is_shown_and_decides_the_new_name(ctx):
58
 
59
 
60
  async def test_the_new_name_follows_the_users_template_and_remove_words(ctx):
61
- await say(ctx, OWNER, "/template {title} - E{episode}")
62
  await say(ctx, OWNER, "/removewords @Anime_X_Universe")
63
  reply = document_message("1147 - One Piece [Dual] 720p @Anime_X_Universe.mkv")
64
  ev = await say(ctx, OWNER, "/check", reply=reply)
 
1
  """/check: what the bot can read from a file's name and caption."""
2
+ from .appkit import OWNER, edit_auto, say
3
  from .tgfakes import FakeMessage, document_message
4
 
5
  NAME = "[S01 E02] Dragon Ball Super Beerus [1080p].mkv"
 
44
 
45
 
46
  async def test_the_current_mode_is_shown_and_decides_the_new_name(ctx):
47
+ await edit_auto(ctx, template="[S{season}E{episode}] {title} [{quality}]")
48
  reply = document_message("random.mkv", caption="One Piece - S01E1147 [720p]")
49
  ev = await say(ctx, OWNER, "/check", reply=reply)
50
  assert "<code>FILENAME</code>" in ev.texts[0]
 
58
 
59
 
60
  async def test_the_new_name_follows_the_users_template_and_remove_words(ctx):
61
+ await edit_auto(ctx, template="{title} - E{episode}")
62
  await say(ctx, OWNER, "/removewords @Anime_X_Universe")
63
  reply = document_message("1147 - One Piece [Dual] 720p @Anime_X_Universe.mkv")
64
  ev = await say(ctx, OWNER, "/check", reply=reply)
tests/test_formats.py CHANGED
@@ -270,7 +270,7 @@ async def test_settings_ops_edit_a_formats_caption_with_the_checks_of_the_auto_f
270
  assert ok and "Caption template saved" in msg and "Unknown" not in msg
271
  assert (await db.get_format(U, fmt["id"]))["caption"] == "<b>{filename}</b> · {size} · {duration}"
272
  ok, msg = await apply_setting(store, U, "caption", "<i>{nonsense}</i>")
273
- assert ok and "Unknown variables" in msg and "{nonsense}" in msg # kept as plain text, as for /caption
274
  ok, msg = await apply_setting(store, U, "caption", "x" * 801)
275
  assert not ok and "too long" in msg and (await db.get_format(U, fmt["id"]))["caption"] == "<i>{nonsense}</i>"
276
  ok, msg = await apply_setting(store, U, "caption", "")
 
270
  assert ok and "Caption template saved" in msg and "Unknown" not in msg
271
  assert (await db.get_format(U, fmt["id"]))["caption"] == "<b>{filename}</b> · {size} · {duration}"
272
  ok, msg = await apply_setting(store, U, "caption", "<i>{nonsense}</i>")
273
+ assert ok and "Unknown variables" in msg and "{nonsense}" in msg # kept as plain text, as for the auto format's caption
274
  ok, msg = await apply_setting(store, U, "caption", "x" * 801)
275
  assert not ok and "too long" in msg and (await db.get_format(U, fmt["id"]))["caption"] == "<i>{nonsense}</i>"
276
  ok, msg = await apply_setting(store, U, "caption", "")
tests/test_formats_app.py CHANGED
@@ -237,7 +237,7 @@ async def test_a_formats_caption_is_its_own_and_can_be_reset(ctx):
237
  assert cb.edits[-1][0] == panels.FORMAT_PROMPTS["caption"] and data_of(cb.edits[-1][1]) == [f"fm:cx:{i}"]
238
  assert "reset" in cb.edits[-1][0]
239
 
240
- ev = await say(ctx, OWNER, "x" * 801) # the same checks as /caption: too long keeps the prompt open
241
  assert "too long" in ev.texts[0] and "Try again" in ev.texts[0] and OWNER in ctx.app.pending
242
  assert (await ctx.db.get_format(OWNER, i))["caption"] == "<b>{filename}</b>"
243
  ev = await say(ctx, OWNER, "<b>{filename}</b> · {size} · {nonsense}")
 
237
  assert cb.edits[-1][0] == panels.FORMAT_PROMPTS["caption"] and data_of(cb.edits[-1][1]) == [f"fm:cx:{i}"]
238
  assert "reset" in cb.edits[-1][0]
239
 
240
+ ev = await say(ctx, OWNER, "x" * 801) # the same checks as the Set Caption prompt: too long keeps the prompt open
241
  assert "too long" in ev.texts[0] and "Try again" in ev.texts[0] and OWNER in ctx.app.pending
242
  assert (await ctx.db.get_format(OWNER, i))["caption"] == "<b>{filename}</b>"
243
  ev = await say(ctx, OWNER, "<b>{filename}</b> · {size} · {nonsense}")
tests/test_richtext.py CHANGED
@@ -86,11 +86,11 @@ def test_emoji_do_not_move_the_formatting():
86
  assert html_of("🔥🔥 [b]Hello[/b] 👨‍👩‍👧 [i]x[/i]") == "🔥🔥 <b>Hello</b> 👨‍👩‍👧 <i>x</i>"
87
 
88
 
89
- def test_the_command_in_front_is_skipped():
90
- text, entities = telegram_text("/caption [b]Hi[/b] there")
91
- assert from_entities(text, entities, start=len("/caption ")) == "<b>Hi</b> there"
92
- whole = types.MessageEntityBold(0, len("/caption Hi")) # formatting that began inside the command is cut at the caption
93
- assert from_entities("/caption Hi", [whole], start=9) == "<b>Hi</b>"
94
 
95
 
96
  def test_start_counts_characters_not_utf16_units():
 
86
  assert html_of("🔥🔥 [b]Hello[/b] 👨‍👩‍👧 [i]x[/i]") == "🔥🔥 <b>Hello</b> 👨‍👩‍👧 <i>x</i>"
87
 
88
 
89
+ def test_a_prefix_in_front_is_skipped():
90
+ text, entities = telegram_text("/words [b]Hi[/b] there")
91
+ assert from_entities(text, entities, start=len("/words ")) == "<b>Hi</b> there"
92
+ whole = types.MessageEntityBold(0, len("/words Hi")) # formatting that began inside the prefix is cut where the caption starts
93
+ assert from_entities("/words Hi", [whole], start=len("/words ")) == "<b>Hi</b>"
94
 
95
 
96
  def test_start_counts_characters_not_utf16_units():
tests/test_smedia.py CHANGED
@@ -10,7 +10,7 @@ from telethon import types
10
 
11
  from bot import app as appmod
12
 
13
- from .appkit import ADMIN_CFG, OWNER, STRANGER, auto_thumb, autofmt, click, data_of, jpeg, say, send_doc
14
  from .tgfakes import FakeEvent, FakeMessage, dispatch, make_file
15
 
16
  GIF_BYTES = b"\x00\x00\x00\x18ftypmp42-fake-animation-bytes"
@@ -297,7 +297,7 @@ async def test_damaged_saved_media_never_breaks_start(ctx):
297
  async def test_the_menu_fits_in_a_caption(ctx):
298
  await choose(ctx, "image")
299
  await send_photo(ctx)
300
- await say(ctx, OWNER, "/template " + "{title} " * 30)
301
  await say(ctx, OWNER, "/start")
302
  assert appmod.visible_len(ctx.client.files[-1][2]["caption"]) < 1024
303
 
 
10
 
11
  from bot import app as appmod
12
 
13
+ from .appkit import ADMIN_CFG, OWNER, STRANGER, auto_thumb, autofmt, click, data_of, edit_auto, jpeg, say, send_doc
14
  from .tgfakes import FakeEvent, FakeMessage, dispatch, make_file
15
 
16
  GIF_BYTES = b"\x00\x00\x00\x18ftypmp42-fake-animation-bytes"
 
297
  async def test_the_menu_fits_in_a_caption(ctx):
298
  await choose(ctx, "image")
299
  await send_photo(ctx)
300
+ await edit_auto(ctx, template="{title} " * 30)
301
  await say(ctx, OWNER, "/start")
302
  assert appmod.visible_len(ctx.client.files[-1][2]["caption"]) < 1024
303
 
tests/tgfakes.py CHANGED
@@ -30,7 +30,7 @@ _ENTITY_TAGS = {
30
  def telegram_text(markup: str) -> Tuple[str, List[Any]]:
31
  """A message the way Telegram delivers it: the plain text and the formatting as entities.
32
 
33
- ``"/caption [b]Hi[/b] [a=https://x.io]a link[/a]"`` -> ``("/caption Hi a link", [command, bold, link])``. Tags: b i u s code
34
  quote spoiler tag at, ``[pre=python]``, ``[a=url]``; tags may overlap. Offsets count UTF-16 units, like Telegram's.
35
  """
36
  text, entities, open_tags, position = "", [], [], 0
 
30
  def telegram_text(markup: str) -> Tuple[str, List[Any]]:
31
  """A message the way Telegram delivers it: the plain text and the formatting as entities.
32
 
33
+ ``"/mode [b]Hi[/b] [a=https://x.io]a link[/a]"`` -> ``("/mode Hi a link", [command, bold, link])``. Tags: b i u s code
34
  quote spoiler tag at, ``[pre=python]``, ``[a=url]``; tags may overlap. Offsets count UTF-16 units, like Telegram's.
35
  """
36
  text, entities, open_tags, position = "", [], [], 0