telepost-bot 2.84.0__tar.gz → 2.84.2__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.84.0 → telepost_bot-2.84.2}/CHANGELOG.md +18 -0
- {telepost_bot-2.84.0/telepost_bot.egg-info → telepost_bot-2.84.2}/PKG-INFO +1 -1
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/database/db_manager.py +14 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/callback_handlers.py +58 -15
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/review.py +327 -43
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/main.py +17 -2
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/services/review_service.py +3 -2
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/build_info.py +1 -1
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/reviews.py +17 -1
- telepost_bot-2.84.2/telepost/telegram/callback_feedback.py +153 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/inbox_worker.py +38 -5
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/review_keyboard.py +59 -3
- {telepost_bot-2.84.0 → telepost_bot-2.84.2/telepost_bot.egg-info}/PKG-INFO +1 -1
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost_bot.egg-info/SOURCES.txt +2 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_review.py +24 -12
- telepost_bot-2.84.2/tests/test_review_card_recovery.py +726 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_review_media_lifecycle.py +8 -2
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_update_inbox.py +399 -12
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/ui/locales/en/LC_MESSAGES/telepost.mo +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/ui/locales/en/LC_MESSAGES/telepost.po +45 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/LICENSE +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/MANIFEST.in +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/README.en.md +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/README.md +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/config/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/config/fly-telepost-policy.example.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/config/review_policy.md +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/config/settings.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/config.ini.example +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/database/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/api_commands.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/automation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/botconfig.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/channel_listener.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/command_handlers.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/conversation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/error_handler.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/index_handlers.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/mode_selection.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/moderation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/preview_handlers.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/publish.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/reaction_stats.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/recovery.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/schedule_status.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/search_handlers.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/stats_handlers.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/handlers/upload.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/mcp_server/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/mcp_server/review_client.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/mcp_server/server.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/models/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/models/state.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/SOURCES.sha256 +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/error-mapping.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/ackresult.candidate_search.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/capabilities.pixivflow.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/event.job.expired.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/event.job.succeeded.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/eventpage.candidate_search.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/job.cancelled.candidate_search.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/job.failed.no_candidate.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/job.queued.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/job.running.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/job.succeeded.candidate_search.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/jobpage.candidate_search.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/task.candidate_search.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/fixtures/task.candidate_search.target_only.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/protocol/v1/protocol.schema.json +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/pyproject.toml +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/run.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/services/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/setup.cfg +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/api/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/admin_ops.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/automation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/delivery_planner.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/editorial.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/hot.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/novel_fallback_card.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/novel_preview.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/pixivflow_jobs.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/post_media.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/posts.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/publication.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/reaction_backfill.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/recovery.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/refetch.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/rerender.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/resubmit.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/review_queue.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/submitter_notify.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/application/telepress_provider.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/automation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/delivery.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/editorial.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/errors.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/hot.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/media.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/navigation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/novel_preview.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/packing.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/presentation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/presentation_policy.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/refetch_state.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/review.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/review_chat.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/domain/submission.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/miniapp/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/miniapp/auth.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/miniapp/rbac.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/miniapp/role_bindings.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/miniapp/session.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/observability/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/observability/audit.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/observability/cli.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/observability/doctor.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/observability/errors.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/observability/redact.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/automation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/columns.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/editorial.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/inbox.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/ledger.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/media_assets.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/moderation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/novel_preview.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/posts.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/protocol_events.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/recovery.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/refetch.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/storage/sqlite/submitter_notifications.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/capture.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/delivery/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/delivery/discussion.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/delivery/executor.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/delivery/gateway.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/delivery/legacy_runner.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/delivery/planner.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/delivery/preparation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/delivery/registry.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/delivery/sender.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/review_expiry.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/review_media.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost/telegram/review_stager.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost_bot.egg-info/dependency_links.txt +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost_bot.egg-info/entry_points.txt +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost_bot.egg-info/requires.txt +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/telepost_bot.egg-info/top_level.txt +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_admin_api.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_anonymous.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_api_commands.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_api_server.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_api_version.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_architecture_application.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_architecture_core.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_automation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_backfill_attribution.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_blacklist.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_bot_language.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_bot_token_logging.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_botconfig.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_boundary.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_callback_contract.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_caption_mentions.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_compatibility.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_config_encoding.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_console_encoding.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_conversation_flow.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_data_class_cleanup.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_database.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_database_first_start.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_database_operations.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_delivery_planner.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_disposition.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_doctor.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_editorial.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_file_validator.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_fly_deploy_contract.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_frozen_support.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_handlers.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_heat_calculator.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_helper_functions.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_hot_domain.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_identity_provenance.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_install_scripts.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_integration.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_legacy_delivery_port_navigation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_logic_flow.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_logical_submissions.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_maintenance_jobs.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_manager_notifications.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_media_decision_audit.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_media_delivery_pipeline.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_media_domain.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_media_preparation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_media_proxy_delivery.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_menu_shortcut_single_reply.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_messages.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_miniapp_api.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_miniapp_auth.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_miniapp_content.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_miniapp_entries.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_miniapp_history.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_miniapp_rbac.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_mixed_media_integrity.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_moderation_and_admin_ux.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_navigation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_no_owner_probe.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_novel_cover_preview.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_novel_delivery_semantics.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_novel_preview.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_observability.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_observability_lifecycle.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_online_reading_root_button.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_packing_ssot.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_polling_server.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_preview_pagination.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_production_handler_graph.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_protocol_contract.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_publication_presentation.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_publish_idempotency.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_publish_regressions.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_publish_reply.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_reaction_backfill.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_reaction_stats.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_recovery.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_refetch.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_refetch_card_state.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_refetch_events.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_refetch_job_lifecycle.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_refetch_reason_normalization.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_refetch_replacement.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_remote_fetch_fallback.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_rerender.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_resubmit.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_review_album_payload.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_review_chat_contract.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_review_consistency.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_review_expiry_recovery.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_review_http_mcp.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_review_service.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_robustness.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_run_dispatcher.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_run_mode.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_run_supervisor.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_schedule_outcomes.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_search_disabled_hint.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_search_recovery.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_security.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_security_headers.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_session_database_handles.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_shutdown.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_streaming_uploads.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_submission_timeout_idempotency.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_telegram_golden_messages.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_telegram_media_cache.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_upload.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_user_friendly_messages.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_webhook_payload_admission.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_webhook_ready.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_webhook_router.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_webhook_secret.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_webhook_secret_logging.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_webhook_static.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/tests/test_work_id_wire.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/ui/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/ui/i18n.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/ui/keyboards.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/ui/messages.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/api_server.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/api_tokens.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/blacklist.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/cache.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/config_wizard.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/console.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/database.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/file_validator.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/heat_calculator.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/helper_functions.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/index_manager.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/logging_config.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/maintenance_jobs.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/polling_server.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/run_mode.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/runtime_policy.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/schedule_watchdog.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/search_engine.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/security_headers.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/submission.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/utils/webhook_server.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/webapp/__init__.py +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/webapp/dist/assets/2d82b92e720462f8dd3b-Bh4Fahcw.svg +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/webapp/dist/assets/index-CLfkW8eZ.css +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/webapp/dist/assets/index-CfjBQN_e.js +0 -0
- {telepost_bot-2.84.0 → telepost_bot-2.84.2}/webapp/dist/index.html +0 -0
|
@@ -8,6 +8,24 @@
|
|
|
8
8
|
> 发布说明面向普通用户:写「对用户有什么改变」,不写内部实现细节(模块名、函数名、配置键);
|
|
9
9
|
> 技术细节放进代码注释或 `docs/`。每个 Release 的正文由本文件对应版本段自动生成。
|
|
10
10
|
|
|
11
|
+
## [2.84.2](https://github.com/redtidev1918/TelePost/compare/v2.84.1...v2.84.2) (2026-10-22)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **review:** answer each callback once and make failure feedback non-destructive ([223b1c1](https://github.com/redtidev1918/TelePost/commit/223b1c1f4e718273253a140c416e8eea58458989))
|
|
17
|
+
* **review:** keep review cards actionable when a callback fails ([455ba69](https://github.com/redtidev1918/TelePost/commit/455ba690e0ac15a95b986c8e83eeaadf6e5375bf))
|
|
18
|
+
* **review:** keep review cards actionable when a callback fails ([1827946](https://github.com/redtidev1918/TelePost/commit/1827946759284ddf9143f913bb22ae2b1a6408dc))
|
|
19
|
+
|
|
20
|
+
## [2.84.1](https://github.com/redtidev1918/TelePost/compare/v2.84.0...v2.84.1) (2026-10-22)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* exclude runtime tasks from update persistence ([b420ed6](https://github.com/redtidev1918/TelePost/commit/b420ed629b2fa1e64d38f2f2a763439b199d956f))
|
|
26
|
+
* harden inbox completion and spoiler replay idempotency ([4c6375c](https://github.com/redtidev1918/TelePost/commit/4c6375c4514cfe2b5ae5f18f55a6169b8763ef7c))
|
|
27
|
+
* prevent runtime task persistence from blocking review callbacks ([21dec7d](https://github.com/redtidev1918/TelePost/commit/21dec7d41965bb436d309715c1e58668ae479d1c))
|
|
28
|
+
|
|
11
29
|
## [2.84.0](https://github.com/redtidev1918/TelePost/compare/v2.83.2...v2.84.0) (2026-10-21)
|
|
12
30
|
|
|
13
31
|
|
|
@@ -107,6 +107,17 @@ async def init_db():
|
|
|
107
107
|
'CREATE INDEX IF NOT EXISTS idx_webhook_inbox_due '
|
|
108
108
|
'ON webhook_inbox(state, next_attempt_at, update_id)')
|
|
109
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
|
+
|
|
110
121
|
# 媒体追加去重日志:同一 (user, update) 跨投稿代只应用一次
|
|
111
122
|
# (epoch 仅作审计),与 submissions 行的更新同事务提交,崩溃后
|
|
112
123
|
# 重放收敛。worker 严格按 update_id 头部顺序处理,正常路径不会
|
|
@@ -1063,6 +1074,9 @@ async def cleanup_old_data():
|
|
|
1063
1074
|
await conn.execute(
|
|
1064
1075
|
"DELETE FROM webhook_inbox WHERE state='done' AND done_at < ?",
|
|
1065
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)")
|
|
1066
1080
|
await conn.execute(
|
|
1067
1081
|
"DELETE FROM webhook_inbox WHERE state='dead' AND updated_at < ?",
|
|
1068
1082
|
(now_ts - 30 * 86400,))
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
"""
|
|
4
4
|
from ui.i18n import tr
|
|
5
5
|
import logging
|
|
6
|
+
import re
|
|
6
7
|
from telegram import Update
|
|
7
8
|
from telegram.ext import CallbackContext, ConversationHandler
|
|
8
9
|
from telegram.constants import ParseMode
|
|
9
10
|
|
|
11
|
+
from telepost.telegram.callback_feedback import answer_callback, has_answered
|
|
10
12
|
from ui.keyboards import Keyboards
|
|
11
13
|
from ui.messages import MessageFormatter
|
|
12
14
|
from database.db_manager import get_db
|
|
@@ -33,16 +35,15 @@ GLOBAL_CALLBACK_PATTERNS = (
|
|
|
33
35
|
|
|
34
36
|
async def _safe_answer(query, text: str = None, **kwargs):
|
|
35
37
|
"""
|
|
36
|
-
|
|
38
|
+
安全应答回调查询(§review-card-recovery)。
|
|
37
39
|
|
|
38
|
-
每个 callback query
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
每个 callback query 只能被应答一次,重复应答会被 Telegram 拒绝。这里统一
|
|
41
|
+
委托给 ``callback_feedback.answer_callback``:先到的应答生效(例如带
|
|
42
|
+
show_alert 的提示),后续重复调用直接跳过且不抛错。
|
|
41
43
|
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
logger.debug(f"回调应答失败(通常为重复应答或已过期): {e}")
|
|
44
|
+
await answer_callback(
|
|
45
|
+
query, text, show_alert=bool(kwargs.get("show_alert", False)),
|
|
46
|
+
)
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
async def handle_callback_query(update: Update, context: CallbackContext):
|
|
@@ -173,17 +174,59 @@ async def handle_callback_query(update: Update, context: CallbackContext):
|
|
|
173
174
|
|
|
174
175
|
except Exception as e:
|
|
175
176
|
logger.error(f"处理回调查询时出错: {e}", exc_info=True)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
# §review-card-recovery: no text-based "UI-only" classification here. The
|
|
178
|
+
# global catch is the *outer* boundary: a Telegram edit failure inside a
|
|
179
|
+
# handler is classified at that call site (is_ui_only_error requires a
|
|
180
|
+
# BadRequest raised by the edit itself), while anything reaching this
|
|
181
|
+
# level may have been raised by a channel publish, media upload or the
|
|
182
|
+
# review DB. Treating it as display noise would mark the update done and
|
|
183
|
+
# silently drop a retryable business failure.
|
|
184
|
+
#
|
|
185
|
+
# Feedback is answer-only when the callback has not been answered yet
|
|
186
|
+
# (handlers answer exactly once, before long work); otherwise the card is
|
|
187
|
+
# rebuilt from the row — a second answer would be dropped by Telegram.
|
|
188
|
+
if not has_answered(query):
|
|
189
|
+
await _safe_answer(
|
|
190
|
+
query, tr('操作失败,已记录;卡片与审核状态保持不变。'),
|
|
191
|
+
show_alert=True,
|
|
179
192
|
)
|
|
180
|
-
|
|
181
|
-
|
|
193
|
+
await _restore_card_after_failure(context, data)
|
|
194
|
+
# Re-raise so PTB's error observers and the durable inbox worker record a
|
|
195
|
+
# bounded retry instead of a silent "done".
|
|
196
|
+
raise
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
_REVIEW_CALLBACK_DATA = re.compile(
|
|
200
|
+
r"^review_(?:approve|reject|spoiler|preview|refetch|block_user|block_api):(\d+)$"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def _restore_card_after_failure(context, data: str) -> None:
|
|
205
|
+
"""Best-effort card rebuild for a review callback that just failed."""
|
|
206
|
+
match = _REVIEW_CALLBACK_DATA.match(data or "")
|
|
207
|
+
if not match:
|
|
208
|
+
return
|
|
209
|
+
bot = getattr(context, "bot", None)
|
|
210
|
+
if bot is None:
|
|
211
|
+
return
|
|
212
|
+
try:
|
|
213
|
+
from handlers.review import restore_review_card
|
|
214
|
+
await restore_review_card(bot, int(match.group(1)))
|
|
215
|
+
except Exception:
|
|
216
|
+
logger.debug("失败后恢复审核卡出错: data=%s", data, exc_info=True)
|
|
182
217
|
|
|
183
218
|
|
|
184
219
|
async def handle_unknown_callback(update: Update, context: CallbackContext):
|
|
185
|
-
"""Final fallback for callback data unclaimed by every business namespace.
|
|
186
|
-
|
|
220
|
+
"""Final fallback for callback data unclaimed by every business namespace.
|
|
221
|
+
|
|
222
|
+
§review-card-recovery: an unroutable click (stale menu, old client, a card
|
|
223
|
+
whose namespace was removed) is answered with a popup. The message is NOT
|
|
224
|
+
edited — editing it here used to strip the keyboard off a message that may
|
|
225
|
+
still carry working buttons.
|
|
226
|
+
"""
|
|
227
|
+
query = update.callback_query
|
|
228
|
+
logger.warning("未路由的回调查询: %s", query.data)
|
|
229
|
+
await _safe_answer(query, tr('❌ 未知操作'), show_alert=True)
|
|
187
230
|
|
|
188
231
|
|
|
189
232
|
async def handle_hot_filter(update: Update, context: CallbackContext):
|
|
@@ -49,6 +49,10 @@ from telepost.telegram.delivery.preparation import PHOTO_MAX_BYTES
|
|
|
49
49
|
from telepost.telegram.delivery.sender import file_id_of as _file_id_of
|
|
50
50
|
from telepost.telegram.review_stager import TelegramReviewStager
|
|
51
51
|
from telepost.telegram import review_keyboard
|
|
52
|
+
from telepost.telegram.callback_feedback import (
|
|
53
|
+
answer_callback,
|
|
54
|
+
is_ui_only_error,
|
|
55
|
+
)
|
|
52
56
|
|
|
53
57
|
logger = logging.getLogger(__name__)
|
|
54
58
|
|
|
@@ -1572,6 +1576,214 @@ async def refresh_refetch_card(bot, review_id: int, *,
|
|
|
1572
1576
|
return False
|
|
1573
1577
|
|
|
1574
1578
|
|
|
1579
|
+
async def _record_card_restored(review_id: int, outcome: str, status: str,
|
|
1580
|
+
actor=None) -> None:
|
|
1581
|
+
try:
|
|
1582
|
+
from telepost.observability import audit
|
|
1583
|
+
await audit.record_event(
|
|
1584
|
+
"review.card_restored", review_id=int(review_id), actor=actor,
|
|
1585
|
+
detail={"status": status, "result": outcome},
|
|
1586
|
+
)
|
|
1587
|
+
except Exception:
|
|
1588
|
+
logger.debug("记录审核卡恢复审计失败: review_id=%s", review_id, exc_info=True)
|
|
1589
|
+
|
|
1590
|
+
|
|
1591
|
+
async def restore_review_card(bot, review_id: int, *, dry_run: bool = False,
|
|
1592
|
+
actor=None) -> dict:
|
|
1593
|
+
"""Re-render a review's control card from its DB row (§review-card-recovery).
|
|
1594
|
+
|
|
1595
|
+
The row is the source of truth and Telegram is a projection: a card that a
|
|
1596
|
+
failed callback replaced with an error text (or that lost its buttons on a
|
|
1597
|
+
replay) is rebuilt in place from ``pending_reviews``. Only an actionable
|
|
1598
|
+
review gets moderation buttons — terminal rows come back READ-ONLY — and
|
|
1599
|
+
review state is NEVER touched here. Nothing is ever reposted: the edit always
|
|
1600
|
+
targets the durable ``control_message_id``.
|
|
1601
|
+
|
|
1602
|
+
Never raises; returns a small JSON-serializable plan/result dict.
|
|
1603
|
+
"""
|
|
1604
|
+
result: dict = {"review_id": int(review_id), "ok": False}
|
|
1605
|
+
try:
|
|
1606
|
+
from telepost.storage.sqlite.reviews import ReviewRepository
|
|
1607
|
+
row = await ReviewRepository().get(int(review_id))
|
|
1608
|
+
except Exception:
|
|
1609
|
+
logger.warning("恢复审核卡:读取审核记录失败 review_id=%s", review_id, exc_info=True)
|
|
1610
|
+
result["reason"] = "read_failed"
|
|
1611
|
+
return result
|
|
1612
|
+
if row is None:
|
|
1613
|
+
result["reason"] = "not_found"
|
|
1614
|
+
return result
|
|
1615
|
+
|
|
1616
|
+
status = _row_value(row, "status") or ""
|
|
1617
|
+
control = _row_value(row, "control_message_id")
|
|
1618
|
+
chat_id = _row_value(row, "review_chat_id") or REVIEW_CHAT_ID
|
|
1619
|
+
result.update({"status": status, "control_message_id": control,
|
|
1620
|
+
"chat_id": str(chat_id)})
|
|
1621
|
+
if not control:
|
|
1622
|
+
# A card was never durably recorded: the queue's
|
|
1623
|
+
# reconcile_incomplete_reviews path owns that row. Never repost here.
|
|
1624
|
+
result["reason"] = "no_control_message"
|
|
1625
|
+
return result
|
|
1626
|
+
|
|
1627
|
+
if status == "pending":
|
|
1628
|
+
if dry_run:
|
|
1629
|
+
result.update({"ok": True, "dry_run": True, "card": "pending"})
|
|
1630
|
+
return result
|
|
1631
|
+
if await refresh_refetch_card(bot, int(review_id)):
|
|
1632
|
+
result.update({"ok": True, "card": "pending", "edit": "edited"})
|
|
1633
|
+
await _record_card_restored(review_id, "edited", status, actor)
|
|
1634
|
+
else:
|
|
1635
|
+
result.update({"ok": False, "reason": "edit_failed", "card": "pending"})
|
|
1636
|
+
return result
|
|
1637
|
+
|
|
1638
|
+
text, markup = review_keyboard.card_from_row(row)
|
|
1639
|
+
result.update({"card": "read_only", "text": text})
|
|
1640
|
+
if dry_run:
|
|
1641
|
+
result.update({"ok": True, "dry_run": True})
|
|
1642
|
+
return result
|
|
1643
|
+
|
|
1644
|
+
from telepost.telegram.callback_feedback import edit_card_message
|
|
1645
|
+
try:
|
|
1646
|
+
outcome = await edit_card_message(bot, chat_id, control, text, markup)
|
|
1647
|
+
except Exception:
|
|
1648
|
+
logger.warning("恢复审核卡:编辑消息失败 review_id=%s", review_id, exc_info=True)
|
|
1649
|
+
result.update({"ok": False, "reason": "edit_failed"})
|
|
1650
|
+
await _record_card_restored(review_id, "failed", status, actor)
|
|
1651
|
+
return result
|
|
1652
|
+
result.update({"ok": outcome in ("edited", "unchanged"), "edit": outcome})
|
|
1653
|
+
if outcome == "gone":
|
|
1654
|
+
result["reason"] = "message_gone"
|
|
1655
|
+
await _record_card_restored(review_id, outcome, status, actor)
|
|
1656
|
+
return result
|
|
1657
|
+
|
|
1658
|
+
|
|
1659
|
+
async def _notify_state_mismatch(query, context, review_id: int, text: str) -> None:
|
|
1660
|
+
"""§review-card-recovery: report a decided/changed review WITHOUT killing the card.
|
|
1661
|
+
|
|
1662
|
+
The callback was already answered before the business call, so a second
|
|
1663
|
+
``answerCallbackQuery`` would be ignored by Telegram and the operator would
|
|
1664
|
+
see nothing. Instead the card is rebuilt from the row: it shows the real
|
|
1665
|
+
state (read-only when terminal) plus the original-link button, which is the
|
|
1666
|
+
non-destructive feedback the operator needs. The reason is logged either way.
|
|
1667
|
+
"""
|
|
1668
|
+
logger.info(
|
|
1669
|
+
"审核操作与当前状态不一致(已应答,改用卡片状态同步): review_id=%s reason=%s",
|
|
1670
|
+
review_id, text,
|
|
1671
|
+
)
|
|
1672
|
+
bot = getattr(context, "bot", None)
|
|
1673
|
+
if bot is None:
|
|
1674
|
+
return
|
|
1675
|
+
await restore_review_card(bot, int(review_id))
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
_TERMINAL_REVIEW_STATUSES = ("rejected", "expired", "superseded")
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
async def _terminal_state_alert(query, context, review_id: int) -> bool:
|
|
1682
|
+
"""Answer a click on an already-decided review, BEFORE any business call.
|
|
1683
|
+
|
|
1684
|
+
This is the only place a state conflict still gets a popup: the callback has
|
|
1685
|
+
not been answered yet, so the alert actually reaches the client. The database
|
|
1686
|
+
state machine stays authoritative — this pre-flight only decides *feedback*;
|
|
1687
|
+
a pending/failed/publishing row still goes through the service CAS, and a
|
|
1688
|
+
published row keeps the publish-reuse ledger path (``duplicate_suppressed``)
|
|
1689
|
+
instead of being short-circuited here.
|
|
1690
|
+
Returns True when the handler must stop.
|
|
1691
|
+
"""
|
|
1692
|
+
row = await review_service.get_row(review_id)
|
|
1693
|
+
if row is None:
|
|
1694
|
+
await _answer(query, tr('审核记录不存在'), show_alert=True)
|
|
1695
|
+
return True
|
|
1696
|
+
status = _row_value(row, "status") or ""
|
|
1697
|
+
if status not in _TERMINAL_REVIEW_STATUSES:
|
|
1698
|
+
return False
|
|
1699
|
+
await _answer(query, f"该投稿当前状态:{status}", show_alert=True)
|
|
1700
|
+
bot = getattr(context, "bot", None)
|
|
1701
|
+
if bot is not None:
|
|
1702
|
+
await restore_review_card(bot, int(review_id))
|
|
1703
|
+
return True
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
async def _edit_card_or_recover(query, context, review_id: int, text: str,
|
|
1707
|
+
markup) -> None:
|
|
1708
|
+
"""Update a card AFTER a business decision has been committed.
|
|
1709
|
+
|
|
1710
|
+
A display failure here must never repeat the business action (publish,
|
|
1711
|
+
reject, refetch) — the inbox would replay the whole handler. The card is
|
|
1712
|
+
therefore best-effort: Telegram-side edit failures are logged and the card
|
|
1713
|
+
is re-rendered read-only from the row; non-UI failures are surfaced in the
|
|
1714
|
+
log and recovered the same way instead of being re-raised.
|
|
1715
|
+
"""
|
|
1716
|
+
try:
|
|
1717
|
+
await query.edit_message_text(
|
|
1718
|
+
text, reply_markup=markup, disable_web_page_preview=True,
|
|
1719
|
+
)
|
|
1720
|
+
return
|
|
1721
|
+
except Exception as exc: # noqa: BLE001 - post-commit display must not raise
|
|
1722
|
+
if is_ui_only_error(exc):
|
|
1723
|
+
logger.info(
|
|
1724
|
+
"审核卡更新未生效(消息不可编辑/未变化,业务已完成): review_id=%s reason=%s",
|
|
1725
|
+
review_id, exc,
|
|
1726
|
+
)
|
|
1727
|
+
else:
|
|
1728
|
+
logger.warning(
|
|
1729
|
+
"审核卡更新失败(业务已完成,不回滚、不重试): review_id=%s",
|
|
1730
|
+
review_id, exc_info=True,
|
|
1731
|
+
)
|
|
1732
|
+
bot = getattr(context, "bot", None)
|
|
1733
|
+
if bot is not None:
|
|
1734
|
+
await restore_review_card(bot, review_id)
|
|
1735
|
+
|
|
1736
|
+
|
|
1737
|
+
async def restore_card_command(update, context):
|
|
1738
|
+
"""`/restore_card <review_id> [--dry-run]` — admin recovery of a lost card.
|
|
1739
|
+
|
|
1740
|
+
The command only ever touches the database of the Bot it is sent to: every
|
|
1741
|
+
Bot owns its own SQLite file and review ids collide across Bots, so an admin
|
|
1742
|
+
must send it to the target Bot itself. Cross-Bot addressing is unsupported
|
|
1743
|
+
and rejected explicitly.
|
|
1744
|
+
"""
|
|
1745
|
+
user = update.effective_user
|
|
1746
|
+
if user is None or user.id not in ADMIN_IDS:
|
|
1747
|
+
await update.message.reply_text(tr('你没有审核权限'))
|
|
1748
|
+
return
|
|
1749
|
+
args = list(getattr(context, "args", None) or [])
|
|
1750
|
+
# Exactly: one review id (``202`` or ``#202``) + optionally one ``--dry-run``.
|
|
1751
|
+
# ``--bot``, unknown flags, extra tokens and duplicated flags are rejected
|
|
1752
|
+
# without touching any Telegram card.
|
|
1753
|
+
valid = (
|
|
1754
|
+
len(args) in (1, 2)
|
|
1755
|
+
and re.fullmatch(r"#?\d+", args[0]) is not None
|
|
1756
|
+
and (len(args) == 1 or args[1] == "--dry-run")
|
|
1757
|
+
)
|
|
1758
|
+
if not valid:
|
|
1759
|
+
await update.message.reply_text(
|
|
1760
|
+
tr('用法:/restore_card <审核ID> [--dry-run](只重建卡片,不改变审核状态)')
|
|
1761
|
+
)
|
|
1762
|
+
return
|
|
1763
|
+
review_id = int(args[0].lstrip("#"))
|
|
1764
|
+
dry_run = len(args) == 2
|
|
1765
|
+
result = await restore_review_card(
|
|
1766
|
+
context.bot, review_id, dry_run=dry_run, actor=user.id,
|
|
1767
|
+
)
|
|
1768
|
+
if not result.get("ok"):
|
|
1769
|
+
await update.message.reply_text(
|
|
1770
|
+
tr('恢复审核卡失败:{p0}').format(p0=result.get("reason") or 'unknown')
|
|
1771
|
+
)
|
|
1772
|
+
return
|
|
1773
|
+
if dry_run:
|
|
1774
|
+
await update.message.reply_text(
|
|
1775
|
+
tr('审核 #{p0} 卡片预览(状态:{p1},未修改任何内容):\n\n{p2}').format(
|
|
1776
|
+
p0=review_id, p1=result.get("status"), p2=result.get("text") or tr('(待审核,将按重抓状态重建)'),
|
|
1777
|
+
)
|
|
1778
|
+
)
|
|
1779
|
+
return
|
|
1780
|
+
await update.message.reply_text(
|
|
1781
|
+
tr('已恢复审核 #{p0} 的卡片(状态:{p1},未改变审核状态)。').format(
|
|
1782
|
+
p0=review_id, p1=result.get("status"),
|
|
1783
|
+
)
|
|
1784
|
+
)
|
|
1785
|
+
|
|
1786
|
+
|
|
1575
1787
|
async def reconcile_incomplete_reviews(bot, *, stale_seconds: float = 60.0) -> int:
|
|
1576
1788
|
"""Repair crash-left review rows by restoring a durable control message."""
|
|
1577
1789
|
repaired = await queue_service.reconcile_incomplete(
|
|
@@ -1677,10 +1889,13 @@ async def _apply_review_group_mask(bot, row, mask_on: bool):
|
|
|
1677
1889
|
|
|
1678
1890
|
# ---- callback handlers ----------------------------------------------------
|
|
1679
1891
|
async def _answer(query, text=None, **kwargs):
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1892
|
+
# §review-card-recovery: Telegram accepts exactly ONE answer per callback
|
|
1893
|
+
# query. Bookkeeping lives in ``callback_feedback.answer_callback`` so every
|
|
1894
|
+
# handler (and the global fallback) shares it; a second answer would be
|
|
1895
|
+
# silently dropped by Telegram, losing the operator-visible message.
|
|
1896
|
+
await answer_callback(
|
|
1897
|
+
query, text, show_alert=bool(kwargs.get("show_alert", False)),
|
|
1898
|
+
)
|
|
1684
1899
|
|
|
1685
1900
|
|
|
1686
1901
|
async def toggle_review_spoiler(update, context):
|
|
@@ -1696,7 +1911,8 @@ async def toggle_review_spoiler(update, context):
|
|
|
1696
1911
|
|
|
1697
1912
|
try:
|
|
1698
1913
|
await review_service.toggle_spoiler(
|
|
1699
|
-
review_id, actor=update.effective_user.id
|
|
1914
|
+
review_id, actor=update.effective_user.id,
|
|
1915
|
+
callback_id=query.id if isinstance(query.id, str) else "",
|
|
1700
1916
|
)
|
|
1701
1917
|
except ReviewNotFoundError:
|
|
1702
1918
|
await _answer(query, tr('审核记录不存在'), show_alert=True)
|
|
@@ -1727,24 +1943,17 @@ async def toggle_review_spoiler(update, context):
|
|
|
1727
1943
|
media_cleared=_row_value(row, "preview_state") == "cleared",
|
|
1728
1944
|
)
|
|
1729
1945
|
)
|
|
1730
|
-
except Exception:
|
|
1731
|
-
# §review-
|
|
1732
|
-
#
|
|
1733
|
-
#
|
|
1734
|
-
#
|
|
1735
|
-
#
|
|
1736
|
-
# (a popup, not a new chat message, so repeated toggles do not spam).
|
|
1946
|
+
except Exception as exc: # noqa: BLE001 - business already committed
|
|
1947
|
+
# §review-card-recovery: the spoiler flag + the review-group mask are
|
|
1948
|
+
# already applied; a keyboard refresh failure must not be re-raised (the
|
|
1949
|
+
# inbox would replay the handler and flip the mask again). Log and rebuild
|
|
1950
|
+
# the card from the row — the callback was answered earlier, so a second
|
|
1951
|
+
# answer would be dropped by Telegram anyway.
|
|
1737
1952
|
logger.warning(
|
|
1738
|
-
"
|
|
1739
|
-
|
|
1740
|
-
review_id,
|
|
1741
|
-
exc_info=True,
|
|
1742
|
-
)
|
|
1743
|
-
await _answer(
|
|
1744
|
-
query,
|
|
1745
|
-
tr('已遮罩,但按钮无法刷新(状态已{p0})。').format(p0=tr('开启') if new_spoiler else tr('关闭')),
|
|
1746
|
-
show_alert=True,
|
|
1953
|
+
"刷新审核键盘失败,已按库内状态重建审核卡: review_id=%s ui_only=%s",
|
|
1954
|
+
review_id, is_ui_only_error(exc), exc_info=True,
|
|
1747
1955
|
)
|
|
1956
|
+
await restore_review_card(bot, review_id)
|
|
1748
1957
|
|
|
1749
1958
|
|
|
1750
1959
|
async def restore_review_preview(update, context):
|
|
@@ -1792,13 +2001,30 @@ async def restore_review_preview(update, context):
|
|
|
1792
2001
|
await _answer(query, tr('预览正在恢复中,请稍候。'))
|
|
1793
2002
|
return
|
|
1794
2003
|
|
|
2004
|
+
# Answer BEFORE the staging uploads: regeneration sends media to Telegram
|
|
2005
|
+
# and can outlive the callback answer window, and a later answer would be
|
|
2006
|
+
# rejected by Telegram (one answer per callback). The restored media itself
|
|
2007
|
+
# is the visible outcome; the card keeps its handle either way.
|
|
2008
|
+
await _answer(query, tr('预览正在恢复中,请稍候。'))
|
|
2009
|
+
|
|
1795
2010
|
from telepost.telegram.review_media import regenerate_review_preview
|
|
1796
2011
|
result = await regenerate_review_preview(context.bot, row)
|
|
1797
2012
|
if not result["ok"]:
|
|
1798
|
-
|
|
2013
|
+
logger.warning(
|
|
2014
|
+
"恢复审核预览失败: review_id=%s reason=%s",
|
|
2015
|
+
review_id, result.get("message"),
|
|
2016
|
+
)
|
|
2017
|
+
chat_id = getattr(getattr(query, "message", None), "chat_id", None)
|
|
2018
|
+
if chat_id is not None:
|
|
2019
|
+
try:
|
|
2020
|
+
await context.bot.send_message(
|
|
2021
|
+
chat_id=chat_id,
|
|
2022
|
+
text=f"⚠️ 审核 #{review_id} 预览恢复失败:{result['message']}",
|
|
2023
|
+
)
|
|
2024
|
+
except Exception: # noqa: BLE001 - notice is best-effort
|
|
2025
|
+
logger.debug("发送预览恢复失败提示失败", exc_info=True)
|
|
2026
|
+
await restore_review_card(context.bot, review_id)
|
|
1799
2027
|
return
|
|
1800
|
-
await _answer(query, tr('预览已恢复({p0} 条消息),将按同样的时限自动清理。').format(
|
|
1801
|
-
p0=len(result["message_ids"])))
|
|
1802
2028
|
try:
|
|
1803
2029
|
await query.edit_message_reply_markup(
|
|
1804
2030
|
reply_markup=_review_keyboard(
|
|
@@ -2003,12 +2229,21 @@ async def approve_review(update, context):
|
|
|
2003
2229
|
if update.effective_user.id not in ADMIN_IDS:
|
|
2004
2230
|
await _answer(query, tr('你没有审核权限'), show_alert=True)
|
|
2005
2231
|
return
|
|
2006
|
-
await _answer(query)
|
|
2007
2232
|
try:
|
|
2008
2233
|
review_id = int(query.data.split(":", 1)[1])
|
|
2009
2234
|
except (ValueError, IndexError):
|
|
2010
|
-
|
|
2235
|
+
# §review-card-recovery: invalid payload is answerable — alert, never
|
|
2236
|
+
# overwrite the card with an error text.
|
|
2237
|
+
await _answer(query, tr('无效的审核记录'), show_alert=True)
|
|
2011
2238
|
return
|
|
2239
|
+
# Pre-flight: an already-decided review is answered HERE (the callback is
|
|
2240
|
+
# still answerable, so the popup actually reaches the operator). The review
|
|
2241
|
+
# state machine (service CAS) stays authoritative for anything not terminal.
|
|
2242
|
+
if await _terminal_state_alert(query, context, review_id):
|
|
2243
|
+
return
|
|
2244
|
+
# Single acknowledgement before the (possibly tens-of-seconds) publish. All
|
|
2245
|
+
# later feedback must be card state sync, not a second answer.
|
|
2246
|
+
await _answer(query)
|
|
2012
2247
|
|
|
2013
2248
|
async def _show_publishing(row):
|
|
2014
2249
|
try:
|
|
@@ -2018,7 +2253,8 @@ async def approve_review(update, context):
|
|
|
2018
2253
|
reply_markup=InlineKeyboardMarkup([]),
|
|
2019
2254
|
)
|
|
2020
2255
|
except Exception:
|
|
2021
|
-
|
|
2256
|
+
logger.debug("发布中卡片更新失败(不改写业务状态): review_id=%s", review_id,
|
|
2257
|
+
exc_info=True)
|
|
2022
2258
|
|
|
2023
2259
|
try:
|
|
2024
2260
|
result = await review_service.approve(
|
|
@@ -2027,20 +2263,25 @@ async def approve_review(update, context):
|
|
|
2027
2263
|
on_claim=_show_publishing,
|
|
2028
2264
|
)
|
|
2029
2265
|
except ReviewNotFoundError:
|
|
2030
|
-
|
|
2266
|
+
logger.info("审核不存在(已应答,改用卡片同步): review_id=%s", review_id)
|
|
2267
|
+
await restore_review_card(context.bot, review_id)
|
|
2031
2268
|
return
|
|
2032
2269
|
except ReviewBusyError:
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
await
|
|
2270
|
+
# Still being published by another claim: the card already carries the
|
|
2271
|
+
# in-progress shape; re-render it instead of a second answer.
|
|
2272
|
+
logger.info("审核正在发布中(已应答): review_id=%s", review_id)
|
|
2273
|
+
await restore_review_card(context.bot, review_id)
|
|
2037
2274
|
return
|
|
2038
2275
|
except PublishFailedError as error:
|
|
2039
2276
|
row = await _load_review_for_action(query, review_id)
|
|
2040
|
-
|
|
2277
|
+
if row is None:
|
|
2278
|
+
await restore_review_card(context.bot, review_id)
|
|
2279
|
+
return
|
|
2280
|
+
await _edit_card_or_recover(
|
|
2281
|
+
query, context, review_id,
|
|
2041
2282
|
tr("""⚠️ 审核 #{p0} {p1}:
|
|
2042
2283
|
{p2}""").format(p0=review_id, p1=error.retry_hint, p2=error.message),
|
|
2043
|
-
|
|
2284
|
+
_review_keyboard(
|
|
2044
2285
|
review_id, row["link"],
|
|
2045
2286
|
spoiler=bool(row["spoiler"]), source=row["source"],
|
|
2046
2287
|
work_id=_work_id_from_link(row["link"] or ""),
|
|
@@ -2051,11 +2292,21 @@ async def approve_review(update, context):
|
|
|
2051
2292
|
),
|
|
2052
2293
|
)
|
|
2053
2294
|
return
|
|
2295
|
+
except ReviewStateError as error:
|
|
2296
|
+
await _notify_state_mismatch(query, context, review_id, error.message)
|
|
2297
|
+
return
|
|
2298
|
+
except ReviewError as error:
|
|
2299
|
+
await _notify_state_mismatch(
|
|
2300
|
+
query, context, review_id,
|
|
2301
|
+
getattr(error, "message", None) or tr('该投稿已处理,无法发布'),
|
|
2302
|
+
)
|
|
2303
|
+
return
|
|
2054
2304
|
|
|
2055
|
-
await
|
|
2305
|
+
await _edit_card_or_recover(
|
|
2306
|
+
query, context, review_id,
|
|
2056
2307
|
tr("""✅ 审核 #{p0} 已发布
|
|
2057
2308
|
{p1}""").format(p0=review_id, p1=result.link or ''),
|
|
2058
|
-
|
|
2309
|
+
None,
|
|
2059
2310
|
)
|
|
2060
2311
|
|
|
2061
2312
|
|
|
@@ -2064,12 +2315,14 @@ async def reject_review(update, context):
|
|
|
2064
2315
|
if update.effective_user.id not in ADMIN_IDS:
|
|
2065
2316
|
await _answer(query, tr('你没有审核权限'), show_alert=True)
|
|
2066
2317
|
return
|
|
2067
|
-
await _answer(query)
|
|
2068
2318
|
try:
|
|
2069
2319
|
review_id = int(query.data.split(":", 1)[1])
|
|
2070
2320
|
except (ValueError, IndexError):
|
|
2071
|
-
await query
|
|
2321
|
+
await _answer(query, tr('无效的审核记录'), show_alert=True)
|
|
2322
|
+
return
|
|
2323
|
+
if await _terminal_state_alert(query, context, review_id):
|
|
2072
2324
|
return
|
|
2325
|
+
await _answer(query)
|
|
2073
2326
|
|
|
2074
2327
|
try:
|
|
2075
2328
|
await review_service.reject(
|
|
@@ -2077,13 +2330,23 @@ async def reject_review(update, context):
|
|
|
2077
2330
|
actor=update.effective_user.id, source="telegram",
|
|
2078
2331
|
)
|
|
2079
2332
|
except ReviewNotFoundError:
|
|
2080
|
-
|
|
2333
|
+
logger.info("审核不存在(已应答,改用卡片同步): review_id=%s", review_id)
|
|
2334
|
+
await restore_review_card(context.bot, review_id)
|
|
2081
2335
|
return
|
|
2082
2336
|
except ReviewStateError as error:
|
|
2083
|
-
await query
|
|
2337
|
+
await _notify_state_mismatch(query, context, review_id, error.message)
|
|
2338
|
+
return
|
|
2339
|
+
except ReviewError as error:
|
|
2340
|
+
await _notify_state_mismatch(
|
|
2341
|
+
query, context, review_id,
|
|
2342
|
+
getattr(error, "message", None) or tr('该投稿已处理,无法拒绝'),
|
|
2343
|
+
)
|
|
2084
2344
|
return
|
|
2085
2345
|
|
|
2086
|
-
await
|
|
2346
|
+
await _edit_card_or_recover(
|
|
2347
|
+
query, context, review_id,
|
|
2348
|
+
tr('❌ 审核 #{p0} 已拒绝').format(p0=review_id), None,
|
|
2349
|
+
)
|
|
2087
2350
|
|
|
2088
2351
|
|
|
2089
2352
|
async def block_review_user(update, context):
|
|
@@ -2120,10 +2383,22 @@ async def block_review_user(update, context):
|
|
|
2120
2383
|
)
|
|
2121
2384
|
await _answer(query, tr('已封禁该投稿人:后续投稿将被自动拒绝'))
|
|
2122
2385
|
try:
|
|
2386
|
+
# §review-card-recovery: append the audit note WITHOUT dropping the
|
|
2387
|
+
# keyboard — losing 发布/拒绝 on a still-pending review is exactly the
|
|
2388
|
+
# reported production failure, only here it would be self-inflicted.
|
|
2123
2389
|
await query.edit_message_text(
|
|
2124
2390
|
tr("""{p0}
|
|
2125
2391
|
|
|
2126
|
-
🚫 投稿人封禁已由管理员 {p1} 记录""").format(p0=query.message.text, p1=update.effective_user.id)
|
|
2392
|
+
🚫 投稿人封禁已由管理员 {p1} 记录""").format(p0=query.message.text, p1=update.effective_user.id),
|
|
2393
|
+
reply_markup=_review_keyboard(
|
|
2394
|
+
int(review_id), row["link"],
|
|
2395
|
+
spoiler=bool(row["spoiler"]), source=row["source"],
|
|
2396
|
+
work_id=_work_id_from_link(row["link"] or ""),
|
|
2397
|
+
submitter_user_id=_row_value(row, "submitter_user_id"),
|
|
2398
|
+
actor_kind=_row_value(row, "actor_kind") or "user",
|
|
2399
|
+
actor_subject=_row_value(row, "actor_subject") or "",
|
|
2400
|
+
media_cleared=_row_value(row, "preview_state") == "cleared",
|
|
2401
|
+
),
|
|
2127
2402
|
)
|
|
2128
2403
|
except Exception as exc:
|
|
2129
2404
|
logger.debug("封禁后更新审核卡失败: %s", exc)
|
|
@@ -2165,7 +2440,16 @@ async def block_review_api(update, context):
|
|
|
2165
2440
|
await query.edit_message_text(
|
|
2166
2441
|
tr("""{p0}
|
|
2167
2442
|
|
|
2168
|
-
🔑 API token 禁用已由管理员 {p1} 记录""").format(p0=query.message.text, p1=update.effective_user.id)
|
|
2443
|
+
🔑 API token 禁用已由管理员 {p1} 记录""").format(p0=query.message.text, p1=update.effective_user.id),
|
|
2444
|
+
reply_markup=_review_keyboard(
|
|
2445
|
+
int(review_id), row["link"],
|
|
2446
|
+
spoiler=bool(row["spoiler"]), source=row["source"],
|
|
2447
|
+
work_id=_work_id_from_link(row["link"] or ""),
|
|
2448
|
+
submitter_user_id=_row_value(row, "submitter_user_id"),
|
|
2449
|
+
actor_kind=_row_value(row, "actor_kind") or "user",
|
|
2450
|
+
actor_subject=_row_value(row, "actor_subject") or "",
|
|
2451
|
+
media_cleared=_row_value(row, "preview_state") == "cleared",
|
|
2452
|
+
),
|
|
2169
2453
|
)
|
|
2170
2454
|
except Exception as exc:
|
|
2171
2455
|
logger.debug("禁用API后更新审核卡失败: %s", exc)
|