Skip to content

Commit 1ef4c1a

Browse files
authored
Merge pull request #13 from z-mio/dev
添加支持速率限制的 Telegram 发送助手,并调整媒体发送逻辑,将说明文字与媒体组合并,同时优雅地处理 FloodWait。 新功能: 引入可复用的 Telegram 消息发送助手,在遇到 FloodWait 速率限制时自动重试。 错误修复: 在消息编辑、内联更新和媒体发送场景下优雅地处理 Telegram 的 FloodWait 错误,通过重试或按需忽略来恢复执行。 增强改进: 统一并调整媒体与说明文字的发送方式,使说明文字附加到最终的媒体或媒体组消息上,而不是单独发送为文本消息。 通过在上传单个媒体和媒体组前发送合适的聊天状态(chat actions),提升用户反馈体验。
2 parents 83203e4 + 92476ed commit 1ef4c1a

File tree

3 files changed

+154
-89
lines changed

3 files changed

+154
-89
lines changed

plugins/inline_parse.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
VideoRef,
99
)
1010
from pyrogram import Client
11+
from pyrogram.errors import FloodWait
1112
from pyrogram.types import (
1213
ChosenInlineResult,
1314
InlineQuery,
@@ -65,7 +66,7 @@ async def report(self, text: str) -> None:
6566
self._last_text = full
6667
try:
6768
await self._cli.edit_inline_text(self._mid, full)
68-
except Exception:
69+
except FloodWait:
6970
pass
7071

7172
async def report_error(self, stage: str, error: Exception) -> None:

plugins/parse.py

Lines changed: 126 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import asyncio
22
import os
3+
from collections.abc import Awaitable, Callable
4+
from itertools import batched
35
from typing import Literal
46

57
from parsehub.types import (
@@ -11,6 +13,7 @@
1113
VideoFile,
1214
)
1315
from pyrogram import Client, enums, filters
16+
from pyrogram.errors import FloodWait
1417
from pyrogram.types import (
1518
InputMediaAnimation,
1619
InputMediaDocument,
@@ -37,6 +40,27 @@
3740

3841
logger = logger.bind(name="Parse")
3942
SKIP_DOWNLOAD_THRESHOLD = 0
43+
MAX_RETRIES = 5
44+
45+
46+
async def _send_with_rate_limit[T](
47+
send_coro_fn: Callable[[], Awaitable[T]],
48+
) -> T:
49+
"""带自动重试的发送包装器。
50+
51+
Args:
52+
send_coro_fn: 返回协程的可调用对象(lambda 或函数),每次重试会重新调用
53+
"""
54+
for attempt in range(MAX_RETRIES):
55+
try:
56+
return await send_coro_fn()
57+
except FloodWait as e:
58+
if attempt < MAX_RETRIES - 1:
59+
logger.warning(f"FloodWait 重试 ({attempt + 1}/{MAX_RETRIES}),等待 {e.value}s")
60+
await asyncio.sleep(e.value)
61+
else:
62+
raise e from e
63+
return None
4064

4165

4266
class MessageStatusReporter(StatusReporter):
@@ -67,12 +91,15 @@ async def dismiss(self) -> None:
6791
await self._msg.delete()
6892

6993
async def _edit_text(self, text: str, **kwargs):
70-
if self._msg is None:
71-
self._msg = await self._user_msg.reply_text(text, **kwargs)
72-
else:
73-
if self._msg.text != text:
74-
await self._msg.edit_text(text, **kwargs)
75-
self._msg.text = text
94+
try:
95+
if self._msg is None:
96+
self._msg = await self._user_msg.reply_text(text, **kwargs)
97+
else:
98+
if self._msg.text != text:
99+
await self._msg.edit_text(text, **kwargs)
100+
self._msg.text = text
101+
except FloodWait:
102+
pass
76103

77104

78105
# ── Handler ──────────────────────────────────────────────────────────
@@ -221,7 +248,6 @@ async def handle_parse(
221248
logger.debug(f"开始上传媒体: media_count={len(result.processed_list)}")
222249
await reporter.report("上 传 中...")
223250
try:
224-
await msg.reply_chat_action(enums.ChatAction.UPLOAD_PHOTO)
225251
cache_entry = await _send_media(msg, parse_result, result.processed_list, caption)
226252
if cache_entry:
227253
await persistent_cache.set(raw_url, cache_entry)
@@ -327,35 +353,42 @@ async def _send_raw(
327353
logger.debug("Raw 模式, 直接上传文件")
328354
await reporter.report("上 传 中...")
329355
try:
330-
await msg.reply_chat_action(enums.ChatAction.UPLOAD_DOCUMENT)
331356
caption = build_caption(result.parse_result)
332-
333357
all_docs: list[InputMediaDocument] = []
334358
livephoto_videos: dict[int, InputMediaDocument] = {}
359+
335360
for idx, processed in enumerate(result.processed_list):
336361
# raw 模式下 processed.output_paths 只有一个文件
337362
file_path = processed.output_paths[0]
338363
all_docs.append(InputMediaDocument(media=str(file_path)))
339364
if isinstance(processed.source, LivePhotoFile):
340365
livephoto_videos[idx] = InputMediaDocument(media=str(processed.source.video_path))
366+
341367
if len(all_docs) == 1:
342-
m = await msg.reply_document(all_docs[0].media, caption=caption, force_document=True)
368+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_DOCUMENT)
369+
m = await _send_with_rate_limit(
370+
lambda: msg.reply_document(all_docs[0].media, caption=caption, force_document=True)
371+
)
343372
if livephoto_videos:
344-
await m.reply_document(livephoto_videos[0].media, force_document=True)
373+
await _send_with_rate_limit(lambda: m.reply_document(livephoto_videos[0].media, force_document=True))
345374
else:
346375
msgs: list[Message] = []
347-
for i in range(0, len(all_docs), 10):
348-
batch = all_docs[i : i + 10]
349-
mg = await msg.reply_media_group(batch) # type: ignore
376+
for batch in batched(all_docs, 10):
377+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_DOCUMENT)
378+
# noinspection PyDefaultArgument
379+
mg = await _send_with_rate_limit(lambda b=list(batch): msg.reply_media_group(b)) # type: ignore
350380
msgs.extend(mg)
351-
await asyncio.sleep(0.5)
352381
if livephoto_videos:
353382
for idx, m in livephoto_videos.items():
354-
await msgs[idx].reply_document(m.media, force_document=True)
355-
await asyncio.sleep(0.5)
356-
await msg.reply_text(
357-
caption,
358-
link_preview_options=LinkPreviewOptions(is_disabled=True),
383+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_DOCUMENT)
384+
await _send_with_rate_limit(
385+
lambda m_=m, idx_=idx: msgs[idx_].reply_document(m_.media, force_document=True)
386+
)
387+
await _send_with_rate_limit(
388+
lambda: msg.reply_text(
389+
caption,
390+
link_preview_options=LinkPreviewOptions(is_disabled=True),
391+
)
359392
)
360393

361394
except Exception as e:
@@ -390,7 +423,7 @@ async def _send_zip(
390423
await reporter.report("上 传 中...")
391424
try:
392425
await msg.reply_chat_action(enums.ChatAction.UPLOAD_DOCUMENT)
393-
await msg.reply_document(str(pack_path), caption=caption)
426+
await _send_with_rate_limit(lambda: msg.reply_document(str(pack_path), caption=caption))
394427
except Exception as e:
395428
logger.opt(exception=e).debug("详细堆栈")
396429
logger.error(f"上传失败: {e}")
@@ -421,28 +454,36 @@ async def _send_single(
421454

422455
try:
423456
if animations:
424-
sent = await msg.reply_animation(animations[0].media, caption=caption)
457+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_PHOTO)
458+
sent = await _send_with_rate_limit(lambda: msg.reply_animation(animations[0].media, caption=caption))
425459
else:
426460
single = photos_videos[0]
427461
match single:
428462
case InputMediaPhoto():
429-
sent = await msg.reply_photo(single.media, caption=caption)
463+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_PHOTO)
464+
sent = await _send_with_rate_limit(lambda: msg.reply_photo(single.media, caption=caption))
430465
case InputMediaVideo():
431-
sent = await msg.reply_video(
432-
single.media,
433-
caption=caption,
434-
video_cover=single.video_cover,
435-
duration=single.duration,
436-
width=single.width,
437-
height=single.height,
438-
supports_streaming=True,
466+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_VIDEO)
467+
sent = await _send_with_rate_limit(
468+
lambda: msg.reply_video(
469+
single.media,
470+
caption=caption,
471+
video_cover=single.video_cover,
472+
duration=single.duration,
473+
width=single.width,
474+
height=single.height,
475+
supports_streaming=True,
476+
)
439477
)
440478

441479
if sent and (cm := _cache_media_from_message(sent)):
442480
media_list.append(cm)
443481
except Exception as e:
444482
logger.warning(f"上传失败 {e}, 使用兼容模式上传")
445-
await msg.reply_document(all_media[0].media, caption=caption, force_document=True)
483+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_DOCUMENT)
484+
await _send_with_rate_limit(
485+
lambda: msg.reply_document(all_media[0].media, caption=caption, force_document=True)
486+
)
446487
return None
447488

448489
return media_list
@@ -461,40 +502,52 @@ async def _send_multi(
461502
not_cache = False
462503

463504
for ani in animations:
505+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_PHOTO)
506+
caption_ = caption if ani == animations[-1] and not photos_videos else ""
464507
try:
465-
sent = await msg.reply_animation(ani.media)
508+
sent = await _send_with_rate_limit(
509+
lambda a=ani, c=caption_: msg.reply_animation(
510+
a.media,
511+
caption=c,
512+
)
513+
)
466514
except Exception as e:
467515
logger.warning(f"上传失败 {e}, 使用兼容模式上传")
468516
not_cache = True
469-
await msg.reply_document(ani.media, force_document=True)
517+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_DOCUMENT)
518+
await _send_with_rate_limit(
519+
lambda a=ani, c=caption_: msg.reply_document(a.media, caption=c, force_document=True)
520+
)
470521
else:
471522
# 过大的 GIF 会返回 document
472523
if sent.document:
473524
media_list.append(CacheMedia(type=CacheMediaType.DOCUMENT, file_id=sent.document.file_id))
474525
else:
475526
media_list.append(CacheMedia(type=CacheMediaType.ANIMATION, file_id=sent.animation.file_id))
476-
await asyncio.sleep(0.5)
477527

