no-name-here commited on
Commit
b243928
Β·
verified Β·
1 Parent(s): 4947828

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +107 -207
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
- Retries on FloodWait. Returns True on success, False on any other failure.
 
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
- for attempt in range(1, PyroConf.MAX_UPLOAD_RETRIES + 1):
1145
- try:
1146
- input_media = media_cls(media=media_path, caption=caption or "")
1147
- await user.edit_message_media(
1148
- chat_id=chat_id,
1149
- message_id=placeholder_msg_id,
1150
- media=input_media,
1151
- )
1152
- LOGGER(__name__).info(
1153
- f"βœ… Placeholder {placeholder_msg_id} replaced with media in-place")
1154
- return True
1155
- except FloodWait as e:
1156
- wait_s = int(getattr(e, "value", 0) or 5)
1157
- LOGGER(__name__).warning(
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 user.send_message(
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 user.send_message(
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 user.send_message(
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 user.send_message(
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 user.send_message(
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 user.send_message(
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 user.edit_message_text(
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 user.send_message(
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 user.edit_message_text(
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 user.edit_message_text(
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 user.edit_message_text(
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 user.edit_message_text(
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 user.send_message(chat_id, text)
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
- # The download worker already exhausted MAX_DOWNLOAD_RETRIES.
2886
- # Before giving up, do one final attempt here: re-fetch the
2887
- # message from Telegram (gets a fresh file_reference) and try
2888
- # the download once more. This catches the most common cause β€”
2889
- # a stale file_reference that expired between fetch and download.
2890
- fname = short_name(result.filename or f"Post #{index + 1}", 35)
2891
- recovered = False
2892
- retry_path = None
2893
- if src_msg and getattr(src_msg, "id", None):
 
 
 
2894
  try:
2895
- LOGGER(__name__).info(
2896
- f"[StrictSonic] Slot {index} β€” re-fetching for final "
2897
- f"download attempt (original error: {result.error})")
2898
- src_chat = getattr(getattr(src_msg, "chat", None), "id", None)
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
- # Truly unrecoverable β€” mark ABANDONED with placeholder
2935
- failed += 1
2936
- LOGGER(__name__).error(
2937
- f"[StrictSonic] Slot {index} abandoned after all retries: "
2938
- f"{result.error}")
2939
- err_text = (
2940
- f"πŸ”² **File Unavailable** β€” Post #{index + 1}\n"
2941
- f"━━━━━━━━━━━━━━━━━━━\n"
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
- _put_kw["placeholder_msg_id"] = _ph2
2966
- await state_mgr.put(index, **_put_kw)
2967
- return
2968
-
2969
- # ── (recovered from download failure β€” fall through to upload) ───
 
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 user.edit_message_text(
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 user.edit_message_text(
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 with a placeholder already in chat.
3107
- # We must NOT delete the placeholder and re-send β€” that creates
3108
- # a gap that breaks sequence. Keep the placeholder as an anchor
3109
- # and mark ABANDONED so /retry can re-attempt at the correct
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"(ph {ph_id}) β€” keeping placeholder, marking ABANDONED")
3117
- icon = "⚠️" if result.is_partial else "πŸ”²"
3118
- label = "Partial β€” re-upload needed" if result.is_partial else "Upload failed"
3119
  try:
3120
- await user.edit_message_text(
3121
  chat_id, ph_id,
3122
- f"{icon} **{label}** β€” Post #{index + 1}\n"
3123
  f"━━━━━━━━━━━━━━━━━━━\n"
3124
  f"πŸ“„ `{short_name(result.filename or f'Post #{index+1}', 35)}`\n\n"
3125
- f"_Could not upload (edit_message_media failed).\n"
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 user.edit_message_text(
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 (no placeholder).
3209
- # - If slot is not ready β†’ yield one event-loop tick, re-check once,
3210
- # THEN send a placeholder and record in pending_replace.
3211
- # Phase B: replace placeholders as downloads finish (FIRST_COMPLETED).
3212
- # Positions are already locked in chat by the placeholders sent in
3213
- # Phase A. Waiting for FIRST_COMPLETED means fast slots never wait
3214
- # behind slow ones β€” the bot stays maximally responsive.
3215
- # Sequence safety: edit_message_media replaces IN-PLACE. If it fails,
3216
- # we keep the placeholder as an anchor (ABANDONED) β€” we never send a
3217
- # new message that could appear in the wrong position.
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 as downloads finish ─────────────
3276
- # Positions are already locked in chat by the placeholders sent in
3277
- # Phase A. We can safely process whichever slot finishes downloading
3278
- # FIRST β€” no need for strict order here. Waiting in strict order
3279
- # would block a fast slot behind a slow one unnecessarily, making
3280
- # the bot appear stalled even when files are ready.
3281
- #
3282
- # Sequence safety: edit_message_media replaces the placeholder
3283
- # IN-PLACE at its existing chat position. No new messages are sent
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
- if remaining:
3292
- idx = remaining.pop(0)
3293
- await state_mgr.put(idx, status=TaskStatus.SKIPPED)
3294
- skipped += 1
3295
- LOGGER(__name__).info(
3296
- f"[StrictSonic] Phase B slot {idx} skipped by user.")
3297
  continue
3298
-
3299
- # Build a wait-task for every remaining slot
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: replacing placeholder for slot {idx}")
3328
- await _send_real(idx)
3329
- await state_mgr.move_status_to_bottom()
3330
- break # restart outer while so wait_map is rebuilt fresh
 
 
 
 
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 user.edit_message_text(
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 user.edit_message_text(
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})",