Spaces:
Paused
Paused
Download tests/test_userbot_edit.py from Mega-Evolutions/ButtonBot: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/spaces/Mega-Evolutions/ButtonBot/resolve/main/tests/test_userbot_edit.py
- Command line
-
hf download hf://spaces/Mega-Evolutions/ButtonBot/tests/test_userbot_edit.py
-
curl -L -o test_userbot_edit.py https://huggingface.co/spaces/Mega-Evolutions/ButtonBot/resolve/main/tests/test_userbot_edit.py
12.7 kB
| """The userbot as an editor: it edits what Telegram won't let the bot edit (text, caption, a new photo or file).""" | |
| import asyncio | |
| import os | |
| import pytest | |
| from telethon import TelegramClient, types | |
| from telethon.sessions import MemorySession | |
| from app.tgutil import MediaFile, fetch_media, media_ref | |
| from app.userbot import UserbotError, edit_problem_text | |
| from .fakes import NOW, make_msg, markup, photo_media, rpc, url_btn | |
| from .test_userbot import FULL, channel, ch_row, make_userbot, run | |
| async def ready_editor(bot, **kw): | |
| ub, made = make_userbot(bot, **kw) | |
| assert await ub.connect() | |
| return ub, made, ub.editor(ch_row(bot)) | |
| def only_right(rights, name): | |
| return getattr(rights, name) and not any(v for k, v in rights.to_dict().items() if k not in ("_", name)) | |
| # ------------------------------------------------------------------------------------------ text | |
| def test_a_text_edit_joins_gets_only_the_edit_right_and_gives_it_back(): | |
| async def go(): | |
| bot = channel() | |
| bot.msgs[5].reply_markup = markup([url_btn("Go", "https://t.me/x")]) | |
| ub, made, e = await ready_editor(bot) | |
| await e.edit(5, text="new text", entities=[types.MessageEntityBold(0, 3)]) | |
| m = bot.msgs[5] | |
| assert m.message == "new text" and [type(x).__name__ for x in m.entities] == ["MessageEntityBold"] | |
| assert m.reply_markup is None # a user account sends no keyboard, so Telegram drops it | |
| assert only_right(bot.admins[777], "edit_messages") | |
| assert e.notes == [ | |
| "made a one-time invite link for the userbot", | |
| "the userbot joined the channel", | |
| "made the userbot an admin with only “Edit messages of others”", | |
| ] | |
| await e.close() | |
| assert bot.admins == {} and not ub.lock.locked() | |
| assert e.notes[-1] == "took the admin right away from the userbot again" | |
| req = made["client"].requests[-1] | |
| assert type(req).__name__ == "EditMessageRequest" and req.media is None and req.reply_markup is None | |
| run(go()) | |
| def test_the_link_preview_choice_is_sent_for_text_posts_only(): | |
| async def go(): | |
| bot = channel() | |
| ub, made, e = await ready_editor(bot) | |
| await e.edit(5, text="a https://x.com", entities=[], preview=False) | |
| await e.edit(6, text="b", entities=[]) | |
| await e.close() | |
| first, second = bot.user_edits | |
| assert first.no_webpage is True and not second.no_webpage | |
| run(go()) | |
| # ----------------------------------------------------------------------------------------- media | |
| def test_a_new_photo_is_uploaded_and_replaces_the_old_one(tmp_path): | |
| async def go(): | |
| bot = channel() | |
| bot.msgs[5].media = photo_media(10) | |
| path = tmp_path / "new.jpg" | |
| path.write_bytes(b"NEW PHOTO") | |
| ub, made, e = await ready_editor(bot) | |
| await e.edit(5, text="cap", entities=[], media=MediaFile(str(path), "photo", "new.jpg")) | |
| await e.close() | |
| m = bot.msgs[5] | |
| assert isinstance(m.media, types.MessageMediaPhoto) and m.media.photo.id != 10 | |
| assert bot._media_data[("photo", m.media.photo.id)] == b"NEW PHOTO" and m.message == "cap" | |
| assert made["client"].uploads == [(str(path), "new.jpg", 9)] | |
| req = bot.user_edits[-1] | |
| assert isinstance(req.media, types.InputMediaUploadedPhoto) and req.message == "cap" | |
| run(go()) | |
| def test_a_file_keeps_its_name_type_and_attributes(tmp_path, kind, forced): | |
| async def go(): | |
| bot = channel() | |
| path = tmp_path / "Episode 12.mkv" | |
| path.write_bytes(b"VIDEO BYTES") | |
| attrs = [types.DocumentAttributeFilename("Episode 12.mkv"), types.DocumentAttributeVideo(5.0, 320, 240)] | |
| ub, made, e = await ready_editor(bot) | |
| await e.edit(5, text="", entities=[], media=MediaFile(str(path), kind, "Episode 12.mkv", "video/x-matroska", attrs)) | |
| await e.close() | |
| req = bot.user_edits[-1] | |
| assert isinstance(req.media, types.InputMediaUploadedDocument) | |
| assert req.media.mime_type == "video/x-matroska" and req.media.attributes == attrs and req.media.force_file == forced | |
| doc = bot.msgs[5].media.document | |
| assert doc.mime_type == "video/x-matroska" and doc.attributes == attrs and doc.size == 11 | |
| run(go()) | |
| def test_a_file_without_a_known_type_is_uploaded_as_a_plain_file(tmp_path): | |
| async def go(): | |
| bot = channel() | |
| path = tmp_path / "x.bin" | |
| path.write_bytes(b"1") | |
| ub, made, e = await ready_editor(bot) | |
| await e.edit(5, text="", entities=[], media=MediaFile(str(path), "document", "x.bin")) | |
| await e.close() | |
| assert bot.user_edits[-1].media.mime_type == "application/octet-stream" | |
| run(go()) | |
| # ------------------------------------------------------------------------------------------ rights | |
| def test_rights_the_userbot_already_has_are_kept_and_given_back(): | |
| async def go(): | |
| bot = channel() | |
| ub, made, e = await ready_editor(bot) | |
| made["client"]._join() | |
| bot.admins[777] = types.ChatAdminRights(delete_messages=True) # e.g. left over from USERBOT_KEEP_ADMIN | |
| await e.edit(5, text="x", entities=[]) | |
| assert bot.admins[777].delete_messages and bot.admins[777].edit_messages | |
| assert "gave the userbot the “Edit messages of others” right on top of the ones it had" in e.notes | |
| await e.close() | |
| assert bot.admins[777].delete_messages and not bot.admins[777].edit_messages | |
| assert e.notes[-1] == "put the userbot's admin rights back as they were" | |
| run(go()) | |
| def test_a_userbot_that_may_already_edit_is_left_alone(): | |
| async def go(): | |
| bot = channel() | |
| ub, made, e = await ready_editor(bot) | |
| made["client"]._join() | |
| bot.admins[777] = types.ChatAdminRights(edit_messages=True) | |
| await e.edit(5, text="x", entities=[]) | |
| await e.close() | |
| assert e.notes == [] and bot.admins[777].edit_messages | |
| assert not [r for r in bot.requests if type(r).__name__ == "EditAdminRequest"] | |
| run(go()) | |
| def test_keep_admin_keeps_the_edit_right(): | |
| async def go(): | |
| bot = channel() | |
| ub, made, e = await ready_editor(bot, keep_admin=True) | |
| await e.edit(5, text="x", entities=[]) | |
| await e.close() | |
| assert only_right(bot.admins[777], "edit_messages") | |
| assert not any("took the admin right away" in n for n in e.notes) | |
| run(go()) | |
| def test_without_the_add_admins_right_the_owner_is_told_what_to_do(): | |
| async def go(): | |
| bot = channel(rights=dict(FULL, add_admins=False)) | |
| ub, made, e = await ready_editor(bot) | |
| made["client"]._join() | |
| with pytest.raises(UserbotError) as exc: | |
| await e.edit(5, text="x", entities=[]) | |
| text = str(exc.value) | |
| assert "edit other people's posts" in text and "Add new admins" in text and "“Edit messages of others”" in text | |
| await e.close() | |
| assert not ub.lock.locked() | |
| run(go()) | |
| def test_an_invite_problem_names_the_right_to_give(): | |
| async def go(): | |
| bot = channel(rights=dict(FULL, invite=False)) # no link can be made and the channel has none | |
| ub, made, e = await ready_editor(bot) | |
| with pytest.raises(UserbotError) as exc: | |
| await e.edit(5, text="x", entities=[]) | |
| assert "“Edit messages of others”" in str(exc.value) and "Delete messages" not in str(exc.value) | |
| await e.close() | |
| run(go()) | |
| # ---------------------------------------------------------------------------------------- failures | |
| def test_a_refused_edit_is_reported_with_the_name_and_cleans_up(): | |
| async def go(): | |
| bot = channel() | |
| bot.fail["user:EditMessageRequest"] = rpc("MessageIdInvalidError") | |
| ub, made, e = await ready_editor(bot) | |
| with pytest.raises(UserbotError) as exc: | |
| await e.edit(5, text="x", entities=[]) | |
| assert "Telegram refused the userbot as well (MessageIdInvalidError)" in str(exc.value) | |
| await e.close() | |
| assert bot.admins == {} and not ub.lock.locked() | |
| run(go()) | |
| def test_a_missing_right_in_telegrams_eyes_is_explained(): | |
| async def go(): | |
| bot = channel() | |
| bot.fail["user:EditMessageRequest"] = rpc("MessageAuthorRequiredError") | |
| ub, made, e = await ready_editor(bot) | |
| with pytest.raises(UserbotError) as exc: | |
| await e.edit(5, text="x", entities=[]) | |
| assert "MessageAuthorRequiredError" in str(exc.value) and "Edit messages of others" in str(exc.value) | |
| await e.close() | |
| run(go()) | |
| def test_nothing_to_change_is_not_an_error(): | |
| async def go(): | |
| bot = channel() | |
| bot.msgs[5].message = "same" | |
| ub, made, e = await ready_editor(bot) | |
| await e.edit(5, text="same", entities=[]) | |
| await e.close() | |
| assert bot.msgs[5].message == "same" and bot.user_edits == [] | |
| run(go()) | |
| def test_an_editor_without_a_connected_userbot_says_so(): | |
| async def go(): | |
| bot = channel() | |
| ub, made = make_userbot(bot, session="") | |
| assert await ub.editor_for(ch_row(bot)) is None | |
| with pytest.raises(UserbotError, match="not connected"): | |
| await ub.editor(ch_row(bot)).edit(5, text="x", entities=[]) | |
| run(go()) | |
| def test_editor_for_connects_the_userbot_when_needed(): | |
| async def go(): | |
| bot = channel() | |
| ub, made = make_userbot(bot) | |
| assert not ub.ready | |
| e = await ub.editor_for(ch_row(bot)) | |
| assert e is not None and ub.ready | |
| await e.close() | |
| run(go()) | |
| def test_an_edit_and_a_clean_up_do_not_overlap(): | |
| async def go(): | |
| bot = channel() | |
| ub, made = make_userbot(bot) | |
| assert await ub.connect() | |
| e, d = ub.editor(ch_row(bot)), ub.deleter(ch_row(bot)) | |
| await e.edit(5, text="x", entities=[]) | |
| task = asyncio.ensure_future(d.delete([1])) | |
| await asyncio.sleep(0.05) | |
| assert not task.done() and 1 in bot.msgs # it waits until the editor lets go | |
| await e.close() | |
| await asyncio.wait_for(task, 5) | |
| await d.close() | |
| assert 1 not in bot.msgs and bot.admins == {} | |
| run(go()) | |
| def test_edit_problem_text(): | |
| ready = type("U", (), {"enabled": True, "ready": True, "last_error": None})() | |
| off = type("U", (), {"enabled": False, "ready": False, "last_error": None})() | |
| down = type("U", (), {"enabled": True, "ready": False, "last_error": "the session was revoked"})() | |
| assert "see /userbot" in edit_problem_text(None) and "see /userbot" in edit_problem_text(off) | |
| assert "the session was revoked" in edit_problem_text(down) | |
| assert edit_problem_text(ready, fallback_error="boom") == "The userbot could not do it either: boom" | |
| assert edit_problem_text(ready) == "" | |
| # ------------------------------------------------------------------------------ the bot's download | |
| def test_fetch_media_names_a_file_like_the_original_and_keeps_its_attributes(tmp_path, monkeypatch): | |
| async def fake_download(self, input_location, file, **kw): | |
| with open(file, "wb") as f: | |
| f.write(b"THE BYTES") | |
| return file | |
| monkeypatch.setattr(TelegramClient, "_download_file", fake_download) | |
| attrs = [types.DocumentAttributeFilename("Episode 12.mkv"), types.DocumentAttributeVideo(5.0, 320, 240)] | |
| doc = types.Document(21, 7, b"fr", NOW, "video/x-matroska", 1000, 2, attrs) | |
| ref = media_ref(make_msg(1, "", media=types.MessageMediaDocument(document=doc))) | |
| async def go(): | |
| client = TelegramClient(MemorySession(), 1, "h") | |
| got = await fetch_media(client, ref, "video", str(tmp_path)) | |
| assert os.path.basename(got.path) == "Episode 12.mkv" and got.name == "Episode 12.mkv" | |
| assert open(got.path, "rb").read() == b"THE BYTES" | |
| assert got.kind == "video" and got.mime_type == "video/x-matroska" | |
| assert [bytes(a) for a in got.attributes] == [bytes(a) for a in attrs] # (stored and read back: same on the wire) | |
| photo = media_ref(make_msg(2, "", media=photo_media(11))) | |
| pic = await fetch_media(client, photo, "photo", str(tmp_path)) | |
| assert pic.path.endswith(".jpg") and pic.mime_type is None and pic.attributes == [] | |
| asyncio.run(go()) | |
| def test_fetch_media_says_so_when_telegram_gives_nothing(tmp_path): | |
| class Silent: | |
| async def download_media(self, media, file=None): | |
| return None | |
| ref = media_ref(make_msg(2, "", media=photo_media(11))) | |
| with pytest.raises(ValueError): | |
| asyncio.run(fetch_media(Silent(), ref, "photo", str(tmp_path))) | |