Spaces:
Running
Running
github-actions[bot] commited on
Commit ·
d55a0af
1
Parent(s): 6c3a31d
Automated sync from GitHub
Browse files- .gitattributes +1 -1
- bot/app.py +9 -70
- bot/db.py +1 -1
- bot/formats.py +1 -1
- bot/panels.py +1 -1
- bot/richtext.py +1 -1
- tests/appkit.py +6 -0
- tests/test_app.py +38 -25
- tests/test_caption_formatting_app.py +46 -47
- tests/test_check.py +3 -3
- tests/test_formats.py +1 -1
- tests/test_formats_app.py +1 -1
- tests/test_richtext.py +5 -5
- tests/test_smedia.py +2 -2
- tests/tgfakes.py +1 -1
.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
|
| 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
|
| 371 |
-
"""The caption the person sent
|
| 372 |
-
|
| 373 |
-
|
| 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(
|
| 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
|
| 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;
|
| 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
|
| 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:
|
| 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. /
|
| 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 & 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
|
| 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,
|
| 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
|
| 170 |
assert await autofmt(ctx) is None
|
| 171 |
-
|
| 172 |
-
assert await autofmt(ctx) is None # only
|
| 173 |
|
| 174 |
-
|
| 175 |
-
assert "Template saved" in
|
| 176 |
-
auto = await autofmt(ctx) #
|
| 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 |
-
|
| 181 |
-
assert "Template of your auto format" in text and "[S{season}E{episode}] {title} [{quality}]" in text
|
| 182 |
|
| 183 |
-
await
|
|
|
|
| 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
|
| 196 |
-
line, blocks = variables_line((await
|
| 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
|
| 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 |
-
|
|
|
|
| 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
|
| 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
|
| 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 |
-
|
| 279 |
assert await auto_thumb(ctx) is None and (await autofmt(ctx))["id"] == auto["id"] # the format stays
|
| 280 |
-
assert "
|
| 281 |
-
|
|
|
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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 |
-
|
| 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 |
-
#
|
| 17 |
# ---------------------------------------------------------------------------
|
| 18 |
|
| 19 |
|
| 20 |
-
async def
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
| 22 |
assert (await autofmt(ctx))["caption"] == STORED
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
-
async def
|
| 27 |
-
await
|
| 28 |
-
text = (await
|
| 29 |
-
assert "
|
| 30 |
assert "<b>" not in text and "<a href" not in text
|
| 31 |
|
| 32 |
|
| 33 |
-
async def
|
| 34 |
-
await
|
| 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
|
| 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
|
| 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
|
| 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
|
| 56 |
assert (await autofmt(ctx))["caption"] == "<b>{filename}</b> <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
|
| 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
|
| 66 |
-
|
| 67 |
-
assert (await autofmt(ctx))["caption"] == "<b>{filename}</b>" and "Caption template saved" in
|
| 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 |
-
|
| 73 |
assert (await autofmt(ctx))["caption"] == "<b>{filename}</b> · <i>{size} left</i>"
|
| 74 |
-
assert "Unknown" not in
|
| 75 |
|
| 76 |
|
| 77 |
async def test_unknown_variables_are_still_reported(ctx):
|
| 78 |
-
|
| 79 |
-
assert "Unknown variables" in
|
| 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
|
| 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 "<b>" 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
|
| 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
|
| 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
|
| 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
|
| 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 "<b>" 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 "<b>" not in text and "<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"] == "<b>{filename}</b> <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
|
| 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
|
| 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
|
| 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
|
| 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
|
| 90 |
-
text, entities = telegram_text("/
|
| 91 |
-
assert from_entities(text, entities, start=len("/
|
| 92 |
-
whole = types.MessageEntityBold(0, len("/
|
| 93 |
-
assert from_entities("/
|
| 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
|
| 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 |
-
``"/
|
| 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
|