github-actions[bot] commited on
Commit
5bcd8c9
ยท
1 Parent(s): 946d0f5

Automated sync from GitHub

Browse files
.gitattributes CHANGED
@@ -1,8 +1,8 @@
1
- *.jpg filter=lfs diff=lfs merge=lfs -text
2
- *.jpeg filter=lfs diff=lfs merge=lfs -text
3
  *.png filter=lfs diff=lfs merge=lfs -text
4
  *.ttf filter=lfs diff=lfs merge=lfs -text
5
  *.otf filter=lfs diff=lfs merge=lfs -text
6
  *.webp filter=lfs diff=lfs merge=lfs -text
7
  *.mp4 filter=lfs diff=lfs merge=lfs -text
8
  *.mp3 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
1
  *.png filter=lfs diff=lfs merge=lfs -text
2
  *.ttf filter=lfs diff=lfs merge=lfs -text
3
  *.otf filter=lfs diff=lfs merge=lfs -text
4
  *.webp filter=lfs diff=lfs merge=lfs -text
5
  *.mp4 filter=lfs diff=lfs merge=lfs -text
6
  *.mp3 filter=lfs diff=lfs merge=lfs -text
7
+ *.jpg filter=lfs diff=lfs merge=lfs -text
8
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
app/common.py CHANGED
@@ -157,9 +157,7 @@ async def push_edit(client, ch, p, *, replace_media: bool = False) -> None:
157
  await with_media(
158
  p.media_file_id,
159
  lambda media: client.edit_message(
160
- # Do not send reply markup while replacing media. Telegram keeps the existing buttons when the field
161
- # is omitted, and rejects an edit that tries to set buttons owned by another bot.
162
- peer, p.message_id, text=p.text or "", formatting_entities=ents, file=media
163
  ),
164
  )
165
  else:
 
157
  await with_media(
158
  p.media_file_id,
159
  lambda media: client.edit_message(
160
+ peer, p.message_id, text=p.text or "", formatting_entities=ents, buttons=markup, file=media
 
 
161
  ),
162
  )
163
  else:
app/handlers/replace.py CHANGED
@@ -383,8 +383,7 @@ def register(ctx: Ctx) -> None:
383
 
384
  try:
385
  res = await apply_channel(
386
- client, db, s, o, batch_id, event.sender_id, edit_delay=cfg.edit_delay, progress=prog,
387
- userbot=ctx.userbot,
388
  )
389
  except Exception as e:
390
  log.exception("apply failed in %s", s.channel.id)
 
383
 
384
  try:
385
  res = await apply_channel(
386
+ client, db, s, o, batch_id, event.sender_id, edit_delay=cfg.edit_delay, progress=prog
 
387
  )
388
  except Exception as e:
389
  log.exception("apply failed in %s", s.channel.id)
app/replace_engine.py CHANGED
@@ -21,7 +21,6 @@ from .tgutil import (
21
  ser_markup,
22
  state_of,
23
  )
24
- from .userbot import UserbotEditor, UserbotError
25
 
26
  log = logging.getLogger(__name__)
27
 
@@ -187,23 +186,6 @@ async def _do_edit(client, peer, m, change, markup, preview) -> None:
187
  await edit_raw(client, peer, m.id, markup=markup)
188
 
189
 
