Spaces:
Paused
Paused
Download tests/test_held_back.py from Mega-Evolutions/ButtonBot: direct link, hf CLI and curl.
- Browser
- Download file 32.6 kB
-
https://huggingface.co/spaces/Mega-Evolutions/ButtonBot/resolve/main/tests/test_held_back.py
- Command line
-
hf download hf://spaces/Mega-Evolutions/ButtonBot/tests/test_held_back.py
-
curl -L -o test_held_back.py https://huggingface.co/spaces/Mega-Evolutions/ButtonBot/resolve/main/tests/test_held_back.py
32.6 kB
| """A post Telegram holds back - a copyright strike shows "This message couldn't be displayed on your device due to | |
| copyright infringement" instead of the post: /shift builds the copy from what My posts saved of it, /repost leaves it | |
| alone. The notice itself is never copied.""" | |
| import asyncio | |
| from telethon import functions, types | |
| from app.repost_engine import RepostOptions, plan_repost, saved_lookup | |
| from app.restrictions import PLACEHOLDER_NOTE | |
| from app.shift_engine import run_shift | |
| from app.tgutil import button_url | |
| from .dbutil import db_url_for, fresh_db | |
| from .fakes import FakeChannelClient, make_msg, markup, photo_media, rpc, service_msg, url_btn | |
| from .harness import norm | |
| from .test_repost_flow import fill, make_app | |
| from .test_restricted import CHANNEL_NOTICE, NOTICE, strike | |
| from .test_shift import SRC, old_posts, shift_app | |
| from .test_sync import saved | |
| def run(coro): | |
| return asyncio.run(coro) | |
| def originals(): | |
| """The posts of the source channel as they were before the strike.""" | |
| return { | |
| 1: make_msg(1, "Welcome", [types.MessageEntityBold(0, 7)]), | |
| 2: make_msg( | |
| 2, "Episode 2 is out", [types.MessageEntityBold(0, 9)], | |
| markup=markup([url_btn("Download", "https://t.me/goku?start=AAA")]), | |
| ), | |
| 3: make_msg(3, "Caption with link", [types.MessageEntityTextUrl(13, 4, "https://t.me/srcch/5")], media=photo_media(31)), | |
| 4: service_msg(4), | |
| 5: make_msg(5, "Fifth", reply_to=2), | |
| 6: make_msg(6, "Sixth"), | |
| } | |
| def held_version(m, flagged=True): | |
| """What Telegram shows of post `m` after the strike: the notice in place of the post (and, if `flagged`, its own | |
| restriction mark on the message). Id, album and reply are still there.""" | |
| h = make_msg( | |
| m.id, NOTICE, out=m.out, grouped_id=m.grouped_id, reply_to=m.reply_to.reply_to_msg_id if m.reply_to else None | |
| ) | |
| return strike(h) if flagged else h | |
| def live_posts(orig, held, flagged=True): | |
| return {i: (held_version(m, flagged) if i in held else m) for i, m in orig.items()} | |
| async def save_rows(db, orig, skip=()): | |
| for i, m in orig.items(): | |
| if i not in skip and not isinstance(m, types.MessageService): | |
| await saved(db, m) | |
| async def held_setup(tmp_path, *, held=(2, 3, 5), no_row=(), flagged=True, file_available=True, orig=None, last=None): | |
| """The source (id 1) after the strike, the destination (id 2) and a shift s1 over all of it; My posts has what the | |
| source's posts looked like before.""" | |
| orig = originals() if orig is None else orig | |
| src = FakeChannelClient(live_posts(orig, held, flagged), username="srcch", title="Anime Source") | |
| dst = src.add_channel(FakeChannelClient(old_posts(), username="dstch", cid=2, title="Anime Backup")) | |
| if file_available: # Telegram still knows the files: they were not taken down | |
| for m in orig.values(): | |
| src._remember_media(m) | |
| db = await fresh_db(db_url_for(tmp_path)) | |
| await db.save_channel(1, 5, "Anime Source", "srcch", 1) | |
| await db.save_channel(2, 6, "Anime Backup", "dstch", 1) | |
| await save_rows(db, orig, no_row) | |
| await db.create_shift("s1", src=SRC, dst_channel_id=2, first_id=1, last_id=last or max(orig), user_id=1) | |
| return src, dst, db, await db.get_channel(2), await db.get_shift("s1") | |
| def forwarded_ids(chan): | |
| return [i for r in chan.requests if isinstance(r, functions.messages.ForwardMessagesRequest) for i in r.id] | |
| def sent(chan, kind): | |
| return [r for r in chan.requests if isinstance(r, kind)] | |
| def shown(chan): | |
| return {i: m.message for i, m in chan.msgs.items() if i > 10} | |
| # ============================================================================================ /shift, engine | |
| def test_a_held_back_post_is_copied_from_my_posts_and_never_as_the_notice(tmp_path): | |
| async def go(): | |
| for flagged in (True, False): # Telegram's own mark on the message, or only the notice as its text | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path / str(flagged), flagged=flagged) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert (res.failed, res.aborted, res.held_back, res.media_lost) == ([], None, [], []) | |
| assert res.from_saved == 3 and res.copied_units == 5 and res.used_saved | |
| # copies: 1->11 2->12 3->13 5->14 6->15 (4 is a service message) | |
| assert shown(dst) == { | |
| 11: "Welcome", 12: "Episode 2 is out", 13: "Caption with link", 14: "Fifth", 15: "Sixth" | |
| } | |
| assert not any(NOTICE in (m.message or "") for m in dst.msgs.values()) | |
| # only the posts Telegram shows as they are were copied by Telegram itself | |
| assert forwarded_ids(dst) == [1, 6] | |
| # text, formatting and buttons come from My posts | |
| assert [(e.offset, e.length) for e in dst.msgs[12].entities] == [(0, 9)] | |
| btn = dst.msgs[12].reply_markup.rows[0].buttons[0] | |
| assert (btn.text, button_url(btn)) == ("Download", "https://t.me/goku?start=AAA") | |
| # the photo of the saved post is there, and the link to the post that was copied later points at its copy | |
| assert dst.msgs[13].media.photo.id == 31 | |
| assert dst.msgs[13].entities[0].url == "https://t.me/dstch/14" | |
| # a post that answered another answers the copy of it | |
| assert dst.msgs[14].reply_to.reply_to_msg_id == 12 | |
| # My posts knows the copies as they are | |
| row = await db.find_by_message(2, 13) | |
| assert (row.text, row.media_kind) == ("Caption with link", "photo") | |
| assert (await db.find_by_message(2, 12)).buttons == [[{"t": "Download", "u": "https://t.me/goku?start=AAA"}]] | |
| # the source is only read | |
| assert not [r for r in src.requests if type(r).__name__.startswith(("Edit", "Delete", "Send", "Forward"))] | |
| await db.close() | |
| run(go()) | |
| def test_a_post_that_telegram_shows_as_it_is_still_comes_from_the_channel(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=(2,)) | |
| # My posts has an older text for post 1 (somebody edited it and the sync did not run yet): the channel wins | |
| row = await db.find_by_message(1, 1) | |
| await db.update_post(row.id, text="an older text") | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.from_saved == 1 and res.held_back == [] | |
| assert dst.msgs[11].message == "Welcome" and dst.msgs[12].message == "Episode 2 is out" | |
| assert forwarded_ids(dst) == [1, 3, 6] # post 5 answers a post, so it is posted again; 2 comes from My posts | |
| await db.close() | |
| run(go()) | |
| def test_a_post_with_telegrams_mark_comes_from_my_posts_even_when_telegram_still_hands_it_out(tmp_path): | |
| """The rule of a struck channel: what My posts saved wins over what Telegram hands out now - however intact that | |
| looks. Only a post without a saved copy is copied as Telegram hands it out (it is the post itself, not a notice).""" | |
| async def go(): | |
| orig = originals() | |
| live = dict(orig) | |
| live[2] = strike(make_msg(2, "Episode 2 is out", [types.MessageEntityBold(0, 9)], markup=orig[2].reply_markup)) | |
| live[3] = strike(make_msg(3, "Caption as Telegram hands it out", media=photo_media(31))) # differs from My posts | |
| src = FakeChannelClient(live, username="srcch", title="Anime Source") | |
| dst = src.add_channel(FakeChannelClient(old_posts(), username="dstch", cid=2, title="Anime Backup")) | |
| for m in orig.values(): | |
| src._remember_media(m) | |
| db = await fresh_db(db_url_for(tmp_path)) | |
| await db.save_channel(1, 5, "Anime Source", "srcch", 1) | |
| await db.save_channel(2, 6, "Anime Backup", "dstch", 1) | |
| await save_rows(db, orig, skip=(2,)) # no saved copy of post 2; post 3 has one | |
| await db.create_shift("s1", src=SRC, dst_channel_id=2, first_id=1, last_id=6, user_id=1) | |
| res = await run_shift(src, db, await db.get_shift("s1"), await db.get_channel(2), 99, delay=0, settle_pause=0) | |
| assert res.held_back == [] and res.failed == [] and res.copied_units == 5 | |
| assert res.from_saved == 1 # post 3 | |
| assert res.live_restricted == 1 # post 2: marked, but nothing saved to use instead and its content is intact | |
| assert shown(dst) == {11: "Welcome", 12: "Episode 2 is out", 13: "Caption with link", 14: "Fifth", 15: "Sixth"} | |
| assert forwarded_ids(dst) == [1, 6] # a restricted post is never copied by Telegram itself | |
| await db.close() | |
| run(go()) | |
| def test_a_post_that_telegram_empties_out_is_held_back_too(tmp_path): | |
| async def go(): | |
| orig = originals() | |
| live = dict(orig) | |
| live[2] = strike(make_msg(2, ""), "Removed after a copyright complaint") # nothing of the post is handed out | |
| src = FakeChannelClient(live, username="srcch", title="Anime Source") | |
| dst = src.add_channel(FakeChannelClient(old_posts(), username="dstch", cid=2, title="Anime Backup")) | |
| db = await fresh_db(db_url_for(tmp_path)) | |
| await db.save_channel(1, 5, "Anime Source", "srcch", 1) | |
| await db.save_channel(2, 6, "Anime Backup", "dstch", 1) | |
| await save_rows(db, orig) | |
| await db.create_shift("s1", src=SRC, dst_channel_id=2, first_id=1, last_id=6, user_id=1) | |
| res = await run_shift(src, db, await db.get_shift("s1"), await db.get_channel(2), 99, delay=0, settle_pause=0) | |
| assert res.from_saved == 1 and res.held_back == [] and res.failed == [] | |
| assert dst.msgs[12].message == "Episode 2 is out" and res.held_why == "Removed after a copyright complaint" | |
| await db.close() | |
| run(go()) | |
| def test_telegrams_notice_is_never_posted_even_when_my_posts_holds_it_as_the_text(tmp_path): | |
| """A saved copy made from a restricted post can hold Telegram's notice as its text (an older version did that), and a | |
| post may really say it: either way it is indistinguishable from the notice, so it is not copied - and reported.""" | |
| async def go(): | |
| orig = originals() | |
| orig[6] = make_msg(6, NOTICE) | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=(2, 3, 5), orig=orig, flagged=False) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.from_saved == 3 and res.held_back == [6] and res.failed == [] | |
| assert shown(dst) == {11: "Welcome", 12: "Episode 2 is out", 13: "Caption with link", 14: "Fifth"} | |
| assert not any(NOTICE in (m.message or "") for m in dst.msgs.values()) | |
| assert 6 not in forwarded_ids(dst) and res.held_why == PLACEHOLDER_NOTE | |
| await db.close() | |
| run(go()) | |
| def test_a_saved_copy_that_holds_the_notice_is_not_used_for_a_restricted_post(tmp_path): | |
| async def go(): | |
| orig = originals() | |
| orig[5] = make_msg(5, "Fifth") # answers nothing | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=(2, 3), no_row=(2,), orig=orig) | |
| await db.create_post( # what an older version saved when it synced the struck post | |
| channel_id=1, message_id=2, status="sent", source="adopted", created_by=1, link_preview=False, | |
| text=NOTICE, entities=[], buttons=[], media_kind=None, media_file_id=None, | |
| ) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| # post 2: Telegram shows the notice and the saved copy is the notice too: nothing to copy; post 3 is fine | |
| assert res.held_back == [2] and res.from_saved == 1 and res.failed == [] | |
| assert shown(dst) == {11: "Welcome", 12: "Caption with link", 13: "Fifth", 14: "Sixth"} | |
| assert not any(NOTICE in (m.message or "") for m in dst.msgs.values()) | |
| await db.close() | |
| run(go()) | |
| def test_an_answer_to_a_post_that_could_not_be_copied_follows_when_that_post_has_its_copy(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=(2,), no_row=(2,)) # post 5 answers post 2 | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.held_back == [2] and res.failed == [(5, "ReplyParentMissing")] # it would answer nothing | |
| assert shown(dst) == {11: "Welcome", 12: "Caption with link", 13: "Sixth"} | |
| await save_rows(db, {2: originals()[2]}) # the saved copy arrives | |
| again = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert again.held_back == [] and again.failed == [] and again.copied_units == 2 | |
| assert dst.msgs[14].message == "Episode 2 is out" and dst.msgs[15].message == "Fifth" | |
| assert dst.msgs[15].reply_to.reply_to_msg_id == 14 # the answer answers the copy | |
| await db.close() | |
| run(go()) | |
| def test_a_held_back_post_without_a_saved_copy_is_skipped_and_reported(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, no_row=(3,)) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.held_back == [3] and res.from_saved == 2 and res.failed == [] and res.aborted is None | |
| assert shown(dst) == {11: "Welcome", 12: "Episode 2 is out", 13: "Fifth", 14: "Sixth"} | |
| assert not any(NOTICE in (m.message or "") for m in dst.msgs.values()) | |
| assert res.held_why == NOTICE | |
| # once My posts has it (a backup put back with /import) the rest can be done | |
| await save_rows(db, {3: originals()[3]}, skip=()) | |
| again = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert again.held_back == [] and again.from_saved == 1 and again.copied_units == 1 and again.skipped_done == 4 | |
| assert dst.msgs[15].message == "Caption with link" and dst.msgs[15].media.photo.id == 31 | |
| await db.close() | |
| run(go()) | |
| def test_a_channel_that_is_not_in_my_posts_at_all_has_nothing_to_copy_held_back_posts_from(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=(1, 2, 3, 5, 6), no_row=(1, 2, 3, 5, 6)) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.held_back == [1, 2, 3, 5, 6] and res.copied_units == 0 and res.failed == [] | |
| assert shown(dst) == {} | |
| await db.close() | |
| run(go()) | |
| def test_a_post_with_nothing_saved_in_it_is_not_posted_empty(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=(6,), no_row=(6,)) | |
| await db.create_post( | |
| channel_id=1, message_id=6, status="sent", source="adopted", created_by=1, link_preview=False, | |
| text=" ", entities=[], buttons=[], media_kind=None, media_file_id=None, | |
| ) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.held_back == [6] and 15 not in dst.msgs | |
| await db.close() | |
| run(go()) | |
| # ------------------------------------------------------------------------------------------------ the media | |
| def test_a_file_reference_that_has_expired_is_tried_again_without_it(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path) | |
| original = dst._do_SendMediaRequest | |
| tries = [] | |
| def picky(req): | |
| tries.append(req.media.id.file_reference) | |
| if req.media.id.file_reference: # the saved reference is old | |
| raise rpc("FileReferenceExpiredError") | |
| return original(req) | |
| dst._do_SendMediaRequest = picky | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert tries == [b"ref31", b""] and res.media_lost == [] and res.failed == [] | |
| assert dst.msgs[13].media.photo.id == 31 | |
| await db.close() | |
| run(go()) | |
| def test_a_file_telegram_refuses_goes_out_without_it_and_is_reported(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, file_available=False) # taken down | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.media_lost == [3] and res.failed == [] and res.from_saved == 3 and res.aborted is None | |
| assert dst.msgs[13].message == "Caption with link" and dst.msgs[13].media is None | |
| assert len(sent(dst, functions.messages.SendMediaRequest)) == 2 # as saved, then without the file reference | |
| # the entry in My posts says what the post really is | |
| row = await db.find_by_message(2, 13) | |
| assert (row.text, row.media_kind, row.media_file_id) == ("Caption with link", None, None) | |
| await db.close() | |
| run(go()) | |
| def test_a_post_that_is_only_a_refused_file_fails_and_the_others_are_copied(tmp_path): | |
| async def go(): | |
| orig = originals() | |
| orig[7] = make_msg(7, "", media=photo_media(71)) | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=(7,), orig=orig, file_available=False, last=7) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.failed == [(7, "MediaEmptyError")] and res.media_lost == [] | |
| assert res.copied_units == 5 and 16 not in dst.msgs # 1 2 3 5 6 were copied; 7 has no text to go out with | |
| await db.close() | |
| run(go()) | |
| def test_a_held_back_album_is_rebuilt_as_an_album(tmp_path): | |
| async def go(): | |
| orig = originals() | |
| orig[8] = make_msg(8, "", media=photo_media(81), grouped_id=80) | |
| orig[9] = make_msg(9, "album caption", [types.MessageEntityBold(0, 5)], media=photo_media(82), grouped_id=80) | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=(8, 9), orig=orig, last=9) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.from_saved == 1 and res.held_back == [] and res.failed == [] # an album is one post | |
| album = [dst.msgs[i] for i in (16, 17)] | |
| assert album[0].grouped_id and album[0].grouped_id == album[1].grouped_id | |
| assert [m.media.photo.id for m in album] == [81, 82] | |
| assert album[1].message == "album caption" and [(e.offset, e.length) for e in album[1].entities] == [(0, 5)] | |
| await db.close() | |
| run(go()) | |
| def test_an_album_of_which_one_picture_has_no_saved_copy_is_skipped_whole(tmp_path): | |
| async def go(): | |
| orig = originals() | |
| orig[8] = make_msg(8, "", media=photo_media(81), grouped_id=80) | |
| orig[9] = make_msg(9, "album caption", media=photo_media(82), grouped_id=80) | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=(8, 9), no_row=(9,), orig=orig, last=9) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.held_back == [8] and 16 not in dst.msgs | |
| await db.close() | |
| run(go()) | |
| # ------------------------------------------------------------------------------------------------ the plan | |
| def test_the_plan_counts_what_is_copied_from_my_posts_and_what_is_skipped(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, no_row=(5,)) | |
| lookup = saved_lookup(db, 1) | |
| plan = await plan_repost(src, SRC, RepostOptions(), saved=lookup, use_saved=True) | |
| # posts 1 2 3 6 are copied (2 and 3 from My posts); 5 is held back without a saved copy; 4 is a service message | |
| assert (plan.units, plan.held_saved, plan.held_skipped, plan.service) == (4, 2, 1, 1) | |
| assert plan.held_why == NOTICE and plan.channel_restricted is None | |
| assert plan.media == 1 and plan.n_buttons == 0 and plan.error is None # counted from the saved posts | |
| # as /repost sees it: nothing held back is copied | |
| plan = await plan_repost(src, SRC, RepostOptions(), saved=lookup) | |
| assert (plan.units, plan.held_saved, plan.held_skipped) == (2, 0, 3) | |
| await db.close() | |
| run(go()) | |
| def test_the_plan_says_so_when_the_whole_channel_is_restricted_and_nothing_can_be_copied(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=(1, 2, 3, 5, 6), no_row=(1, 2, 3, 5, 6)) | |
| src.restriction = NOTICE | |
| plan = await plan_repost(src, SRC, RepostOptions(), saved=saved_lookup(db, 1), use_saved=True) | |
| assert plan.channel_restricted == NOTICE and plan.units == 0 and plan.held_skipped == 5 | |
| assert "holds back every post" in plan.error and "no saved copy" in plan.error | |
| await db.close() | |
| run(go()) | |
| def test_a_normal_channel_is_planned_as_before(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=()) | |
| plan = await plan_repost(src, SRC, RepostOptions(), saved=saved_lookup(db, 1), use_saved=True) | |
| assert (plan.units, plan.held_saved, plan.held_skipped, plan.held_why, plan.channel_restricted) == (5, 0, 0, None, None) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.from_saved == 0 and res.held_back == [] and res.copied_units == 5 | |
| await db.close() | |
| run(go()) | |
| # ============================================================================================ the owner's view | |
| async def held_app(tmp_path, *, held=(2, 3, 5), no_row=(), file_available=True): | |
| app, dst = await shift_app(tmp_path) | |
| orig = originals() | |
| app.tg._media.clear() # the account knows no file yet (shift_app had filled the channel with other posts) | |
| fill(app.tg, live_posts(orig, held)) | |
| if file_available: | |
| for m in orig.values(): | |
| app.tg._remember_media(m) | |
| await save_rows(app.db, orig, no_row) | |
| return app, dst | |
| def buttons(app): | |
| return [t for row in app.out.last_buttons() for t in row] | |
| def test_the_shift_plan_and_result_explain_what_comes_from_my_posts(tmp_path): | |
| async def go(): | |
| app, dst = await held_app(tmp_path, no_row=(5,)) | |
| app.tg.restriction = CHANNEL_NOTICE # the whole channel is struck: My posts is used for every post it has | |
| await app.text("/shift @srcch @dstch") | |
| plan = norm(app.out.last_text) | |
| assert "Telegram restricts Anime Source itself" in plan and "unavailable due to copyright infringement" in plan | |
| assert "4 post(s) of Anime Source are restricted by Telegram" in plan | |
| assert "copied from what My posts saved of them" in plan and "not from what Telegram shows now" in plan | |
| assert "Telegram shows only its notice for 1 more post(s)" in plan and "no saved copy" in plan and "/import" in plan | |
| assert "4 posts" in plan # 1, 2, 3 and 6 are copied | |
| assert "The new posts are read back" in plan | |
| assert not any(isinstance(r, functions.messages.SendMessageRequest) for r in dst.requests) # a plan posts nothing | |
| await app.press(app.out.callback_data("Start copying")) | |
| done = norm(app.out.last_text) | |
| assert "4 post(s) are restricted by Telegram" in done and "copied from what My posts saved of them" in done | |
| assert "1 post(s): Telegram shows only its notice and My posts has no saved copy of them" in done and "#5" in done | |
| assert "not everything was copied" in done # so the rest can be tried again later | |
| assert any("Try the rest again" in t for t in buttons(app)) | |
| assert "hides" not in done # nothing the new posts show is restricted | |
| assert not any(NOTICE in (m.message or "") for m in dst.msgs.values()) | |
| assert forwarded_ids(dst) == [] # a struck channel: nothing is copied by Telegram itself | |
| # the missing copy arrives (a backup put back with /import): the rest can be copied | |
| await save_rows(app.db, {5: originals()[5]}) | |
| await app.press(app.out.callback_data("Try the rest again")) | |
| done = norm(app.out.last_text) | |
| assert "message(s) copied" in done and "Undo" in " ".join(buttons(app)) | |
| assert dst.msgs[15].message == "Fifth" | |
| await app.db.close() | |
| run(go()) | |
| def test_a_refused_file_is_named_in_the_result(tmp_path): | |
| async def go(): | |
| app, dst = await held_app(tmp_path, file_available=False) | |
| await app.text("/shift @srcch @dstch") | |
| await app.press(app.out.callback_data("Start copying")) | |
| done = norm(app.out.last_text) | |
| assert "1 of them went out without their media" in done and "#3" in done | |
| await app.db.close() | |
| run(go()) | |
| def test_the_usage_tells_about_posts_telegram_holds_back(tmp_path): | |
| async def go(): | |
| app, _ = await shift_app(tmp_path) | |
| await app.text("/shift") | |
| text = norm(app.out.last_text) | |
| assert "copyright infringement" in text and "copied from what My posts saved of it" in text | |
| await app.db.close() | |
| run(go()) | |
| # ============================================================================================ /repost | |
| def repost_posts(): | |
| return { | |
| 1: make_msg(1, "First post"), | |
| 2: make_msg(2, "Second post", markup=markup([url_btn("Watch", "https://t.me/goku?start=BBB")])), | |
| 3: make_msg(3, "Third post"), | |
| } | |
| def test_a_repost_never_copies_the_notice_and_never_deletes_what_it_could_not_copy(tmp_path): | |
| async def go(): | |
| orig = repost_posts() | |
| app = await make_app(tmp_path, msgs=live_posts(orig, (2,))) | |
| await save_rows(app.db, orig) | |
| await app.text("/repost --no-typed") | |
| await app.press(app.out.callback_data("Test")) | |
| plan = norm(app.out.last_text) | |
| assert "Telegram holds back 1 post(s) of Test" in plan and "Copying the notice would be wrong" in plan | |
| assert "2 posts" in plan # posts 1 and 3 | |
| await app.press(app.out.callback_data("Start copying")) | |
| done = norm(app.out.last_text) | |
| assert "1 post(s) are held back by Telegram" in done and "#2" in done and "the old posts are not deleted" in done | |
| assert "not everything was copied" in done | |
| assert not any("Delete the old posts" in t for t in buttons(app)) # nothing may be deleted | |
| # the channel: the three posts, the held back one stays as it is, two copies at the end - and no notice copy | |
| texts = [m.message for _, m in sorted(app.tg.msgs.items())] | |
| assert texts == ["First post", NOTICE, "Third post", "First post", "Third post"] | |
| await app.db.close() | |
| run(go()) | |
| def test_a_repost_of_a_channel_without_held_back_posts_is_unchanged(tmp_path): | |
| async def go(): | |
| app = await make_app(tmp_path, msgs=repost_posts()) | |
| await app.text("/repost --no-typed") | |
| await app.press(app.out.callback_data("Test")) | |
| assert "holds back" not in norm(app.out.last_text) | |
| await app.press(app.out.callback_data("Start copying")) | |
| done = norm(app.out.last_text) | |
| assert "all posts copied in order" in done and "held back" not in done | |
| assert any("Delete the old posts" in t for t in buttons(app)) | |
| await app.db.close() | |
| run(go()) | |
| # ============================================================================ a source that can't be read at all | |
| def unreadable(chan): | |
| """The channel is banned or private for the bot: nothing in it can be read any more.""" | |
| chan.fail["GetMessagesRequest"] = rpc("ChannelPrivateError") | |
| chan.fail["GetFullChannelRequest"] = rpc("ChannelPrivateError") | |
| def test_a_source_that_cannot_be_read_is_copied_from_my_posts_alone(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=()) | |
| unreadable(src) | |
| await db.set_mark("s1", "fromdb") | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.from_db and res.used_saved and res.failed == [] and res.aborted is None and res.held_back == [] | |
| assert res.from_saved == 5 and res.copied_units == 5 | |
| # copies: 1->11 2->12 3->13 5->14 6->15 (there is nothing saved of the service message 4) | |
| assert shown(dst) == {11: "Welcome", 12: "Episode 2 is out", 13: "Caption with link", 14: "Fifth", 15: "Sixth"} | |
| assert forwarded_ids(dst) == [] # nothing is copied by Telegram: the source can't be read | |
| btn = dst.msgs[12].reply_markup.rows[0].buttons[0] | |
| assert (btn.text, button_url(btn)) == ("Download", "https://t.me/goku?start=AAA") | |
| assert dst.msgs[13].media.photo.id == 31 | |
| assert dst.msgs[13].entities[0].url == "https://t.me/dstch/14" # the link to post 5 follows its copy | |
| assert dst.msgs[14].reply_to is None # My posts does not know what a post answered | |
| assert (await db.find_by_message(2, 13)).media_kind == "photo" | |
| await db.close() | |
| run(go()) | |
| def test_the_range_is_respected_and_a_stopped_shift_goes_on_from_my_posts(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=()) | |
| unreadable(src) | |
| await db.set_mark("s1", "fromdb") | |
| calls = [] | |
| async def prog(r): | |
| calls.append(r.copied_units) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0, progress=prog, should_stop=lambda: len(calls) >= 2) | |
| assert res.stopped and res.copied_units == 2 and shown(dst) == {11: "Welcome", 12: "Episode 2 is out"} | |
| again = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert again.from_db and not again.stopped and again.copied_units == 3 and again.skipped_done == 2 | |
| assert shown(dst) == {11: "Welcome", 12: "Episode 2 is out", 13: "Caption with link", 14: "Fifth", 15: "Sixth"} | |
| await db.close() | |
| run(go()) | |
| def test_a_post_with_nothing_to_post_is_not_taken_from_my_posts(tmp_path): | |
| async def go(): | |
| src, dst, db, dst_row, shift = await held_setup(tmp_path, held=(), no_row=(2,)) | |
| await db.create_post( | |
| channel_id=1, message_id=2, status="sent", source="adopted", created_by=1, link_preview=False, | |
| text="", entities=[], buttons=[], media_kind=None, media_file_id=None, | |
| ) | |
| unreadable(src) | |
| await db.set_mark("s1", "fromdb") | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.copied_units == 4 and res.failed == [] | |
| assert shown(dst) == {11: "Welcome", 12: "Caption with link", 13: "Fifth", 14: "Sixth"} | |
| await db.close() | |
| run(go()) | |
| def test_the_shift_command_offers_my_posts_when_the_source_can_not_be_read(tmp_path): | |
| async def go(): | |
| app, dst = await held_app(tmp_path, held=()) | |
| unreadable(app.tg) | |
| await app.text("/shift @srcch @dstch") | |
| plan = norm(app.out.last_text) | |
| assert "I can't read Anime Source" in plan and "My posts has the saved posts of it" in plan | |
| assert "5 posts" in plan and "Post ids 1-6" in plan | |
| assert "replies and albums" in plan and "from what My posts saved of it" in plan | |
| assert dst.msgs.keys() == set(range(1, 11)) # a plan posts nothing | |
| await app.press(app.out.callback_data("Start copying")) | |
| done = norm(app.out.last_text) | |
| assert "5 message(s) copied" in done and "The source can't be read, so 5 post(s) were copied" in done | |
| sid = app.out.callback_data("Undo").split(":")[1] | |
| assert await app.db.marks_of(sid) == {"fromdb"} | |
| assert dst.msgs[12].message == "Episode 2 is out" | |
| await app.press(app.out.callback_data("Undo")) | |
| await app.press(app.out.callback_data("Yes, remove")) | |
| assert "5 copied message(s) removed" in norm(app.out.last_text) | |
| await app.db.close() | |
| run(go()) | |
| def test_the_range_of_a_shift_from_my_posts(tmp_path): | |
| async def go(): | |
| app, dst = await held_app(tmp_path, held=()) | |
| unreadable(app.tg) | |
| await app.text("/shift @srcch @dstch 2 3") | |
| plan = norm(app.out.last_text) | |
| assert "Post ids 2-3" in plan and "2 posts" in plan | |
| await app.press(app.out.callback_data("Start copying")) | |
| assert [m.message for i, m in sorted(dst.msgs.items()) if i > 10] == ["Episode 2 is out", "Caption with link"] | |
| await app.db.close() | |
| run(go()) | |
| def test_an_unreadable_source_that_my_posts_does_not_know_is_still_an_error(tmp_path): | |
| async def go(): | |
| app, dst = await shift_app(tmp_path) # My posts has nothing of the source | |
| unreadable(app.tg) | |
| await app.text("/shift @srcch @dstch") | |
| text = norm(app.out.last_text) | |
| assert "I can't read Anime Source" in text and "no saved posts of it" in text | |
| assert not any(isinstance(r, functions.messages.SendMessageRequest) for r in dst.requests) | |
| await app.db.close() | |
| run(go()) | |