telepost-bot 2.83.2__tar.gz → 2.84.0__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.
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/CHANGELOG.md +18 -0
- {telepost_bot-2.83.2/telepost_bot.egg-info → telepost_bot-2.84.0}/PKG-INFO +1 -1
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/database/db_manager.py +98 -1
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/callback_handlers.py +4 -1
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/conversation.py +11 -2
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/review.py +91 -31
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/upload.py +2 -1
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/main.py +32 -1
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/submitter_notify.py +6 -1
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/build_info.py +1 -1
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/observability/doctor.py +114 -0
- telepost_bot-2.84.0/telepost/storage/sqlite/inbox.py +155 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/reviews.py +125 -5
- telepost_bot-2.84.0/telepost/telegram/inbox_worker.py +144 -0
- telepost_bot-2.84.0/telepost/telegram/review_expiry.py +114 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/review_keyboard.py +11 -1
- telepost_bot-2.84.0/telepost/telegram/review_media.py +264 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0/telepost_bot.egg-info}/PKG-INFO +1 -1
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost_bot.egg-info/SOURCES.txt +8 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_doctor.py +57 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_review.py +2 -1
- telepost_bot-2.84.0/tests/test_review_expiry_recovery.py +163 -0
- telepost_bot-2.84.0/tests/test_review_media_lifecycle.py +445 -0
- telepost_bot-2.84.0/tests/test_update_inbox.py +493 -0
- telepost_bot-2.84.0/tests/test_webhook_payload_admission.py +119 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_webhook_static.py +37 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/ui/locales/en/LC_MESSAGES/telepost.mo +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/ui/locales/en/LC_MESSAGES/telepost.po +66 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/submission.py +41 -6
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/webhook_server.py +60 -21
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/LICENSE +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/MANIFEST.in +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/README.en.md +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/README.md +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/config/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/config/fly-telepost-policy.example.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/config/review_policy.md +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/config/settings.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/config.ini.example +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/database/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/api_commands.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/automation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/botconfig.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/channel_listener.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/command_handlers.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/error_handler.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/index_handlers.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/mode_selection.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/moderation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/preview_handlers.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/publish.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/reaction_stats.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/recovery.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/schedule_status.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/search_handlers.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/handlers/stats_handlers.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/mcp_server/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/mcp_server/review_client.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/mcp_server/server.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/models/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/models/state.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/SOURCES.sha256 +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/error-mapping.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/ackresult.candidate_search.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/capabilities.pixivflow.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/event.job.expired.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/event.job.succeeded.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/eventpage.candidate_search.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/job.cancelled.candidate_search.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/job.failed.no_candidate.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/job.queued.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/job.running.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/job.succeeded.candidate_search.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/jobpage.candidate_search.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/task.candidate_search.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/fixtures/task.candidate_search.target_only.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/protocol/v1/protocol.schema.json +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/pyproject.toml +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/run.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/services/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/services/review_service.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/setup.cfg +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/api/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/admin_ops.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/automation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/delivery_planner.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/editorial.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/hot.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/novel_fallback_card.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/novel_preview.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/pixivflow_jobs.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/post_media.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/posts.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/publication.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/reaction_backfill.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/recovery.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/refetch.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/rerender.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/resubmit.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/review_queue.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/application/telepress_provider.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/automation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/delivery.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/editorial.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/errors.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/hot.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/media.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/navigation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/novel_preview.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/packing.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/presentation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/presentation_policy.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/refetch_state.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/review.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/review_chat.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/domain/submission.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/miniapp/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/miniapp/auth.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/miniapp/rbac.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/miniapp/role_bindings.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/miniapp/session.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/observability/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/observability/audit.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/observability/cli.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/observability/errors.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/observability/redact.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/automation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/columns.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/editorial.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/ledger.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/media_assets.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/moderation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/novel_preview.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/posts.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/protocol_events.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/recovery.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/refetch.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/storage/sqlite/submitter_notifications.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/capture.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/delivery/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/delivery/discussion.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/delivery/executor.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/delivery/gateway.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/delivery/legacy_runner.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/delivery/planner.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/delivery/preparation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/delivery/registry.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/delivery/sender.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost/telegram/review_stager.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost_bot.egg-info/dependency_links.txt +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost_bot.egg-info/entry_points.txt +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost_bot.egg-info/requires.txt +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/telepost_bot.egg-info/top_level.txt +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_admin_api.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_anonymous.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_api_commands.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_api_server.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_api_version.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_architecture_application.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_architecture_core.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_automation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_backfill_attribution.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_blacklist.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_bot_language.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_bot_token_logging.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_botconfig.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_boundary.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_callback_contract.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_caption_mentions.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_compatibility.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_config_encoding.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_console_encoding.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_conversation_flow.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_data_class_cleanup.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_database.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_database_first_start.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_database_operations.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_delivery_planner.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_disposition.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_editorial.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_file_validator.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_fly_deploy_contract.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_frozen_support.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_handlers.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_heat_calculator.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_helper_functions.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_hot_domain.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_identity_provenance.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_install_scripts.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_integration.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_legacy_delivery_port_navigation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_logic_flow.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_logical_submissions.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_maintenance_jobs.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_manager_notifications.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_media_decision_audit.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_media_delivery_pipeline.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_media_domain.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_media_preparation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_media_proxy_delivery.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_menu_shortcut_single_reply.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_messages.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_miniapp_api.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_miniapp_auth.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_miniapp_content.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_miniapp_entries.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_miniapp_history.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_miniapp_rbac.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_mixed_media_integrity.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_moderation_and_admin_ux.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_navigation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_no_owner_probe.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_novel_cover_preview.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_novel_delivery_semantics.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_novel_preview.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_observability.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_observability_lifecycle.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_online_reading_root_button.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_packing_ssot.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_polling_server.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_preview_pagination.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_production_handler_graph.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_protocol_contract.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_publication_presentation.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_publish_idempotency.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_publish_regressions.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_publish_reply.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_reaction_backfill.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_reaction_stats.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_recovery.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_refetch.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_refetch_card_state.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_refetch_events.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_refetch_job_lifecycle.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_refetch_reason_normalization.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_refetch_replacement.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_remote_fetch_fallback.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_rerender.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_resubmit.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_review_album_payload.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_review_chat_contract.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_review_consistency.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_review_http_mcp.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_review_service.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_robustness.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_run_dispatcher.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_run_mode.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_run_supervisor.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_schedule_outcomes.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_search_disabled_hint.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_search_recovery.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_security.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_security_headers.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_session_database_handles.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_shutdown.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_streaming_uploads.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_submission_timeout_idempotency.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_telegram_golden_messages.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_telegram_media_cache.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_upload.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_user_friendly_messages.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_webhook_ready.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_webhook_router.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_webhook_secret.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_webhook_secret_logging.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/tests/test_work_id_wire.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/ui/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/ui/i18n.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/ui/keyboards.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/ui/messages.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/api_server.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/api_tokens.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/blacklist.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/cache.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/config_wizard.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/console.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/database.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/file_validator.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/heat_calculator.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/helper_functions.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/index_manager.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/logging_config.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/maintenance_jobs.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/polling_server.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/run_mode.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/runtime_policy.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/schedule_watchdog.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/search_engine.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/utils/security_headers.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/webapp/__init__.py +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/webapp/dist/assets/2d82b92e720462f8dd3b-Bh4Fahcw.svg +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/webapp/dist/assets/index-CLfkW8eZ.css +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/webapp/dist/assets/index-CfjBQN_e.js +0 -0
- {telepost_bot-2.83.2 → telepost_bot-2.84.0}/webapp/dist/index.html +0 -0
|
@@ -8,6 +8,24 @@
|
|
|
8
8
|
> 发布说明面向普通用户:写「对用户有什么改变」,不写内部实现细节(模块名、函数名、配置键);
|
|
9
9
|
> 技术细节放进代码注释或 `docs/`。每个 Release 的正文由本文件对应版本段自动生成。
|
|
10
10
|
|
|
11
|
+
## [2.84.0](https://github.com/redtidev1918/TelePost/compare/v2.83.2...v2.84.0) (2026-10-21)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **review:** bound review preview media by Telegram's 48h deletion window ([b480e4a](https://github.com/redtidev1918/TelePost/commit/b480e4a7e0b1675ba22dc802b94a67fc67e67b9c))
|
|
17
|
+
* **review:** bound review preview media by Telegram's deletion window ([65da48b](https://github.com/redtidev1918/TelePost/commit/65da48bd59e92d25c94d762752a4f7f39795b5c7))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* durable Telegram update inbox with recoverable submission conversations ([1b7f66d](https://github.com/redtidev1918/TelePost/commit/1b7f66d72fa5797a1ffb99fd45a4353a41eb201c))
|
|
23
|
+
* durable webhook inbox with recoverable submission conversations ([6d6b5ec](https://github.com/redtidev1918/TelePost/commit/6d6b5ec5988d42d458136f18a548e6fdb311f509))
|
|
24
|
+
* harden webhook payload admission and miniapp static guard ([a0cf21d](https://github.com/redtidev1918/TelePost/commit/a0cf21d697f21e2535b5ece6d5cb47fe2955857c))
|
|
25
|
+
* harden webhook payload admission and miniapp static guard ([a621807](https://github.com/redtidev1918/TelePost/commit/a621807264311ef58908741bad4a0d5c5fc77552))
|
|
26
|
+
* make review expiry recoverable with durable submitter notifications ([f6189d2](https://github.com/redtidev1918/TelePost/commit/f6189d2f5f1f86e3c44bc549f24941b6e8be1bdf))
|
|
27
|
+
* make review expiry recoverable with durable submitter notifications ([c322d40](https://github.com/redtidev1918/TelePost/commit/c322d40c6402ea3fd809cbc87fc3459c34631dd6))
|
|
28
|
+
|
|
11
29
|
## [2.83.2](https://github.com/redtidev1918/TelePost/compare/v2.83.1...v2.83.2) (2026-10-15)
|
|
12
30
|
|
|
13
31
|
|
|
@@ -76,6 +76,52 @@ 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
|
+
# 媒体追加去重日志:同一 (user, update) 跨投稿代只应用一次
|
|
111
|
+
# (epoch 仅作审计),与 submissions 行的更新同事务提交,崩溃后
|
|
112
|
+
# 重放收敛。worker 严格按 update_id 头部顺序处理,正常路径不会
|
|
113
|
+
# 出现「旧代 update 在新会话之后执行」;跨代匹配是乱序的硬防线。
|
|
114
|
+
await conn.execute('''
|
|
115
|
+
CREATE TABLE IF NOT EXISTS submission_media_log (
|
|
116
|
+
user_id INTEGER NOT NULL,
|
|
117
|
+
update_id INTEGER NOT NULL,
|
|
118
|
+
epoch INTEGER NOT NULL DEFAULT 0,
|
|
119
|
+
entry TEXT NOT NULL,
|
|
120
|
+
applied_at REAL NOT NULL,
|
|
121
|
+
PRIMARY KEY (user_id, update_id)
|
|
122
|
+
)
|
|
123
|
+
''')
|
|
124
|
+
|
|
79
125
|
# API 访问令牌(用于 /api/v1 自动化投稿,仅存哈希不存明文)
|
|
80
126
|
await conn.execute('''
|
|
81
127
|
CREATE TABLE IF NOT EXISTS api_tokens (
|
|
@@ -138,6 +184,41 @@ async def init_db():
|
|
|
138
184
|
logger.info("已添加 target_id 字段到 pending_reviews 表")
|
|
139
185
|
except Exception:
|
|
140
186
|
pass # 字段已存在
|
|
187
|
+
# NULL leaves historical expiry untouched; new expirations checkpoint
|
|
188
|
+
# their Telegram effects independently of the terminal review state.
|
|
189
|
+
columns = {r[1] for r in await (await conn.execute(
|
|
190
|
+
'PRAGMA table_info(pending_reviews)')).fetchall()}
|
|
191
|
+
for column, ddl in (
|
|
192
|
+
('expiry_progress', 'TEXT'),
|
|
193
|
+
('expiry_next_attempt_at', 'REAL'),
|
|
194
|
+
):
|
|
195
|
+
if column not in columns:
|
|
196
|
+
await conn.execute(f'ALTER TABLE pending_reviews ADD COLUMN {column} {ddl}')
|
|
197
|
+
await conn.execute(
|
|
198
|
+
'CREATE INDEX IF NOT EXISTS idx_review_expiry_due '
|
|
199
|
+
'ON pending_reviews(expiry_next_attempt_at) '
|
|
200
|
+
'WHERE expiry_next_attempt_at IS NOT NULL')
|
|
201
|
+
|
|
202
|
+
# Time-bounded review-media lifecycle (§review-media-ttl):
|
|
203
|
+
# preview media is deleted ~36h after sending (Telegram only allows
|
|
204
|
+
# deletion within 48h) while the control card stays actionable;
|
|
205
|
+
# admins regenerate previews on demand from the stored file_ids.
|
|
206
|
+
# NULL preview_state on legacy rows means 'present' with the
|
|
207
|
+
# review's created_at as the TTL anchor.
|
|
208
|
+
columns = {r[1] for r in await (await conn.execute(
|
|
209
|
+
'PRAGMA table_info(pending_reviews)')).fetchall()}
|
|
210
|
+
for column, ddl in (
|
|
211
|
+
('preview_state', 'TEXT'),
|
|
212
|
+
('preview_sent_at', 'REAL'),
|
|
213
|
+
('preview_attempts', 'INTEGER NOT NULL DEFAULT 0'),
|
|
214
|
+
):
|
|
215
|
+
if column not in columns:
|
|
216
|
+
await conn.execute(f'ALTER TABLE pending_reviews ADD COLUMN {column} {ddl}')
|
|
217
|
+
await conn.execute(
|
|
218
|
+
'CREATE INDEX IF NOT EXISTS idx_review_preview_due '
|
|
219
|
+
"ON pending_reviews(COALESCE(preview_sent_at, created_at)) "
|
|
220
|
+
"WHERE status='pending'")
|
|
221
|
+
|
|
141
222
|
# Generic, bounded provenance for API submissions (any client).
|
|
142
223
|
# TelePost only stores/displays these; it never interprets them.
|
|
143
224
|
for column, ddl in (
|
|
@@ -961,7 +1042,8 @@ async def cleanup_old_data():
|
|
|
961
1042
|
c = await conn.cursor()
|
|
962
1043
|
await c.execute(
|
|
963
1044
|
"DELETE FROM pending_reviews "
|
|
964
|
-
"WHERE status IN ('rejected','published','deleted','failed','expired') AND updated_at < ?"
|
|
1045
|
+
"WHERE status IN ('rejected','published','deleted','failed','expired') AND updated_at < ? "
|
|
1046
|
+
"AND expiry_next_attempt_at IS NULL",
|
|
965
1047
|
(review_cutoff,),
|
|
966
1048
|
)
|
|
967
1049
|
logger.info("已清理过期审核记录(保留 %d 天)", review_retention_days)
|
|
@@ -973,6 +1055,21 @@ async def cleanup_old_data():
|
|
|
973
1055
|
)
|
|
974
1056
|
logger.info("已清理过期 API 通知幂等记录(保留 %d 天)", review_retention_days)
|
|
975
1057
|
|
|
1058
|
+
# Durable inbox 保留策略:已完成记录 7 天后清理;dead(永久失败、需
|
|
1059
|
+
# 人工核对)保留 30 天。pending/processing 与未过期重试行绝不由这里的
|
|
1060
|
+
# 普通过期清理删除——它们只属于启动恢复与 worker。
|
|
1061
|
+
now_ts = datetime.now().timestamp()
|
|
1062
|
+
async with get_db() as conn:
|
|
1063
|
+
await conn.execute(
|
|
1064
|
+
"DELETE FROM webhook_inbox WHERE state='done' AND done_at < ?",
|
|
1065
|
+
(now_ts - 7 * 86400,))
|
|
1066
|
+
await conn.execute(
|
|
1067
|
+
"DELETE FROM webhook_inbox WHERE state='dead' AND updated_at < ?",
|
|
1068
|
+
(now_ts - 30 * 86400,))
|
|
1069
|
+
await conn.execute(
|
|
1070
|
+
"DELETE FROM submission_media_log WHERE applied_at < ?",
|
|
1071
|
+
(now_ts - 7 * 86400,))
|
|
1072
|
+
|
|
976
1073
|
# Append-only audit: prune old rows, but keep every event attached to a
|
|
977
1074
|
# still-open review (preparing/pending/publishing/failed) regardless of
|
|
978
1075
|
# 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
|
-
|
|
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
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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:
|
|
@@ -1734,6 +1724,7 @@ async def toggle_review_spoiler(update, context):
|
|
|
1734
1724
|
submitter_user_id=_row_value(row, "submitter_user_id"),
|
|
1735
1725
|
actor_kind=_row_value(row, "actor_kind") or "user",
|
|
1736
1726
|
actor_subject=_row_value(row, "actor_subject") or "",
|
|
1727
|
+
media_cleared=_row_value(row, "preview_state") == "cleared",
|
|
1737
1728
|
)
|
|
1738
1729
|
)
|
|
1739
1730
|
except Exception:
|
|
@@ -1756,6 +1747,75 @@ async def toggle_review_spoiler(update, context):
|
|
|
1756
1747
|
)
|
|
1757
1748
|
|
|
1758
1749
|
|
|
1750
|
+
async def restore_review_preview(update, context):
|
|
1751
|
+
"""§review-media-ttl: regenerate a cleaned preview from stored file_ids.
|
|
1752
|
+
|
|
1753
|
+
Only a still-pending review with cleared media can regenerate (the CAS in
|
|
1754
|
+
begin_preview_regen); expired/rejected/published/superseded rows keep
|
|
1755
|
+
their history untouched. A failure keeps whatever was actually sent as
|
|
1756
|
+
the current preview, so repeated clicks never duplicate media."""
|
|
1757
|
+
query = update.callback_query
|
|
1758
|
+
if update.effective_user.id not in ADMIN_IDS:
|
|
1759
|
+
await _answer(query, tr('你没有审核权限'), show_alert=True)
|
|
1760
|
+
return
|
|
1761
|
+
try:
|
|
1762
|
+
review_id = int(query.data.split(":", 1)[1])
|
|
1763
|
+
except (ValueError, IndexError):
|
|
1764
|
+
await _answer(query, tr('无效的审核记录'), show_alert=True)
|
|
1765
|
+
return
|
|
1766
|
+
|
|
1767
|
+
from telepost.storage.sqlite.reviews import ReviewRepository
|
|
1768
|
+
row = await review_service.get_row(review_id)
|
|
1769
|
+
if row is None:
|
|
1770
|
+
await _answer(query, tr('审核记录不存在'), show_alert=True)
|
|
1771
|
+
return
|
|
1772
|
+
if _row_value(row, "status") != "pending":
|
|
1773
|
+
await _answer(query, tr('该投稿已处理,无法恢复预览'), show_alert=True)
|
|
1774
|
+
return
|
|
1775
|
+
state = _row_value(row, "preview_state")
|
|
1776
|
+
if state != "cleared":
|
|
1777
|
+
# Each non-cleared state has its own honest answer; never resend.
|
|
1778
|
+
if state == "regenerating":
|
|
1779
|
+
await _answer(query, tr('预览正在恢复中,请稍候。'))
|
|
1780
|
+
elif state == "restricted":
|
|
1781
|
+
await _answer(query, tr('预览媒体已超出 Telegram 删除时限,仍在审核群中,'
|
|
1782
|
+
'需人工清理。'), show_alert=True)
|
|
1783
|
+
elif state == "uncertain":
|
|
1784
|
+
await _answer(query, tr('上次恢复结果不确定,请在审核群核对媒体是否已重发。'),
|
|
1785
|
+
show_alert=True)
|
|
1786
|
+
elif state == "partial":
|
|
1787
|
+
await _answer(query, tr('预览媒体正在清理,暂不可恢复。'))
|
|
1788
|
+
else: # 'present' / NULL: media is still on screen
|
|
1789
|
+
await _answer(query, tr('预览媒体当前可用,无需恢复。'))
|
|
1790
|
+
return
|
|
1791
|
+
if not await ReviewRepository().begin_preview_regen(review_id):
|
|
1792
|
+
await _answer(query, tr('预览正在恢复中,请稍候。'))
|
|
1793
|
+
return
|
|
1794
|
+
|
|
1795
|
+
from telepost.telegram.review_media import regenerate_review_preview
|
|
1796
|
+
result = await regenerate_review_preview(context.bot, row)
|
|
1797
|
+
if not result["ok"]:
|
|
1798
|
+
await _answer(query, result["message"], show_alert=True)
|
|
1799
|
+
return
|
|
1800
|
+
await _answer(query, tr('预览已恢复({p0} 条消息),将按同样的时限自动清理。').format(
|
|
1801
|
+
p0=len(result["message_ids"])))
|
|
1802
|
+
try:
|
|
1803
|
+
await query.edit_message_reply_markup(
|
|
1804
|
+
reply_markup=_review_keyboard(
|
|
1805
|
+
review_id, row["link"], spoiler=bool(row["spoiler"]),
|
|
1806
|
+
source=row["source"],
|
|
1807
|
+
work_id=_work_id_from_link(row["link"] or ""),
|
|
1808
|
+
submitter_user_id=_row_value(row, "submitter_user_id"),
|
|
1809
|
+
actor_kind=_row_value(row, "actor_kind") or "user",
|
|
1810
|
+
actor_subject=_row_value(row, "actor_subject") or "",
|
|
1811
|
+
media_cleared=False,
|
|
1812
|
+
)
|
|
1813
|
+
)
|
|
1814
|
+
except Exception:
|
|
1815
|
+
logger.debug("恢复预览后刷新审核卡键盘失败: review_id=%s", review_id,
|
|
1816
|
+
exc_info=True)
|
|
1817
|
+
|
|
1818
|
+
|
|
1759
1819
|
def _submit_pixivflow_refetch(target_id: str, request_id: str,
|
|
1760
1820
|
correlation_id: str = "") -> dict:
|
|
1761
1821
|
"""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
|
|
@@ -461,6 +461,14 @@ async def main():
|
|
|
461
461
|
|
|
462
462
|
# Review DB is the source of truth. Repair any row left between preview
|
|
463
463
|
# staging and control-message creation before exposing /ready to producers.
|
|
464
|
+
# §durable-inbox 先于一切恢复:重放的 Update 会重建其业务写入,之后
|
|
465
|
+
# reconcile 才能扫到完整的审核/发布状态。Webhook 模式下这是 200 确认后
|
|
466
|
+
# 崩溃不丢稿的恢复通道;Polling 模式下表为空、worker 空转(无副作用)。
|
|
467
|
+
from telepost.telegram.inbox_worker import UpdateInboxWorker
|
|
468
|
+
update_inbox = UpdateInboxWorker(application)
|
|
469
|
+
await update_inbox.recover()
|
|
470
|
+
update_inbox.start()
|
|
471
|
+
application.bot_data["update_inbox_worker"] = update_inbox
|
|
464
472
|
await reconcile_incomplete_reviews(application.bot, stale_seconds=0)
|
|
465
473
|
# §refetch-lifecycle 启动即扫:重抓是一等持久 Job,重启后台账是唯一真相。
|
|
466
474
|
# 在 /ready 之前一次性接管所有非终态 attempt(远端已终态的经状态机收敛),
|
|
@@ -652,6 +660,15 @@ async def shutdown(
|
|
|
652
660
|
await application.updater.stop()
|
|
653
661
|
except Exception as e:
|
|
654
662
|
logger.warning(f"停止 Updater 失败(可忽略): {e}")
|
|
663
|
+
# §durable-inbox 停机:先停 Update worker,再走 Application 的收尾
|
|
664
|
+
# (stop 会等待队列与 create_task 任务收敛,持久化随后落盘)。
|
|
665
|
+
try:
|
|
666
|
+
inbox_worker = application.bot_data.get("update_inbox_worker")
|
|
667
|
+
if inbox_worker is not None:
|
|
668
|
+
await inbox_worker.stop()
|
|
669
|
+
logger.info("Update inbox worker 已停止")
|
|
670
|
+
except Exception as e:
|
|
671
|
+
logger.warning(f"停止 Update inbox worker 失败(可忽略): {e}")
|
|
655
672
|
# §refetch-lifecycle 停机:显式停掉 30 秒心跳作业并留下「心跳已停止」的持久
|
|
656
673
|
# 事实,在途 attempt 保持非终态(重启后由启动即扫接管),绝不静默消失。
|
|
657
674
|
try:
|
|
@@ -791,8 +808,14 @@ def setup_application(application):
|
|
|
791
808
|
# 添加会话处理器
|
|
792
809
|
logger.info("注册会话处理器...")
|
|
793
810
|
from handlers.conversation import build_submission_conversation
|
|
794
|
-
|
|
811
|
+
# §durable-inbox:挂了 persistence 就让投稿会话持久化(重启可恢复)。
|
|
812
|
+
# PTB 要求宿主 Application 必须已有 persistence,故按实际情况传值;
|
|
813
|
+
# 无 persistence 的裸 Application(测试/嵌入)保持内存态旧行为。
|
|
814
|
+
conv_handler = build_submission_conversation(
|
|
815
|
+
persistent=bool(application.persistence))
|
|
795
816
|
application.add_handler(conv_handler, group=2)
|
|
817
|
+
if application.persistence:
|
|
818
|
+
logger.info("投稿会话已启用持久化(PicklePersistence)")
|
|
796
819
|
logger.info("会话处理器注册完成")
|
|
797
820
|
except Exception as e:
|
|
798
821
|
logger.error(f"注册会话处理器失败: {e}", exc_info=True)
|
|
@@ -833,6 +856,14 @@ def setup_application(application):
|
|
|
833
856
|
await cleanup_old_data()
|
|
834
857
|
await expire_stale_reviews(context.bot)
|
|
835
858
|
await cleanup_superseded_reviews(context.bot)
|
|
859
|
+
# §review-media-ttl:先把崩溃遗留的「恢复中」claim 收敛成可见的
|
|
860
|
+
# uncertain,再删到期(默认 36h)的审核预览媒体;控制卡保留。
|
|
861
|
+
from telepost.telegram.review_media import (
|
|
862
|
+
clean_review_preview_media,
|
|
863
|
+
resolve_stale_preview_regens,
|
|
864
|
+
)
|
|
865
|
+
await resolve_stale_preview_regens()
|
|
866
|
+
await clean_review_preview_media(context.bot)
|
|
836
867
|
await poll_refetch_jobs(context.bot, force=True)
|
|
837
868
|
await reconcile_incomplete_reviews(context.bot)
|
|
838
869
|
from telepost.application.submitter_notify import (
|
|
@@ -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
|
-
|
|
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.
|
|
11
|
+
RELEASE_VERSION = "2.84.0" # x-release-please-version
|
|
12
12
|
RELEASE_COMMIT = "dev"
|
|
13
13
|
BUILD_DATE = "dev"
|
|
14
14
|
SERVICE = "telepost"
|
|
@@ -53,10 +53,12 @@ CHECK_CODES: Tuple[str, ...] = (
|
|
|
53
53
|
"db_integrity",
|
|
54
54
|
"refetch_stuck",
|
|
55
55
|
"refetch_jobs",
|
|
56
|
+
"webhook_inbox",
|
|
56
57
|
"refetch_active_invariant",
|
|
57
58
|
"review_queue_orphans",
|
|
58
59
|
"review_queue_publishing",
|
|
59
60
|
"review_queue_counts",
|
|
61
|
+
"review_preview_lifecycle",
|
|
60
62
|
"delivery_outbox",
|
|
61
63
|
"audit_events_recent",
|
|
62
64
|
)
|
|
@@ -576,6 +578,62 @@ def _check_refetch_jobs(
|
|
|
576
578
|
collector.add("refetch_jobs", level, _prefix(db_path, message), details)
|
|
577
579
|
|
|
578
580
|
|
|
581
|
+
# ---- check 2b: durable update inbox (§durable-inbox) ------------------------
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _check_webhook_inbox(
|
|
585
|
+
db_path: str, conn: sqlite3.Connection, collector: _Collector
|
|
586
|
+
) -> None:
|
|
587
|
+
"""Webhook durable inbox: backlog age and permanently failed updates."""
|
|
588
|
+
columns = _table_columns(conn, "webhook_inbox")
|
|
589
|
+
if not columns:
|
|
590
|
+
collector.add(
|
|
591
|
+
"webhook_inbox", SKIP,
|
|
592
|
+
_prefix(db_path, "webhook_inbox 表不存在(旧库或 Polling-only 部署),跳过"),
|
|
593
|
+
)
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
pending = processing = dead = 0
|
|
597
|
+
oldest_pending_age: Optional[float] = None
|
|
598
|
+
for row in conn.execute(
|
|
599
|
+
"SELECT state, COUNT(*) AS n, MIN(next_attempt_at) AS oldest "
|
|
600
|
+
"FROM webhook_inbox GROUP BY state"
|
|
601
|
+
).fetchall():
|
|
602
|
+
if row["state"] == "pending":
|
|
603
|
+
pending = int(row["n"])
|
|
604
|
+
if row["oldest"] is not None:
|
|
605
|
+
oldest_pending_age = max(0.0, collector.now - float(row["oldest"]))
|
|
606
|
+
elif row["state"] == "processing":
|
|
607
|
+
processing = int(row["n"])
|
|
608
|
+
elif row["state"] == "dead":
|
|
609
|
+
dead = int(row["n"])
|
|
610
|
+
|
|
611
|
+
summary = f"Inbox: pending: {pending} processing: {processing} dead: {dead}"
|
|
612
|
+
details: Dict[str, Any] = {
|
|
613
|
+
"path": db_path,
|
|
614
|
+
"pending": pending,
|
|
615
|
+
"processing": processing,
|
|
616
|
+
"dead": dead,
|
|
617
|
+
"oldest_pending_age_seconds": (
|
|
618
|
+
round(oldest_pending_age, 1) if oldest_pending_age is not None else None
|
|
619
|
+
),
|
|
620
|
+
"summary": summary,
|
|
621
|
+
}
|
|
622
|
+
# dead = 已确认接收但重试耗尽的 Update:用户可见的丢稿,必须报红。
|
|
623
|
+
# pending 积压超过 1 小时说明 worker 停摆或持续失败,先黄后查。
|
|
624
|
+
if dead:
|
|
625
|
+
level = CRIT
|
|
626
|
+
elif pending and oldest_pending_age is not None and oldest_pending_age > 3600:
|
|
627
|
+
level = WARN
|
|
628
|
+
else:
|
|
629
|
+
level = OK
|
|
630
|
+
age_text = (
|
|
631
|
+
f"{oldest_pending_age:.0f}s" if oldest_pending_age is not None else "-"
|
|
632
|
+
)
|
|
633
|
+
message = f"{summary}|最老待处理年龄 {age_text}"
|
|
634
|
+
collector.add("webhook_inbox", level, _prefix(db_path, message), details)
|
|
635
|
+
|
|
636
|
+
|
|
579
637
|
# ---- check 3: one-active-attempt-per-chain invariant -----------------------
|
|
580
638
|
|
|
581
639
|
|
|
@@ -869,6 +927,60 @@ def _check_review_queue_counts(
|
|
|
869
927
|
)
|
|
870
928
|
|
|
871
929
|
|
|
930
|
+
# ---- check 6b: review preview media lifecycle (§review-media-ttl) ----------
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _check_review_preview_lifecycle(
|
|
934
|
+
db_path: str, conn: sqlite3.Connection, collector: _Collector
|
|
935
|
+
) -> None:
|
|
936
|
+
"""Surface preview media that cleanup could not (or could not safely) delete.
|
|
937
|
+
|
|
938
|
+
``restricted`` = Telegram refuses deletion (past 48h / permission);
|
|
939
|
+
``uncertain`` = a regeneration died before recording message ids, so a
|
|
940
|
+
human must check the review group. Both are permanent, visible states —
|
|
941
|
+
WARN, never a silently "cleaned" review.
|
|
942
|
+
"""
|
|
943
|
+
columns = _table_columns(conn, "pending_reviews")
|
|
944
|
+
if not columns or "preview_state" not in columns:
|
|
945
|
+
collector.add(
|
|
946
|
+
"review_preview_lifecycle",
|
|
947
|
+
SKIP,
|
|
948
|
+
_prefix(db_path, "pending_reviews.preview_state 列不存在,跳过"),
|
|
949
|
+
)
|
|
950
|
+
return
|
|
951
|
+
counts = {
|
|
952
|
+
str(row["state"]): int(row["n"])
|
|
953
|
+
for row in conn.execute(
|
|
954
|
+
"SELECT COALESCE(preview_state, 'present') AS state, "
|
|
955
|
+
"COUNT(*) AS n FROM pending_reviews "
|
|
956
|
+
"WHERE status='pending' GROUP BY state"
|
|
957
|
+
).fetchall()
|
|
958
|
+
}
|
|
959
|
+
restricted = counts.get("restricted", 0)
|
|
960
|
+
uncertain = counts.get("uncertain", 0)
|
|
961
|
+
needs_manual = restricted + uncertain
|
|
962
|
+
summary = ", ".join(f"{key}={counts[key]}" for key in sorted(counts)) or "空"
|
|
963
|
+
collector.add(
|
|
964
|
+
"review_preview_lifecycle",
|
|
965
|
+
WARN if needs_manual else OK,
|
|
966
|
+
_prefix(
|
|
967
|
+
db_path,
|
|
968
|
+
f"审核预览媒体按状态计数:{summary}"
|
|
969
|
+
+ (
|
|
970
|
+
f";{needs_manual} 条需人工处理"
|
|
971
|
+
f"(restricted={restricted} uncertain={uncertain})"
|
|
972
|
+
if needs_manual
|
|
973
|
+
else ""
|
|
974
|
+
),
|
|
975
|
+
),
|
|
976
|
+
{
|
|
977
|
+
"path": db_path,
|
|
978
|
+
"counts": counts,
|
|
979
|
+
"needs_manual": needs_manual,
|
|
980
|
+
},
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
|
|
872
984
|
# ---- check 7: delivery ledger / outbox -------------------------------------
|
|
873
985
|
|
|
874
986
|
|
|
@@ -1078,10 +1190,12 @@ def _check_database(db_path: str, collector: _Collector) -> None:
|
|
|
1078
1190
|
return
|
|
1079
1191
|
has_active = _check_refetch_stuck(db_path, conn, collector)
|
|
1080
1192
|
_check_refetch_jobs(db_path, conn, collector)
|
|
1193
|
+
_check_webhook_inbox(db_path, conn, collector)
|
|
1081
1194
|
_check_refetch_active_invariant(db_path, conn, collector)
|
|
1082
1195
|
_check_review_queue_orphans(db_path, conn, collector)
|
|
1083
1196
|
_check_review_queue_publishing(db_path, conn, collector)
|
|
1084
1197
|
_check_review_queue_counts(db_path, conn, collector)
|
|
1198
|
+
_check_review_preview_lifecycle(db_path, conn, collector)
|
|
1085
1199
|
_check_delivery_outbox(db_path, conn, collector)
|
|
1086
1200
|
_check_audit_events_recent(db_path, conn, collector, has_active)
|
|
1087
1201
|
except sqlite3.Error as exc:
|