478528
try:
479-
for i in range(0, len(photos_videos), 10):
480-
batch = photos_videos[i : i + 10]
481-
sent_msgs = await msg.reply_media_group(batch)
529+
for batch in batched(photos_videos, 10):
530+
if batch[-1] == photos_videos[-1]:
531+
batch[0].caption = caption
532+
533+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_PHOTO)
534+
# noinspection PyDefaultArgument
535+
sent_msgs = await _send_with_rate_limit(lambda b=list(batch): msg.reply_media_group(media=b))
482536
for m in sent_msgs:
483537
if cm := _cache_media_from_message(m):
484538
media_list.append(cm)
485539
except Exception as e:
486540
logger.warning(f"上传失败 {e}, 使用兼容模式上传")
487541
input_documents = [InputMediaDocument(media=item.media) for item in photos_videos]
488-
for i in range(0, len(input_documents), 10):
489-
batch = input_documents[i : i + 10]
490-
await msg.reply_media_group(batch) # type: ignore
491-
await asyncio.sleep(0.5)
542+
for batch in batched(input_documents, 10):
543+
if batch[-1] == input_documents[-1]:
544+
batch[0].caption = caption
545+
546+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_DOCUMENT)
547+
# noinspection PyDefaultArgument
548+
await _send_with_rate_limit(lambda b=list(batch): msg.reply_media_group(media=b)) # type: ignore
492549
return None
493550

