Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -1131,8 +1131,10 @@ async def edit_placeholder_with_media(
|
|
| 1131 |
effectively replacing the placeholder with the real content at the exact same
|
| 1132 |
chat position.
|
| 1133 |
|
| 1134 |
-
|
|
|
|
| 1135 |
"""
|
|
|
|
| 1136 |
type_map = {
|
| 1137 |
"photo": InputMediaPhoto,
|
| 1138 |
"video": InputMediaVideo,
|
|
@@ -1141,32 +1143,20 @@ async def edit_placeholder_with_media(
|
|
| 1141 |
}
|
| 1142 |
media_cls = type_map.get(media_type, InputMediaDocument)
|
| 1143 |
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
f"edit_message_media FloodWait {wait_s}s (attempt {attempt}/"
|
| 1159 |
-
f"{PyroConf.MAX_UPLOAD_RETRIES})")
|
| 1160 |
-
if attempt < PyroConf.MAX_UPLOAD_RETRIES:
|
| 1161 |
-
await asyncio.sleep(wait_s + 1)
|
| 1162 |
-
continue
|
| 1163 |
-
LOGGER(__name__).error("edit_message_media FloodWait retries exhausted")
|
| 1164 |
-
return False
|
| 1165 |
-
except Exception as e:
|
| 1166 |
-
LOGGER(__name__).warning(
|
| 1167 |
-
f"edit_message_media failed (attempt {attempt}): {e}")
|
| 1168 |
-
return False
|
| 1169 |
-
return False
|
| 1170 |
|
| 1171 |
|
| 1172 |
async def _send_with_flood_retry(send_fn, *args, retries: int = 3, **kwargs) -> Optional[Any]:
|
|
@@ -2050,7 +2040,7 @@ async def _send_one_slot(
|
|
| 2050 |
fname = short_name(result.filename or f"Post #{index + 1}", 35)
|
| 2051 |
LOGGER(__name__).error(f"[Sender] Slot {index} abandoned: {result.error}")
|
| 2052 |
try:
|
| 2053 |
-
placeholder = await
|
| 2054 |
chat_id,
|
| 2055 |
f"π² **File Unavailable** β Post #{index + 1}\n"
|
| 2056 |
f"βββββββββββββββββββ\n"
|
|
@@ -2127,7 +2117,7 @@ async def _send_one_slot(
|
|
| 2127 |
LOGGER(__name__).warning(
|
| 2128 |
f"Group item missing/failed: {ifname} β {ierr}")
|
| 2129 |
try:
|
| 2130 |
-
ph = await
|
| 2131 |
chat_id,
|
| 2132 |
f"π² **File Unavailable** (group item)\n"
|
| 2133 |
f"βββββββββββββββββββ\n"
|
|
@@ -2165,7 +2155,7 @@ async def _send_one_slot(
|
|
| 2165 |
failed = 1
|
| 2166 |
LOGGER(__name__).error(f"[Sender] Slot {index}: file gone before send")
|
| 2167 |
try:
|
| 2168 |
-
ph = await
|
| 2169 |
chat_id,
|
| 2170 |
f"π² **File Lost** β Post #{index + 1}\n\n"
|
| 2171 |
f"`{os.path.basename(result.media_path)}`\n"
|
|
@@ -2220,7 +2210,7 @@ async def _send_one_slot(
|
|
| 2220 |
await state_mgr.put(index, status=TaskStatus.FAILED, error=str(e))
|
| 2221 |
LOGGER(__name__).error(f"[Sender] Send failed slot {index}: {e}")
|
| 2222 |
try:
|
| 2223 |
-
ph = await
|
| 2224 |
chat_id,
|
| 2225 |
f"π² **Upload Failed** β Post #{index + 1}\n\n"
|
| 2226 |
f"`{str(e)[:200]}`\n_Use `/retry` to try again._")
|
|
@@ -2355,9 +2345,7 @@ async def _sonic_sender(
|
|
| 2355 |
continue # placeholder already sent β skip
|
| 2356 |
if kill_event.is_set():
|
| 2357 |
break
|
| 2358 |
-
# Need a placeholder for this gap
|
| 2359 |
-
# MUST use the user client β edit_message_media can only edit
|
| 2360 |
-
# messages sent by the same account (MESSAGE_AUTHOR_REQUIRED).
|
| 2361 |
gap_ts = state_mgr.state_map.get(gap)
|
| 2362 |
fname = gap_ts.filename if gap_ts else f"Post #{gap + 1}"
|
| 2363 |
fsize = gap_ts.file_size if gap_ts else 0
|
|
@@ -2366,7 +2354,7 @@ async def _sonic_sender(
|
|
| 2366 |
LOGGER(__name__).info(
|
| 2367 |
f"[Sonic] Creating gap placeholder for slot {gap}: {fname}")
|
| 2368 |
try:
|
| 2369 |
-
ph = await
|
| 2370 |
chat_id,
|
| 2371 |
f"β‘ **Downloadingβ¦** β Post #{gap + 1}\n"
|
| 2372 |
f"βββββββββββββββββββ\n"
|
|
@@ -2384,7 +2372,7 @@ async def _sonic_sender(
|
|
| 2384 |
f"[Sonic] FloodWait {wait_s}s sending gap placeholder")
|
| 2385 |
await asyncio.sleep(wait_s + 1)
|
| 2386 |
try:
|
| 2387 |
-
ph = await
|
| 2388 |
chat_id,
|
| 2389 |
f"β‘ **Downloadingβ¦** β Post #{gap + 1}\n"
|
| 2390 |
f"π `{short_name(fname, 35)}`\n"
|
|
@@ -2428,7 +2416,7 @@ async def _sonic_sender(
|
|
| 2428 |
ph_id = sonic_ph_map.get(index)
|
| 2429 |
if ph_id:
|
| 2430 |
try:
|
| 2431 |
-
await
|
| 2432 |
chat_id, ph_id,
|
| 2433 |
f"π² **File Unavailable** β Post #{index + 1}\n"
|
| 2434 |
f"βββββββββββββββββββ\n"
|
|
@@ -2440,7 +2428,7 @@ async def _sonic_sender(
|
|
| 2440 |
else:
|
| 2441 |
# No placeholder yet β send an abandoned placeholder
|
| 2442 |
try:
|
| 2443 |
-
ph = await
|
| 2444 |
chat_id,
|
| 2445 |
f"π² **File Unavailable** β Post #{index + 1}\n"
|
| 2446 |
f"βββββββββββββββββββ\n"
|
|
@@ -2529,7 +2517,7 @@ async def _sonic_sender(
|
|
| 2529 |
# Update the gap placeholder to show group result
|
| 2530 |
try:
|
| 2531 |
icon = "β
" if group_failed_count == 0 else "β οΈ"
|
| 2532 |
-
await
|
| 2533 |
chat_id, ph_id,
|
| 2534 |
f"{icon} **Media Group** β Post #{index + 1}\n"
|
| 2535 |
f"`{group_sent_count}` files sent"
|
|
@@ -2557,7 +2545,7 @@ async def _sonic_sender(
|
|
| 2557 |
ph_id = sonic_ph_map.get(index)
|
| 2558 |
if ph_id:
|
| 2559 |
try:
|
| 2560 |
-
await
|
| 2561 |
chat_id, ph_id,
|
| 2562 |
f"π² **File Lost** β Post #{index + 1}\n"
|
| 2563 |
f"_File removed before upload. Use `/retry`._")
|
|
@@ -2638,7 +2626,7 @@ async def _sonic_sender(
|
|
| 2638 |
f"{str(chat_id).lstrip('-100')}/{sent_msg.id}")
|
| 2639 |
icon = "β οΈ" if result.is_partial else "β
"
|
| 2640 |
label = "Partial" if result.is_partial else "Uploaded"
|
| 2641 |
-
await
|
| 2642 |
chat_id, ph_id,
|
| 2643 |
f"{icon} **{label}** β Post #{index + 1}\n"
|
| 2644 |
f"[Jump to message β]({link})",
|
|
@@ -2674,7 +2662,7 @@ async def _sonic_sender(
|
|
| 2674 |
try:
|
| 2675 |
link = (f"https://t.me/c/"
|
| 2676 |
f"{str(chat_id).lstrip('-100')}/{sent_msg.id}")
|
| 2677 |
-
await
|
| 2678 |
chat_id, ph_id,
|
| 2679 |
f"β
**Forwarded** β Post #{index + 1}\n"
|
| 2680 |
f"[Jump to message β]({link})",
|
|
@@ -2841,12 +2829,9 @@ async def _strict_sonic_sender(
|
|
| 2841 |
f"π `{url}`\n\n"
|
| 2842 |
f"_Will be replaced automatically when ready._"
|
| 2843 |
)
|
| 2844 |
-
# MUST use the user client β edit_message_media can only edit messages
|
| 2845 |
-
# sent by the same account. Bot-sent placeholders cannot be edited
|
| 2846 |
-
# by the user client (Telegram returns MESSAGE_AUTHOR_REQUIRED).
|
| 2847 |
for attempt in range(2):
|
| 2848 |
try:
|
| 2849 |
-
ph = await
|
| 2850 |
sonic_ph_map[gap] = ph.id
|
| 2851 |
if gap_ts:
|
| 2852 |
gap_ts.placeholder_msg_id = ph.id
|
|
@@ -2882,91 +2867,39 @@ async def _strict_sonic_sender(
|
|
| 2882 |
|
| 2883 |
# ββ Error / abandoned ββββββββββββββββββββββββββββββββββββββββββββ
|
| 2884 |
if result.error and not result.media_path and result.media_type != "group":
|
| 2885 |
-
|
| 2886 |
-
|
| 2887 |
-
|
| 2888 |
-
|
| 2889 |
-
|
| 2890 |
-
|
| 2891 |
-
|
| 2892 |
-
|
| 2893 |
-
|
|
|
|
|
|
|
|
|
|
| 2894 |
try:
|
| 2895 |
-
|
| 2896 |
-
|
| 2897 |
-
|
| 2898 |
-
|
| 2899 |
-
if src_chat:
|
| 2900 |
-
fresh_msg = await user.get_messages(
|
| 2901 |
-
chat_id=src_chat, message_ids=src_msg.id)
|
| 2902 |
-
if fresh_msg and fresh_msg.id and has_downloadable_media(fresh_msg):
|
| 2903 |
-
retry_filename = get_file_name(fresh_msg.id, fresh_msg)
|
| 2904 |
-
retry_path = get_download_path(
|
| 2905 |
-
state_mgr.reply_to_id, retry_filename)
|
| 2906 |
-
rpath, rerr, rpartial = await download_single_message(
|
| 2907 |
-
fresh_msg, retry_path,
|
| 2908 |
-
retries=1,
|
| 2909 |
-
_src_chat_id=src_chat)
|
| 2910 |
-
if rpath:
|
| 2911 |
-
LOGGER(__name__).info(
|
| 2912 |
-
f"[StrictSonic] β
Final retry download succeeded "
|
| 2913 |
-
f"for slot {index}")
|
| 2914 |
-
# Patch result in-place so the upload path below handles it
|
| 2915 |
-
result.media_path = rpath
|
| 2916 |
-
result.media_type = (
|
| 2917 |
-
"photo" if fresh_msg.photo else
|
| 2918 |
-
"video" if fresh_msg.video else
|
| 2919 |
-
"audio" if fresh_msg.audio else
|
| 2920 |
-
"document")
|
| 2921 |
-
result.filename = retry_filename
|
| 2922 |
-
result.is_partial = rpartial
|
| 2923 |
-
result.error = rerr
|
| 2924 |
-
recovered = True
|
| 2925 |
-
except Exception as re_err:
|
| 2926 |
-
LOGGER(__name__).warning(
|
| 2927 |
-
f"[StrictSonic] Final retry attempt failed for slot "
|
| 2928 |
-
f"{index}: {re_err}")
|
| 2929 |
-
|
| 2930 |
-
if recovered:
|
| 2931 |
-
# Fall through to the single-media upload path below
|
| 2932 |
-
pass
|
| 2933 |
else:
|
| 2934 |
-
|
| 2935 |
-
|
| 2936 |
-
|
| 2937 |
-
|
| 2938 |
-
|
| 2939 |
-
|
| 2940 |
-
|
| 2941 |
-
|
| 2942 |
-
f"π `{fname}`\n"
|
| 2943 |
-
f"β οΈ _{result.error[:150] if result.error else 'Download failed'}_\n\n"
|
| 2944 |
-
f"_Use `/retry` to attempt re-download._"
|
| 2945 |
-
)
|
| 2946 |
-
if ph_id:
|
| 2947 |
-
try:
|
| 2948 |
-
await user.edit_message_text(chat_id, ph_id, err_text)
|
| 2949 |
-
except Exception as e:
|
| 2950 |
-
LOGGER(__name__).debug(
|
| 2951 |
-
f"[StrictSonic] Placeholder update failed: {e}")
|
| 2952 |
-
else:
|
| 2953 |
-
try:
|
| 2954 |
-
ph = await user.send_message(chat_id, err_text)
|
| 2955 |
-
sonic_ph_map[index] = ph.id
|
| 2956 |
-
except Exception:
|
| 2957 |
-
pass
|
| 2958 |
-
_ph2 = sonic_ph_map.get(index)
|
| 2959 |
-
if index in state_mgr.state_map:
|
| 2960 |
-
state_mgr.state_map[index].status = TaskStatus.ABANDONED
|
| 2961 |
-
if _ph2:
|
| 2962 |
-
state_mgr.state_map[index].placeholder_msg_id = _ph2
|
| 2963 |
-
_put_kw: dict = {"status": TaskStatus.ABANDONED}
|
| 2964 |
if _ph2:
|
| 2965 |
-
|
| 2966 |
-
|
| 2967 |
-
|
| 2968 |
-
|
| 2969 |
-
|
|
|
|
| 2970 |
|
| 2971 |
# ββ Media group ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2972 |
if result.media_type == "group" and result.media_path:
|
|
@@ -3026,7 +2959,7 @@ async def _strict_sonic_sender(
|
|
| 3026 |
if ph_id:
|
| 3027 |
try:
|
| 3028 |
icon = "β
" if group_failed_count == 0 else "β οΈ"
|
| 3029 |
-
await
|
| 3030 |
chat_id, ph_id,
|
| 3031 |
f"{icon} **Media Group** β Post #{index + 1}\n"
|
| 3032 |
f"`{group_sent_count}` files sent"
|
|
@@ -3055,7 +2988,7 @@ async def _strict_sonic_sender(
|
|
| 3055 |
f"[StrictSonic] Slot {index}: file gone before upload")
|
| 3056 |
if ph_id:
|
| 3057 |
try:
|
| 3058 |
-
await
|
| 3059 |
chat_id, ph_id,
|
| 3060 |
f"π² **File Lost** β Post #{index + 1}\n"
|
| 3061 |
f"_File removed before upload. Use `/retry`._")
|
|
@@ -3103,26 +3036,22 @@ async def _strict_sonic_sender(
|
|
| 3103 |
src_to_dst_map[src_msg.id] = ph_id
|
| 3104 |
await state_mgr.put(index, sent_msg_id=ph_id)
|
| 3105 |
elif ph_id:
|
| 3106 |
-
# edit_message_media failed
|
| 3107 |
-
#
|
| 3108 |
-
#
|
| 3109 |
-
#
|
| 3110 |
-
# position later. The file on disk is cleaned up here because
|
| 3111 |
-
# /retry will re-download fresh.
|
| 3112 |
failed += 1
|
| 3113 |
cleanup_download(result.media_path)
|
| 3114 |
LOGGER(__name__).warning(
|
| 3115 |
f"[StrictSonic] edit_message_media failed for slot {index} "
|
| 3116 |
-
f"(
|
| 3117 |
-
icon = "β οΈ" if result.is_partial else "π²"
|
| 3118 |
-
label = "Partial β re-upload needed" if result.is_partial else "Upload failed"
|
| 3119 |
try:
|
| 3120 |
-
await
|
| 3121 |
chat_id, ph_id,
|
| 3122 |
-
f"
|
| 3123 |
f"βββββββββββββββββββ\n"
|
| 3124 |
f"π `{short_name(result.filename or f'Post #{index+1}', 35)}`\n\n"
|
| 3125 |
-
f"
|
| 3126 |
f"Use `/retry` to re-upload at this position._")
|
| 3127 |
except Exception as edit_err:
|
| 3128 |
LOGGER(__name__).debug(
|
|
@@ -3186,7 +3115,7 @@ async def _strict_sonic_sender(
|
|
| 3186 |
try:
|
| 3187 |
link = (f"https://t.me/c/"
|
| 3188 |
f"{str(chat_id).lstrip('-100')}/{sent_msg.id}")
|
| 3189 |
-
await
|
| 3190 |
chat_id, ph_id,
|
| 3191 |
f"β
**Forwarded** β Post #{index + 1}\n"
|
| 3192 |
f"[Jump to message β]({link})",
|
|
@@ -3205,16 +3134,16 @@ async def _strict_sonic_sender(
|
|
| 3205 |
|
| 3206 |
# ββ chat_writer: THE ONLY coroutine that ever touches chat βββββββββββ
|
| 3207 |
# Phase A: walk write_pointer from 0..n-1 in strict order.
|
| 3208 |
-
# - If slot is ready β send real content immediately
|
| 3209 |
-
# - If slot is not ready β yield one event-loop tick
|
| 3210 |
-
#
|
| 3211 |
-
#
|
| 3212 |
-
#
|
| 3213 |
-
#
|
| 3214 |
-
#
|
| 3215 |
-
#
|
| 3216 |
-
#
|
| 3217 |
-
# new message that
|
| 3218 |
async def chat_writer():
|
| 3219 |
nonlocal skipped
|
| 3220 |
write_pointer = 0
|
|
@@ -3272,62 +3201,33 @@ async def _strict_sonic_sender(
|
|
| 3272 |
pending_replace.append(write_pointer)
|
| 3273 |
write_pointer += 1
|
| 3274 |
|
| 3275 |
-
# ββ Phase B: replace placeholders
|
| 3276 |
-
#
|
| 3277 |
-
#
|
| 3278 |
-
#
|
| 3279 |
-
#
|
| 3280 |
-
#
|
| 3281 |
-
|
| 3282 |
-
|
| 3283 |
-
|
| 3284 |
-
# that could land in the wrong order. If edit_message_media fails
|
| 3285 |
-
# (no edit rights etc.), we keep the placeholder as an anchor and
|
| 3286 |
-
# mark ABANDONED β we never send a new out-of-order message.
|
| 3287 |
-
remaining = list(pending_replace)
|
| 3288 |
-
while remaining and not kill_event.is_set():
|
| 3289 |
if skip_event.is_set():
|
| 3290 |
skip_event.clear()
|
| 3291 |
-
|
| 3292 |
-
|
| 3293 |
-
|
| 3294 |
-
skipped
|
| 3295 |
-
LOGGER(__name__).info(
|
| 3296 |
-
f"[StrictSonic] Phase B slot {idx} skipped by user.")
|
| 3297 |
continue
|
| 3298 |
-
|
| 3299 |
-
|
| 3300 |
-
wait_map: Dict[asyncio.Task, int] = {}
|
| 3301 |
-
for idx in remaining:
|
| 3302 |
-
if ready_events[idx].is_set():
|
| 3303 |
-
t = asyncio.ensure_future(asyncio.sleep(0))
|
| 3304 |
-
else:
|
| 3305 |
-
t = asyncio.ensure_future(ready_events[idx].wait())
|
| 3306 |
-
wait_map[t] = idx
|
| 3307 |
-
|
| 3308 |
-
if not wait_map:
|
| 3309 |
-
break
|
| 3310 |
-
|
| 3311 |
-
done_tasks, _ = await asyncio.wait(
|
| 3312 |
-
list(wait_map.keys()), return_when=asyncio.FIRST_COMPLETED)
|
| 3313 |
-
|
| 3314 |
-
# Cancel the waiter tasks that didn't fire
|
| 3315 |
-
for t in wait_map:
|
| 3316 |
-
if t not in done_tasks:
|
| 3317 |
-
t.cancel()
|
| 3318 |
-
|
| 3319 |
-
# Process the first slot that became ready
|
| 3320 |
-
for done_task in done_tasks:
|
| 3321 |
-
idx = wait_map[done_task]
|
| 3322 |
-
if idx in remaining:
|
| 3323 |
-
remaining.remove(idx)
|
| 3324 |
-
if kill_event.is_set():
|
| 3325 |
-
break
|
| 3326 |
LOGGER(__name__).info(
|
| 3327 |
-
f"[StrictSonic] Phase B:
|
| 3328 |
-
await
|
| 3329 |
-
|
| 3330 |
-
break
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3331 |
|
| 3332 |
await chat_writer()
|
| 3333 |
return {"sent": sent, "failed": failed, "skipped": skipped}
|
|
@@ -3782,7 +3682,7 @@ async def handle_retry(chat_id: int, reply_to_id: int):
|
|
| 3782 |
if ts.placeholder_msg_id and sent_msg:
|
| 3783 |
try:
|
| 3784 |
link = f"https://t.me/c/{str(chat_id).lstrip('-100')}/{sent_msg.id}"
|
| 3785 |
-
await
|
| 3786 |
chat_id, ts.placeholder_msg_id,
|
| 3787 |
f"β
**Fixed!** β Post #{ts.index + 1}\n"
|
| 3788 |
f"_Text forwarded._\n"
|
|
@@ -3917,7 +3817,7 @@ async def handle_retry(chat_id: int, reply_to_id: int):
|
|
| 3917 |
link = f"https://t.me/c/{str(chat_id).lstrip('-100')}/{sent_msg.id}"
|
| 3918 |
icon = "β οΈ" if is_partial else "β
"
|
| 3919 |
label = "Still Partial" if is_partial else "Fixed!"
|
| 3920 |
-
await
|
| 3921 |
chat_id, ts.placeholder_msg_id,
|
| 3922 |
f"{icon} **{label}** β Post #{ts.index + 1}\n"
|
| 3923 |
f"[Jump to message β]({link})",
|
|
|
|
| 1131 |
effectively replacing the placeholder with the real content at the exact same
|
| 1132 |
chat position.
|
| 1133 |
|
| 1134 |
+
Falls back gracefully if edit_message_media fails for any reason.
|
| 1135 |
+
Returns True if the in-place replacement succeeded, False otherwise.
|
| 1136 |
"""
|
| 1137 |
+
# Build the appropriate InputMedia wrapper
|
| 1138 |
type_map = {
|
| 1139 |
"photo": InputMediaPhoto,
|
| 1140 |
"video": InputMediaVideo,
|
|
|
|
| 1143 |
}
|
| 1144 |
media_cls = type_map.get(media_type, InputMediaDocument)
|
| 1145 |
|
| 1146 |
+
try:
|
| 1147 |
+
input_media = media_cls(media=media_path, caption=caption or "")
|
| 1148 |
+
await user.edit_message_media(
|
| 1149 |
+
chat_id=chat_id,
|
| 1150 |
+
message_id=placeholder_msg_id,
|
| 1151 |
+
media=input_media,
|
| 1152 |
+
)
|
| 1153 |
+
LOGGER(__name__).info(
|
| 1154 |
+
f"β
Placeholder {placeholder_msg_id} replaced with media in-place")
|
| 1155 |
+
return True
|
| 1156 |
+
except Exception as e:
|
| 1157 |
+
LOGGER(__name__).warning(
|
| 1158 |
+
f"edit_message_media failed (will fall back to reply): {e}")
|
| 1159 |
+
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1160 |
|
| 1161 |
|
| 1162 |
async def _send_with_flood_retry(send_fn, *args, retries: int = 3, **kwargs) -> Optional[Any]:
|
|
|
|
| 2040 |
fname = short_name(result.filename or f"Post #{index + 1}", 35)
|
| 2041 |
LOGGER(__name__).error(f"[Sender] Slot {index} abandoned: {result.error}")
|
| 2042 |
try:
|
| 2043 |
+
placeholder = await bot.send_message(
|
| 2044 |
chat_id,
|
| 2045 |
f"π² **File Unavailable** β Post #{index + 1}\n"
|
| 2046 |
f"βββββββββββββββββββ\n"
|
|
|
|
| 2117 |
LOGGER(__name__).warning(
|
| 2118 |
f"Group item missing/failed: {ifname} β {ierr}")
|
| 2119 |
try:
|
| 2120 |
+
ph = await bot.send_message(
|
| 2121 |
chat_id,
|
| 2122 |
f"π² **File Unavailable** (group item)\n"
|
| 2123 |
f"βββββββββββββββββββ\n"
|
|
|
|
| 2155 |
failed = 1
|
| 2156 |
LOGGER(__name__).error(f"[Sender] Slot {index}: file gone before send")
|
| 2157 |
try:
|
| 2158 |
+
ph = await bot.send_message(
|
| 2159 |
chat_id,
|
| 2160 |
f"π² **File Lost** β Post #{index + 1}\n\n"
|
| 2161 |
f"`{os.path.basename(result.media_path)}`\n"
|
|
|
|
| 2210 |
await state_mgr.put(index, status=TaskStatus.FAILED, error=str(e))
|
| 2211 |
LOGGER(__name__).error(f"[Sender] Send failed slot {index}: {e}")
|
| 2212 |
try:
|
| 2213 |
+
ph = await bot.send_message(
|
| 2214 |
chat_id,
|
| 2215 |
f"π² **Upload Failed** β Post #{index + 1}\n\n"
|
| 2216 |
f"`{str(e)[:200]}`\n_Use `/retry` to try again._")
|
|
|
|
| 2345 |
continue # placeholder already sent β skip
|
| 2346 |
if kill_event.is_set():
|
| 2347 |
break
|
| 2348 |
+
# Need a placeholder for this gap
|
|
|
|
|
|
|
| 2349 |
gap_ts = state_mgr.state_map.get(gap)
|
| 2350 |
fname = gap_ts.filename if gap_ts else f"Post #{gap + 1}"
|
| 2351 |
fsize = gap_ts.file_size if gap_ts else 0
|
|
|
|
| 2354 |
LOGGER(__name__).info(
|
| 2355 |
f"[Sonic] Creating gap placeholder for slot {gap}: {fname}")
|
| 2356 |
try:
|
| 2357 |
+
ph = await bot.send_message(
|
| 2358 |
chat_id,
|
| 2359 |
f"β‘ **Downloadingβ¦** β Post #{gap + 1}\n"
|
| 2360 |
f"βββββββββββββββββββ\n"
|
|
|
|
| 2372 |
f"[Sonic] FloodWait {wait_s}s sending gap placeholder")
|
| 2373 |
await asyncio.sleep(wait_s + 1)
|
| 2374 |
try:
|
| 2375 |
+
ph = await bot.send_message(
|
| 2376 |
chat_id,
|
| 2377 |
f"β‘ **Downloadingβ¦** β Post #{gap + 1}\n"
|
| 2378 |
f"π `{short_name(fname, 35)}`\n"
|
|
|
|
| 2416 |
ph_id = sonic_ph_map.get(index)
|
| 2417 |
if ph_id:
|
| 2418 |
try:
|
| 2419 |
+
await bot.edit_message_text(
|
| 2420 |
chat_id, ph_id,
|
| 2421 |
f"π² **File Unavailable** β Post #{index + 1}\n"
|
| 2422 |
f"βββββββββββββββββββ\n"
|
|
|
|
| 2428 |
else:
|
| 2429 |
# No placeholder yet β send an abandoned placeholder
|
| 2430 |
try:
|
| 2431 |
+
ph = await bot.send_message(
|
| 2432 |
chat_id,
|
| 2433 |
f"π² **File Unavailable** β Post #{index + 1}\n"
|
| 2434 |
f"βββββββββββββββββββ\n"
|
|
|
|
| 2517 |
# Update the gap placeholder to show group result
|
| 2518 |
try:
|
| 2519 |
icon = "β
" if group_failed_count == 0 else "β οΈ"
|
| 2520 |
+
await bot.edit_message_text(
|
| 2521 |
chat_id, ph_id,
|
| 2522 |
f"{icon} **Media Group** β Post #{index + 1}\n"
|
| 2523 |
f"`{group_sent_count}` files sent"
|
|
|
|
| 2545 |
ph_id = sonic_ph_map.get(index)
|
| 2546 |
if ph_id:
|
| 2547 |
try:
|
| 2548 |
+
await bot.edit_message_text(
|
| 2549 |
chat_id, ph_id,
|
| 2550 |
f"π² **File Lost** β Post #{index + 1}\n"
|
| 2551 |
f"_File removed before upload. Use `/retry`._")
|
|
|
|
| 2626 |
f"{str(chat_id).lstrip('-100')}/{sent_msg.id}")
|
| 2627 |
icon = "β οΈ" if result.is_partial else "β
"
|
| 2628 |
label = "Partial" if result.is_partial else "Uploaded"
|
| 2629 |
+
await bot.edit_message_text(
|
| 2630 |
chat_id, ph_id,
|
| 2631 |
f"{icon} **{label}** β Post #{index + 1}\n"
|
| 2632 |
f"[Jump to message β]({link})",
|
|
|
|
| 2662 |
try:
|
| 2663 |
link = (f"https://t.me/c/"
|
| 2664 |
f"{str(chat_id).lstrip('-100')}/{sent_msg.id}")
|
| 2665 |
+
await bot.edit_message_text(
|
| 2666 |
chat_id, ph_id,
|
| 2667 |
f"β
**Forwarded** β Post #{index + 1}\n"
|
| 2668 |
f"[Jump to message β]({link})",
|
|
|
|
| 2829 |
f"π `{url}`\n\n"
|
| 2830 |
f"_Will be replaced automatically when ready._"
|
| 2831 |
)
|
|
|
|
|
|
|
|
|
|
| 2832 |
for attempt in range(2):
|
| 2833 |
try:
|
| 2834 |
+
ph = await bot.send_message(chat_id, text)
|
| 2835 |
sonic_ph_map[gap] = ph.id
|
| 2836 |
if gap_ts:
|
| 2837 |
gap_ts.placeholder_msg_id = ph.id
|
|
|
|
| 2867 |
|
| 2868 |
# ββ Error / abandoned ββββββββββββββββββββββββββββββββββββββββββββ
|
| 2869 |
if result.error and not result.media_path and result.media_type != "group":
|
| 2870 |
+
failed += 1
|
| 2871 |
+
fname = short_name(result.filename or f"Post #{index + 1}", 35)
|
| 2872 |
+
LOGGER(__name__).error(
|
| 2873 |
+
f"[StrictSonic] Slot {index} abandoned: {result.error}")
|
| 2874 |
+
err_text = (
|
| 2875 |
+
f"π² **File Unavailable** β Post #{index + 1}\n"
|
| 2876 |
+
f"βββββββββββββββββββ\n"
|
| 2877 |
+
f"π `{fname}`\n"
|
| 2878 |
+
f"β οΈ _{result.error[:150]}_\n\n"
|
| 2879 |
+
f"_Use `/retry` to attempt re-download._"
|
| 2880 |
+
)
|
| 2881 |
+
if ph_id:
|
| 2882 |
try:
|
| 2883 |
+
await bot.edit_message_text(chat_id, ph_id, err_text)
|
| 2884 |
+
except Exception as e:
|
| 2885 |
+
LOGGER(__name__).debug(
|
| 2886 |
+
f"[StrictSonic] Placeholder update failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2887 |
else:
|
| 2888 |
+
try:
|
| 2889 |
+
ph = await bot.send_message(chat_id, err_text)
|
| 2890 |
+
sonic_ph_map[index] = ph.id
|
| 2891 |
+
except Exception:
|
| 2892 |
+
pass
|
| 2893 |
+
_ph2 = sonic_ph_map.get(index)
|
| 2894 |
+
if index in state_mgr.state_map:
|
| 2895 |
+
state_mgr.state_map[index].status = TaskStatus.ABANDONED
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2896 |
if _ph2:
|
| 2897 |
+
state_mgr.state_map[index].placeholder_msg_id = _ph2
|
| 2898 |
+
_put_kw: dict = {"status": TaskStatus.ABANDONED}
|
| 2899 |
+
if _ph2:
|
| 2900 |
+
_put_kw["placeholder_msg_id"] = _ph2
|
| 2901 |
+
await state_mgr.put(index, **_put_kw)
|
| 2902 |
+
return
|
| 2903 |
|
| 2904 |
# ββ Media group ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2905 |
if result.media_type == "group" and result.media_path:
|
|
|
|
| 2959 |
if ph_id:
|
| 2960 |
try:
|
| 2961 |
icon = "β
" if group_failed_count == 0 else "β οΈ"
|
| 2962 |
+
await bot.edit_message_text(
|
| 2963 |
chat_id, ph_id,
|
| 2964 |
f"{icon} **Media Group** β Post #{index + 1}\n"
|
| 2965 |
f"`{group_sent_count}` files sent"
|
|
|
|
| 2988 |
f"[StrictSonic] Slot {index}: file gone before upload")
|
| 2989 |
if ph_id:
|
| 2990 |
try:
|
| 2991 |
+
await bot.edit_message_text(
|
| 2992 |
chat_id, ph_id,
|
| 2993 |
f"π² **File Lost** β Post #{index + 1}\n"
|
| 2994 |
f"_File removed before upload. Use `/retry`._")
|
|
|
|
| 3036 |
src_to_dst_map[src_msg.id] = ph_id
|
| 3037 |
await state_mgr.put(index, sent_msg_id=ph_id)
|
| 3038 |
elif ph_id:
|
| 3039 |
+
# edit_message_media failed AND a placeholder is already in chat.
|
| 3040 |
+
# Sending a new message here would break sequence (it would appear
|
| 3041 |
+
# below later slots). Instead: mark ABANDONED and edit the placeholder
|
| 3042 |
+
# to a clear error notice so the user can /retry at the correct position.
|
|
|
|
|
|
|
| 3043 |
failed += 1
|
| 3044 |
cleanup_download(result.media_path)
|
| 3045 |
LOGGER(__name__).warning(
|
| 3046 |
f"[StrictSonic] edit_message_media failed for slot {index} "
|
| 3047 |
+
f"(placeholder {ph_id}) β marking ABANDONED for /retry")
|
|
|
|
|
|
|
| 3048 |
try:
|
| 3049 |
+
await bot.edit_message_text(
|
| 3050 |
chat_id, ph_id,
|
| 3051 |
+
f"π² **Upload Failed** β Post #{index + 1}\n"
|
| 3052 |
f"βββββββββββββββββββ\n"
|
| 3053 |
f"π `{short_name(result.filename or f'Post #{index+1}', 35)}`\n\n"
|
| 3054 |
+
f"_edit_message_media failed (permissions or unsupported type).\n"
|
| 3055 |
f"Use `/retry` to re-upload at this position._")
|
| 3056 |
except Exception as edit_err:
|
| 3057 |
LOGGER(__name__).debug(
|
|
|
|
| 3115 |
try:
|
| 3116 |
link = (f"https://t.me/c/"
|
| 3117 |
f"{str(chat_id).lstrip('-100')}/{sent_msg.id}")
|
| 3118 |
+
await bot.edit_message_text(
|
| 3119 |
chat_id, ph_id,
|
| 3120 |
f"β
**Forwarded** β Post #{index + 1}\n"
|
| 3121 |
f"[Jump to message β]({link})",
|
|
|
|
| 3134 |
|
| 3135 |
# ββ chat_writer: THE ONLY coroutine that ever touches chat βββββββββββ
|
| 3136 |
# Phase A: walk write_pointer from 0..n-1 in strict order.
|
| 3137 |
+
# - If slot is ready β send real content immediately.
|
| 3138 |
+
# - If slot is not ready β yield one event-loop tick (gives just-
|
| 3139 |
+
# finishing downloads a chance to land), re-check once, THEN send
|
| 3140 |
+
# a placeholder and record in pending_replace.
|
| 3141 |
+
# Phase B: replace placeholders in strict ascending index order.
|
| 3142 |
+
# Waiting for each slot in order guarantees that the fallback path
|
| 3143 |
+
# (when edit_message_media fails) also produces messages top-to-bottom.
|
| 3144 |
+
# If edit_message_media fails AND a placeholder is in chat, we mark
|
| 3145 |
+
# ABANDONED and edit the placeholder to an error notice β we never
|
| 3146 |
+
# send a new message that would appear below later slots.
|
| 3147 |
async def chat_writer():
|
| 3148 |
nonlocal skipped
|
| 3149 |
write_pointer = 0
|
|
|
|
| 3201 |
pending_replace.append(write_pointer)
|
| 3202 |
write_pointer += 1
|
| 3203 |
|
| 3204 |
+
# ββ Phase B: replace placeholders in strict slot order βββββββββββ
|
| 3205 |
+
# We wait for each slot in ascending index order. Because every slot
|
| 3206 |
+
# already has a placeholder holding its position in chat, the visual
|
| 3207 |
+
# sequence is already established. Processing in order here ensures
|
| 3208 |
+
# that the fallback "send new message" path (when edit_message_media
|
| 3209 |
+
# fails) also produces messages in the correct top-to-bottom order.
|
| 3210 |
+
for idx in pending_replace:
|
| 3211 |
+
if kill_event.is_set():
|
| 3212 |
+
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3213 |
if skip_event.is_set():
|
| 3214 |
skip_event.clear()
|
| 3215 |
+
await state_mgr.put(idx, status=TaskStatus.SKIPPED)
|
| 3216 |
+
skipped += 1
|
| 3217 |
+
LOGGER(__name__).info(
|
| 3218 |
+
f"[StrictSonic] Phase B slot {idx} skipped by user.")
|
|
|
|
|
|
|
| 3219 |
continue
|
| 3220 |
+
# Wait for this specific slot to finish downloading
|
| 3221 |
+
if not ready_events[idx].is_set():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3222 |
LOGGER(__name__).info(
|
| 3223 |
+
f"[StrictSonic] Phase B: waiting for slot {idx}β¦")
|
| 3224 |
+
await ready_events[idx].wait()
|
| 3225 |
+
if kill_event.is_set():
|
| 3226 |
+
break
|
| 3227 |
+
LOGGER(__name__).info(
|
| 3228 |
+
f"[StrictSonic] Phase B: replacing placeholder for slot {idx}")
|
| 3229 |
+
await _send_real(idx)
|
| 3230 |
+
await state_mgr.move_status_to_bottom()
|
| 3231 |
|
| 3232 |
await chat_writer()
|
| 3233 |
return {"sent": sent, "failed": failed, "skipped": skipped}
|
|
|
|
| 3682 |
if ts.placeholder_msg_id and sent_msg:
|
| 3683 |
try:
|
| 3684 |
link = f"https://t.me/c/{str(chat_id).lstrip('-100')}/{sent_msg.id}"
|
| 3685 |
+
await bot.edit_message_text(
|
| 3686 |
chat_id, ts.placeholder_msg_id,
|
| 3687 |
f"β
**Fixed!** β Post #{ts.index + 1}\n"
|
| 3688 |
f"_Text forwarded._\n"
|
|
|
|
| 3817 |
link = f"https://t.me/c/{str(chat_id).lstrip('-100')}/{sent_msg.id}"
|
| 3818 |
icon = "β οΈ" if is_partial else "β
"
|
| 3819 |
label = "Still Partial" if is_partial else "Fixed!"
|
| 3820 |
+
await bot.edit_message_text(
|
| 3821 |
chat_id, ts.placeholder_msg_id,
|
| 3822 |
f"{icon} **{label}** β Post #{ts.index + 1}\n"
|
| 3823 |
f"[Jump to message β]({link})",
|