telepost-bot 2.83.2__tar.gz → 2.84.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/CHANGELOG.md +27 -0
  2. {telepost_bot-2.83.2/telepost_bot.egg-info → telepost_bot-2.84.1}/PKG-INFO +1 -1
  3. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/database/db_manager.py +112 -1
  4. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/callback_handlers.py +4 -1
  5. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/conversation.py +11 -2
  6. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/review.py +93 -32
  7. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/upload.py +2 -1
  8. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/main.py +46 -3
  9. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/services/review_service.py +3 -2
  10. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/submitter_notify.py +6 -1
  11. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/build_info.py +1 -1
  12. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/observability/doctor.py +114 -0
  13. telepost_bot-2.84.1/telepost/storage/sqlite/inbox.py +155 -0
  14. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/reviews.py +142 -6
  15. telepost_bot-2.84.1/telepost/telegram/inbox_worker.py +177 -0
  16. telepost_bot-2.84.1/telepost/telegram/review_expiry.py +114 -0
  17. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/review_keyboard.py +11 -1
  18. telepost_bot-2.84.1/telepost/telegram/review_media.py +264 -0
  19. {telepost_bot-2.83.2 → telepost_bot-2.84.1/telepost_bot.egg-info}/PKG-INFO +1 -1
  20. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost_bot.egg-info/SOURCES.txt +8 -0
  21. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_doctor.py +57 -0
  22. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_review.py +2 -1
  23. telepost_bot-2.84.1/tests/test_review_expiry_recovery.py +163 -0
  24. telepost_bot-2.84.1/tests/test_review_media_lifecycle.py +445 -0
  25. telepost_bot-2.84.1/tests/test_update_inbox.py +879 -0
  26. telepost_bot-2.84.1/tests/test_webhook_payload_admission.py +119 -0
  27. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_webhook_static.py +37 -0
  28. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/ui/locales/en/LC_MESSAGES/telepost.mo +0 -0
  29. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/ui/locales/en/LC_MESSAGES/telepost.po +66 -0
  30. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/submission.py +41 -6
  31. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/webhook_server.py +60 -21
  32. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/LICENSE +0 -0
  33. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/MANIFEST.in +0 -0
  34. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/README.en.md +0 -0
  35. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/README.md +0 -0
  36. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/config/__init__.py +0 -0
  37. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/config/fly-telepost-policy.example.json +0 -0
  38. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/config/review_policy.md +0 -0
  39. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/config/settings.py +0 -0
  40. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/config.ini.example +0 -0
  41. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/database/__init__.py +0 -0
  42. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/__init__.py +0 -0
  43. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/api_commands.py +0 -0
  44. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/automation.py +0 -0
  45. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/botconfig.py +0 -0
  46. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/channel_listener.py +0 -0
  47. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/command_handlers.py +0 -0
  48. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/error_handler.py +0 -0
  49. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/index_handlers.py +0 -0
  50. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/mode_selection.py +0 -0
  51. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/moderation.py +0 -0
  52. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/preview_handlers.py +0 -0
  53. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/publish.py +0 -0
  54. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/reaction_stats.py +0 -0
  55. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/recovery.py +0 -0
  56. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/schedule_status.py +0 -0
  57. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/search_handlers.py +0 -0
  58. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/handlers/stats_handlers.py +0 -0
  59. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/mcp_server/__init__.py +0 -0
  60. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/mcp_server/review_client.py +0 -0
  61. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/mcp_server/server.py +0 -0
  62. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/models/__init__.py +0 -0
  63. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/models/state.py +0 -0
  64. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/__init__.py +0 -0
  65. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/SOURCES.sha256 +0 -0
  66. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/__init__.py +0 -0
  67. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/error-mapping.json +0 -0
  68. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/ackresult.candidate_search.json +0 -0
  69. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/capabilities.pixivflow.json +0 -0
  70. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/event.job.expired.json +0 -0
  71. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/event.job.succeeded.json +0 -0
  72. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/eventpage.candidate_search.json +0 -0
  73. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/job.cancelled.candidate_search.json +0 -0
  74. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/job.failed.no_candidate.json +0 -0
  75. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/job.queued.json +0 -0
  76. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/job.running.json +0 -0
  77. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/job.succeeded.candidate_search.json +0 -0
  78. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/jobpage.candidate_search.json +0 -0
  79. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/task.candidate_search.json +0 -0
  80. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/fixtures/task.candidate_search.target_only.json +0 -0
  81. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/protocol/v1/protocol.schema.json +0 -0
  82. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/pyproject.toml +0 -0
  83. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/run.py +0 -0
  84. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/services/__init__.py +0 -0
  85. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/setup.cfg +0 -0
  86. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/__init__.py +0 -0
  87. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/api/__init__.py +0 -0
  88. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/__init__.py +0 -0
  89. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/admin_ops.py +0 -0
  90. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/automation.py +0 -0
  91. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/delivery_planner.py +0 -0
  92. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/editorial.py +0 -0
  93. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/hot.py +0 -0
  94. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/novel_fallback_card.py +0 -0
  95. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/novel_preview.py +0 -0
  96. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/pixivflow_jobs.py +0 -0
  97. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/post_media.py +0 -0
  98. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/posts.py +0 -0
  99. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/publication.py +0 -0
  100. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/reaction_backfill.py +0 -0
  101. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/recovery.py +0 -0
  102. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/refetch.py +0 -0
  103. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/rerender.py +0 -0
  104. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/resubmit.py +0 -0
  105. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/review_queue.py +0 -0
  106. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/application/telepress_provider.py +0 -0
  107. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/__init__.py +0 -0
  108. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/automation.py +0 -0
  109. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/delivery.py +0 -0
  110. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/editorial.py +0 -0
  111. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/errors.py +0 -0
  112. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/hot.py +0 -0
  113. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/media.py +0 -0
  114. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/navigation.py +0 -0
  115. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/novel_preview.py +0 -0
  116. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/packing.py +0 -0
  117. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/presentation.py +0 -0
  118. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/presentation_policy.py +0 -0
  119. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/refetch_state.py +0 -0
  120. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/review.py +0 -0
  121. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/review_chat.py +0 -0
  122. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/domain/submission.py +0 -0
  123. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/miniapp/__init__.py +0 -0
  124. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/miniapp/auth.py +0 -0
  125. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/miniapp/rbac.py +0 -0
  126. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/miniapp/role_bindings.py +0 -0
  127. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/miniapp/session.py +0 -0
  128. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/observability/__init__.py +0 -0
  129. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/observability/audit.py +0 -0
  130. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/observability/cli.py +0 -0
  131. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/observability/errors.py +0 -0
  132. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/observability/redact.py +0 -0
  133. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/__init__.py +0 -0
  134. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/__init__.py +0 -0
  135. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/automation.py +0 -0
  136. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/columns.py +0 -0
  137. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/editorial.py +0 -0
  138. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/ledger.py +0 -0
  139. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/media_assets.py +0 -0
  140. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/moderation.py +0 -0
  141. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/novel_preview.py +0 -0
  142. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/posts.py +0 -0
  143. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/protocol_events.py +0 -0
  144. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/recovery.py +0 -0
  145. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/refetch.py +0 -0
  146. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/storage/sqlite/submitter_notifications.py +0 -0
  147. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/__init__.py +0 -0
  148. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/capture.py +0 -0
  149. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/delivery/__init__.py +0 -0
  150. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/delivery/discussion.py +0 -0
  151. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/delivery/executor.py +0 -0
  152. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/delivery/gateway.py +0 -0
  153. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/delivery/legacy_runner.py +0 -0
  154. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/delivery/planner.py +0 -0
  155. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/delivery/preparation.py +0 -0
  156. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/delivery/registry.py +0 -0
  157. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/delivery/sender.py +0 -0
  158. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost/telegram/review_stager.py +0 -0
  159. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost_bot.egg-info/dependency_links.txt +0 -0
  160. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost_bot.egg-info/entry_points.txt +0 -0
  161. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost_bot.egg-info/requires.txt +0 -0
  162. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/telepost_bot.egg-info/top_level.txt +0 -0
  163. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_admin_api.py +0 -0
  164. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_anonymous.py +0 -0
  165. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_api_commands.py +0 -0
  166. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_api_server.py +0 -0
  167. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_api_version.py +0 -0
  168. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_architecture_application.py +0 -0
  169. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_architecture_core.py +0 -0
  170. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_automation.py +0 -0
  171. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_backfill_attribution.py +0 -0
  172. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_blacklist.py +0 -0
  173. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_bot_language.py +0 -0
  174. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_bot_token_logging.py +0 -0
  175. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_botconfig.py +0 -0
  176. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_boundary.py +0 -0
  177. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_callback_contract.py +0 -0
  178. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_caption_mentions.py +0 -0
  179. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_compatibility.py +0 -0
  180. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_config_encoding.py +0 -0
  181. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_console_encoding.py +0 -0
  182. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_conversation_flow.py +0 -0
  183. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_data_class_cleanup.py +0 -0
  184. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_database.py +0 -0
  185. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_database_first_start.py +0 -0
  186. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_database_operations.py +0 -0
  187. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_delivery_planner.py +0 -0
  188. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_disposition.py +0 -0
  189. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_editorial.py +0 -0
  190. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_file_validator.py +0 -0
  191. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_fly_deploy_contract.py +0 -0
  192. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_frozen_support.py +0 -0
  193. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_handlers.py +0 -0
  194. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_heat_calculator.py +0 -0
  195. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_helper_functions.py +0 -0
  196. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_hot_domain.py +0 -0
  197. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_identity_provenance.py +0 -0
  198. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_install_scripts.py +0 -0
  199. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_integration.py +0 -0
  200. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_legacy_delivery_port_navigation.py +0 -0
  201. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_logic_flow.py +0 -0
  202. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_logical_submissions.py +0 -0
  203. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_maintenance_jobs.py +0 -0
  204. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_manager_notifications.py +0 -0
  205. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_media_decision_audit.py +0 -0
  206. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_media_delivery_pipeline.py +0 -0
  207. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_media_domain.py +0 -0
  208. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_media_preparation.py +0 -0
  209. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_media_proxy_delivery.py +0 -0
  210. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_menu_shortcut_single_reply.py +0 -0
  211. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_messages.py +0 -0
  212. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_miniapp_api.py +0 -0
  213. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_miniapp_auth.py +0 -0
  214. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_miniapp_content.py +0 -0
  215. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_miniapp_entries.py +0 -0
  216. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_miniapp_history.py +0 -0
  217. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_miniapp_rbac.py +0 -0
  218. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_mixed_media_integrity.py +0 -0
  219. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_moderation_and_admin_ux.py +0 -0
  220. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_navigation.py +0 -0
  221. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_no_owner_probe.py +0 -0
  222. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_novel_cover_preview.py +0 -0
  223. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_novel_delivery_semantics.py +0 -0
  224. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_novel_preview.py +0 -0
  225. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_observability.py +0 -0
  226. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_observability_lifecycle.py +0 -0
  227. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_online_reading_root_button.py +0 -0
  228. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_packing_ssot.py +0 -0
  229. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_polling_server.py +0 -0
  230. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_preview_pagination.py +0 -0
  231. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_production_handler_graph.py +0 -0
  232. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_protocol_contract.py +0 -0
  233. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_publication_presentation.py +0 -0
  234. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_publish_idempotency.py +0 -0
  235. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_publish_regressions.py +0 -0
  236. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_publish_reply.py +0 -0
  237. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_reaction_backfill.py +0 -0
  238. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_reaction_stats.py +0 -0
  239. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_recovery.py +0 -0
  240. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_refetch.py +0 -0
  241. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_refetch_card_state.py +0 -0
  242. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_refetch_events.py +0 -0
  243. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_refetch_job_lifecycle.py +0 -0
  244. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_refetch_reason_normalization.py +0 -0
  245. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_refetch_replacement.py +0 -0
  246. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_remote_fetch_fallback.py +0 -0
  247. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_rerender.py +0 -0
  248. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_resubmit.py +0 -0
  249. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_review_album_payload.py +0 -0
  250. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_review_chat_contract.py +0 -0
  251. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_review_consistency.py +0 -0
  252. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_review_http_mcp.py +0 -0
  253. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_review_service.py +0 -0
  254. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_robustness.py +0 -0
  255. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_run_dispatcher.py +0 -0
  256. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_run_mode.py +0 -0
  257. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_run_supervisor.py +0 -0
  258. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_schedule_outcomes.py +0 -0
  259. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_search_disabled_hint.py +0 -0
  260. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_search_recovery.py +0 -0
  261. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_security.py +0 -0
  262. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_security_headers.py +0 -0
  263. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_session_database_handles.py +0 -0
  264. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_shutdown.py +0 -0
  265. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_streaming_uploads.py +0 -0
  266. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_submission_timeout_idempotency.py +0 -0
  267. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_telegram_golden_messages.py +0 -0
  268. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_telegram_media_cache.py +0 -0
  269. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_upload.py +0 -0
  270. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_user_friendly_messages.py +0 -0
  271. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_webhook_ready.py +0 -0
  272. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_webhook_router.py +0 -0
  273. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_webhook_secret.py +0 -0
  274. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_webhook_secret_logging.py +0 -0
  275. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/tests/test_work_id_wire.py +0 -0
  276. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/ui/__init__.py +0 -0
  277. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/ui/i18n.py +0 -0
  278. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/ui/keyboards.py +0 -0
  279. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/ui/messages.py +0 -0
  280. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/__init__.py +0 -0
  281. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/api_server.py +0 -0
  282. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/api_tokens.py +0 -0
  283. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/blacklist.py +0 -0
  284. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/cache.py +0 -0
  285. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/config_wizard.py +0 -0
  286. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/console.py +0 -0
  287. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/database.py +0 -0
  288. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/file_validator.py +0 -0
  289. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/heat_calculator.py +0 -0
  290. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/helper_functions.py +0 -0
  291. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/index_manager.py +0 -0
  292. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/logging_config.py +0 -0
  293. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/maintenance_jobs.py +0 -0
  294. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/polling_server.py +0 -0
  295. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/run_mode.py +0 -0
  296. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/runtime_policy.py +0 -0
  297. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/schedule_watchdog.py +0 -0
  298. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/search_engine.py +0 -0
  299. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/utils/security_headers.py +0 -0
  300. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/webapp/__init__.py +0 -0
  301. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/webapp/dist/assets/2d82b92e720462f8dd3b-Bh4Fahcw.svg +0 -0
  302. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/webapp/dist/assets/index-CLfkW8eZ.css +0 -0
  303. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/webapp/dist/assets/index-CfjBQN_e.js +0 -0
  304. {telepost_bot-2.83.2 → telepost_bot-2.84.1}/webapp/dist/index.html +0 -0