494-
await msg.reply_text(
495-
caption,
496-
link_preview_options=LinkPreviewOptions(is_disabled=True),
497-
)
498551
return None if not_cache else media_list
499552

500553

@@ -554,34 +607,45 @@ async def _send_cached_single(msg: Message, m: CacheMedia, caption: str) -> None
554607
"""从缓存发送单个媒体。"""
555608
match m.type:
556609
case CacheMediaType.PHOTO:
557-
await msg.reply_photo(m.file_id, caption=caption)
610+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_PHOTO)
611+
await _send_with_rate_limit(lambda: msg.reply_photo(m.file_id, caption=caption))
558612
case CacheMediaType.VIDEO:
559-
await msg.reply_video(m.file_id, caption=caption, supports_streaming=True, video_cover=m.cover_file_id)
613+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_VIDEO)
614+
await _send_with_rate_limit(
615+
lambda: msg.reply_video(
616+
m.file_id, caption=caption, supports_streaming=True, video_cover=m.cover_file_id
617+
)
618+
)
560619
case CacheMediaType.ANIMATION:
561-
await msg.reply_animation(m.file_id, caption=caption)
620+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_PHOTO)
621+
await _send_with_rate_limit(lambda: msg.reply_animation(m.file_id, caption=caption))
562622
case CacheMediaType.DOCUMENT:
563-
await msg.reply_document(m.file_id, caption=caption, force_document=True)
623+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_DOCUMENT)
624+
await _send_with_rate_limit(lambda: msg.reply_document(m.file_id, caption=caption, force_document=True))
564625

565626

566627
async def _send_cached_multi(msg: Message, media: list[CacheMedia], caption: str) -> None:
567628
"""从缓存发送多个媒体。"""
568629
animations = [m for m in media if m.type == CacheMediaType.ANIMATION]
569630
others = [m for m in media if m.type != CacheMediaType.ANIMATION]
570631

571-
for m in animations:
572-
await msg.reply_animation(m.file_id)
573-
await asyncio.sleep(0.5)
632+
for ani in animations:
633+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_PHOTO)
634+
await _send_with_rate_limit(
635+
lambda a=ani: msg.reply_animation(
636+
a.file_id,
637+
caption=caption if a == animations[-1] and not others else "",
638+
)
639+
)
574640

575-
if others:
576-
media_group = _build_cached_media_group(others)
577-
for i in range(0, len(media_group), 10):
578-
await msg.reply_media_group(media_group[i : i + 10])
579-
await asyncio.sleep(0.5)
641+
media_group = _build_cached_media_group(others)
642+
for batch in batched(media_group, 10):
643+
if batch[-1] == media_group[-1]:
644+
batch[0].caption = caption
580645

581-
await msg.reply_text(
582-
caption,
583-
link_preview_options=LinkPreviewOptions(is_disabled=True),
584-
)
646+
await msg.reply_chat_action(enums.ChatAction.UPLOAD_PHOTO)
647+
# noinspection PyDefaultArgument
648+
await _send_with_rate_limit(lambda m=list(batch): msg.reply_media_group(m))
585649

586650

587651
def _build_cached_media_group(

0 commit comments

Comments
 (0)