Spaces:
Paused
Paused
File size: 16,983 Bytes
65651b1 c71e4ac 65651b1 | 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 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 | """What TELEGRAM does to a channel (a copyright strike ...) is not an admin's edit: My posts stay exactly as saved."""
from telethon import types
from app.restrictions import PLACEHOLDER_NOTE, is_placeholder_text, message_restriction, restriction_of
from app.sync_engine import MASS_NOTE, diff_post, split_mass_text, sync_channel, sync_ids
from .fakes import markup, url_btn
from .harness import norm
from .test_sync import foreign, looked, old_msg, opts, run, world
from .test_syncer import dms, make_app, three as three_posts
NOTICE = "This message couldn't be displayed on your device due to copyright infringement."
CHANNEL_NOTICE = "This channel is unavailable due to copyright infringement." # what Telegram says of a struck channel
def strike(m, text=NOTICE):
"""Telegram holds a message back: its own restriction mark on it."""
m.restriction_reason = [types.RestrictionReason(platform="all", reason="copyright", text=text)]
return m
def seven():
return {i: old_msg(i, f"post number {i}", markup=markup([url_btn("Watch", f"https://t.me/a/{i}")])) for i in range(1, 8)}
# ================================================================================================ recognising it
def test_the_notice_telegram_shows_instead_of_a_post_is_recognised():
for text in (
NOTICE,
"This message couldn’t be displayed on your device due to copyright infringement",
" This message couldn't be displayed\non your device due to copyright infringement. ",
"This channel can't be displayed because it violated Telegram's Terms of Service.",
"This post isn't available in your country due to copyright.",
):
assert is_placeholder_text(text), text
for text in (
"",
None,
"Episode 12 is out now",
"This video is not available in HD", # says "not available" but nothing about a restriction
"Watch: this message couldn't be displayed on your device", # not the notice itself
"This message couldn't be displayed on your device due to copyright infringement. " + "x" * 300, # a long post
):
assert not is_placeholder_text(text), text
def test_telegrams_own_marks_are_read_from_messages_and_channels():
plain = old_msg(1, "hello")
assert restriction_of(plain) is None and message_restriction(plain) is None
held = strike(old_msg(2, "hello"), "Removed after a copyright complaint")
assert restriction_of(held) == "Removed after a copyright complaint"
assert message_restriction(held) == "Removed after a copyright complaint"
two = old_msg(3, "x")
two.restriction_reason = [
types.RestrictionReason("ios", "sensitive", "Sensitive content"),
types.RestrictionReason("android", "sensitive", "Sensitive content"), # the same words twice: said once
types.RestrictionReason("all", "copyright", ""), # no words: its code is used
]
assert restriction_of(two) == "Sensitive content; copyright"
nameless = old_msg(4, "x")
nameless.restriction_reason = [types.RestrictionReason("all", "terms", "")]
assert restriction_of(nameless) == "terms"
chan = types.Channel(id=1, title="t", photo=types.ChatPhotoEmpty(), date=None, restricted=True)
assert restriction_of(chan) == "restricted by Telegram"
assert restriction_of(types.Channel(id=1, title="t", photo=types.ChatPhotoEmpty(), date=None)) is None
def test_a_text_the_saved_post_already_has_is_the_owners_own_wording():
m = old_msg(1, NOTICE)
assert message_restriction(m) == PLACEHOLDER_NOTE # an unknown message: it is the notice
assert message_restriction(m, "post number 1") == PLACEHOLDER_NOTE # the saved post said something else
assert message_restriction(m, NOTICE) is None # the saved post says exactly that: nothing changed
def test_many_posts_that_suddenly_show_one_text_are_held_back_but_few_are_not(tmp_path):
async def go():
chan, db, ch = await world(tmp_path, {i: old_msg(i, f"post number {i}") for i in range(1, 8)})
rows = {p.message_id: p for p in await db.sent_posts(1)}
def edits_to(text, ids):
return [(rows[i], diff_post(rows[i], old_msg(i, text))) for i in ids]
keep, held = split_mass_text(edits_to("Gone.", range(1, 6)))
assert keep == [] and len(held) == 5
keep, held = split_mass_text(edits_to("Gone.", range(1, 5))) # four are below the line
assert len(keep) == 4 and held == []
keep, held = split_mass_text(edits_to("", range(1, 8))) # all texts removed: a person can do that
assert len(keep) == 7 and held == []
keep, held = split_mass_text(edits_to("Gone.", range(1, 6)) + edits_to("Other", [6, 7]))
assert [p.message_id for p, _ in keep] == [6, 7] and len(held) == 5
await db.close()
run(go())
def test_edits_that_all_have_the_same_text_but_the_same_saved_text_are_a_real_edit(tmp_path):
async def go():
msgs = {i: old_msg(i, "Join us") for i in range(1, 8)} # seven posts that said exactly the same
chan, db, ch = await world(tmp_path, msgs)
for m in msgs.values():
m.message = "Join us now"
rep = await sync_channel(chan, db, ch, opts())
assert len(rep.edited) == 7 and rep.restricted == []
await db.close()
run(go())
# =============================================================================================== the whole channel
def test_a_channel_telegram_restricted_is_left_alone(tmp_path):
async def go():
chan, db, ch = await looked(tmp_path, seven())
before = await db.get_sync(1)
chan.restriction = NOTICE
del chan.msgs[2]
chan.msgs[1].message = "edited by an admin"
new = foreign(chan, "a new post")
chan._pts += 2
rep = await sync_channel(chan, db, ch, opts())
assert rep.restricted_channel == NOTICE and rep.error is None
assert (rep.deleted, rep.edited, rep.adopted, rep.restricted) == ([], [], [], [])
assert sorted(p.message_id for p in await db.sent_posts(1)) == list(range(1, 8)) # nothing removed
assert (await db.find_by_message(1, 1)).text == "post number 1" # nothing edited
assert await db.find_by_message(1, new.id) is None # nothing added
after = await db.get_sync(1)
assert (after.last_top, after.last_pts) == (before.last_top, before.last_pts) # the mark did not move
chan.restriction = None # the strike is lifted: the next look is the normal one again
rep = await sync_channel(chan, db, ch, opts())
assert rep.restricted_channel is None
assert (rep.deleted, rep.edited, rep.adopted) == ([2], [1], [new.id])
await db.close()
run(go())
def test_the_first_look_at_a_restricted_channel_changes_nothing_either(tmp_path):
async def go():
chan, db, ch = await world(tmp_path, seven())
chan.restriction = NOTICE
for m in chan.msgs.values():
m.message = NOTICE
rep = await sync_channel(chan, db, ch, opts())
assert rep.restricted_channel == NOTICE and rep.first_look is False
assert await db.get_sync(1) is None
assert [p.text for p in await db.sent_posts(1)] == [f"post number {i}" for i in range(1, 8)]
await db.close()
run(go())
# ============================================================================================ a copyright strike
def test_every_post_turning_into_the_notice_changes_nothing_in_my_posts(tmp_path):
"""The case that was asked about: Telegram swaps the text of EVERY post for its notice."""
async def go():
chan, db, ch = await looked(tmp_path, seven())
for m in chan.msgs.values():
m.message, m.entities, m.reply_markup = NOTICE, None, None # what the strike leaves in the channel
chan._pts += 7
rep = await sync_channel(chan, db, ch, opts())
assert rep.edited == [] and rep.deleted == [] and rep.buttons_missing == []
assert sorted(rep.restricted) == list(range(1, 8)) and rep.restricted_why == PLACEHOLDER_NOTE
for i, p in enumerate(await db.sent_posts(1), 1):
assert p.text == f"post number {i}" and p.buttons == [[{"t": "Watch", "u": f"https://t.me/a/{i}"}]]
await db.close()
run(go())
def test_posts_carrying_telegrams_restriction_mark_are_left_as_saved(tmp_path):
async def go():
chan, db, ch = await looked(tmp_path, seven())
for i in (2, 3):
strike(chan.msgs[i])
chan.msgs[i].message = "(something else)"
chan.msgs[4].message = "a real edit by an admin"
chan._pts += 3
rep = await sync_channel(chan, db, ch, opts())
assert sorted(rep.restricted) == [2, 3] and rep.restricted_why == NOTICE
assert rep.edited == [4] # everything else is looked at as usual
assert (await db.find_by_message(1, 2)).text == "post number 2"
assert (await db.find_by_message(1, 4)).text == "a real edit by an admin"
await db.close()
run(go())
def test_a_post_that_is_really_gone_is_still_removed_while_others_are_held_back(tmp_path):
async def go():
chan, db, ch = await looked(tmp_path, seven())
strike(chan.msgs[2])
chan.msgs[2].message = NOTICE
del chan.msgs[6]
chan._pts += 2
rep = await sync_channel(chan, db, ch, opts())
assert rep.deleted == [6] and rep.restricted == [2]
assert (await db.find_by_message(1, 2)).text == "post number 2"
await db.close()
run(go())
def test_a_notice_does_not_stop_a_normal_edit_of_another_post(tmp_path):
async def go():
chan, db, ch = await looked(tmp_path, seven())
chan.msgs[1].message = NOTICE
chan.msgs[5].message = "the admin fixed a typo"
rep = await sync_channel(chan, db, ch, opts())
assert rep.restricted == [1] and rep.edited == [5]
await db.close()
run(go())
def test_five_posts_that_all_show_the_same_unknown_text_are_held_back(tmp_path):
"""The notice in another language: it is not in any list, but nobody edits five posts into one text."""
async def go():
chan, db, ch = await looked(tmp_path, seven())
for i in range(1, 6):
chan.msgs[i].message = "Este mensaje no se puede mostrar en tu dispositivo."
rep = await sync_channel(chan, db, ch, opts())
assert sorted(rep.restricted) == [1, 2, 3, 4, 5] and rep.edited == [] and rep.restricted_why == MASS_NOTE
assert (await db.find_by_message(1, 1)).text == "post number 1"
await db.close()
run(go())
def test_four_posts_with_the_same_new_text_are_ordinary_edits(tmp_path):
async def go():
chan, db, ch = await looked(tmp_path, seven())
for i in range(1, 5):
chan.msgs[i].message = "Moved to the new channel."
rep = await sync_channel(chan, db, ch, opts())
assert sorted(rep.edited) == [1, 2, 3, 4] and rep.restricted == []
await db.close()
run(go())
# ================================================================================================= new posts
def test_new_posts_that_telegram_holds_back_are_not_added(tmp_path):
async def go():
chan, db, ch = await looked(tmp_path, three_posts_dict())
a = foreign(chan, "a normal new post")
b = strike(foreign(chan, "hidden by Telegram"))
c = foreign(chan, NOTICE)
rep = await sync_channel(chan, db, ch, opts())
assert rep.adopted == [a.id] and sorted(rep.restricted) == [b.id, c.id]
assert await db.find_by_message(1, b.id) is None and await db.find_by_message(1, c.id) is None
again = await sync_channel(chan, db, ch, opts()) # looked at once, not over and over
assert again.restricted == [] and again.adopted == []
await db.close()
run(go())
def three_posts_dict():
return {m.id: m for m in three_posts()}
# =============================================================================================== the live look
def test_the_live_look_leaves_a_restricted_post_alone(tmp_path):
async def go():
chan, db, ch = await looked(tmp_path, seven())
strike(chan.msgs[3])
chan.msgs[3].message = NOTICE
chan.msgs[4].message = "a real edit"
rep = await sync_ids(chan, db, ch, [3, 4], opts())
assert rep.restricted == [3] and rep.edited == [4]
assert (await db.find_by_message(1, 3)).text == "post number 3"
await db.close()
run(go())
def test_the_live_look_holds_back_a_burst_of_identical_texts(tmp_path):
async def go():
chan, db, ch = await looked(tmp_path, seven())
for i in range(1, 6):
chan.msgs[i].message = "Blocked."
rep = await sync_ids(chan, db, ch, [1, 2, 3, 4, 5], opts())
assert sorted(rep.restricted) == [1, 2, 3, 4, 5] and rep.edited == []
await db.close()
run(go())
def test_the_live_look_does_not_add_a_restricted_new_post(tmp_path):
async def go():
chan, db, ch = await looked(tmp_path, seven())
m = foreign(chan, NOTICE)
rep = await sync_ids(chan, db, ch, [m.id], opts(), adopt_ids=[m.id])
assert rep.adopted == [] and rep.restricted == [m.id]
await db.close()
run(go())
# ============================================================================== what "everything looks deleted" means
def test_posts_really_deleted_by_an_admin_all_go_even_when_that_is_everything(tmp_path):
async def go():
chan, db, ch = await looked(tmp_path, seven())
chan.msgs.clear()
chan._pts += 7 # seven deletions are seven events
rep = await sync_channel(chan, db, ch, opts())
assert sorted(rep.deleted) == list(range(1, 8)) and rep.anomaly is None
assert await db.sent_posts(1) == []
await db.close()
run(go())
def test_posts_that_merely_vanish_from_view_are_not_removed_without_the_events(tmp_path):
async def go():
chan, db, ch = await looked(tmp_path, seven())
chan.msgs.clear() # nothing happened in the channel: the bot just sees nothing
rep = await sync_channel(chan, db, ch, opts())
assert rep.deleted == [] and rep.anomaly and "All 7" in rep.anomaly
chan._pts += 3 # three events are not enough to explain seven missing posts
rep = await sync_channel(chan, db, ch, opts())
assert rep.deleted == [] and rep.anomaly
assert len(await db.sent_posts(1)) == 7
await db.close()
run(go())
# ================================================================================================= the owners
def test_sync_tells_what_it_left_alone_in_a_restricted_channel(tmp_path):
async def go():
app = await make_app(tmp_path, posts=three_posts())
await app.text("/sync") # the first look
app.tg.restriction = NOTICE
app.tg.msgs[2].message = NOTICE
await app.text("/sync")
text = norm(app.out.last_text)
assert "Telegram has restricted this channel" in text and "copyright infringement" in text
assert "stay exactly as they are" in text and "Everything matches" not in text
assert (await app.db.find_by_message(1, 2)).text == "second"
await app.db.close()
run(go())
def test_sync_tells_which_posts_it_held_back(tmp_path):
async def go():
app = await make_app(tmp_path, posts=three_posts())
await app.text("/sync")
strike(app.tg.msgs[2])
app.tg.msgs[2].message = "(hidden)"
await app.text("/sync")
text = norm(app.out.last_text)
assert "1 post(s) are held back by Telegram" in text and "left exactly as saved" in text and "message 2" in text
assert "Everything matches" not in text
await app.db.close()
run(go())
def test_the_owners_are_told_about_a_restriction_once_not_every_check(tmp_path):
async def go():
app = await make_app(tmp_path, posts=three_posts())
sy = app.ctx.syncer
app.tg.restriction = NOTICE
await sy.run_all()
await sy.run_all()
await sy.run_all()
told = [t for t in dms(app) if "restricted" in t]
assert len(told) == 1 and "Anime Channel" in told[0] and "stay exactly as they are" in told[0]
sy._told.clear() # a week later
await sy.run_all()
assert len([t for t in dms(app) if "restricted" in t]) == 2
await app.db.close()
run(go())
def test_the_owners_are_told_about_held_back_posts_too(tmp_path):
async def go():
app = await make_app(tmp_path, posts=three_posts())
sy = app.ctx.syncer
await sy.run_all()
strike(app.tg.msgs[3])
await sy.run_all()
await sy.run_all()
told = [t for t in dms(app) if "holds back" in t]
assert len(told) == 1 and "1 post(s)" in told[0]
await app.db.close()
run(go())
|