Spaces:
Paused
Paused
github-actions[bot] commited on
Commit ยท
5bcd8c9
1
Parent(s): 946d0f5
Automated sync from GitHub
Browse files- .gitattributes +2 -2
- app/common.py +1 -3
- app/handlers/replace.py +1 -2
- app/replace_engine.py +10 -69
- app/tgutil.py +2 -2
- app/userbot.py +1 -269
- tests/test_wire.py +1 -1
.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 |
-
|
| 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 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 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(
|
| 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,
|
| 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
|
| 383 |
-
"
|
| 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
|
| 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
|
| 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):
|