@@ -8,6 +8,33 @@
8
8
  > 发布说明面向普通用户:写「对用户有什么改变」,不写内部实现细节(模块名、函数名、配置键);
9
9
  > 技术细节放进代码注释或 `docs/`。每个 Release 的正文由本文件对应版本段自动生成。
10
10
 
11
+ ## [2.84.1](https://github.com/redtidev1918/TelePost/compare/v2.84.0...v2.84.1) (2026-10-22)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * exclude runtime tasks from update persistence ([b420ed6](https://github.com/redtidev1918/TelePost/commit/b420ed629b2fa1e64d38f2f2a763439b199d956f))
17
+ * harden inbox completion and spoiler replay idempotency ([4c6375c](https://github.com/redtidev1918/TelePost/commit/4c6375c4514cfe2b5ae5f18f55a6169b8763ef7c))
18
+ * prevent runtime task persistence from blocking review callbacks ([21dec7d](https://github.com/redtidev1918/TelePost/commit/21dec7d41965bb436d309715c1e58668ae479d1c))
19
+
20
+ ## [2.84.0](https://github.com/redtidev1918/TelePost/compare/v2.83.2...v2.84.0) (2026-10-21)
21
+
22
+
23
+ ### Features
24
+
25
+ * **review:** bound review preview media by Telegram's 48h deletion window ([b480e4a](https://github.com/redtidev1918/TelePost/commit/b480e4a7e0b1675ba22dc802b94a67fc67e67b9c))
26
+ * **review:** bound review preview media by Telegram's deletion window ([65da48b](https://github.com/redtidev1918/TelePost/commit/65da48bd59e92d25c94d762752a4f7f39795b5c7))
27
+
28
+
29
+ ### Bug Fixes
30
+
31
+ * durable Telegram update inbox with recoverable submission conversations ([1b7f66d](https://github.com/redtidev1918/TelePost/commit/1b7f66d72fa5797a1ffb99fd45a4353a41eb201c))
32
+ * durable webhook inbox with recoverable submission conversations ([6d6b5ec](https://github.com/redtidev1918/TelePost/commit/6d6b5ec5988d42d458136f18a548e6fdb311f509))
33
+ * harden webhook payload admission and miniapp static guard ([a0cf21d](https://github.com/redtidev1918/TelePost/commit/a0cf21d697f21e2535b5ece6d5cb47fe2955857c))
34
+ * harden webhook payload admission and miniapp static guard ([a621807](https://github.com/redtidev1918/TelePost/commit/a621807264311ef58908741bad4a0d5c5fc77552))
35
+ * make review expiry recoverable with durable submitter notifications ([f6189d2](https://github.com/redtidev1918/TelePost/commit/f6189d2f5f1f86e3c44bc549f24941b6e8be1bdf))
36
+ * make review expiry recoverable with durable submitter notifications ([c322d40](https://github.com/redtidev1918/TelePost/commit/c322d40c6402ea3fd809cbc87fc3459c34631dd6))
37
+
11
38
  ## [2.83.2](https://github.com/redtidev1918/TelePost/compare/v2.83.1...v2.83.2) (2026-10-15)
12
39
 
13
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: telepost-bot
3
- Version: 2.83.2
3
+ Version: 2.84.1
4
4
  Summary: Telegram submission, review, and publication bot with a Mini App, HTTP API, and multi-bot supervisor.
5
5
  Author: redtidev1918
6
6
  License-Expression: MIT
@@ -76,6 +76,63 @@ async def init_db():
76
76
  except Exception:
77
77
  pass # 字段已存在
78
78
 
79
+ # 投稿会话代数:/submit 重建会话时递增。媒体追加的 update_id
80
+ # 去重以(user_id, epoch, update_id)为键,重放只会命中同一代,
81
+ # 旧代重放不会污染新会话(§durable-inbox)。
82
+ try:
83
+ await conn.execute(
84
+ "ALTER TABLE submissions ADD COLUMN epoch INTEGER NOT NULL DEFAULT 0")
85
+ logger.info("已添加 epoch 字段到 submissions 表")
86
+ except Exception:
87
+ pass # 字段已存在
88
+
89
+ # Durable webhook inbox(§durable-inbox):Telegram Update 在 HTTP 200
90
+ # 之前先落库;进程崩溃后由启动恢复按 update_id 顺序重放。
91
+ # 每个 Bot 独立数据库,update_id 主键即 (bot, update_id) 唯一。
92
+ await conn.execute('''
93
+ CREATE TABLE IF NOT EXISTS webhook_inbox (
94
+ update_id INTEGER PRIMARY KEY,
95
+ payload TEXT NOT NULL,
96
+ state TEXT NOT NULL DEFAULT 'pending',
97
+ attempts INTEGER NOT NULL DEFAULT 0,
98
+ last_error TEXT DEFAULT '',
99
+ received_at REAL NOT NULL,
100
+ updated_at REAL NOT NULL,
101
+ lease_until REAL,
102
+ next_attempt_at REAL NOT NULL DEFAULT 0,
103
+ done_at REAL
104
+ )
105
+ ''')
106
+ await conn.execute(
107
+ 'CREATE INDEX IF NOT EXISTS idx_webhook_inbox_due '
108
+ 'ON webhook_inbox(state, next_attempt_at, update_id)')
109
+
110
+ # A toggle and its callback receipt commit together; Inbox replay
111
+ # must not invert the same review a second time.
112
+ await conn.execute('''
113
+ CREATE TABLE IF NOT EXISTS review_spoiler_callbacks (
114
+ callback_id TEXT NOT NULL,
115
+ review_id INTEGER NOT NULL,
116
+ created_at REAL NOT NULL,
117
+ PRIMARY KEY (callback_id, review_id)
118
+ )
119
+ ''')
120
+
121
+ # 媒体追加去重日志:同一 (user, update) 跨投稿代只应用一次
122
+ # (epoch 仅作审计),与 submissions 行的更新同事务提交,崩溃后
123
+ # 重放收敛。worker 严格按 update_id 头部顺序处理,正常路径不会
124
+ # 出现「旧代 update 在新会话之后执行」;跨代匹配是乱序的硬防线。
125
+ await conn.execute('''
126
+ CREATE TABLE IF NOT EXISTS submission_media_log (
127
+ user_id INTEGER NOT NULL,
128
+ update_id INTEGER NOT NULL,
129
+ epoch INTEGER NOT NULL DEFAULT 0,
130
+ entry TEXT NOT NULL,
131
+ applied_at REAL NOT NULL,
132
+ PRIMARY KEY (user_id, update_id)
133
+ )
134
+ ''')
135
+
79
136
  # API 访问令牌(用于 /api/v1 自动化投稿,仅存哈希不存明文)
80
137
  await conn.execute('''
81
138
  CREATE TABLE IF NOT EXISTS api_tokens (
@@ -138,6 +195,41 @@ async def init_db():
138
195
  logger.info("已添加 target_id 字段到 pending_reviews 表")
139
196
  except Exception:
140
197
  pass # 字段已存在
198
+ # NULL leaves historical expiry untouched; new expirations checkpoint
199
+ # their Telegram effects independently of the terminal review state.
200
+ columns = {r[1] for r in await (await conn.execute(
201
+ 'PRAGMA table_info(pending_reviews)')).fetchall()}
202
+ for column, ddl in (
203
+ ('expiry_progress', 'TEXT'),
204
+ ('expiry_next_attempt_at', 'REAL'),
205
+ ):
206
+ if column not in columns:
207
+ await conn.execute(f'ALTER TABLE pending_reviews ADD COLUMN {column} {ddl}')
208
+ await conn.execute(
209
+ 'CREATE INDEX IF NOT EXISTS idx_review_expiry_due '
210
+ 'ON pending_reviews(expiry_next_attempt_at) '
211
+ 'WHERE expiry_next_attempt_at IS NOT NULL')
212
+
213
+ # Time-bounded review-media lifecycle (§review-media-ttl):
214
+ # preview media is deleted ~36h after sending (Telegram only allows
215
+ # deletion within 48h) while the control card stays actionable;
216
+ # admins regenerate previews on demand from the stored file_ids.
217
+ # NULL preview_state on legacy rows means 'present' with the
218
+ # review's created_at as the TTL anchor.
219
+ columns = {r[1] for r in await (await conn.execute(
220
+ 'PRAGMA table_info(pending_reviews)')).fetchall()}
221
+ for column, ddl in (
222
+ ('preview_state', 'TEXT'),
223
+ ('preview_sent_at', 'REAL'),
224
+ ('preview_attempts', 'INTEGER NOT NULL DEFAULT 0'),
225
+ ):
226
+ if column not in columns:
227
+ await conn.execute(f'ALTER TABLE pending_reviews ADD COLUMN {column} {ddl}')
228
+ await conn.execute(
229
+ 'CREATE INDEX IF NOT EXISTS idx_review_preview_due '
230
+ "ON pending_reviews(COALESCE(preview_sent_at, created_at)) "
231
+ "WHERE status='pending'")
232
+
141
233
  # Generic, bounded provenance for API submissions (any client).
142
234
  # TelePost only stores/displays these; it never interprets them.
143
235
  for column, ddl in (
@@ -961,7 +1053,8 @@ async def cleanup_old_data():
961
1053
  c = await conn.cursor()
962
1054
  await c.execute(
963
1055
  "DELETE FROM pending_reviews "
964
- "WHERE status IN ('rejected','published','deleted','failed','expired') AND updated_at < ?",
1056
+ "WHERE status IN ('rejected','published','deleted','failed','expired') AND updated_at < ? "
1057
+ "AND expiry_next_attempt_at IS NULL",
965
1058
  (review_cutoff,),
966
1059
  )
967
1060
  logger.info("已清理过期审核记录(保留 %d 天)", review_retention_days)
@@ -973,6 +1066,24 @@ async def cleanup_old_data():
973
1066
  )
974
1067
  logger.info("已清理过期 API 通知幂等记录(保留 %d 天)", review_retention_days)
975
1068
 
1069
+ # Durable inbox 保留策略:已完成记录 7 天后清理;dead(永久失败、需
1070
+ # 人工核对)保留 30 天。pending/processing 与未过期重试行绝不由这里的
1071
+ # 普通过期清理删除——它们只属于启动恢复与 worker。
1072
+ now_ts = datetime.now().timestamp()
1073
+ async with get_db() as conn:
1074
+ await conn.execute(
1075
+ "DELETE FROM webhook_inbox WHERE state='done' AND done_at < ?",
1076
+ (now_ts - 7 * 86400,))
1077
+ await conn.execute(
1078
+ "DELETE FROM review_spoiler_callbacks WHERE review_id NOT IN "
1079
+ "(SELECT id FROM pending_reviews)")
1080
+ await conn.execute(
1081
+ "DELETE FROM webhook_inbox WHERE state='dead' AND updated_at < ?",
1082
+ (now_ts - 30 * 86400,))
1083
+ await conn.execute(
1084
+ "DELETE FROM submission_media_log WHERE applied_at < ?",
1085
+ (now_ts - 7 * 86400,))
1086
+
976
1087
  # Append-only audit: prune old rows, but keep every event attached to a
977
1088
  # still-open review (preparing/pending/publishing/failed) regardless of
978
1089
  # age. Unattached events (review_id IS NULL) follow the age cutoff.
@@ -19,7 +19,7 @@ from handlers.search_handlers import (
19
19
  logger = logging.getLogger(__name__)
20
20
 
21
21
  GLOBAL_CALLBACK_PATTERNS = (
22
- r"^review_(?:approve|reject|spoiler|refetch|block_user|block_api):",
22
+ r"^review_(?:approve|reject|spoiler|preview|refetch|block_user|block_api):",
23
23
  r"^admin_block:",
24
24
  r"^sched_recover\|",
25
25
  r"^(?:hot_filter_|hot_limit_|hot_refresh$)",
@@ -74,6 +74,9 @@ async def handle_callback_query(update: Update, context: CallbackContext):
74
74
  elif data.startswith("review_spoiler:"):
75
75
  from handlers.review import toggle_review_spoiler
76
76
  return await toggle_review_spoiler(update, context)
77
+ elif data.startswith("review_preview:"):
78
+ from handlers.review import restore_review_preview
79
+ return await restore_review_preview(update, context)
77
80
  elif data.startswith("review_refetch:"):
78
81
  from handlers.review import refetch_review
79
82
  return await refetch_review(update, context)
@@ -34,8 +34,13 @@ def _exclusive(callback):
34
34
  return exclusive
35
35
 
36
36
 
37
- def build_submission_conversation() -> ConversationHandler:
38
- """构建投稿 ConversationHandler:唯一入口 /submit 与「开始投稿」按钮。"""
37
+ def build_submission_conversation(*, persistent: bool = False) -> ConversationHandler:
38
+ """构建投稿 ConversationHandler:唯一入口 /submit 与「开始投稿」按钮。
39
+
40
+ persistent=True 需要宿主 Application 已挂载 persistence(PTB 硬性要求);
41
+ 生产 main.py 总是先挂 PicklePersistence 再 setup_application,由后者按
42
+ ``application.persistence`` 决定传值。
43
+ """
39
44
  media_filters = (
40
45
  filters.PHOTO | filters.VIDEO | filters.ANIMATION | filters.AUDIO
41
46
  | filters.Document.ALL
@@ -71,4 +76,8 @@ def build_submission_conversation() -> ConversationHandler:
71
76
  },
72
77
  fallbacks=[CommandHandler("cancel", _exclusive(cancel))],
73
78
  name="submission_conversation",
79
+ # §durable-inbox:会话状态经已挂载的 PicklePersistence 落盘,重启后
80
+ # 投稿会话可继续。inbox worker 在标记 Update 完成前显式刷新持久化,
81
+ # 使「业务已写库、状态未落盘」的崩溃窗口由重放收敛而不是丢失。
82
+ persistent=persistent,
74
83
  )
@@ -200,14 +200,15 @@ def _stager(bot) -> TelegramReviewStager:
200
200
  # ---- back-compat UI names -------------------------------------------------
201
201
  def _review_keyboard(review_id, link="", *, spoiler=False, source="api",
202
202
  work_id="", failed=False, submitter_user_id=None,
203
- actor_kind="user", actor_subject=""):
203
+ actor_kind="user", actor_subject="",
204
+ media_cleared=False):
204
205
  # §submission-entrypoint: review/staging cards never expose the public
205
206
  # submission acquisition CTA (it belongs only to final Channel Publications).
206
207
  return review_keyboard.review_keyboard(
207
208
  review_id, link, spoiler=spoiler, source=source,
208
209
  work_id=work_id, failed=failed,
209
210
  submitter_user_id=submitter_user_id, actor_kind=actor_kind,
210
- actor_subject=actor_subject,
211
+ actor_subject=actor_subject, media_cleared=media_cleared,
211
212
  )
212
213
 
213
214
 
@@ -496,41 +497,30 @@ async def _find_published_work(target_id, work_type, work_id):
496
497
  )
497
498
 
498
499
 
499
- async def _notify_chat_submitter(bot, row, text: str):
500
- if row["source"] != "chat":
501
- return
502
- try:
503
- await bot.send_message(chat_id=row["user_id"], text=text)
504
- except Exception:
505
- logger.warning("通知聊天投稿人审核结果失败: review_id=%s", row["id"],
506
- exc_info=True)
500
+ _expiry_lock = asyncio.Lock()
507
501
 
508
502
 
509
503
  async def expire_stale_reviews(bot, *, now: Optional[float] = None) -> int:
510
- """Expire stale pending reviews and remove their Telegram preview messages."""
511
- if PENDING_REVIEW_RETENTION_DAYS <= 0:
512
- return 0
504
+ """Expire new rows and resume incomplete effects, including after disabling expiry."""
513
505
  from telepost.storage.sqlite.reviews import ReviewRepository
506
+ from telepost.telegram.review_expiry import finish_expiry
514
507
 
515
508
  current_time = time.time() if now is None else now
516
- cutoff = current_time - PENDING_REVIEW_RETENTION_DAYS * 86400
517
- error = (
518
- f"pending review expired after {PENDING_REVIEW_RETENTION_DAYS} days"
519
- )
520
- rows = await ReviewRepository().expire_pending(
521
- cutoff=cutoff, now=current_time,
522
- batch_size=PENDING_REVIEW_CLEANUP_BATCH_SIZE, error=error,
523
- )
524
- for row in rows:
525
- await _delete_messages(bot, _review_message_ids(row))
526
- await _notify_chat_submitter(
527
- bot, row,
528
- tr('⌛ 你的投稿超过 {p0} 天未审核,已自动过期。').format(p0=PENDING_REVIEW_RETENTION_DAYS),
529
- )
530
- if rows:
531
- logger.info("已过期并清理 %d 条待审核投稿(保留 %d 天)",
532
- len(rows), PENDING_REVIEW_RETENTION_DAYS)
533
- return len(rows)
509
+ async with _expiry_lock:
510
+ repo = ReviewRepository()
511
+ rows = []
512
+ if PENDING_REVIEW_RETENTION_DAYS > 0:
513
+ rows = await repo.expire_pending(
514
+ cutoff=current_time - PENDING_REVIEW_RETENTION_DAYS * 86400,
515
+ now=current_time, batch_size=PENDING_REVIEW_CLEANUP_BATCH_SIZE,
516
+ error=f"pending review expired after {PENDING_REVIEW_RETENTION_DAYS} days")
517
+ for row in await repo.expiry_due(now=current_time, limit=PENDING_REVIEW_CLEANUP_BATCH_SIZE):
518
+ try:
519
+ await finish_expiry(bot, row, now=current_time)
520
+ except Exception as exc:
521
+ logger.warning('Expiry checkpoint failed: review_id=%s error_class=%s',
522
+ row['id'], type(exc).__name__)
523
+ return len(rows)
534
524
 
535
525
 
536
526
  async def cleanup_superseded_reviews(bot, *, now: Optional[float] = None) -> int:
@@ -1706,7 +1696,8 @@ async def toggle_review_spoiler(update, context):
1706
1696
 
1707
1697
  try:
1708
1698
  await review_service.toggle_spoiler(
1709
- review_id, actor=update.effective_user.id
1699
+ review_id, actor=update.effective_user.id,
1700
+ callback_id=query.id if isinstance(query.id, str) else "",
1710
1701
  )
1711
1702
  except ReviewNotFoundError:
1712
1703
  await _answer(query, tr('审核记录不存在'), show_alert=True)
@@ -1734,6 +1725,7 @@ async def toggle_review_spoiler(update, context):
1734
1725
  submitter_user_id=_row_value(row, "submitter_user_id"),
1735
1726
  actor_kind=_row_value(row, "actor_kind") or "user",
1736
1727
  actor_subject=_row_value(row, "actor_subject") or "",
1728
+ media_cleared=_row_value(row, "preview_state") == "cleared",
1737
1729
  )
1738
1730
  )
1739
1731
  except Exception:
@@ -1756,6 +1748,75 @@ async def toggle_review_spoiler(update, context):
1756
1748
  )
1757
1749
 
1758
1750
 
1751
+ async def restore_review_preview(update, context):
1752
+ """§review-media-ttl: regenerate a cleaned preview from stored file_ids.
1753
+
1754
+ Only a still-pending review with cleared media can regenerate (the CAS in
1755
+ begin_preview_regen); expired/rejected/published/superseded rows keep
1756
+ their history untouched. A failure keeps whatever was actually sent as
1757
+ the current preview, so repeated clicks never duplicate media."""
1758
+ query = update.callback_query
1759
+ if update.effective_user.id not in ADMIN_IDS:
1760
+ await _answer(query, tr('你没有审核权限'), show_alert=True)
1761
+ return
1762
+ try:
1763
+ review_id = int(query.data.split(":", 1)[1])
1764
+ except (ValueError, IndexError):
1765
+ await _answer(query, tr('无效的审核记录'), show_alert=True)
1766
+ return
1767
+
1768
+ from telepost.storage.sqlite.reviews import ReviewRepository
1769
+ row = await review_service.get_row(review_id)
1770
+ if row is None:
1771
+ await _answer(query, tr('审核记录不存在'), show_alert=True)
1772
+ return
1773
+ if _row_value(row, "status") != "pending":
1774
+ await _answer(query, tr('该投稿已处理,无法恢复预览'), show_alert=True)
1775
+ return
1776
+ state = _row_value(row, "preview_state")
1777
+ if state != "cleared":
1778
+ # Each non-cleared state has its own honest answer; never resend.
1779
+ if state == "regenerating":
1780
+ await _answer(query, tr('预览正在恢复中,请稍候。'))
1781
+ elif state == "restricted":
1782
+ await _answer(query, tr('预览媒体已超出 Telegram 删除时限,仍在审核群中,'
1783
+ '需人工清理。'), show_alert=True)
1784
+ elif state == "uncertain":
1785
+ await _answer(query, tr('上次恢复结果不确定,请在审核群核对媒体是否已重发。'),
1786
+ show_alert=True)
1787
+ elif state == "partial":
1788
+ await _answer(query, tr('预览媒体正在清理,暂不可恢复。'))
1789
+ else: # 'present' / NULL: media is still on screen
1790
+ await _answer(query, tr('预览媒体当前可用,无需恢复。'))
1791
+ return
1792
+ if not await ReviewRepository().begin_preview_regen(review_id):
1793
+ await _answer(query, tr('预览正在恢复中,请稍候。'))
1794
+ return
1795
+
1796
+ from telepost.telegram.review_media import regenerate_review_preview
1797
+ result = await regenerate_review_preview(context.bot, row)
1798
+ if not result["ok"]:
1799
+ await _answer(query, result["message"], show_alert=True)
1800
+ return
1801
+ await _answer(query, tr('预览已恢复({p0} 条消息),将按同样的时限自动清理。').format(
1802
+ p0=len(result["message_ids"])))
1803
+ try:
1804
+ await query.edit_message_reply_markup(
1805
+ reply_markup=_review_keyboard(
1806
+ review_id, row["link"], spoiler=bool(row["spoiler"]),
1807
+ source=row["source"],
1808
+ work_id=_work_id_from_link(row["link"] or ""),
1809
+ submitter_user_id=_row_value(row, "submitter_user_id"),
1810
+ actor_kind=_row_value(row, "actor_kind") or "user",
1811
+ actor_subject=_row_value(row, "actor_subject") or "",
1812
+ media_cleared=False,
1813
+ )
1814
+ )
1815
+ except Exception:
1816
+ logger.debug("恢复预览后刷新审核卡键盘失败: review_id=%s", review_id,
1817
+ exc_info=True)
1818
+
1819
+
1759
1820
  def _submit_pixivflow_refetch(target_id: str, request_id: str,
1760
1821
  correlation_id: str = "") -> dict:
1761
1822
  """Submit through the PixivFlow JOB PORT (no URL building here).
@@ -91,7 +91,8 @@ async def handle_upload(update: Update, context: CallbackContext) -> int:
91
91
  )
92
92
  return STATE["UPLOAD"]
93
93
 
94
- count = await append_entry(user_id, entry)
94
+ count = await append_entry(user_id, entry,
95
+ update_id=getattr(update, "update_id", None))
95
96
  if count == 0:
96
97
  await message.reply_text(MessageFormatter.session_expired(), parse_mode="HTML")
97
98
  return ConversationHandler.END
@@ -20,7 +20,8 @@ from telegram.ext import (
20
20
  CallbackContext,
21
21
  ApplicationHandlerStop,
22
22
  CallbackQueryHandler,
23
- PicklePersistence
23
+ PicklePersistence,
24
+ PersistenceInput,
24
25
  )
25
26
  from dotenv import load_dotenv
26
27
 
@@ -447,7 +448,7 @@ async def main():
447
448
  persistence_dir = os.path.dirname(DB_PATH) or "data"
448
449
  persistence_path = os.path.join(persistence_dir, "persistence.pickle")
449
450
  os.makedirs(persistence_dir, exist_ok=True)
450
- persistence = PicklePersistence(filepath=persistence_path)
451
+ persistence = build_persistence(persistence_path)
451
452
  application = Application.builder().token(token).persistence(persistence).build()
452
453
 
453
454
  # 设置应用程序
@@ -461,6 +462,14 @@ async def main():
461
462
 
462
463
  # Review DB is the source of truth. Repair any row left between preview
463
464
  # staging and control-message creation before exposing /ready to producers.
465
+ # §durable-inbox 先于一切恢复:重放的 Update 会重建其业务写入,之后
466
+ # reconcile 才能扫到完整的审核/发布状态。Webhook 模式下这是 200 确认后
467
+ # 崩溃不丢稿的恢复通道;Polling 模式下表为空、worker 空转(无副作用)。
468
+ from telepost.telegram.inbox_worker import UpdateInboxWorker
469
+ update_inbox = UpdateInboxWorker(application)
470
+ await update_inbox.recover()
471
+ update_inbox.start()
472
+ application.bot_data["update_inbox_worker"] = update_inbox
464
473
  await reconcile_incomplete_reviews(application.bot, stale_seconds=0)
465
474
  # §refetch-lifecycle 启动即扫:重抓是一等持久 Job,重启后台账是唯一真相。
466
475
  # 在 /ready 之前一次性接管所有非终态 attempt(远端已终态的经状态机收敛),
@@ -652,6 +661,15 @@ async def shutdown(
652
661
  await application.updater.stop()
653
662
  except Exception as e:
654
663
  logger.warning(f"停止 Updater 失败(可忽略): {e}")
664
+ # §durable-inbox 停机:先停 Update worker,再走 Application 的收尾
665
+ # (stop 会等待队列与 create_task 任务收敛,持久化随后落盘)。
666
+ try:
667
+ inbox_worker = application.bot_data.get("update_inbox_worker")
668
+ if inbox_worker is not None:
669
+ await inbox_worker.stop()
670
+ logger.info("Update inbox worker 已停止")
671
+ except Exception as e:
672
+ logger.warning(f"停止 Update inbox worker 失败(可忽略): {e}")
655
673
  # §refetch-lifecycle 停机:显式停掉 30 秒心跳作业并留下「心跳已停止」的持久
656
674
  # 事实,在途 attempt 保持非终态(重启后由启动即扫接管),绝不静默消失。
657
675
  try:
@@ -678,6 +696,17 @@ async def shutdown(
678
696
  await application.stop()
679
697
  await application.shutdown()
680
698
 
699
+ def build_persistence(filepath):
700
+ """Persist conversations and user/chat state, excluding process-local handles."""
701
+ # bot_data contains live Inbox workers and scheduler jobs, which PTB cannot
702
+ # deepcopy/pickle. It also holds the intentionally process-local rate window.
703
+ # Callback buttons use strings, not PTB's arbitrary callback-data cache.
704
+ return PicklePersistence(
705
+ filepath=filepath,
706
+ store_data=PersistenceInput(bot_data=False, callback_data=False),
707
+ )
708
+
709
+
681
710
  def setup_application(application):
682
711
  """
683
712
  初始化和配置应用程序
@@ -791,8 +820,14 @@ def setup_application(application):
791
820
  # 添加会话处理器
792
821
  logger.info("注册会话处理器...")
793
822
  from handlers.conversation import build_submission_conversation
794
- conv_handler = build_submission_conversation()
823
+ # §durable-inbox:挂了 persistence 就让投稿会话持久化(重启可恢复)。
824
+ # PTB 要求宿主 Application 必须已有 persistence,故按实际情况传值;
825
+ # 无 persistence 的裸 Application(测试/嵌入)保持内存态旧行为。
826
+ conv_handler = build_submission_conversation(
827
+ persistent=bool(application.persistence))
795
828
  application.add_handler(conv_handler, group=2)
829
+ if application.persistence:
830
+ logger.info("投稿会话已启用持久化(PicklePersistence)")
796
831
  logger.info("会话处理器注册完成")
797
832
  except Exception as e:
798
833
  logger.error(f"注册会话处理器失败: {e}", exc_info=True)
@@ -833,6 +868,14 @@ def setup_application(application):
833
868
  await cleanup_old_data()
834
869
  await expire_stale_reviews(context.bot)
835
870
  await cleanup_superseded_reviews(context.bot)
871
+ # §review-media-ttl:先把崩溃遗留的「恢复中」claim 收敛成可见的
872
+ # uncertain,再删到期(默认 36h)的审核预览媒体;控制卡保留。
873
+ from telepost.telegram.review_media import (
874
+ clean_review_preview_media,
875
+ resolve_stale_preview_regens,
876
+ )
877
+ await resolve_stale_preview_regens()
878
+ await clean_review_preview_media(context.bot)
836
879
  await poll_refetch_jobs(context.bot, force=True)
837
880
  await reconcile_incomplete_reviews(context.bot)
838
881
  from telepost.application.submitter_notify import (
@@ -791,8 +791,9 @@ class ReviewService:
791
791
  source=source, spoiler=bool(spoiler))
792
792
  return ActionResult(int(review_id), row["status"], False)
793
793
 
794
- async def toggle_spoiler(self, review_id: int, *, actor: Any = None) -> ActionResult:
795
- changed, row = await self._repo.toggle_spoiler(int(review_id))
794
+ async def toggle_spoiler(self, review_id: int, *, actor: Any = None,
795
+ callback_id: str = "") -> ActionResult:
796
+ changed, row = await self._repo.toggle_spoiler(int(review_id), callback_id=callback_id)
796
797
  if row is None:
797
798
  raise ReviewNotFoundError("审核记录不存在")
798
799
  if not changed:
@@ -277,7 +277,12 @@ async def flush_submitter_notifications(bot, *, limit: int = 20) -> int:
277
277
  for row in rows:
278
278
  try:
279
279
  payload = json.loads(row["payload"] or "{}")
280
- text = format_publication_message(payload, include_changes)
280
+ if row['kind'] == 'review_expired':
281
+ days = str(payload['reason']).split(' after ')[-1].split(' ')[0]
282
+ text = tr('你的投稿 #{review_id} 因超过 {days} 天未审核,已自动过期。\n\n这不代表稿件被管理员拒绝。\n如仍需要投稿,可以重新提交。').format(
283
+ review_id=payload['review_id'], days=days)
284
+ else:
285
+ text = format_publication_message(payload, include_changes)
281
286
  result = await bot.send_message(
282
287
  chat_id=int(row["telegram_user_id"]), text=text
283
288
  )
@@ -8,7 +8,7 @@ to the entry points; when present it overrides these committed defaults.
8
8
  # Bumped automatically by release-please (extra-files in
9
9
  # release-please-config.json). Release bundles and images override it at build
10
10
  # time with the exact version/commit; this constant is the local-dev fallback.
11
- RELEASE_VERSION = "2.83.2" # x-release-please-version
11
+ RELEASE_VERSION = "2.84.1" # x-release-please-version
12
12
  RELEASE_COMMIT = "dev"
13
13
  BUILD_DATE = "dev"
14
14
  SERVICE = "telepost"