190
- # Errors that mean the bot cannot edit the message (usually because buttons belong to
191
- # another bot), but a userbot (normal user account) might still be able to edit the
192
- # text/caption while leaving the buttons alone.
193
- _USERBOT_FALLBACK_ERRORS = {
194
- "MessageIdInvalidError",
195
- "MessageAuthorRequiredError",
196
- "InlineBotRequiredError",
197
- }
198
-
199
-
200
- async def _do_edit_via_userbot(editor: UserbotEditor, m, change, preview) -> None:
201
- """Edit only the text/caption of a message through the userbot. Buttons stay as they are."""
202
- if not (change.text_changed or change.entities_changed):
203
- return # nothing the userbot can help with (button-only change)
204
- await editor.edit_message(m.id, text=change.new_text, entities=change.new_entities, preview=preview)
205
-
206
-
207
  async def apply_channel(
208
  client,
209
  db,
@@ -214,7 +196,6 @@ async def apply_channel(
214
  *,
215
  edit_delay: float = 1.2,
216
  progress: Optional[Callable[[ApplyResult], Awaitable[None]]] = None,
217
- userbot=None,
218
  ) -> ApplyResult:
219
  ch = scan.channel
220
  peer = peer_of(ch)
@@ -223,7 +204,6 @@ async def apply_channel(
223
  ids = sorted(scan.infos)
224
  buf: list = []
225
  streak, last_name = 0, None
226
- editor: Optional[UserbotEditor] = None # created lazily on the first fallback
227
 
228
  async def flush() -> None:
229
  nonlocal buf
@@ -250,7 +230,6 @@ async def apply_channel(
250
  preview = preview_flag(m)
251
  markup = change.new_markup if change.new_markup is not None else m.reply_markup
252
  old_state = state_of(m.message, m.entities, m.reply_markup, preview)
253
- used_userbot = False
254
  try:
255
  await flood_retry(lambda: _do_edit(client, peer, m, change, markup, preview))
256
  except errors.MessageNotModifiedError:
@@ -258,57 +237,21 @@ async def apply_channel(
258
  continue
259
  except errors.RPCError as e:
260
  name = type(e).__name__
261
- # ---- userbot fallback: edit text/caption only, skip buttons ----
262
- if name in _USERBOT_FALLBACK_ERRORS and (change.text_changed or change.entities_changed):
263
- if userbot is not None and userbot.enabled:
264
- if not userbot.ready:
265
- await userbot.connect()
266
- if userbot.ready:
267
- try:
268
- if editor is None:
269
- editor = userbot.editor(ch)
270
- await flood_retry(
271
- lambda: _do_edit_via_userbot(editor, m, change, preview)
272
- )
273
- used_userbot = True
274
- log.info(
275
- "edit of %s/%s succeeded via userbot (bot got %s)",
276
- ch.id, m.id, name,
277
- )
278
- except errors.MessageNotModifiedError:
279
- res.unchanged += 1
280
- continue
281
- except (errors.RPCError, UserbotError) as ue:
282
- ub_name = type(ue).__name__
283
- log.warning(
284
- "userbot edit of %s/%s also failed: %s",
285
- ch.id, m.id, ub_name,
286
- )
287
- res.failed.append((m.id, f"{name}+{ub_name}"))
288
- streak = streak + 1 if name == last_name else 1
289
- last_name = name
290
- if streak >= ABORT_AFTER:
291
- res.aborted = name
292
- break
293
- continue
294
- if not used_userbot:
295
- log.warning("edit of %s/%s failed: %s", ch.id, m.id, name)
296
- res.failed.append((m.id, name))
297
- streak = streak + 1 if name == last_name else 1
298
- last_name = name
299
- if streak >= ABORT_AFTER:
300
- res.aborted = name
301
- break
302
- continue
303
  streak, last_name = 0, None
304
  res.edited += 1
305
  kind, fid = media_info(m)
306
- # when the userbot edited, it could only change text/entities (not buttons)
307
- effective_markup = m.reply_markup if used_userbot else markup
308
  adopt = {
309
  "text": change.new_text,
310
  "entities": ser_entities(change.new_entities),
311
- "buttons": ser_markup(effective_markup),
312
  "link_preview": bool(preview),
313
  }
314
  if kind:
@@ -317,7 +260,7 @@ async def apply_channel(
317
  {
318
  "message_id": m.id,
319
  "old_state": old_state,
320
- "new_state": state_of(change.new_text, change.new_entities, effective_markup, preview),
321
  "adopt": adopt,
322
  }
323
  )
@@ -329,8 +272,6 @@ async def apply_channel(
329
  if res.aborted:
330
  break
331
  finally:
332
- if editor is not None:
333
- await editor.close()
334
  await flush()
335
  return res
336
 
 
21
  ser_markup,
22
  state_of,
23
  )
 
24
 
25
  log = logging.getLogger(__name__)
26
 
 
186
  await edit_raw(client, peer, m.id, markup=markup)
187
 
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  async def apply_channel(
190
  client,
191
  db,
 
196
  *,
197
  edit_delay: float = 1.2,
198
  progress: Optional[Callable[[ApplyResult], Awaitable[None]]] = None,
 
199
  ) -> ApplyResult:
200
  ch = scan.channel
201
  peer = peer_of(ch)
 
204
  ids = sorted(scan.infos)
205
  buf: list = []
206
  streak, last_name = 0, None
 
207
 
208
  async def flush() -> None:
209
  nonlocal buf
 
230
  preview = preview_flag(m)
231
  markup = change.new_markup if change.new_markup is not None else m.reply_markup
232
  old_state = state_of(m.message, m.entities, m.reply_markup, preview)
 
233
  try:
234
  await flood_retry(lambda: _do_edit(client, peer, m, change, markup, preview))
235
  except errors.MessageNotModifiedError:
 
237
  continue
238
  except errors.RPCError as e:
239
  name = type(e).__name__
240
+ log.warning("edit of %s/%s failed: %s", ch.id, m.id, name)
241
+ res.failed.append((m.id, name))
242
+ streak = streak + 1 if name == last_name else 1
243
+ last_name = name
244
+ if streak >= ABORT_AFTER:
245
+ res.aborted = name
246
+ break
247
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  streak, last_name = 0, None
249
  res.edited += 1
250
  kind, fid = media_info(m)
 
 
251
  adopt = {
252
  "text": change.new_text,
253
  "entities": ser_entities(change.new_entities),
254
+ "buttons": ser_markup(markup),
255
  "link_preview": bool(preview),
256
  }
257
  if kind:
 
260
  {
261
  "message_id": m.id,
262
  "old_state": old_state,
263
+ "new_state": state_of(change.new_text, change.new_entities, markup, preview),
264
  "adopt": adopt,
265
  }
266
  )
 
272
  if res.aborted:
273
  break
274
  finally:
 
 
275
  await flush()
276
  return res
277
 
app/tgutil.py CHANGED
@@ -379,8 +379,8 @@ _HINTS = {
379
  "InlineBotRequiredError": "That post was sent through another bot's inline mode, so only that bot can edit it.",
380
  "MessageEditTimeExpiredError": "Telegram no longer allows editing that message.",
381
  "MessageIdInvalidError": (
382
- "Telegram won't let this bot edit that post. Its buttons were left untouched. If the post is still there, "
383
- "it was likely sent by another bot or through inline mode; only its original bot can edit it."
384
  ),
385
  "ButtonUrlInvalidError": "Telegram rejected one of the button links.",
386
  "ReplyMarkupInvalidError": "Telegram rejected the buttons.",
 
379
  "InlineBotRequiredError": "That post was sent through another bot's inline mode, so only that bot can edit it.",
380
  "MessageEditTimeExpiredError": "Telegram no longer allows editing that message.",
381
  "MessageIdInvalidError": (
382
+ "Telegram won't let the bot change that message (or it was deleted). If the post is still there, "
383
+ "the usual reason is that its buttons were added by another bot: only that bot may change them."
384
  ),
385
  "ButtonUrlInvalidError": "Telegram rejected one of the button links.",
386
  "ReplyMarkupInvalidError": "Telegram rejected the buttons.",
app/userbot.py CHANGED
@@ -20,7 +20,7 @@ from typing import Optional
20
  from telethon import TelegramClient, errors, functions, types, utils
21
  from telethon.sessions import StringSession
22
 
23
- from .tgutil import edit_raw, flood_retry, get_rights, peer_of
24
 
25
  log = logging.getLogger(__name__)
26
 
@@ -117,9 +117,6 @@ class Userbot:
117
  def deleter(self, ch) -> "UserbotDeleter":
118
  return UserbotDeleter(self, ch)
119
 
120
- def editor(self, ch) -> "UserbotEditor":
121
- return UserbotEditor(self, ch)
122
-
123
  async def fallback_for(self, ch):
124
  """(delete function, deleter) to hand to the delete step, or (None, None) when no userbot is usable.
125
 
@@ -401,271 +398,6 @@ class UserbotDeleter:
401
  )
402
 
403
 
404
- class UserbotEditor:
405
- """Everything needed to edit messages in ONE channel with the userbot (text/caption + media only, no buttons).
406
-
407
- The userbot (a normal user account) can edit any message in a channel where it has
408
- the \u201cEdit messages of others\u201d admin right, even when a bot cannot because the inline
409
- buttons belong to another bot. Buttons are left untouched.
410
-
411
- Call close() when done, whatever happened.
412
- """
413
-
414
- def __init__(self, ub: Userbot, ch):
415
- self.ub = ub
416
- self.ch = ch
417
- self.peer = None
418
- self.notes: list = []
419
- self._ready = False
420
- self._holds_lock = False
421
- self._promoted = False
422
- self._bot_user = None
423
- self._link: Optional[str] = None
424
-
425
- async def edit_message(self, message_id: int, *, text: str, entities=None, preview=None) -> None:
426
- """Edit only the text/caption (and entities) of a message via the userbot. Buttons are not touched."""
427
- if not self.ub.ready:
428
- raise UserbotError(f"The userbot is not connected: {self.ub.last_error or 'unknown reason'}")
429
- if not self._ready:
430
- if not self._holds_lock:
431
- await self.ub.lock.acquire()
432
- self._holds_lock = True
433
- await self._prepare()
434
- self._ready = True
435
- await edit_raw(self.ub.client, self.peer, message_id, text=text, entities=entities, preview=preview)
436
-
437
- async def close(self) -> None:
438
- """Undo what this editor changed: take the admin right away again, revoke the temporary link."""
439
- bot, ch = self.ub.bot, self.ch
440
- try:
441
- if self._promoted and self._bot_user is not None and not self.ub.keep_admin:
442
- try:
443
- await bot(
444
- functions.channels.EditAdminRequest(
445
- channel=peer_of(ch), user_id=self._bot_user, admin_rights=types.ChatAdminRights(), rank=""
446
- )
447
- )
448
- self.notes.append("took the admin right away from the userbot again")
449
- except Exception as e:
450
- log.warning("could not demote the userbot in %s: %s", ch.id, type(e).__name__)
451
- self.notes.append("could not take the admin right away from the userbot - please do it by hand")
452
- if self._link:
453
- try:
454
- await bot(
455
- functions.messages.EditExportedChatInviteRequest(peer=peer_of(ch), link=self._link, revoked=True)
456
- )
457
- except Exception as e:
458
- log.info("could not revoke the temporary invite link: %s", type(e).__name__)
459
- finally:
460
- self._promoted, self._link = False, None
461
- if self._holds_lock:
462
- self._holds_lock = False
463
- self.ub.lock.release()
464
-
465
- # ----------------------------------------------------------------- prepare
466
- async def _prepare(self) -> None:
467
- peer = await self._member_peer()
468
- if peer is None:
469
- peer = await self._join()
470
- self.peer = peer
471
- await self._ensure_edit_right()
472
-
473
- async def _member_peer(self):
474
- ub = self.ub.client
475
- try:
476
- peer = await ub.get_input_entity(types.PeerChannel(self.ch.id))
477
- if not isinstance(peer, types.InputPeerChannel):
478
- return None
479
- await ub(functions.channels.GetParticipantRequest(peer, types.InputPeerSelf()))
480
- except (errors.RPCError, ValueError, TypeError):
481
- return None
482
- return peer
483
-
484
- async def _invite_link(self) -> Optional[str]:
485
- bot, peer = self.ub.bot, peer_of(self.ch)
486
- try:
487
- expires = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1)
488
- res = await bot(
489
- functions.messages.ExportChatInviteRequest(peer=peer, expire_date=expires, usage_limit=1, title="Edit helper")
490
- )
491
- link = getattr(res, "link", None)
492
- if link:
493
- self._link = link
494
- self.notes.append("made a one-time invite link for the userbot")
495
- return link
496
- except errors.RPCError as e:
497
- log.info("could not create an invite link in %s: %s", self.ch.id, type(e).__name__)
498
- try:
499
- full = await bot(functions.channels.GetFullChannelRequest(peer))
500
- link = getattr(getattr(full.full_chat, "exported_invite", None), "link", None)
501
- if link:
502
- self.notes.append("used the channel's own invite link")
503
- return link
504
- except errors.RPCError as e:
505
- log.info("could not read the invite link of %s: %s", self.ch.id, type(e).__name__)
506
- return None
507
-
508
- async def _join(self):
509
- link = await self._invite_link()
510
- h = parse_invite_hash(link) if link else None
511
- if h:
512
- peer = await self._import_invite(h)
513
- if peer is not None:
514
- self.notes.append("the userbot joined the channel")
515
- return peer
516
- if self.ch.username:
517
- peer = await self._join_public(self.ch.username)
518
- self.notes.append("the userbot joined the channel by its public name")
519
- return peer
520
- raise UserbotError(
521
- "I could not get the userbot into this private channel: Telegram gave me no invite link. "
522
- "Give the bot the \u201cInvite users via link\u201d admin right, or add the userbot to the channel yourself "
523
- "(with the \u201cEdit messages of others\u201d admin right)."
524
- )
525
-
526
- async def _import_invite(self, h: str):
527
- ub = self.ub.client
528
- try:
529
- res = await flood_retry(lambda: ub(functions.messages.ImportChatInviteRequest(h)))
530
- except errors.UserAlreadyParticipantError:
531
- return await self._peer_when_already_in(h)
532
- except errors.InviteRequestSentError:
533
- raise UserbotError(
534
- "This channel\u2019s invite link needs an admin to approve new members. Approve the userbot\u2019s join request "
535
- "(or add the userbot yourself) and try again."
536
- )
537
- except (errors.InviteHashExpiredError, errors.InviteHashInvalidError, errors.InviteHashEmptyError):
538
- return None
539
- except (errors.ChannelsTooMuchError, errors.UserChannelsTooMuchError):
540
- raise UserbotError("The userbot account is in too many channels. Leave a few with that account and try again.")
541
- except errors.UserBannedInChannelError:
542
- raise UserbotError("The userbot account is banned in this channel. Unban it first.")
543
- return self._peer_from_updates(res)
544
-
545
- def _peer_from_updates(self, res):
546
- chats = [c for c in (getattr(res, "chats", None) or []) if isinstance(c, types.Channel)]
547
- for c in chats:
548
- if c.id == self.ch.id:
549
- return utils.get_input_peer(c)
550
- if len(chats) == 1:
551
- return utils.get_input_peer(chats[0])
552
- raise UserbotError("The userbot joined, but Telegram did not tell me which channel it is. Try again.")
553
-
554
- async def _peer_when_already_in(self, h: str):
555
- ub = self.ub.client
556
- try:
557
- info = await ub(functions.messages.CheckChatInviteRequest(h))
558
- if isinstance(info, types.ChatInviteAlready):
559
- return utils.get_input_peer(info.chat)
560
- except errors.RPCError:
561
- pass
562
- async for d in ub.iter_dialogs():
563
- if getattr(d.entity, "id", None) == self.ch.id and isinstance(d.entity, types.Channel):
564
- return utils.get_input_peer(d.entity)
565
- raise UserbotError("The userbot is in the channel, but I could not find it in its chat list.")
566
-
567
- async def _join_public(self, username: str):
568
- ub = self.ub.client
569
- try:
570
- peer = await ub.get_input_entity(username)
571
- try:
572
- await flood_retry(lambda: ub(functions.channels.JoinChannelRequest(peer)))
573
- except errors.UserAlreadyParticipantError:
574
- pass
575
- except (errors.ChannelsTooMuchError, errors.UserChannelsTooMuchError):
576
- raise UserbotError("The userbot account is in too many channels. Leave a few with that account and try again.")
577
- except errors.UserBannedInChannelError:
578
- raise UserbotError("The userbot account is banned in this channel. Unban it first.")
579
- except (errors.UsernameNotOccupiedError, errors.UsernameInvalidError, errors.ChannelPrivateError):
580
- raise UserbotError(f"The userbot could not open @{username}. Has the channel\u2019s name changed?")
581
- return peer
582
-
583
- # ------------------------------------------------------------------ rights
584
- async def _userbot_can_edit(self) -> bool:
585
- try:
586
- res = await self.ub.client(functions.channels.GetParticipantRequest(self.peer, types.InputPeerSelf()))
587
- except errors.RPCError:
588
- return False
589
- p = res.participant
590
- if isinstance(p, types.ChannelParticipantCreator):
591
- return True
592
- if isinstance(p, types.ChannelParticipantAdmin):
593
- return bool(getattr(p.admin_rights, "edit_messages", False))
594
- return False
595
-
596
- async def _ensure_edit_right(self) -> None:
597
- if await self._userbot_can_edit():
598
- return
599
- rights = await get_rights(self.ub.bot, self.ch)
600
- if rights is None or not (rights.admin and rights.add_admins):
601
- raise UserbotError(
602
- "The userbot is in the channel but may not edit messages there, and the bot may not add admins. "
603
- "Give the bot the \u201cAdd new admins\u201d right (it then promotes the userbot by itself), or make the userbot "
604
- "an admin with \u201cEdit messages of others\u201d yourself."
605
- )
606
- user = await self._bot_view_of_userbot()
607
- try:
608
- await self.ub.bot(
609
- functions.channels.EditAdminRequest(
610
- channel=peer_of(self.ch),
611
- user_id=user,
612
- admin_rights=types.ChatAdminRights(edit_messages=True),
613
- rank="",
614
- )
615
- )
616
- except errors.RPCError as e:
617
- name = type(e).__name__
618
- hint = {
619
- "RightForbiddenError": "The bot may not give that right.",
620
- "UserAdminInvalidError": "The bot may not change that user\u2019s admin status.",
621
- "AdminsTooMuchError": "The channel already has the maximum number of admins.",
622
- "ChatAdminRequiredError": "The bot needs the \u201cAdd new admins\u201d right.",
623
- }.get(name, str(e)[:150])
624
- raise UserbotError(f"I could not make the userbot an admin ({name}). {hint}")
625
- self._promoted, self._bot_user = True, user
626
- self.notes.append('made the userbot an admin with only \u201cEdit messages of others\u201d')
627
- for _ in range(12):
628
- if await self._userbot_can_edit():
629
- return
630
- await asyncio.sleep(0.5)
631
- raise UserbotError("I made the userbot an admin, but Telegram still does not let it edit there. Check its rights.")
632
-
633
- async def _bot_view_of_userbot(self):
634
- bot, me = self.ub.bot, self.ub.me
635
- uid = me.id
636
- try:
637
- ent = await bot.get_input_entity(uid)
638
- if isinstance(ent, types.InputPeerUser):
639
- return types.InputUser(ent.user_id, ent.access_hash)
640
- except Exception:
641
- pass
642
- queries = [types.ChannelParticipantsRecent()]
643
- for q in (getattr(me, "username", None), getattr(me, "first_name", None)):
644
- if q:
645
- queries.append(types.ChannelParticipantsSearch(q))
646
- for flt in queries:
647
- try:
648
- res = await bot(
649
- functions.channels.GetParticipantsRequest(channel=peer_of(self.ch), filter=flt, offset=0, limit=200, hash=0)
650
- )
651
- except errors.RPCError:
652
- continue
653
- for u in getattr(res, "users", None) or []:
654
- if getattr(u, "id", None) == uid and getattr(u, "access_hash", None) is not None:
655
- return types.InputUser(u.id, u.access_hash)
656
- if getattr(me, "username", None):
657
- try:
658
- ent = await bot.get_input_entity(me.username)
659
- if isinstance(ent, types.InputPeerUser):
660
- return types.InputUser(ent.user_id, ent.access_hash)
661
- except Exception:
662
- pass
663
- raise UserbotError(
664
- "The userbot joined, but the bot cannot find that account in the channel\u2019s member list, so it cannot "
665
- "promote it. Make the userbot an admin with \u201cEdit messages of others\u201d yourself."
666
- )
667
-
668
-
669
  def delete_problem_text(ub, *, bot_error: Optional[str] = None, fallback_error: Optional[str] = None, tried: bool = False) -> str:
670
  """Why some messages could not be deleted, in words for the owner."""
671
  parts = []
 
20
  from telethon import TelegramClient, errors, functions, types, utils
21
  from telethon.sessions import StringSession
22
 
23
+ from .tgutil import flood_retry, get_rights, peer_of
24
 
25
  log = logging.getLogger(__name__)
26
 
 
117
  def deleter(self, ch) -> "UserbotDeleter":
118
  return UserbotDeleter(self, ch)
119
 
 
 
 
120
  async def fallback_for(self, ch):
121
  """(delete function, deleter) to hand to the delete step, or (None, None) when no userbot is usable.
122
 
 
398
  )
399
 
400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  def delete_problem_text(ub, *, bot_error: Optional[str] = None, fallback_error: Optional[str] = None, tried: bool = False) -> str:
402
  """Why some messages could not be deleted, in words for the owner."""
403
  parts = []
tests/test_wire.py CHANGED
@@ -115,7 +115,7 @@ def test_media_replacement_edit(client):
115
  req = capture(push_edit(client, CH, post(media_kind="photo", media_file_id=ref), replace_media=True))
116
  assert isinstance(req, functions.messages.EditMessageRequest)
117
  assert isinstance(req.media, types.InputMediaPhoto) and req.message == "Hello ๐Ÿ˜€"
118
- assert req.reply_markup is None # omit markup: preserve existing buttons, including another bot's buttons
119
 
120
 
121
  def test_reads_deletes_and_rights_requests(client):
 
115
  req = capture(push_edit(client, CH, post(media_kind="photo", media_file_id=ref), replace_media=True))
116
  assert isinstance(req, functions.messages.EditMessageRequest)
117
  assert isinstance(req.media, types.InputMediaPhoto) and req.message == "Hello ๐Ÿ˜€"
118
+ assert req.reply_markup is not None
119
 
120
 
121
  def test_reads_deletes_and_rights_requests(client):