Spaces:
Paused
Paused
File size: 12,686 Bytes
848377f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | """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)))
|