ButtonBot / tests /test_userbot_edit.py
github-actions[bot]
Automated sync from GitHub
848377f
Raw History Blame Contribute Delete
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())
@pytest.mark.parametrize("kind,forced", [("video", None), ("animation", None), ("audio", None), ("document", True)])
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)))