From 87da92f47ba178833d9462e191bcaf3b1b431b36 Mon Sep 17 00:00:00 2001 From: Fraenkiman Date: Sat, 9 May 2026 21:18:04 +0200 Subject: [PATCH] Documentation Update 2.4.1 Part2 - Fallback implemented for DELETE /api/v1/statuses/:id without delete_media=1. - Documentation Update part 2 --- fp-plugins/mastodon/Function-Organigram.md | 738 ++++++------- fp-plugins/mastodon/Plugin-Process-Flow.md | 1158 ++++++++++++++------ fp-plugins/mastodon/README.md | 113 +- fp-plugins/mastodon/doc_mastodon.txt | 48 +- fp-plugins/mastodon/plugin.mastodon.php | 111 +- 5 files changed, 1456 insertions(+), 712 deletions(-) diff --git a/fp-plugins/mastodon/Function-Organigram.md b/fp-plugins/mastodon/Function-Organigram.md index d92b12e7..5dbdb4fe 100644 --- a/fp-plugins/mastodon/Function-Organigram.md +++ b/fp-plugins/mastodon/Function-Organigram.md @@ -14,7 +14,7 @@ The layout is intentionally hierarchical so responsibilities, call paths, and im - Recently added FlatPress configuration reuse, centralized plugin-state detection, FlatPress I/O helpers, explicit `FILE_PERMISSIONS` handling for Mastodon runtime files, the compact APCu-capable scheduler-state summary, append-only rotated sync logging, and file-backed synchronization cooldown guards are reflected explicitly. - The companion-plugin diagnostics for BBCode, PhotoSwipe, AudioVideo, Tag, and Emoticons are covered. - The admin-side Mastodon instance-information snapshot, manual refresh button, exact-version display, and reuse of cached instance capabilities for later sync runs are covered. -- Mastodon instance-dependent URL budgeting, bounded PHP execution-budget refreshes, central per-run Mastodon API rate-limit guards with request/media-upload/delete budgets, persistent cross-run media-upload/delete/account-status-page windows, OAuth scope discovery with a strict `profile`-scope preference and an older-instance fallback to `read:accounts`, media-type-aware asynchronous media-upload readiness polling for longer AudioVideo processing, AudioVideo optional endtag descriptions for local export and remote import, scheduled-run recent-content windows, Core post-success-hook-driven dirty queues for changed older mapped content, a Mastodon local-write guard for remote mirror operations, targeted non-full local candidate lists, optional rotating known-thread reply checks, best-effort cleanup of unattached uploaded Mastodon media before a failed final posting finishes, follow-up deletion synchronization with scheduled-window-limited remote existence lookups and progress cursors for large mapping sets, the admin toggle that can disable deletion synchronization, comment tombstones that block stale re-imports of deleted remote replies, early protection of locally deleted exported FlatPress comments before the next content sync, local reattachment of surviving descendant replies to the synchronized entry status during deletion follow-up, targeted descendant-recheck queues with a dedicated `comment_rechecks` follow-up scope, 5-minute scheduled-sync cooldown guards, state-write failure reporting, and the separate persistence of content-sync and deletion-sync counters are reflected explicitly. +- Mastodon instance-dependent URL budgeting, cached instance-version capability checks for status media-attribute edits and `delete_media` status deletion cleanup, bounded PHP execution-budget refreshes, central per-run Mastodon API rate-limit guards with request/media-upload/delete budgets, persistent cross-run media-upload/delete/account-status-page windows, OAuth scope discovery with a strict `profile`-scope preference and an older-instance fallback to `read:accounts`, media-type-aware asynchronous media-upload readiness polling for longer AudioVideo processing, AudioVideo optional endtag descriptions for local export and remote import, scheduled-run recent-content windows, Core post-success-hook-driven dirty queues for changed older mapped content, a Mastodon local-write guard for remote mirror operations, targeted non-full local candidate lists, optional rotating known-thread reply checks, best-effort cleanup of unattached uploaded Mastodon media before a failed final posting finishes, follow-up deletion synchronization with scheduled-window-limited remote existence lookups and progress cursors for large mapping sets, the admin toggle that can disable deletion synchronization, comment tombstones that block stale re-imports of deleted remote replies, early protection of locally deleted exported FlatPress comments before the next content sync, local reattachment of surviving descendant replies to the synchronized entry status during deletion follow-up, targeted descendant-recheck queues with a dedicated `comment_rechecks` follow-up scope, 5-minute scheduled-sync cooldown guards, state-write failure reporting, and the separate persistence of content-sync and deletion-sync counters are reflected explicitly. - FlatPress timeoffset-aware remote import timestamps for Mastodon statuses and replies are reflected explicitly. - FlatPress-local admin time display for the daily synchronization time, last content sync, and last deletion sync is reflected explicitly while keeping the stored synchronization time in UTC. - Friendly deletion-sync guards for normalized comment mapping metadata are reflected explicitly. @@ -81,7 +81,7 @@ The plugin file currently contains **337** callable functions/methods documented The first boolean argument means "explicitly triggered / bypass the daily due check"; the optional second boolean controls whether the automatic scheduled window is ignored. This keeps normal admin-triggered synchronization aligned with scheduled behavior (`true, false`) while preserving explicit full checks (`true, true`). Non-forced scheduled runs are gated by `plugin_mastodon_sync_guard_active()` and mark a 5-minute `content` cooldown through `plugin_mastodon_sync_guard_mark()` before network work starts. After acquiring the sync lock, the function starts a per-run Mastodon API guard through `plugin_mastodon_rate_limit_guard_start()`, so general requests, media uploads, status deletes, persistent cross-run media/delete windows, persistent account-status paging windows, and Mastodon `X-RateLimit-*` headers can stop the run cleanly. If both directions finish successfully, the function marks a follow-up deletion pass as pending for a later web request and stores a `deletions_not_before` timestamp at least 5 minutes after the completed content sync. A completed sync is reported as failed if `plugin_mastodon_state_write()` cannot persist the updated state; the plugin no longer places the full mapping state into APCu. - `plugin_mastodon_run_deletion_sync()` - Runs the real deletion synchronization in a separate follow-up request. It first checks `plugin_mastodon_should_run_deletion_sync()` so the user-controlled admin toggle can disable the delete pass, then checks `plugin_mastodon_deletion_sync_due()` so the delete pass cannot start before the persisted 5-minute separation window has passed. It applies the same 5-minute cooldown guard with the `deletion` kind for non-forced scheduled runs, starts the per-run Mastodon API guard after acquiring the sync lock, and every remote status delete must pass both the per-run delete budget and the persistent cross-run delete window. It resets only `deletion_stats` while preserving `content_stats` from the last content sync, gates all mapped items through `plugin_mastodon_mapping_matches_sync_start()` so the delete pass stays inside the manual sync-start window, and uses `plugin_mastodon_mapping_matches_deletion_lookup_window()` to limit only remote existence lookups for still-existing local items to the automatic scheduled 7/14/30-day window. Local deletions are still propagated to Mastodon when they are inside the manual sync-start range, even if they are outside the automatic scheduled window. Large full delete passes advance through `deletion_cursor_entries` and `deletion_cursor_comments` via `plugin_mastodon_mapping_keys_after_cursor()`, so later runs continue after the last successfully checked mapping instead of repeating the first batch. The function then either runs the full reconciliation pass or a targeted `comment_rechecks` follow-up scope via `plugin_mastodon_state_has_comment_recheck_scope()`. Direct descendants of newly deleted replies are queued with `plugin_mastodon_queue_comment_descendant_remote_rechecks()`, surviving direct child replies can be reattached to the synchronized entry status through `plugin_mastodon_reattach_local_comment_to_entry_status()`, and pending deep-thread cleanup is processed breadth-first through `plugin_mastodon_process_pending_comment_remote_rechecks()`. A successful delete pass is reported as failed if the resulting state cannot be persisted. + Runs the real deletion synchronization in a separate follow-up request. It first checks `plugin_mastodon_should_run_deletion_sync()` so the user-controlled admin toggle can disable the delete pass, then checks `plugin_mastodon_deletion_sync_due()` so the delete pass cannot start before the persisted 5-minute separation window has passed. It applies the same 5-minute cooldown guard with the `deletion` kind for non-forced scheduled runs, starts the per-run Mastodon API guard after acquiring the sync lock, and every remote status delete must pass both the per-run delete budget and the persistent cross-run delete window; for Mastodon servers before 4.4.0 it can omit `delete_media=1` from the first request when cached instance information proves the older version, and otherwise retries once without that query parameter when the server rejects it. It resets only `deletion_stats` while preserving `content_stats` from the last content sync, gates all mapped items through `plugin_mastodon_mapping_matches_sync_start()` so the delete pass stays inside the manual sync-start window, and uses `plugin_mastodon_mapping_matches_deletion_lookup_window()` to limit only remote existence lookups for still-existing local items to the automatic scheduled 7/14/30-day window. Local deletions are still propagated to Mastodon when they are inside the manual sync-start range, even if they are outside the automatic scheduled window. Large full delete passes advance through `deletion_cursor_entries` and `deletion_cursor_comments` via `plugin_mastodon_mapping_keys_after_cursor()`, so later runs continue after the last successfully checked mapping instead of repeating the first batch. The function then either runs the full reconciliation pass or a targeted `comment_rechecks` follow-up scope via `plugin_mastodon_state_has_comment_recheck_scope()`. Direct descendants of newly deleted replies are queued with `plugin_mastodon_queue_comment_descendant_remote_rechecks()`, surviving direct child replies can be reattached to the synchronized entry status through `plugin_mastodon_reattach_local_comment_to_entry_status()`, and pending deep-thread cleanup is processed breadth-first through `plugin_mastodon_process_pending_comment_remote_rechecks()`. A successful delete pass is reported as failed if the resulting state cannot be persisted. ### Local → remote export path @@ -150,16 +150,16 @@ The FlatPress Core emits the new `entry_saved`, `entry_deleted`, `comment_saved` ## A. Entry points and admin integration - `plugin_mastodon_head()` — line 2216 — Print Mastodon profile metadata into the HTML head. -- `plugin_mastodon_maybe_sync()` — line 9915 — Run the scheduled synchronization when the current request is due, using the compact scheduler summary before any full state load. -- `plugin_mastodon_run_sync()` — line 9832 — Run a full synchronization cycle. -- `plugin_mastodon_run_deletion_sync()` — line 9501 — Run the deferred deletion synchronization in a follow-up request after content sync completed, with scheduled-window-limited remote existence lookups and progress cursors. -- `plugin_mastodon_sync_due()` — line 9802 — Determine whether the scheduled synchronization is currently due. -- `plugin_mastodon_admin_boolean_label()` — line 9947 — Return a localized yes/no/unknown label for admin diagnostics. -- `plugin_mastodon_admin_add_info_row()` — line 9962 — Add one populated admin diagnostics row to the instance-information table. -- `plugin_mastodon_admin_assign()` — line 10081 — Assign plugin data to Smarty for the admin panel, including compact scheduler status/statistics and cached instance-information rows. -- `setup()` — line 10129 — Register the Mastodon admin panel template and assign plugin data to Smarty. -- `main()` — line 10134 — Keep the admin panel lifecycle compatible with FlatPress without extra processing. -- `onsubmit()` — line 10138 — Process configuration saves, OAuth actions, authorization-code exchange, manual instance-information refreshes, and the manual synchronization trigger. +- `plugin_mastodon_maybe_sync()` — line 10000 — Run the scheduled synchronization when the current request is due, using the compact scheduler summary before any full state load. +- `plugin_mastodon_run_sync()` — line 9917 — Run a full synchronization cycle. +- `plugin_mastodon_run_deletion_sync()` — line 9586 — Run the deferred deletion synchronization in a follow-up request after content sync completed, with scheduled-window-limited remote existence lookups and progress cursors. +- `plugin_mastodon_sync_due()` — line 9887 — Determine whether the scheduled synchronization is currently due. +- `plugin_mastodon_admin_boolean_label()` — line 10032 — Return a localized yes/no/unknown label for admin diagnostics. +- `plugin_mastodon_admin_add_info_row()` — line 10047 — Add one populated admin diagnostics row to the instance-information table. +- `plugin_mastodon_admin_assign()` — line 10166 — Assign plugin data to Smarty for the admin panel, including compact scheduler status/statistics and cached instance-information rows. +- `setup()` — line 10214 — Register the Mastodon admin panel template and assign plugin data to Smarty. +- `main()` — line 10219 — Keep the admin panel lifecycle compatible with FlatPress without extra processing. +- `onsubmit()` — line 10223 — Process configuration saves, OAuth actions, authorization-code exchange, manual instance-information refreshes, and the manual synchronization trigger. ## B. Defaults, configuration, secrets, and centralized FlatPress feature toggles @@ -217,13 +217,13 @@ The FlatPress Core emits the new `entry_saved`, `entry_deleted`, `comment_saved` - `plugin_mastodon_should_check_old_thread_replies()` — line 2445 — Check whether known synchronized Mastodon threads should be checked for replies in rotating batches. - `plugin_mastodon_normalize_delete_sync_enabled()` — line 2454 — Normalize the toggle that enables or disables the follow-up deletion synchronization. - `plugin_mastodon_should_run_deletion_sync()` — line 2463 — Check whether the follow-up deletion synchronization is enabled. -- `plugin_mastodon_enabled_plugin_state()` — line 4515 — Determine whether a FlatPress plugin is enabled in the centralized plugin configuration. -- `plugin_mastodon_tag_plugin_active()` — line 4580 — Determine whether the Tag plugin is active for the current FlatPress request. -- `plugin_mastodon_bbcode_plugin_active()` — line 4597 — Determine whether the BBCode plugin is active for the current FlatPress request. -- `plugin_mastodon_photoswipe_plugin_active()` — line 4614 — Determine whether the PhotoSwipe plugin is active for the current FlatPress request. -- `plugin_mastodon_audiovideo_plugin_active()` — line 4631 — Determine whether the AudioVideo plugin is active for the current FlatPress request. -- `plugin_mastodon_emoticons_plugin_active()` — line 4648 — Determine whether the Emoticons plugin is active for the current FlatPress request. -- `plugin_mastodon_companion_plugins_status()` — line 4663 — Return the status of companion FlatPress plugins used for the full Mastodon feature set. +- `plugin_mastodon_enabled_plugin_state()` — line 4541 — Determine whether a FlatPress plugin is enabled in the centralized plugin configuration. +- `plugin_mastodon_tag_plugin_active()` — line 4606 — Determine whether the Tag plugin is active for the current FlatPress request. +- `plugin_mastodon_bbcode_plugin_active()` — line 4623 — Determine whether the BBCode plugin is active for the current FlatPress request. +- `plugin_mastodon_photoswipe_plugin_active()` — line 4640 — Determine whether the PhotoSwipe plugin is active for the current FlatPress request. +- `plugin_mastodon_audiovideo_plugin_active()` — line 4657 — Determine whether the AudioVideo plugin is active for the current FlatPress request. +- `plugin_mastodon_emoticons_plugin_active()` — line 4674 — Determine whether the Emoticons plugin is active for the current FlatPress request. +- `plugin_mastodon_companion_plugins_status()` — line 4689 — Return the status of companion FlatPress plugins used for the full Mastodon feature set. ## C. Caching, filesystem helpers, logging, and persisted state @@ -288,48 +288,49 @@ The FlatPress Core emits the new `entry_saved`, `entry_deleted`, `comment_saved` - `plugin_mastodon_scheduler_state_normalize()` — line 2940 — Normalize scheduler summary fields without touching full mapping arrays. - `plugin_mastodon_scheduler_state_from_state()` — line 2978 — Build the lightweight scheduler summary from a full runtime state. - `plugin_mastodon_scheduler_state_write()` — line 2998 — Persist the compact scheduler summary; write failures only disable the optimization. -- `plugin_mastodon_scheduler_state_read()` — line 3022 — Load the compact scheduler summary through the APCu-capable FlatPress file I/O path and rebuild it from full state only when missing, invalid, or stale. -- `plugin_mastodon_state_write()` — line 3063 — Persist the runtime state to disk and refresh the compact scheduler summary after successful writes, without caching the full state in APCu. -- `plugin_mastodon_normalize_deletions_pending_scope()` — line 3088 — Normalize the targeted deletion-follow-up scope marker. -- `plugin_mastodon_state_normalize()` — line 3101 — Normalize a runtime state array and fill in missing keys. -- `plugin_mastodon_state_comment_key()` — line 3152 — Build the compound state key used for comment mappings. -- `plugin_mastodon_state_set_entry_mapping()` — line 3167 — Store the mapping between a local entry and a remote status. -- `plugin_mastodon_state_set_comment_mapping()` — line 3205 — Store the mapping between a local comment and a remote status. -- `plugin_mastodon_state_remove_entry_mapping()` — line 3240 — Remove the mapping between a local entry and a remote status. -- `plugin_mastodon_state_remove_comment_mapping()` — line 3262 — Remove the mapping between a local comment and a remote status. -- `plugin_mastodon_state_set_dirty_entry()` — line 3287 — Add an older changed entry to the persistent dirty queue. -- `plugin_mastodon_state_remove_dirty_entry()` — line 3307 — Remove an entry from the dirty queue after successful synchronization or cleanup. -- `plugin_mastodon_state_has_dirty_entry()` — line 3320 — Check whether an entry is queued for synchronization outside the scheduled window. -- `plugin_mastodon_state_set_dirty_comment()` — line 3333 — Add an older changed comment to the persistent dirty queue. -- `plugin_mastodon_state_remove_dirty_comment()` — line 3356 — Remove a comment from the dirty queue after successful synchronization or cleanup. -- `plugin_mastodon_state_has_dirty_comment()` — line 3370 — Check whether a comment is queued for synchronization outside the scheduled window. -- `plugin_mastodon_local_write_guard_enter()` — line 3379 — Increase the depth guard while the plugin mirrors remote Mastodon data into FlatPress. -- `plugin_mastodon_local_write_guard_leave()` — line 3390 — Decrease the local-write guard depth after a guarded FlatPress write/delete. -- `plugin_mastodon_local_write_guard_active()` — line 3402 — Check whether Mastodon-owned local writes should suppress dirty hook handling. -- `plugin_mastodon_dirty_tracking_options()` — line 3410 — Return normalized options for post-success dirty hooks, or an empty array when guard/configuration prevents state writes. -- `plugin_mastodon_on_entry_saved()` — line 3429 — Handle Core `entry_saved` and queue changed mapped local entries when they require a later targeted export. -- `plugin_mastodon_on_entry_deleted()` — line 3478 — Handle Core `entry_deleted`, clear dirty-entry state, and mark mapped local deletions for the deletion follow-up pass. -- `plugin_mastodon_on_comment_saved()` — line 3505 — Handle Core `comment_saved` and queue changed mapped local comments without exporting unrelated old comments. -- `plugin_mastodon_on_comment_deleted()` — line 3563 — Handle Core `comment_deleted`, clear dirty-comment state, and mark mapped local comment deletions for the deletion follow-up pass. -- `plugin_mastodon_state_get_entry_meta()` — line 3588 — Return mapping metadata for a local entry. -- `plugin_mastodon_state_entry_remote_media()` — line 3631 — Return normalized stored remote media descriptors for one entry mapping. -- `plugin_mastodon_state_entry_media_attachment_signature()` — line 3661 — Return the stored local attachment signature for one entry mapping. -- `plugin_mastodon_state_entry_media_description_signature()` — line 3671 — Return the stored local media-description signature for one entry mapping. -- `plugin_mastodon_state_set_entry_media_meta()` — line 3602 — Persist cached remote media IDs plus local attachment/description signatures for a synchronized entry. -- `plugin_mastodon_state_get_comment_meta()` — line 3683 — Return mapping metadata for a local comment. -- `plugin_mastodon_state_set_comment_tombstone()` — line 3697 — Store a tombstone that blocks stale re-imports of one deleted remote comment. -- `plugin_mastodon_state_has_comment_tombstone()` — line 3717 — Check whether one remote Mastodon comment status was tombstoned locally. -- `plugin_mastodon_protect_locally_deleted_exported_comments()` — line 3728 — Tombstone locally deleted exported FlatPress comment mappings before the next content sync can stale-reimport them from Mastodon thread context. -- `plugin_mastodon_reattach_local_comment_to_entry_status()` — line 3774 — Remove a local imported reply parent link and reattach the surviving reply to the synchronized entry status after its remote parent reply disappeared. -- `plugin_mastodon_state_remove_pending_comment_remote_recheck()` — line 3834 — Remove one pending descendant recheck marker. -- `plugin_mastodon_state_get_pending_comment_remote_recheck()` — line 3848 — Return one pending descendant recheck marker. -- `plugin_mastodon_state_set_pending_comment_remote_recheck()` — line 3863 — Mark one local comment for follow-up verification after an ancestor disappeared remotely. -- `plugin_mastodon_state_set_deletions_pending()` — line 3889 — Persist whether another deletion follow-up request is pending, which scope it should run, and when it may start. -- `plugin_mastodon_deletion_sync_due()` — line 3903 — Check whether the pending deletion synchronization may start after its persisted not-before timestamp. -- `plugin_mastodon_state_has_comment_recheck_scope()` — line 3931 — Check whether the next deletion follow-up request should run only the targeted descendant recheck scope. -- `plugin_mastodon_build_comment_remote_child_index()` — line 3940 — Build a direct-child index for mapped remote reply trees. -- `plugin_mastodon_queue_comment_descendant_remote_rechecks()` — line 3968 — Queue only the direct mapped local children of one deleted remote comment for additional verification passes. -- `plugin_mastodon_process_pending_comment_remote_rechecks()` — line 4007 — Process pending descendant rechecks breadth-first so deeper reply chains can converge within the same targeted follow-up request. +- `plugin_mastodon_scheduler_state_decode_fresh()` — line 3024 — Decode a scheduler-state JSON payload only when its embedded full-state signature is current, allowing a stale FlatPress cached read to be corrected before loading full `state.json`. +- `plugin_mastodon_scheduler_state_read()` — line 3043 — Load the compact scheduler summary through the APCu-capable FlatPress file I/O path, retry with an uncached scheduler-state read if a host returns stale cached content, and rebuild from full state only when missing, invalid, or truly stale. +- `plugin_mastodon_state_write()` — line 3089 — Persist the runtime state to disk and refresh the compact scheduler summary after successful writes, without caching the full state in APCu. +- `plugin_mastodon_normalize_deletions_pending_scope()` — line 3114 — Normalize the targeted deletion-follow-up scope marker. +- `plugin_mastodon_state_normalize()` — line 3127 — Normalize a runtime state array and fill in missing keys. +- `plugin_mastodon_state_comment_key()` — line 3178 — Build the compound state key used for comment mappings. +- `plugin_mastodon_state_set_entry_mapping()` — line 3193 — Store the mapping between a local entry and a remote status. +- `plugin_mastodon_state_set_comment_mapping()` — line 3231 — Store the mapping between a local comment and a remote status. +- `plugin_mastodon_state_remove_entry_mapping()` — line 3266 — Remove the mapping between a local entry and a remote status. +- `plugin_mastodon_state_remove_comment_mapping()` — line 3288 — Remove the mapping between a local comment and a remote status. +- `plugin_mastodon_state_set_dirty_entry()` — line 3313 — Add an older changed entry to the persistent dirty queue. +- `plugin_mastodon_state_remove_dirty_entry()` — line 3333 — Remove an entry from the dirty queue after successful synchronization or cleanup. +- `plugin_mastodon_state_has_dirty_entry()` — line 3346 — Check whether an entry is queued for synchronization outside the scheduled window. +- `plugin_mastodon_state_set_dirty_comment()` — line 3359 — Add an older changed comment to the persistent dirty queue. +- `plugin_mastodon_state_remove_dirty_comment()` — line 3382 — Remove a comment from the dirty queue after successful synchronization or cleanup. +- `plugin_mastodon_state_has_dirty_comment()` — line 3396 — Check whether a comment is queued for synchronization outside the scheduled window. +- `plugin_mastodon_local_write_guard_enter()` — line 3405 — Increase the depth guard while the plugin mirrors remote Mastodon data into FlatPress. +- `plugin_mastodon_local_write_guard_leave()` — line 3416 — Decrease the local-write guard depth after a guarded FlatPress write/delete. +- `plugin_mastodon_local_write_guard_active()` — line 3428 — Check whether Mastodon-owned local writes should suppress dirty hook handling. +- `plugin_mastodon_dirty_tracking_options()` — line 3436 — Return normalized options for post-success dirty hooks, or an empty array when guard/configuration prevents state writes. +- `plugin_mastodon_on_entry_saved()` — line 3455 — Handle Core `entry_saved` and queue changed mapped local entries when they require a later targeted export. +- `plugin_mastodon_on_entry_deleted()` — line 3504 — Handle Core `entry_deleted`, clear dirty-entry state, and mark mapped local deletions for the deletion follow-up pass. +- `plugin_mastodon_on_comment_saved()` — line 3531 — Handle Core `comment_saved` and queue changed mapped local comments without exporting unrelated old comments. +- `plugin_mastodon_on_comment_deleted()` — line 3589 — Handle Core `comment_deleted`, clear dirty-comment state, and mark mapped local comment deletions for the deletion follow-up pass. +- `plugin_mastodon_state_get_entry_meta()` — line 3614 — Return mapping metadata for a local entry. +- `plugin_mastodon_state_entry_remote_media()` — line 3657 — Return normalized stored remote media descriptors for one entry mapping. +- `plugin_mastodon_state_entry_media_attachment_signature()` — line 3687 — Return the stored local attachment signature for one entry mapping. +- `plugin_mastodon_state_entry_media_description_signature()` — line 3697 — Return the stored local media-description signature for one entry mapping. +- `plugin_mastodon_state_set_entry_media_meta()` — line 3628 — Persist cached remote media IDs plus local attachment/description signatures for a synchronized entry. +- `plugin_mastodon_state_get_comment_meta()` — line 3709 — Return mapping metadata for a local comment. +- `plugin_mastodon_state_set_comment_tombstone()` — line 3723 — Store a tombstone that blocks stale re-imports of one deleted remote comment. +- `plugin_mastodon_state_has_comment_tombstone()` — line 3743 — Check whether one remote Mastodon comment status was tombstoned locally. +- `plugin_mastodon_protect_locally_deleted_exported_comments()` — line 3754 — Tombstone locally deleted exported FlatPress comment mappings before the next content sync can stale-reimport them from Mastodon thread context. +- `plugin_mastodon_reattach_local_comment_to_entry_status()` — line 3800 — Remove a local imported reply parent link and reattach the surviving reply to the synchronized entry status after its remote parent reply disappeared. +- `plugin_mastodon_state_remove_pending_comment_remote_recheck()` — line 3860 — Remove one pending descendant recheck marker. +- `plugin_mastodon_state_get_pending_comment_remote_recheck()` — line 3874 — Return one pending descendant recheck marker. +- `plugin_mastodon_state_set_pending_comment_remote_recheck()` — line 3889 — Mark one local comment for follow-up verification after an ancestor disappeared remotely. +- `plugin_mastodon_state_set_deletions_pending()` — line 3915 — Persist whether another deletion follow-up request is pending, which scope it should run, and when it may start. +- `plugin_mastodon_deletion_sync_due()` — line 3929 — Check whether the pending deletion synchronization may start after its persisted not-before timestamp. +- `plugin_mastodon_state_has_comment_recheck_scope()` — line 3957 — Check whether the next deletion follow-up request should run only the targeted descendant recheck scope. +- `plugin_mastodon_build_comment_remote_child_index()` — line 3966 — Build a direct-child index for mapped remote reply trees. +- `plugin_mastodon_queue_comment_descendant_remote_rechecks()` — line 3994 — Queue only the direct mapped local children of one deleted remote comment for additional verification passes. +- `plugin_mastodon_process_pending_comment_remote_rechecks()` — line 4033 — Process pending descendant rechecks breadth-first so deeper reply chains can converge within the same targeted follow-up request. ## D. Date, timestamp, visibility, and threading helpers @@ -349,170 +350,172 @@ The FlatPress Core emits the new `entry_saved`, `entry_deleted`, `comment_saved` - `plugin_mastodon_mapping_keys_after_cursor()` — line 2770 — Return sorted mapping keys after a saved deletion cursor so large delete passes can continue across runs. - `plugin_mastodon_remote_status_matches_content_window()` — line 2649 — Determine whether a remote Mastodon status is inside the active content synchronization window. - `plugin_mastodon_mapping_matches_sync_start()` — line 2660 — Determine whether a stored synchronization mapping still belongs to the active sync-start window. -- `plugin_mastodon_parse_iso_datetime()` — line 4117 — Parse an ISO date/time string into FlatPress date format. -- `plugin_mastodon_parse_iso_timestamp()` — line 4135 — Parse an ISO date/time value into a Unix timestamp. -- `plugin_mastodon_remote_status_timestamp()` — line 4157 — Resolve the best FlatPress-adjusted timestamp for a remote Mastodon status. -- `plugin_mastodon_remote_status_visibility()` — line 4176 — Return the normalized visibility of a remote Mastodon status. -- `plugin_mastodon_remote_status_is_importable()` — line 4189 — Determine whether a remote Mastodon status may be imported. -- `plugin_mastodon_comment_parent_fields()` — line 4201 — Return the comment fields that may contain a parent reference. +- `plugin_mastodon_parse_iso_datetime()` — line 4143 — Parse an ISO date/time string into FlatPress date format. +- `plugin_mastodon_parse_iso_timestamp()` — line 4161 — Parse an ISO date/time value into a Unix timestamp. +- `plugin_mastodon_remote_status_timestamp()` — line 4183 — Resolve the best FlatPress-adjusted timestamp for a remote Mastodon status. +- `plugin_mastodon_remote_status_visibility()` — line 4202 — Return the normalized visibility of a remote Mastodon status. +- `plugin_mastodon_remote_status_is_importable()` — line 4215 — Determine whether a remote Mastodon status may be imported. +- `plugin_mastodon_comment_parent_fields()` — line 4227 — Return the comment fields that may contain a parent reference. - `plugin_mastodon_normalize_boolean_option()` — line 2366 — Normalize a boolean-like option value to the stored string representation. -- `plugin_mastodon_normalize_comment_parent_id()` — line 4210 — Normalize a stored local comment parent identifier. -- `plugin_mastodon_detect_local_comment_parent_id()` — line 4227 — Detect the local parent comment identifier from comment data. -- `plugin_mastodon_resolve_comment_reply_target()` — line 4249 — Resolve the remote reply target for a local comment export. -- `plugin_mastodon_list_local_comment_ids()` — line 4302 — Scan the FlatPress comment directory directly so local reply export is not blocked by stale comment-list caches. +- `plugin_mastodon_normalize_comment_parent_id()` — line 4236 — Normalize a stored local comment parent identifier. +- `plugin_mastodon_detect_local_comment_parent_id()` — line 4253 — Detect the local parent comment identifier from comment data. +- `plugin_mastodon_resolve_comment_reply_target()` — line 4275 — Resolve the remote reply target for a local comment export. +- `plugin_mastodon_list_local_comment_ids()` — line 4328 — Scan the FlatPress comment directory directly so local reply export is not blocked by stale comment-list caches. ## E. Text, URLs, language strings, tags, emojis, and BBCode/HTML conversion -- `plugin_mastodon_guess_subject()` — line 4339 — Guess a subject line from imported plain text. -- `plugin_mastodon_html_entity_decode()` — line 4381 — Decode HTML entities using the plugin defaults. -- `plugin_mastodon_blog_base_url()` — line 4390 — Return the absolute base URL of the current FlatPress installation. -- `plugin_mastodon_extract_url_token()` — line 4426 — Extract the URL token from a BBCode or attribute fragment. -- `plugin_mastodon_absolute_url()` — line 4443 — Convert a URL or path into an absolute URL when possible. -- `plugin_mastodon_lang_string()` — line 4485 — Return a localized plugin string or a provided fallback. -- `plugin_mastodon_normalize_tag_list()` — line 4711 — Normalize a list of tag labels. -- `plugin_mastodon_extend_time_limit()` — line 6939 — Refresh or raise the PHP execution time budget for long-running Mastodon work without lowering an existing higher or unlimited limit. -- `plugin_mastodon_extract_flatpress_tags()` — line 4742 — Extract FlatPress Tag plugin labels from an entry body. -- `plugin_mastodon_strip_flatpress_tag_bbcode()` — line 4767 — Remove Tag plugin BBCode blocks from entry content. -- `plugin_mastodon_mastodon_hashtag_footer()` — line 4784 — Convert FlatPress tag labels into a Mastodon hashtag footer line. -- `plugin_mastodon_remote_status_tags()` — line 4805 — Collect remote Mastodon tags from a status entity. -- `plugin_mastodon_strip_trailing_mastodon_hashtag_footer()` — line 4832 — Remove a trailing Mastodon hashtag footer from imported plain text. -- `plugin_mastodon_build_flatpress_tag_bbcode()` — line 4895 — Build Tag plugin BBCode from a list of remote Mastodon tags. -- `plugin_mastodon_emoticon_entity_to_unicode()` — line 4908 — Convert an emoticon HTML entity into a Unicode character. -- `plugin_mastodon_emoticon_map()` — line 4921 — Return the FlatPress emoticon-to-Unicode lookup map. -- `plugin_mastodon_replace_emoticon_shortcodes_with_unicode()` — line 4975 — Replace FlatPress emoticon shortcodes with Unicode glyphs. -- `plugin_mastodon_prepare_emoticons_for_mastodon()` — line 4990 — Convert active FlatPress emoticon shortcodes to standard Unicode emoji before Mastodon export. -- `plugin_mastodon_replace_unicode_emoticons_with_shortcodes()` — line 5003 — Replace Unicode emoticons with FlatPress shortcodes. -- `plugin_mastodon_is_public_host()` — line 5025 — Determine whether a host name resolves to a public endpoint. -- `plugin_mastodon_public_url_for_mastodon()` — line 5048 — Return a Mastodon-safe public URL or an empty string. -- `plugin_mastodon_plain_text_from_bbcode()` — line 5067 — Convert FlatPress BBCode into plain text for Mastodon export, removing complete AudioVideo player tags including optional description endtag content. -- `plugin_mastodon_subject_line_is_noise()` — line 5111 — Determine whether an extracted line should be ignored as a subject. -- `plugin_mastodon_domains_match()` — line 5139 — Determine whether two host names belong to the same domain family. -- `plugin_mastodon_cleanup_imported_text()` — line 5153 — Clean imported text before saving it to FlatPress. -- `plugin_mastodon_dom_children_to_flatpress()` — line 5207 — Convert DOM child nodes into FlatPress BBCode text. -- `plugin_mastodon_dom_node_to_flatpress()` — line 5225 — Convert a single DOM node into FlatPress BBCode text. -- `plugin_mastodon_public_entry_url()` — line 5334 — Return the public URL for a FlatPress entry. -- `plugin_mastodon_public_comments_url()` — line 5361 — Return the public comments URL for a FlatPress entry. -- `plugin_mastodon_public_comment_url()` — line 5389 — Return the public URL for a specific FlatPress comment. -- `plugin_mastodon_mastodon_html_to_flatpress()` — line 5404 — Convert Mastodon HTML content into FlatPress BBCode. -- `plugin_mastodon_flatpress_to_mastodon()` — line 5506 — Convert FlatPress content into Mastodon-ready plain text. -- `plugin_mastodon_limit_text()` — line 5623 — Limit text to a maximum number of characters. +- `plugin_mastodon_guess_subject()` — line 4365 — Guess a subject line from imported plain text. +- `plugin_mastodon_html_entity_decode()` — line 4407 — Decode HTML entities using the plugin defaults. +- `plugin_mastodon_blog_base_url()` — line 4416 — Return the absolute base URL of the current FlatPress installation. +- `plugin_mastodon_extract_url_token()` — line 4452 — Extract the URL token from a BBCode or attribute fragment. +- `plugin_mastodon_absolute_url()` — line 4469 — Convert a URL or path into an absolute URL when possible. +- `plugin_mastodon_lang_string()` — line 4511 — Return a localized plugin string or a provided fallback. +- `plugin_mastodon_normalize_tag_list()` — line 4737 — Normalize a list of tag labels. +- `plugin_mastodon_extend_time_limit()` — line 6965 — Refresh or raise the PHP execution time budget for long-running Mastodon work without lowering an existing higher or unlimited limit. +- `plugin_mastodon_extract_flatpress_tags()` — line 4768 — Extract FlatPress Tag plugin labels from an entry body. +- `plugin_mastodon_strip_flatpress_tag_bbcode()` — line 4793 — Remove Tag plugin BBCode blocks from entry content. +- `plugin_mastodon_mastodon_hashtag_footer()` — line 4810 — Convert FlatPress tag labels into a Mastodon hashtag footer line. +- `plugin_mastodon_remote_status_tags()` — line 4831 — Collect remote Mastodon tags from a status entity. +- `plugin_mastodon_strip_trailing_mastodon_hashtag_footer()` — line 4858 — Remove a trailing Mastodon hashtag footer from imported plain text. +- `plugin_mastodon_build_flatpress_tag_bbcode()` — line 4921 — Build Tag plugin BBCode from a list of remote Mastodon tags. +- `plugin_mastodon_emoticon_entity_to_unicode()` — line 4934 — Convert an emoticon HTML entity into a Unicode character. +- `plugin_mastodon_emoticon_map()` — line 4947 — Return the FlatPress emoticon-to-Unicode lookup map. +- `plugin_mastodon_replace_emoticon_shortcodes_with_unicode()` — line 5001 — Replace FlatPress emoticon shortcodes with Unicode glyphs. +- `plugin_mastodon_prepare_emoticons_for_mastodon()` — line 5016 — Convert active FlatPress emoticon shortcodes to standard Unicode emoji before Mastodon export. +- `plugin_mastodon_replace_unicode_emoticons_with_shortcodes()` — line 5029 — Replace Unicode emoticons with FlatPress shortcodes. +- `plugin_mastodon_is_public_host()` — line 5051 — Determine whether a host name resolves to a public endpoint. +- `plugin_mastodon_public_url_for_mastodon()` — line 5074 — Return a Mastodon-safe public URL or an empty string. +- `plugin_mastodon_plain_text_from_bbcode()` — line 5093 — Convert FlatPress BBCode into plain text for Mastodon export, removing complete AudioVideo player tags including optional description endtag content. +- `plugin_mastodon_subject_line_is_noise()` — line 5137 — Determine whether an extracted line should be ignored as a subject. +- `plugin_mastodon_domains_match()` — line 5165 — Determine whether two host names belong to the same domain family. +- `plugin_mastodon_cleanup_imported_text()` — line 5179 — Clean imported text before saving it to FlatPress. +- `plugin_mastodon_dom_children_to_flatpress()` — line 5233 — Convert DOM child nodes into FlatPress BBCode text. +- `plugin_mastodon_dom_node_to_flatpress()` — line 5251 — Convert a single DOM node into FlatPress BBCode text. +- `plugin_mastodon_public_entry_url()` — line 5360 — Return the public URL for a FlatPress entry. +- `plugin_mastodon_public_comments_url()` — line 5387 — Return the public comments URL for a FlatPress entry. +- `plugin_mastodon_public_comment_url()` — line 5415 — Return the public URL for a specific FlatPress comment. +- `plugin_mastodon_mastodon_html_to_flatpress()` — line 5430 — Convert Mastodon HTML content into FlatPress BBCode. +- `plugin_mastodon_flatpress_to_mastodon()` — line 5532 — Convert FlatPress content into Mastodon-ready plain text. +- `plugin_mastodon_limit_text()` — line 5649 — Limit text to a maximum number of characters. ## F. Local content access, media processing, hashing, and export ordering -- `plugin_mastodon_entry_hash()` — line 5645 — Build a change-detection hash for a FlatPress entry. -- `plugin_mastodon_comment_hash()` — line 5657 — Build a change-detection hash for a FlatPress comment. -- `plugin_mastodon_remote_status_author_label()` — line 8773 — Build a readable author label for quoted Mastodon replies. -- `plugin_mastodon_strip_leading_quote_block()` — line 8806 — Remove one leading BBCode quote block so imported reply quotes do not compound indefinitely. -- `plugin_mastodon_imported_reply_quote_payload()` — line 8839 — Resolve the author and body that should be quoted for an imported Mastodon reply. -- `plugin_mastodon_build_imported_reply_quote()` — line 8882 — Build the optional BBCode quote block for an imported Mastodon reply. -- `plugin_mastodon_safe_path_component()` — line 5675 — Sanitize a string so it can be used as a path component. -- `plugin_mastodon_safe_filename()` — line 5690 — Sanitize a file name for local storage. -- `plugin_mastodon_normalize_media_relative_path()` — line 5703 — Normalize a FlatPress media path and reject absolute, URL, or traversal paths. -- `plugin_mastodon_media_relative_to_absolute()` — line 5726 — Resolve a FlatPress media path to an absolute file path. -- `plugin_mastodon_media_prepare_directory()` — line 5739 — Ensure that a media directory exists. -- `plugin_mastodon_media_delete_tree()` — line 5755 — Delete a directory tree used for imported media. -- `plugin_mastodon_media_copy_tree()` — line 5782 — Copy a directory tree used for media synchronization. -- `plugin_mastodon_bbcode_attr_escape()` — line 5820 — Escape a value for safe BBCode attribute usage. -- `plugin_mastodon_bbcode_text_escape()` — line 5832 — Escape plain text embedded between BBCode tags, used for imported AudioVideo media descriptions. -- `plugin_mastodon_media_guess_mime_type()` — line 5854 — Guess the MIME type of a local media file. -- `plugin_mastodon_media_type_from_mime()` — line 5921 — Classify a MIME type or file extension as image, video, or audio for Mastodon media handling. -- `plugin_mastodon_extension_from_mime_type()` — line 5955 — Resolve a safe file extension from a MIME type with a fallback. -- `plugin_mastodon_media_parse_tag_attributes()` — line 6076 — Parse key/value attributes from a FlatPress media tag. -- `plugin_mastodon_media_description_from_bbcode_content()` — line 6107 — Normalize optional AudioVideo BBCode content into a Mastodon media description. -- `plugin_mastodon_instance_supported_media_mime_types()` — line 6002 — Return the MIME types advertised by the configured Mastodon instance. -- `plugin_mastodon_instance_media_size_limit()` — line 6022 — Return the configured byte-size limit for an image, video/GIFV, or audio upload. -- `plugin_mastodon_validate_local_media_item()` — line 6049 — Validate one local media file against available instance MIME and byte-size limits before upload. -- `plugin_mastodon_media_extract_default_path()` — line 6133 — Extract the default path parameter from FlatPress media BBCode such as `[img=...]`, `[gallery=...]`, `[audioplayer="..."]`, and `[videoplayer="..."]`. -- `plugin_mastodon_add_local_media_item()` — line 6162 — Add one normalized local media item while deduplicating and enforcing an expected media family. -- `plugin_mastodon_collect_local_entry_media()` — line 6211 — Collect local images, galleries, audio, video, optional AudioVideo endtag descriptions, and video poster thumbnails referenced by an entry. -- `plugin_mastodon_prepare_entry_media_items()` — line 6349 — Normalize collected local media items into reusable path/description tuples. -- `plugin_mastodon_entry_media_attachment_signature_from_items()` — line 6380 — Hash only the attachment identity of normalized media items. -- `plugin_mastodon_entry_media_description_signature_from_items()` — line 6403 — Hash only the alt-text portion of normalized media items. -- `plugin_mastodon_entry_media_signature()` — line 6422 — Build a combined attachment+description signature for media references contained in entry content. -- `plugin_mastodon_remote_media_attachment_type()` — line 6439 — Normalize a Mastodon attachment type, including extension-based fallbacks for older or incomplete payloads. -- `plugin_mastodon_remote_status_media_attachments()` — line 6466 — Extract supported image, audio, video, and GIFV attachments from a remote Mastodon status. -- `plugin_mastodon_remote_status_image_attachments()` — line 6496 — Extract image attachments from a remote Mastodon status. -- `plugin_mastodon_remote_media_source_url()` — line 6505 — Resolve the best downloadable source URL for a remote attachment. -- `plugin_mastodon_remote_media_source_urls()` — line 6523 — Resolve direct-download fallback candidates for a remote attachment; audio/video/GIFV avoid `preview_url` as a file-source fallback, while images may use it. -- `plugin_mastodon_remote_media_description()` — line 6545 — Resolve the best description for a remote attachment. -- `plugin_mastodon_remote_media_focus()` — line 6559 — Normalize a Mastodon media focus string when present. -- `plugin_mastodon_remote_media_descriptors_from_status()` — line 6576 — Extract reusable media descriptors (`id`, `description`, `focus`) from a Mastodon status payload. -- `plugin_mastodon_remote_media_descriptors_from_media_ids()` — line 6607 — Build fallback reusable media descriptors from already-known IDs and local media items. -- `plugin_mastodon_media_download()` — line 6674 — Download a remote media asset with an extended media-transfer timeout. -- `plugin_mastodon_remote_download_basename()` — line 6688 — Build a safe local basename for a downloaded remote image, audio, video, GIFV, or poster. -- `plugin_mastodon_store_remote_media_url()` — line 6718 — Download and persist one remote media URL. -- `plugin_mastodon_build_imported_media_bbcode()` — line 6740 — Build FlatPress BBCode for imported remote media attachments: images become `[img]`/`[gallery]`, audio becomes `[audioplayer]`, and video/GIFV becomes `[videoplayer]` with imported optional description endtag text and an imported poster when available; alternate direct media URLs are retried before an attachment is skipped. -- `plugin_mastodon_collect_entry_files()` — line 7707 — Collect entry files recursively from the FlatPress content tree. -- `plugin_mastodon_local_item_timestamp()` — line 7734 — Resolve the best timestamp for a local FlatPress item. -- `plugin_mastodon_compare_local_entries_for_export()` — line 7763 — Compare local FlatPress entries for Mastodon export order. -- `plugin_mastodon_test_note_local_entry_parse()` — line 7782 — Simulation-only no-op counter hook used to prove targeted scheduled scans avoid parsing all old local entries. -- `plugin_mastodon_dirty_entry_id_lookup()` — line 7794 — Collect local entry IDs that must be parsed because an entry or one of its comments is present in the dirty queues. -- `plugin_mastodon_should_parse_local_entry_for_sync()` — line 7825 — Decide whether one entry belongs to the active scheduled window, the dirty target set, or an explicit full scan. -- `plugin_mastodon_list_local_entries_for_sync()` — line 7850 — Build the local export candidate list for scheduled/non-full runs from active-window entries plus dirty targets while preserving full repair scans. -- `plugin_mastodon_list_local_entries()` — line 7891 — List local FlatPress entry identifiers. +- `plugin_mastodon_entry_hash()` — line 5671 — Build a change-detection hash for a FlatPress entry. +- `plugin_mastodon_comment_hash()` — line 5683 — Build a change-detection hash for a FlatPress comment. +- `plugin_mastodon_remote_status_author_label()` — line 8858 — Build a readable author label for quoted Mastodon replies. +- `plugin_mastodon_strip_leading_quote_block()` — line 8891 — Remove one leading BBCode quote block so imported reply quotes do not compound indefinitely. +- `plugin_mastodon_imported_reply_quote_payload()` — line 8924 — Resolve the author and body that should be quoted for an imported Mastodon reply. +- `plugin_mastodon_build_imported_reply_quote()` — line 8967 — Build the optional BBCode quote block for an imported Mastodon reply. +- `plugin_mastodon_safe_path_component()` — line 5701 — Sanitize a string so it can be used as a path component. +- `plugin_mastodon_safe_filename()` — line 5716 — Sanitize a file name for local storage. +- `plugin_mastodon_normalize_media_relative_path()` — line 5729 — Normalize a FlatPress media path and reject absolute, URL, or traversal paths. +- `plugin_mastodon_media_relative_to_absolute()` — line 5752 — Resolve a FlatPress media path to an absolute file path. +- `plugin_mastodon_media_prepare_directory()` — line 5765 — Ensure that a media directory exists. +- `plugin_mastodon_media_delete_tree()` — line 5781 — Delete a directory tree used for imported media. +- `plugin_mastodon_media_copy_tree()` — line 5808 — Copy a directory tree used for media synchronization. +- `plugin_mastodon_bbcode_attr_escape()` — line 5846 — Escape a value for safe BBCode attribute usage. +- `plugin_mastodon_bbcode_text_escape()` — line 5858 — Escape plain text embedded between BBCode tags, used for imported AudioVideo media descriptions. +- `plugin_mastodon_media_guess_mime_type()` — line 5880 — Guess the MIME type of a local media file. +- `plugin_mastodon_media_type_from_mime()` — line 5947 — Classify a MIME type or file extension as image, video, or audio for Mastodon media handling. +- `plugin_mastodon_extension_from_mime_type()` — line 5981 — Resolve a safe file extension from a MIME type with a fallback. +- `plugin_mastodon_media_parse_tag_attributes()` — line 6102 — Parse key/value attributes from a FlatPress media tag. +- `plugin_mastodon_media_description_from_bbcode_content()` — line 6133 — Normalize optional AudioVideo BBCode content into a Mastodon media description. +- `plugin_mastodon_instance_supported_media_mime_types()` — line 6028 — Return the MIME types advertised by the configured Mastodon instance. +- `plugin_mastodon_instance_media_size_limit()` — line 6048 — Return the configured byte-size limit for an image, video/GIFV, or audio upload. +- `plugin_mastodon_validate_local_media_item()` — line 6075 — Validate one local media file against available instance MIME and byte-size limits before upload. +- `plugin_mastodon_media_extract_default_path()` — line 6159 — Extract the default path parameter from FlatPress media BBCode such as `[img=...]`, `[gallery=...]`, `[audioplayer="..."]`, and `[videoplayer="..."]`. +- `plugin_mastodon_add_local_media_item()` — line 6188 — Add one normalized local media item while deduplicating and enforcing an expected media family. +- `plugin_mastodon_collect_local_entry_media()` — line 6237 — Collect local images, galleries, audio, video, optional AudioVideo endtag descriptions, and video poster thumbnails referenced by an entry. +- `plugin_mastodon_prepare_entry_media_items()` — line 6375 — Normalize collected local media items into reusable path/description tuples. +- `plugin_mastodon_entry_media_attachment_signature_from_items()` — line 6406 — Hash only the attachment identity of normalized media items. +- `plugin_mastodon_entry_media_description_signature_from_items()` — line 6429 — Hash only the alt-text portion of normalized media items. +- `plugin_mastodon_entry_media_signature()` — line 6448 — Build a combined attachment+description signature for media references contained in entry content. +- `plugin_mastodon_remote_media_attachment_type()` — line 6465 — Normalize a Mastodon attachment type, including extension-based fallbacks for older or incomplete payloads. +- `plugin_mastodon_remote_status_media_attachments()` — line 6492 — Extract supported image, audio, video, and GIFV attachments from a remote Mastodon status. +- `plugin_mastodon_remote_status_image_attachments()` — line 6522 — Extract image attachments from a remote Mastodon status. +- `plugin_mastodon_remote_media_source_url()` — line 6531 — Resolve the best downloadable source URL for a remote attachment. +- `plugin_mastodon_remote_media_source_urls()` — line 6549 — Resolve direct-download fallback candidates for a remote attachment; audio/video/GIFV avoid `preview_url` as a file-source fallback, while images may use it. +- `plugin_mastodon_remote_media_description()` — line 6571 — Resolve the best description for a remote attachment. +- `plugin_mastodon_remote_media_focus()` — line 6585 — Normalize a Mastodon media focus string when present. +- `plugin_mastodon_remote_media_descriptors_from_status()` — line 6602 — Extract reusable media descriptors (`id`, `description`, `focus`) from a Mastodon status payload. +- `plugin_mastodon_remote_media_descriptors_from_media_ids()` — line 6633 — Build fallback reusable media descriptors from already-known IDs and local media items. +- `plugin_mastodon_media_download()` — line 6700 — Download a remote media asset with an extended media-transfer timeout. +- `plugin_mastodon_remote_download_basename()` — line 6714 — Build a safe local basename for a downloaded remote image, audio, video, GIFV, or poster. +- `plugin_mastodon_store_remote_media_url()` — line 6744 — Download and persist one remote media URL. +- `plugin_mastodon_build_imported_media_bbcode()` — line 6766 — Build FlatPress BBCode for imported remote media attachments: images become `[img]`/`[gallery]`, audio becomes `[audioplayer]`, and video/GIFV becomes `[videoplayer]` with imported optional description endtag text and an imported poster when available; alternate direct media URLs are retried before an attachment is skipped. +- `plugin_mastodon_collect_entry_files()` — line 7757 — Collect entry files recursively from the FlatPress content tree. +- `plugin_mastodon_local_item_timestamp()` — line 7784 — Resolve the best timestamp for a local FlatPress item. +- `plugin_mastodon_compare_local_entries_for_export()` — line 7813 — Compare local FlatPress entries for Mastodon export order. +- `plugin_mastodon_test_note_local_entry_parse()` — line 7832 — Simulation-only no-op counter hook used to prove targeted scheduled scans avoid parsing all old local entries. +- `plugin_mastodon_dirty_entry_id_lookup()` — line 7844 — Collect local entry IDs that must be parsed because an entry or one of its comments is present in the dirty queues. +- `plugin_mastodon_should_parse_local_entry_for_sync()` — line 7875 — Decide whether one entry belongs to the active scheduled window, the dirty target set, or an explicit full scan. +- `plugin_mastodon_list_local_entries_for_sync()` — line 7900 — Build the local export candidate list for scheduled/non-full runs from active-window entries plus dirty targets while preserving full repair scans. +- `plugin_mastodon_list_local_entries()` — line 7941 — List local FlatPress entry identifiers. ## G. HTTP transport, PHP timeout budgeting, instance capability lookup, status-length budgeting, OAuth, Mastodon API calls, and media upload -- `plugin_mastodon_extend_time_limit()` — line 6939 — Refresh or raise the PHP execution time budget for long-running Mastodon work without lowering an existing higher or unlimited limit. -- `plugin_mastodon_instance_document()` — line 6970 — Load and cache the compact Mastodon instance document, preferring the saved FlatPress snapshot before APCu and live network fetches. -- `plugin_mastodon_instance_version()` — line 7014 — Extract the human-readable Mastodon server version from the cached instance document. -- `plugin_mastodon_instance_supports_status_media_attributes()` — line 7032 — Decide whether `PUT /api/v1/statuses/:id` may safely use `media_attributes` for in-place alt-text edits. -- `plugin_mastodon_instance_configuration()` — line 7049 — Return the normalized `configuration` subtree from the cached Mastodon instance document. -- `plugin_mastodon_instance_media_limit()` — line 7059 — Return the media attachment limit of the configured instance. -- `plugin_mastodon_instance_media_description_limit()` — line 7072 — Return the media description length limit of the configured instance. -- `plugin_mastodon_instance_url_reserved_length()` — line 7085 — Return the reserved Mastodon character budget used for each URL. -- `plugin_mastodon_instance_registration_summary()` — line 9983 — Summarize the cached registration policy advertised by the instance for the admin diagnostics table. -- `plugin_mastodon_admin_instance_info_rows()` — line 10009 — Build the localized admin-table rows from the cached instance-information snapshot without forcing another live request. -- `plugin_mastodon_status_text_length()` — line 7099 — Calculate the Mastodon-visible status length with instance URL budgeting. -- `plugin_mastodon_limit_status_text()` — line 7131 — Truncate status text using Mastodon URL-budget rules. -- `plugin_mastodon_http_request_multipart()` — line 7210 — Perform a multipart HTTP request. -- `plugin_mastodon_fetch_media_attachment()` — line 7343 — Fetch a single Mastodon media attachment by ID. -- `plugin_mastodon_media_processing_attempts()` — line 7408 — Calculate media-type- and size-aware polling attempts for asynchronous Mastodon media processing. -- `plugin_mastodon_media_transfer_timeout()` — line 7432 — Calculate longer upload transfer timeouts for audio/video/GIFV while keeping image uploads lighter. -- `plugin_mastodon_wait_for_media_attachment()` — line 7452 — Poll an asynchronously processed Mastodon media attachment until it is ready or times out, including pending audio/video responses without `preview_url`. -- `plugin_mastodon_upload_media_items()` — line 7501 — Upload local media items to Mastodon and collect the created media IDs; AudioVideo posters are sent as Mastodon `thumbnail` multipart fields for video uploads. -- `plugin_mastodon_parse_http_response_headers()` — line 7929 — Parse raw HTTP response headers. -- `plugin_mastodon_stream_context_request()` — line 7959 — Perform an HTTP request through a stream context fallback. -- `plugin_mastodon_status_media_attributes()` — line 6641 — Build the `media_attributes` array used for in-place status edits of already attached media. -- `plugin_mastodon_prepare_entry_media_sync_plan()` — line 7618 — Decide whether an entry should upload fresh media, reuse stored IDs, or reuse IDs plus `media_attributes`. -- `plugin_mastodon_array_is_list()` — line 7999 — Detect whether a PHP array is a zero-based list that should use `[]` form-field notation. -- `plugin_mastodon_array_contains_only_form_scalars()` — line 8019 — Detect whether a list can be serialized as repeated scalar `[]` fields. -- `plugin_mastodon_http_build_query()` — line 8037 — Build an application/x-www-form-urlencoded query string, emitting Rack-compatible Mastodon array fields such as `media_ids[]` and nested `media_attributes[][description]`. -- `plugin_mastodon_http_request()` — line 8093 — Perform an HTTP request using cURL or the stream fallback. -- `plugin_mastodon_mastodon_api()` — line 8211 — Call the Mastodon API and return the raw HTTP response. -- `plugin_mastodon_mastodon_json()` — line 8260 — Call the Mastodon API and decode a JSON response. -- `plugin_mastodon_response_error_message()` — line 8275 — Extract the most useful error message from an API response. +- `plugin_mastodon_extend_time_limit()` — line 6965 — Refresh or raise the PHP execution time budget for long-running Mastodon work without lowering an existing higher or unlimited limit. +- `plugin_mastodon_instance_document()` — line 6996 — Load and cache the compact Mastodon instance document, preferring the saved FlatPress snapshot before APCu and live network fetches. +- `plugin_mastodon_instance_version()` — line 7040 — Extract the human-readable Mastodon server version from the cached instance document. +- `plugin_mastodon_instance_supports_status_media_attributes()` — line 7058 — Decide whether `PUT /api/v1/statuses/:id` may safely use `media_attributes` for in-place alt-text edits. +- `plugin_mastodon_instance_supports_status_delete_media()` — line 7082 — Use cached or stored instance-version information to decide whether `DELETE /api/v1/statuses/:id?delete_media=1` is documented as supported, without spending an extra network request during deletion synchronization. +- `plugin_mastodon_instance_configuration()` — line 7099 — Return the normalized `configuration` subtree from the cached Mastodon instance document. +- `plugin_mastodon_instance_media_limit()` — line 7109 — Return the media attachment limit of the configured instance. +- `plugin_mastodon_instance_media_description_limit()` — line 7122 — Return the media description length limit of the configured instance. +- `plugin_mastodon_instance_url_reserved_length()` — line 7135 — Return the reserved Mastodon character budget used for each URL. +- `plugin_mastodon_instance_registration_summary()` — line 10068 — Summarize the cached registration policy advertised by the instance for the admin diagnostics table. +- `plugin_mastodon_admin_instance_info_rows()` — line 10094 — Build the localized admin-table rows from the cached instance-information snapshot without forcing another live request. +- `plugin_mastodon_status_text_length()` — line 7149 — Calculate the Mastodon-visible status length with instance URL budgeting. +- `plugin_mastodon_limit_status_text()` — line 7181 — Truncate status text using Mastodon URL-budget rules. +- `plugin_mastodon_http_request_multipart()` — line 7260 — Perform a multipart HTTP request. +- `plugin_mastodon_fetch_media_attachment()` — line 7393 — Fetch a single Mastodon media attachment by ID. +- `plugin_mastodon_media_processing_attempts()` — line 7458 — Calculate media-type- and size-aware polling attempts for asynchronous Mastodon media processing. +- `plugin_mastodon_media_transfer_timeout()` — line 7482 — Calculate longer upload transfer timeouts for audio/video/GIFV while keeping image uploads lighter. +- `plugin_mastodon_wait_for_media_attachment()` — line 7502 — Poll an asynchronously processed Mastodon media attachment until it is ready or times out, including pending audio/video responses without `preview_url`. +- `plugin_mastodon_upload_media_items()` — line 7551 — Upload local media items to Mastodon and collect the created media IDs; AudioVideo posters are sent as Mastodon `thumbnail` multipart fields for video uploads. +- `plugin_mastodon_parse_http_response_headers()` — line 7979 — Parse raw HTTP response headers. +- `plugin_mastodon_stream_context_request()` — line 8009 — Perform an HTTP request through a stream context fallback. +- `plugin_mastodon_status_media_attributes()` — line 6667 — Build the `media_attributes` array used for in-place status edits of already attached media. +- `plugin_mastodon_prepare_entry_media_sync_plan()` — line 7668 — Decide whether an entry should upload fresh media, reuse stored IDs, or reuse IDs plus `media_attributes`. +- `plugin_mastodon_array_is_list()` — line 8049 — Detect whether a PHP array is a zero-based list that should use `[]` form-field notation. +- `plugin_mastodon_array_contains_only_form_scalars()` — line 8069 — Detect whether a list can be serialized as repeated scalar `[]` fields. +- `plugin_mastodon_http_build_query()` — line 8087 — Build an application/x-www-form-urlencoded query string, emitting Rack-compatible Mastodon array fields such as `media_ids[]` and nested `media_attributes[][description]`. +- `plugin_mastodon_http_request()` — line 8143 — Perform an HTTP request using cURL or the stream fallback. +- `plugin_mastodon_mastodon_api()` — line 8261 — Call the Mastodon API and return the raw HTTP response. +- `plugin_mastodon_mastodon_json()` — line 8310 — Call the Mastodon API and decode a JSON response. +- `plugin_mastodon_response_error_message()` — line 8325 — Extract the most useful error message from an API response. - `plugin_mastodon_oauth_legacy_scopes()` — line 563 — Return the legacy OAuth scope string used by older registrations. - `plugin_mastodon_oauth_profile_scopes()` — line 571 — Return the stricter scope string that uses `profile` for `verify_credentials`. - `plugin_mastodon_oauth_server_metadata()` — line 580 — Discover OAuth server metadata from `/.well-known/oauth-authorization-server`. - `plugin_mastodon_oauth_supported_scopes()` — line 599 — Extract the discoverable scope list from OAuth server metadata. - `plugin_mastodon_oauth_scope_supported()` — line 636 — Check whether the configured Mastodon instance supports a given OAuth scope. - `plugin_mastodon_oauth_preferred_scopes()` — line 657 — Prefer `profile` on current instances and fall back to `read:accounts` on older ones. -- `plugin_mastodon_register_app()` — line 8304 — Register the FlatPress application on the configured Mastodon instance with the preferred discoverable scope set. -- `plugin_mastodon_build_authorize_url()` — line 8327 — Build the OAuth authorization URL using the scopes that the registered app may safely request. -- `plugin_mastodon_exchange_code_for_token()` — line 8347 — Exchange an OAuth authorization code for an access token using the same negotiated scope string. -- `plugin_mastodon_verify_credentials()` — line 8377 — Verify the currently configured access token. -- `plugin_mastodon_instance_character_limit()` — line 8394 — Return the status character limit of the configured instance. -- `plugin_mastodon_fetch_account_statuses()` — line 8409 — Fetch statuses for the authenticated Mastodon account. -- `plugin_mastodon_fetch_status_context()` — line 8456 — Fetch the conversation context for a Mastodon status. -- `plugin_mastodon_fetch_status()` — line 8467 — Fetch a single Mastodon status. -- `plugin_mastodon_delete_status()` — line 8478 — Delete a Mastodon status, including media when requested. -- `plugin_mastodon_status_missing_response()` — line 8491 — Check whether an API response means that the referenced Mastodon status no longer exists. -- `plugin_mastodon_create_status()` — line 8504 — Create a Mastodon status. -- `plugin_mastodon_update_status()` — line 8531 — Update an existing Mastodon status. +- `plugin_mastodon_register_app()` — line 8354 — Register the FlatPress application on the configured Mastodon instance with the preferred discoverable scope set. +- `plugin_mastodon_build_authorize_url()` — line 8377 — Build the OAuth authorization URL using the scopes that the registered app may safely request. +- `plugin_mastodon_exchange_code_for_token()` — line 8397 — Exchange an OAuth authorization code for an access token using the same negotiated scope string. +- `plugin_mastodon_verify_credentials()` — line 8427 — Verify the currently configured access token. +- `plugin_mastodon_instance_character_limit()` — line 8444 — Return the status character limit of the configured instance. +- `plugin_mastodon_fetch_account_statuses()` — line 8459 — Fetch statuses for the authenticated Mastodon account. +- `plugin_mastodon_fetch_status_context()` — line 8506 — Fetch the conversation context for a Mastodon status. +- `plugin_mastodon_fetch_status()` — line 8517 — Fetch a single Mastodon status. +- `plugin_mastodon_delete_status()` — line 8528 — Delete a Mastodon status; cached Mastodon versions before 4.4.0 omit `delete_media=1`, while unknown servers first try the media-cleanup variant and retry once without the query parameter on legacy-style rejection responses. +- `plugin_mastodon_delete_status_should_retry_without_delete_media()` — line 8556 — Decide whether a failed status DELETE should be retried without `delete_media=1`, while avoiding retries for missing statuses or active rate-limit stops. +- `plugin_mastodon_status_missing_response()` — line 8576 — Check whether an API response means that the referenced Mastodon status no longer exists. +- `plugin_mastodon_create_status()` — line 8589 — Create a Mastodon status. +- `plugin_mastodon_update_status()` — line 8616 — Update an existing Mastodon status. ## H. Import/export builders and synchronization orchestration -- `plugin_mastodon_build_entry_status_text()` — line 8557 — Build the status body used when exporting a FlatPress entry. -- `plugin_mastodon_build_comment_status_text()` — line 8625 — Build the status body used when exporting a FlatPress comment. -- `plugin_mastodon_import_remote_entry()` — line 8663 — Import a remote Mastodon status into FlatPress as an entry. -- `plugin_mastodon_import_remote_comment()` — line 8924 — Import a remote Mastodon reply into FlatPress as a comment while respecting comment tombstones, including early tombstones for locally deleted exported comments. -- `plugin_mastodon_import_remote_context_descendants()` — line 9013 — Import remote Mastodon replies from a fetched thread context while blocking tombstoned parent/child replies. -- `plugin_mastodon_old_thread_context_rotation_limit()` — line 9114 — Return the maximum number of known synchronized threads checked for replies per content sync run. -- `plugin_mastodon_collect_known_entry_context_targets()` — line 9130 — Collect known synchronized entry threads for optional rotating reply-context refreshes while respecting the synchronization start-date window. -- `plugin_mastodon_sync_remote_to_local()` — line 9203 — Synchronize remote Mastodon content into FlatPress with the durable start-date lower bound, scheduled-run window, and optional known-thread reply rotation. -- `plugin_mastodon_sync_local_to_remote()` — line 9270 — Synchronize local FlatPress content to Mastodon, including remote-sourced entry comment export, scheduled-run window filtering, dirty-queue processing, media-plan reuse of stored `media_ids`, and version-aware in-place alt-text updates. -- `plugin_mastodon_run_deletion_sync()` — line 9501 — Reconcile mapped deletions between FlatPress and Mastodon in a separate follow-up request, limiting scheduled remote existence lookups to the active window while cursoring large mapping sets. +- `plugin_mastodon_build_entry_status_text()` — line 8642 — Build the status body used when exporting a FlatPress entry. +- `plugin_mastodon_build_comment_status_text()` — line 8710 — Build the status body used when exporting a FlatPress comment. +- `plugin_mastodon_import_remote_entry()` — line 8748 — Import a remote Mastodon status into FlatPress as an entry. +- `plugin_mastodon_import_remote_comment()` — line 9009 — Import a remote Mastodon reply into FlatPress as a comment while respecting comment tombstones, including early tombstones for locally deleted exported comments. +- `plugin_mastodon_import_remote_context_descendants()` — line 9098 — Import remote Mastodon replies from a fetched thread context while blocking tombstoned parent/child replies. +- `plugin_mastodon_old_thread_context_rotation_limit()` — line 9199 — Return the maximum number of known synchronized threads checked for replies per content sync run. +- `plugin_mastodon_collect_known_entry_context_targets()` — line 9215 — Collect known synchronized entry threads for optional rotating reply-context refreshes while respecting the synchronization start-date window. +- `plugin_mastodon_sync_remote_to_local()` — line 9288 — Synchronize remote Mastodon content into FlatPress with the durable start-date lower bound, scheduled-run window, and optional known-thread reply rotation. +- `plugin_mastodon_sync_local_to_remote()` — line 9355 — Synchronize local FlatPress content to Mastodon, including remote-sourced entry comment export, scheduled-run window filtering, dirty-queue processing, media-plan reuse of stored `media_ids`, and version-aware in-place alt-text updates. +- `plugin_mastodon_run_deletion_sync()` — line 9586 — Reconcile mapped deletions between FlatPress and Mastodon in a separate follow-up request, limiting scheduled remote existence lookups to the active window while cursoring large mapping sets. ## Recommended reading order for new developers @@ -584,10 +587,10 @@ When changing the plugin, these clusters usually need to stay in sync: A change in one of these areas often requires corresponding updates in the simulation script. ## Alphabetical appendix -- `main()` — line 10134 — Keep the admin panel lifecycle compatible with FlatPress without extra processing. -- `onsubmit()` — line 10138 — Process configuration saves, OAuth actions, including app registration and authorization-code exchange, and the manual synchronization trigger. -- `plugin_mastodon_absolute_url()` — line 4443 — Convert a URL or path into an absolute URL when possible. -- `plugin_mastodon_admin_assign()` — line 10081 — Assign plugin data to Smarty for the admin panel. +- `main()` — line 10219 — Keep the admin panel lifecycle compatible with FlatPress without extra processing. +- `onsubmit()` — line 10223 — Process configuration saves, OAuth actions, including app registration and authorization-code exchange, and the manual synchronization trigger. +- `plugin_mastodon_absolute_url()` — line 4469 — Convert a URL or path into an absolute URL when possible. +- `plugin_mastodon_admin_assign()` — line 10166 — Assign plugin data to Smarty for the admin panel. - `plugin_mastodon_apcu_cache_key()` — line 1125 — Build the namespaced APCu key used by this plugin. - `plugin_mastodon_apcu_delete()` — line 1167 — Delete a value from APCu using FlatPress `apcu_delete_key()` when available. - `plugin_mastodon_apcu_enabled()` — line 1116 — Check whether shared APCu caching is available for the plugin. @@ -625,26 +628,26 @@ A change in one of these areas often requires corresponding updates in the simul - `plugin_mastodon_rate_limit_blocked_reason()` — line 1789 — Return the current rate-limit block reason. - `plugin_mastodon_rate_limit_blocked_response()` — line 1801 — Build the synthetic `429` response used for locally blocked requests. - `plugin_mastodon_rate_limit_state_error()` — line 1817 — Return the rate-limit reason that should be written to synchronization state. -- `plugin_mastodon_bbcode_attr_escape()` — line 5820 — Escape a value for safe BBCode attribute usage. -- `plugin_mastodon_bbcode_text_escape()` — line 5832 — Escape plain text embedded between BBCode tags. -- `plugin_mastodon_bbcode_plugin_active()` — line 4597 — Determine whether the BBCode plugin is active for the current FlatPress request. -- `plugin_mastodon_blog_base_url()` — line 4390 — Return the absolute base URL of the current FlatPress installation. -- `plugin_mastodon_build_authorize_url()` — line 8327 — Build the OAuth authorization URL using the scopes that the registered app may safely request. -- `plugin_mastodon_build_comment_status_text()` — line 8625 — Build the status body used when exporting a FlatPress comment. -- `plugin_mastodon_build_entry_status_text()` — line 8557 — Build the status body used when exporting a FlatPress entry. -- `plugin_mastodon_build_flatpress_tag_bbcode()` — line 4895 — Build Tag plugin BBCode from a list of remote Mastodon tags. -- `plugin_mastodon_build_imported_media_bbcode()` — line 6740 — Build FlatPress BBCode for imported remote media attachments, including AudioVideo optional description endtag text. -- `plugin_mastodon_cleanup_imported_text()` — line 5153 — Clean imported text before saving it to FlatPress. -- `plugin_mastodon_cleanup_uploaded_media()` — line 7372 — Best-effort cleanup for uploaded Mastodon media that never reached a final status request. -- `plugin_mastodon_collect_entry_files()` — line 7707 — Collect entry files recursively from the FlatPress content tree. -- `plugin_mastodon_collect_known_entry_context_targets()` — line 9130 — Collect known synchronized entry threads that should have their Mastodon reply context refreshed while respecting the synchronization start-date window. -- `plugin_mastodon_collect_local_entry_media()` — line 6211 — Collect local images, galleries, AudioVideo media, optional AudioVideo endtag descriptions, and video poster thumbnails referenced by an entry. -- `plugin_mastodon_comment_hash()` — line 5657 — Build a change-detection hash for a FlatPress comment. -- `plugin_mastodon_comment_parent_fields()` — line 4201 — Return the comment fields that may contain a parent reference. -- `plugin_mastodon_companion_plugins_status()` — line 4663 — Return the status of companion FlatPress plugins used for the full Mastodon feature set. -- `plugin_mastodon_compare_local_entries_for_export()` — line 7763 — Compare local FlatPress entries for Mastodon export order. +- `plugin_mastodon_bbcode_attr_escape()` — line 5846 — Escape a value for safe BBCode attribute usage. +- `plugin_mastodon_bbcode_text_escape()` — line 5858 — Escape plain text embedded between BBCode tags. +- `plugin_mastodon_bbcode_plugin_active()` — line 4623 — Determine whether the BBCode plugin is active for the current FlatPress request. +- `plugin_mastodon_blog_base_url()` — line 4416 — Return the absolute base URL of the current FlatPress installation. +- `plugin_mastodon_build_authorize_url()` — line 8377 — Build the OAuth authorization URL using the scopes that the registered app may safely request. +- `plugin_mastodon_build_comment_status_text()` — line 8710 — Build the status body used when exporting a FlatPress comment. +- `plugin_mastodon_build_entry_status_text()` — line 8642 — Build the status body used when exporting a FlatPress entry. +- `plugin_mastodon_build_flatpress_tag_bbcode()` — line 4921 — Build Tag plugin BBCode from a list of remote Mastodon tags. +- `plugin_mastodon_build_imported_media_bbcode()` — line 6766 — Build FlatPress BBCode for imported remote media attachments, including AudioVideo optional description endtag text. +- `plugin_mastodon_cleanup_imported_text()` — line 5179 — Clean imported text before saving it to FlatPress. +- `plugin_mastodon_cleanup_uploaded_media()` — line 7422 — Best-effort cleanup for uploaded Mastodon media that never reached a final status request. +- `plugin_mastodon_collect_entry_files()` — line 7757 — Collect entry files recursively from the FlatPress content tree. +- `plugin_mastodon_collect_known_entry_context_targets()` — line 9215 — Collect known synchronized entry threads that should have their Mastodon reply context refreshed while respecting the synchronization start-date window. +- `plugin_mastodon_collect_local_entry_media()` — line 6237 — Collect local images, galleries, AudioVideo media, optional AudioVideo endtag descriptions, and video poster thumbnails referenced by an entry. +- `plugin_mastodon_comment_hash()` — line 5683 — Build a change-detection hash for a FlatPress comment. +- `plugin_mastodon_comment_parent_fields()` — line 4227 — Return the comment fields that may contain a parent reference. +- `plugin_mastodon_companion_plugins_status()` — line 4689 — Return the status of companion FlatPress plugins used for the full Mastodon feature set. +- `plugin_mastodon_compare_local_entries_for_export()` — line 7813 — Compare local FlatPress entries for Mastodon export order. - `plugin_mastodon_configured_status_language()` — line 2278 — Read the configured FlatPress locale and return the Mastodon language code. -- `plugin_mastodon_create_status()` — line 8504 — Create a Mastodon status. +- `plugin_mastodon_create_status()` — line 8589 — Create a Mastodon status. - `plugin_mastodon_date_matches_content_window()` — line 2594 — Combine the durable sync-start lower bound with the scheduled-run recent-content window. - `plugin_mastodon_date_matches_sync_start()` — line 2559 — Determine whether a content date passes the configured sync start date. - `plugin_mastodon_datetime_date_key()` — line 2535 — Normalize a stored date/datetime string to the sync-start date-key format. @@ -652,32 +655,33 @@ A change in one of these areas often requires corresponding updates in the simul - `plugin_mastodon_default_deletion_stats()` — line 520 — Return the default counters for the last deletion synchronization. - `plugin_mastodon_default_options()` — line 120 — Return the default plugin option values. - `plugin_mastodon_default_state()` — line 533 — Return the default runtime state structure, including the targeted deletion-follow-up scope marker and deletion progress cursors. -- `plugin_mastodon_delete_media_attachment()` — line 7353 — Delete an uploaded Mastodon media attachment before it is attached to a final status. -- `plugin_mastodon_delete_status()` — line 8478 — Delete a Mastodon status, including media when requested. -- `plugin_mastodon_detect_local_comment_parent_id()` — line 4227 — Detect the local parent comment identifier from comment data. -- `plugin_mastodon_dom_children_to_flatpress()` — line 5207 — Convert DOM child nodes into FlatPress BBCode text. -- `plugin_mastodon_dom_node_to_flatpress()` — line 5225 — Convert a single DOM node into FlatPress BBCode text. -- `plugin_mastodon_domains_match()` — line 5139 — Determine whether two host names belong to the same domain family. -- `plugin_mastodon_emoticon_entity_to_unicode()` — line 4908 — Convert an emoticon HTML entity into a Unicode character. -- `plugin_mastodon_emoticon_map()` — line 4921 — Return the FlatPress emoticon-to-Unicode lookup map. -- `plugin_mastodon_emoticons_plugin_active()` — line 4648 — Determine whether the Emoticons plugin is active for the current FlatPress request. -- `plugin_mastodon_enabled_plugin_state()` — line 4515 — Determine whether a FlatPress plugin is enabled in the centralized plugin configuration. +- `plugin_mastodon_delete_media_attachment()` — line 7403 — Delete an uploaded Mastodon media attachment before it is attached to a final status. +- `plugin_mastodon_delete_status()` — line 8528 — Delete a Mastodon status; cached Mastodon versions before 4.4.0 omit `delete_media=1`, while unknown servers first try the media-cleanup variant and retry once without the query parameter on legacy-style rejection responses. +- `plugin_mastodon_delete_status_should_retry_without_delete_media()` — line 8556 — Decide whether a failed status DELETE should be retried without `delete_media=1`, while avoiding retries for missing statuses or active rate-limit stops. +- `plugin_mastodon_detect_local_comment_parent_id()` — line 4253 — Detect the local parent comment identifier from comment data. +- `plugin_mastodon_dom_children_to_flatpress()` — line 5233 — Convert DOM child nodes into FlatPress BBCode text. +- `plugin_mastodon_dom_node_to_flatpress()` — line 5251 — Convert a single DOM node into FlatPress BBCode text. +- `plugin_mastodon_domains_match()` — line 5165 — Determine whether two host names belong to the same domain family. +- `plugin_mastodon_emoticon_entity_to_unicode()` — line 4934 — Convert an emoticon HTML entity into a Unicode character. +- `plugin_mastodon_emoticon_map()` — line 4947 — Return the FlatPress emoticon-to-Unicode lookup map. +- `plugin_mastodon_emoticons_plugin_active()` — line 4674 — Determine whether the Emoticons plugin is active for the current FlatPress request. +- `plugin_mastodon_enabled_plugin_state()` — line 4541 — Determine whether a FlatPress plugin is enabled in the centralized plugin configuration. - `plugin_mastodon_ensure_state_dir()` — line 2788 — Ensure that the plugin runtime directory exists. -- `plugin_mastodon_entry_hash()` — line 5645 — Build a change-detection hash for a FlatPress entry. -- `plugin_mastodon_entry_media_signature()` — line 6422 — Build a signature for media references contained in entry content. -- `plugin_mastodon_exchange_code_for_token()` — line 8347 — Exchange an OAuth authorization code for an access token using the same negotiated scope string. -- `plugin_mastodon_extension_from_mime_type()` — line 5955 — Resolve a safe file extension from a MIME type. -- `plugin_mastodon_extend_time_limit()` — line 6939 — Refresh or raise the PHP execution time budget for long-running Mastodon work without lowering an existing higher or unlimited limit. -- `plugin_mastodon_extract_flatpress_tags()` — line 4742 — Extract FlatPress Tag plugin labels from an entry body. -- `plugin_mastodon_extract_url_token()` — line 4426 — Extract the URL token from a BBCode or attribute fragment. +- `plugin_mastodon_entry_hash()` — line 5671 — Build a change-detection hash for a FlatPress entry. +- `plugin_mastodon_entry_media_signature()` — line 6448 — Build a signature for media references contained in entry content. +- `plugin_mastodon_exchange_code_for_token()` — line 8397 — Exchange an OAuth authorization code for an access token using the same negotiated scope string. +- `plugin_mastodon_extension_from_mime_type()` — line 5981 — Resolve a safe file extension from a MIME type. +- `plugin_mastodon_extend_time_limit()` — line 6965 — Refresh or raise the PHP execution time budget for long-running Mastodon work without lowering an existing higher or unlimited limit. +- `plugin_mastodon_extract_flatpress_tags()` — line 4768 — Extract FlatPress Tag plugin labels from an entry body. +- `plugin_mastodon_extract_url_token()` — line 4452 — Extract the URL token from a BBCode or attribute fragment. - `plugin_mastodon_fediverse_creator_value()` — line 2198 — Build the fediverse creator meta value. -- `plugin_mastodon_fetch_account_statuses()` — line 8409 — Fetch statuses for the authenticated Mastodon account. -- `plugin_mastodon_fetch_media_attachment()` — line 7343 — Fetch a single Mastodon media attachment by ID. -- `plugin_mastodon_fetch_status()` — line 8467 — Fetch a single Mastodon status. -- `plugin_mastodon_fetch_status_context()` — line 8456 — Fetch the conversation context for a Mastodon status. +- `plugin_mastodon_fetch_account_statuses()` — line 8459 — Fetch statuses for the authenticated Mastodon account. +- `plugin_mastodon_fetch_media_attachment()` — line 7393 — Fetch a single Mastodon media attachment by ID. +- `plugin_mastodon_fetch_status()` — line 8517 — Fetch a single Mastodon status. +- `plugin_mastodon_fetch_status_context()` — line 8506 — Fetch the conversation context for a Mastodon status. - `plugin_mastodon_file_prestat()` — line 1827 — Read a cheap file metadata snapshot for cache validation. - `plugin_mastodon_file_prestat_signature()` — line 1847 — Convert a file metadata snapshot into a stable cache signature. -- `plugin_mastodon_flatpress_to_mastodon()` — line 5506 — Convert FlatPress content into Mastodon-ready plain text. +- `plugin_mastodon_flatpress_to_mastodon()` — line 5532 — Convert FlatPress content into Mastodon-ready plain text. - `plugin_mastodon_fp_config()` — line 751 — Return the FlatPress configuration, preferring the early-loaded core cache. - `plugin_mastodon_fp_config_value()` — line 787 — Read a nested FlatPress configuration value. - `plugin_mastodon_fp_timeoffset_seconds()` — line 803 — Return the configured FlatPress time offset in seconds for exact local admin-time conversion. @@ -688,64 +692,64 @@ A change in one of these areas often requires corresponding updates in the simul - `plugin_mastodon_sync_time_local_to_utc()` — line 891 — Convert the FlatPress-local admin synchronization time back to stored UTC. - `plugin_mastodon_format_admin_datetime()` — line 902 — Format stored UTC timestamps for the admin panel using FlatPress `timeoffset`, date format, and time format. - `plugin_mastodon_get_options()` — line 1858 — Load the saved plugin options and merge them with defaults. -- `plugin_mastodon_guess_subject()` — line 4339 — Guess a subject line from imported plain text. +- `plugin_mastodon_guess_subject()` — line 4365 — Guess a subject line from imported plain text. - `plugin_mastodon_head()` — line 2216 — Print Mastodon profile metadata into the HTML head. -- `plugin_mastodon_html_entity_decode()` — line 4381 — Decode HTML entities using the plugin defaults. -- `plugin_mastodon_http_build_query()` — line 8037 — Build an application/x-www-form-urlencoded query string. -- `plugin_mastodon_http_request()` — line 8093 — Perform an HTTP request using cURL or the stream fallback. -- `plugin_mastodon_http_request_multipart()` — line 7210 — Perform a multipart HTTP request. -- `plugin_mastodon_import_remote_comment()` — line 8924 — Import a remote Mastodon reply into FlatPress as a comment while respecting comment tombstones, including early tombstones for locally deleted exported comments. -- `plugin_mastodon_import_remote_context_descendants()` — line 9013 — Import remote Mastodon replies from a fetched thread context while blocking tombstoned parent/child replies. -- `plugin_mastodon_import_remote_entry()` — line 8663 — Import a remote Mastodon status into FlatPress as an entry. +- `plugin_mastodon_html_entity_decode()` — line 4407 — Decode HTML entities using the plugin defaults. +- `plugin_mastodon_http_build_query()` — line 8087 — Build an application/x-www-form-urlencoded query string. +- `plugin_mastodon_http_request()` — line 8143 — Perform an HTTP request using cURL or the stream fallback. +- `plugin_mastodon_http_request_multipart()` — line 7260 — Perform a multipart HTTP request. +- `plugin_mastodon_import_remote_comment()` — line 9009 — Import a remote Mastodon reply into FlatPress as a comment while respecting comment tombstones, including early tombstones for locally deleted exported comments. +- `plugin_mastodon_import_remote_context_descendants()` — line 9098 — Import remote Mastodon replies from a fetched thread context while blocking tombstoned parent/child replies. +- `plugin_mastodon_import_remote_entry()` — line 8748 — Import a remote Mastodon status into FlatPress as an entry. - `plugin_mastodon_instance_authority()` — line 2152 — Return the Mastodon instance authority used in fediverse creator metadata. -- `plugin_mastodon_instance_character_limit()` — line 8394 — Return the status character limit of the configured instance. -- `plugin_mastodon_instance_configuration()` — line 7049 — Load and cache the Mastodon instance configuration document. -- `plugin_mastodon_instance_media_description_limit()` — line 7072 — Return the media description length limit of the configured instance. -- `plugin_mastodon_instance_media_limit()` — line 7059 — Return the media attachment limit of the configured instance. -- `plugin_mastodon_instance_url_reserved_length()` — line 7085 — Return the reserved Mastodon character budget used for each URL. +- `plugin_mastodon_instance_character_limit()` — line 8444 — Return the status character limit of the configured instance. +- `plugin_mastodon_instance_configuration()` — line 7099 — Load and cache the Mastodon instance configuration document. +- `plugin_mastodon_instance_media_description_limit()` — line 7122 — Return the media description length limit of the configured instance. +- `plugin_mastodon_instance_media_limit()` — line 7109 — Return the media attachment limit of the configured instance. +- `plugin_mastodon_instance_url_reserved_length()` — line 7135 — Return the reserved Mastodon character budget used for each URL. - `plugin_mastodon_io_append_file()` — line 1013 — Append to a file with `FILE_APPEND | LOCK_EX`, without re-reading/re-writing the complete log payload. - `plugin_mastodon_io_read_file()` — line 936 — Read a file through the FlatPress I/O layer when available, allowing the core APCu file hotcache for small files. - `plugin_mastodon_io_read_file_uncached()` — line 952 — Read a file without FlatPress request-local caches. - `plugin_mastodon_file_permissions_mode()` — line 970 — Return the FlatPress `FILE_PERMISSIONS` mode used for plugin runtime files. -- `plugin_mastodon_admin_add_info_row()` — line 9962 — Add one admin diagnostics row when the value is available. -- `plugin_mastodon_admin_boolean_label()` — line 9947 — Return a localized yes/no/unknown label for admin diagnostics. +- `plugin_mastodon_admin_add_info_row()` — line 10047 — Add one admin diagnostics row when the value is available. +- `plugin_mastodon_admin_boolean_label()` — line 10032 — Return a localized yes/no/unknown label for admin diagnostics. - `plugin_mastodon_apply_file_permissions()` — line 979 — Apply FlatPress `FILE_PERMISSIONS` to a plugin runtime file. - `plugin_mastodon_io_write_file()` — line 992 — Write a file through the FlatPress I/O layer when available and enforce `FILE_PERMISSIONS` after successful writes. -- `plugin_mastodon_is_public_host()` — line 5025 — Determine whether a host name resolves to a public endpoint. -- `plugin_mastodon_lang_string()` — line 4485 — Return a localized plugin string or a provided fallback. -- `plugin_mastodon_limit_status_text()` — line 7131 — Truncate status text using Mastodon URL-budget rules. -- `plugin_mastodon_limit_text()` — line 5623 — Limit text to a maximum number of characters. -- `plugin_mastodon_list_local_entries()` — line 7891 — List local FlatPress entry identifiers. +- `plugin_mastodon_is_public_host()` — line 5051 — Determine whether a host name resolves to a public endpoint. +- `plugin_mastodon_lang_string()` — line 4511 — Return a localized plugin string or a provided fallback. +- `plugin_mastodon_limit_status_text()` — line 7181 — Truncate status text using Mastodon URL-budget rules. +- `plugin_mastodon_limit_text()` — line 5649 — Limit text to a maximum number of characters. +- `plugin_mastodon_list_local_entries()` — line 7941 — List local FlatPress entry identifiers. - `plugin_mastodon_local_item_date_key()` — line 2486 — Determine the date key of a local FlatPress entry or comment. - `plugin_mastodon_local_item_matches_content_window()` — line 2628 — Determine whether a local FlatPress item is inside the active content synchronization window. - `plugin_mastodon_local_item_matches_sync_start()` — line 2616 — Determine whether a local FlatPress item should be synchronized. -- `plugin_mastodon_local_item_timestamp()` — line 7734 — Resolve the best timestamp for a local FlatPress item. +- `plugin_mastodon_local_item_timestamp()` — line 7784 — Resolve the best timestamp for a local FlatPress item. - `plugin_mastodon_log()` — line 2797 — Append a line to the rotated append-only plugin sync log. - `plugin_mastodon_log_flush_skip_summaries()` — line 2842 — Flush aggregated skip counters as concise summary lines. - `plugin_mastodon_log_max_bytes()` — line 1053 — Return the sync.log rotation size limit. - `plugin_mastodon_log_rotate_files()` — line 1064 — Return the number of retained rotated sync logs. - `plugin_mastodon_log_skip()` — line 2812 — Aggregate high-volume skip messages by reason until the current sync phase ends. - `plugin_mastodon_mapping_matches_sync_start()` — line 2660 — Determine whether a stored synchronization mapping still belongs to the active sync-start window. -- `plugin_mastodon_mastodon_api()` — line 8211 — Call the Mastodon API and return the raw HTTP response. -- `plugin_mastodon_mastodon_hashtag_footer()` — line 4784 — Convert FlatPress tag labels into a Mastodon hashtag footer line. -- `plugin_mastodon_mastodon_html_to_flatpress()` — line 5404 — Convert Mastodon HTML content into FlatPress BBCode. -- `plugin_mastodon_mastodon_json()` — line 8260 — Call the Mastodon API and decode a JSON response. -- `plugin_mastodon_maybe_sync()` — line 9915 — Run the scheduled synchronization when the current request is due. -- `plugin_mastodon_media_copy_tree()` — line 5782 — Copy a directory tree used for media synchronization. -- `plugin_mastodon_media_delete_tree()` — line 5755 — Delete a directory tree used for imported media. -- `plugin_mastodon_media_download()` — line 6674 — Download a remote media asset. -- `plugin_mastodon_media_guess_mime_type()` — line 5854 — Guess the MIME type of a local media file. -- `plugin_mastodon_media_description_from_bbcode_content()` — line 6107 — Normalize optional AudioVideo BBCode content into a Mastodon media description. -- `plugin_mastodon_media_parse_tag_attributes()` — line 6076 — Parse key/value attributes from a FlatPress media tag. -- `plugin_mastodon_media_prepare_directory()` — line 5739 — Ensure that a media directory exists. -- `plugin_mastodon_media_relative_to_absolute()` — line 5726 — Resolve a FlatPress media path to an absolute file path. -- `plugin_mastodon_media_processing_attempts()` — line 7408 — Calculate media-type- and size-aware polling attempts for asynchronous Mastodon media processing. -- `plugin_mastodon_media_transfer_timeout()` — line 7432 — Calculate media-type- and size-aware HTTP transfer timeouts for uploads. -- `plugin_mastodon_media_type_from_mime()` — line 5921 — Classify a MIME type or extension as image, video, or audio. -- `plugin_mastodon_normalize_comment_parent_id()` — line 4210 — Normalize a stored local comment parent identifier. +- `plugin_mastodon_mastodon_api()` — line 8261 — Call the Mastodon API and return the raw HTTP response. +- `plugin_mastodon_mastodon_hashtag_footer()` — line 4810 — Convert FlatPress tag labels into a Mastodon hashtag footer line. +- `plugin_mastodon_mastodon_html_to_flatpress()` — line 5430 — Convert Mastodon HTML content into FlatPress BBCode. +- `plugin_mastodon_mastodon_json()` — line 8310 — Call the Mastodon API and decode a JSON response. +- `plugin_mastodon_maybe_sync()` — line 10000 — Run the scheduled synchronization when the current request is due. +- `plugin_mastodon_media_copy_tree()` — line 5808 — Copy a directory tree used for media synchronization. +- `plugin_mastodon_media_delete_tree()` — line 5781 — Delete a directory tree used for imported media. +- `plugin_mastodon_media_download()` — line 6700 — Download a remote media asset. +- `plugin_mastodon_media_guess_mime_type()` — line 5880 — Guess the MIME type of a local media file. +- `plugin_mastodon_media_description_from_bbcode_content()` — line 6133 — Normalize optional AudioVideo BBCode content into a Mastodon media description. +- `plugin_mastodon_media_parse_tag_attributes()` — line 6102 — Parse key/value attributes from a FlatPress media tag. +- `plugin_mastodon_media_prepare_directory()` — line 5765 — Ensure that a media directory exists. +- `plugin_mastodon_media_relative_to_absolute()` — line 5752 — Resolve a FlatPress media path to an absolute file path. +- `plugin_mastodon_media_processing_attempts()` — line 7458 — Calculate media-type- and size-aware polling attempts for asynchronous Mastodon media processing. +- `plugin_mastodon_media_transfer_timeout()` — line 7482 — Calculate media-type- and size-aware HTTP transfer timeouts for uploads. +- `plugin_mastodon_media_type_from_mime()` — line 5947 — Classify a MIME type or extension as image, video, or audio. +- `plugin_mastodon_normalize_comment_parent_id()` — line 4236 — Normalize a stored local comment parent identifier. - `plugin_mastodon_normalize_delete_sync_enabled()` — line 2454 — Normalize the toggle that enables or disables the follow-up deletion synchronization. - `plugin_mastodon_normalize_head_username()` — line 2113 — Normalize the configured Mastodon username for HTML head metadata. -- `plugin_mastodon_normalize_media_relative_path()` — line 5703 — Normalize a FlatPress media path and reject unsafe paths. +- `plugin_mastodon_normalize_media_relative_path()` — line 5729 — Normalize a FlatPress media path and reject unsafe paths. - `plugin_mastodon_normalize_import_synced_comments_as_entries()` — line 2400 — Normalize the toggle that allows importing already synchronized local comments as entries. - `plugin_mastodon_normalize_instance_url()` — line 2080 — Normalize the configured Mastodon instance URL. - `plugin_mastodon_normalize_old_thread_reply_check()` — line 2436 — Normalize the toggle that enables rotating context checks for known synchronized Mastodon threads. @@ -753,7 +757,7 @@ A change in one of these areas often requires corresponding updates in the simul - `plugin_mastodon_normalize_status_language()` — line 2256 — Normalize a FlatPress locale string to a Mastodon-compatible ISO 639-1 code. - `plugin_mastodon_normalize_sync_start_date()` — line 2315 — Normalize the configured sync start date. - `plugin_mastodon_normalize_sync_time()` — line 2298 — Normalize the configured daily sync time. -- `plugin_mastodon_normalize_tag_list()` — line 4711 — Normalize a list of tag labels. +- `plugin_mastodon_normalize_tag_list()` — line 4737 — Normalize a list of tag labels. - `plugin_mastodon_normalize_update_local_from_remote()` — line 2382 — Normalize the toggle that controls whether existing local content may be updated from remote Mastodon data. - `plugin_mastodon_oauth_legacy_scopes()` — line 563 — Return the legacy OAuth scope string used before scope discovery was added. - `plugin_mastodon_oauth_preferred_scopes()` — line 657 — Prefer the narrow `profile` scope on current instances and fall back to `read:accounts` on older ones. @@ -762,41 +766,41 @@ A change in one of these areas often requires corresponding updates in the simul - `plugin_mastodon_oauth_scopes()` — line 674 — Return the OAuth scopes that the currently registered app may safely request. - `plugin_mastodon_oauth_server_metadata()` — line 580 — Discover and cache OAuth authorization-server metadata from `/.well-known/oauth-authorization-server`. - `plugin_mastodon_oauth_supported_scopes()` — line 599 — Parse the discoverable OAuth scopes supported by the configured Mastodon instance. -- `plugin_mastodon_old_thread_context_rotation_limit()` — line 9114 — Return the maximum number of known synchronized threads checked for replies per content sync run. -- `plugin_mastodon_parse_http_response_headers()` — line 7929 — Parse raw HTTP response headers. -- `plugin_mastodon_parse_iso_datetime()` — line 4117 — Parse an ISO date/time string into FlatPress date format. -- `plugin_mastodon_parse_iso_timestamp()` — line 4135 — Parse an ISO date/time value into a Unix timestamp. -- `plugin_mastodon_photoswipe_plugin_active()` — line 4614 — Determine whether the PhotoSwipe plugin is active for the current FlatPress request. -- `plugin_mastodon_plain_text_from_bbcode()` — line 5067 — Convert FlatPress BBCode into plain text for Mastodon export, removing complete AudioVideo player tags including optional description content. +- `plugin_mastodon_old_thread_context_rotation_limit()` — line 9199 — Return the maximum number of known synchronized threads checked for replies per content sync run. +- `plugin_mastodon_parse_http_response_headers()` — line 7979 — Parse raw HTTP response headers. +- `plugin_mastodon_parse_iso_datetime()` — line 4143 — Parse an ISO date/time string into FlatPress date format. +- `plugin_mastodon_parse_iso_timestamp()` — line 4161 — Parse an ISO date/time value into a Unix timestamp. +- `plugin_mastodon_photoswipe_plugin_active()` — line 4640 — Determine whether the PhotoSwipe plugin is active for the current FlatPress request. +- `plugin_mastodon_plain_text_from_bbcode()` — line 5093 — Convert FlatPress BBCode into plain text for Mastodon export, removing complete AudioVideo player tags including optional description content. - `plugin_mastodon_profile_url()` — line 2182 — Build the public Mastodon profile URL used for the rel-me link. -- `plugin_mastodon_public_comment_url()` — line 5389 — Return the public URL for a specific FlatPress comment. -- `plugin_mastodon_public_comments_url()` — line 5361 — Return the public comments URL for a FlatPress entry. -- `plugin_mastodon_public_entry_url()` — line 5334 — Return the public URL for a FlatPress entry. -- `plugin_mastodon_public_url_for_mastodon()` — line 5048 — Return a Mastodon-safe public URL or an empty string. -- `plugin_mastodon_register_app()` — line 8304 — Register the FlatPress application on the configured Mastodon instance with the preferred discoverable scope set. -- `plugin_mastodon_remote_media_description()` — line 6545 — Resolve the best description for a remote attachment. -- `plugin_mastodon_remote_media_source_url()` — line 6505 — Resolve the best downloadable source URL for a remote attachment. -- `plugin_mastodon_remote_media_source_urls()` — line 6523 — Resolve direct-download fallback candidates for a remote attachment. +- `plugin_mastodon_public_comment_url()` — line 5415 — Return the public URL for a specific FlatPress comment. +- `plugin_mastodon_public_comments_url()` — line 5387 — Return the public comments URL for a FlatPress entry. +- `plugin_mastodon_public_entry_url()` — line 5360 — Return the public URL for a FlatPress entry. +- `plugin_mastodon_public_url_for_mastodon()` — line 5074 — Return a Mastodon-safe public URL or an empty string. +- `plugin_mastodon_register_app()` — line 8354 — Register the FlatPress application on the configured Mastodon instance with the preferred discoverable scope set. +- `plugin_mastodon_remote_media_description()` — line 6571 — Resolve the best description for a remote attachment. +- `plugin_mastodon_remote_media_source_url()` — line 6531 — Resolve the best downloadable source URL for a remote attachment. +- `plugin_mastodon_remote_media_source_urls()` — line 6549 — Resolve direct-download fallback candidates for a remote attachment. - `plugin_mastodon_remote_status_date_key()` — line 2509 — Determine the date key of a remote Mastodon status. -- `plugin_mastodon_remote_status_image_attachments()` — line 6496 — Extract image attachments from a remote Mastodon status. -- `plugin_mastodon_remote_status_is_importable()` — line 4189 — Determine whether a remote Mastodon status may be imported. +- `plugin_mastodon_remote_status_image_attachments()` — line 6522 — Extract image attachments from a remote Mastodon status. +- `plugin_mastodon_remote_status_is_importable()` — line 4215 — Determine whether a remote Mastodon status may be imported. - `plugin_mastodon_remote_status_matches_content_window()` — line 2649 — Determine whether a remote Mastodon status is inside the active content synchronization window. - `plugin_mastodon_remote_status_matches_sync_start()` — line 2638 — Determine whether a remote Mastodon status should be synchronized. -- `plugin_mastodon_remote_status_tags()` — line 4805 — Collect remote Mastodon tags from a status entity. -- `plugin_mastodon_remote_status_timestamp()` — line 4157 — Resolve the best FlatPress-adjusted timestamp for a remote Mastodon status. -- `plugin_mastodon_remote_status_visibility()` — line 4176 — Return the normalized visibility of a remote Mastodon status. -- `plugin_mastodon_replace_emoticon_shortcodes_with_unicode()` — line 4975 — Replace FlatPress emoticon shortcodes with Unicode glyphs. -- `plugin_mastodon_replace_unicode_emoticons_with_shortcodes()` — line 5003 — Replace Unicode emoticons with FlatPress shortcodes. -- `plugin_mastodon_resolve_comment_reply_target()` — line 4249 — Resolve the remote reply target for a local comment export. -- `plugin_mastodon_list_local_comment_ids()` — line 4302 — Scan the FlatPress comment directory directly so local reply export is not blocked by stale comment-list caches. -- `plugin_mastodon_response_error_message()` — line 8275 — Extract the most useful error message from an API response. -- `plugin_mastodon_run_deletion_sync()` — line 9501 — Run the deferred deletion synchronization in a follow-up request after content sync completed, with scheduled-window-limited remote existence lookups and progress cursors. -- `plugin_mastodon_run_sync()` — line 9832 — Run a full synchronization cycle. +- `plugin_mastodon_remote_status_tags()` — line 4831 — Collect remote Mastodon tags from a status entity. +- `plugin_mastodon_remote_status_timestamp()` — line 4183 — Resolve the best FlatPress-adjusted timestamp for a remote Mastodon status. +- `plugin_mastodon_remote_status_visibility()` — line 4202 — Return the normalized visibility of a remote Mastodon status. +- `plugin_mastodon_replace_emoticon_shortcodes_with_unicode()` — line 5001 — Replace FlatPress emoticon shortcodes with Unicode glyphs. +- `plugin_mastodon_replace_unicode_emoticons_with_shortcodes()` — line 5029 — Replace Unicode emoticons with FlatPress shortcodes. +- `plugin_mastodon_resolve_comment_reply_target()` — line 4275 — Resolve the remote reply target for a local comment export. +- `plugin_mastodon_list_local_comment_ids()` — line 4328 — Scan the FlatPress comment directory directly so local reply export is not blocked by stale comment-list caches. +- `plugin_mastodon_response_error_message()` — line 8325 — Extract the most useful error message from an API response. +- `plugin_mastodon_run_deletion_sync()` — line 9586 — Run the deferred deletion synchronization in a follow-up request after content sync completed, with scheduled-window-limited remote existence lookups and progress cursors. +- `plugin_mastodon_run_sync()` — line 9917 — Run a full synchronization cycle. - `plugin_mastodon_runtime_cache_clear()` — line 736 — Clear one request-local plugin cache bucket or the complete cache. - `plugin_mastodon_runtime_cache_get()` — line 694 — Return a value from the request-local plugin cache. - `plugin_mastodon_runtime_cache_set()` — line 718 — Store a value in the request-local plugin cache. -- `plugin_mastodon_safe_filename()` — line 5690 — Sanitize a file name for local storage. -- `plugin_mastodon_safe_path_component()` — line 5675 — Sanitize a string so it can be used as a path component. +- `plugin_mastodon_safe_filename()` — line 5716 — Sanitize a file name for local storage. +- `plugin_mastodon_safe_path_component()` — line 5701 — Sanitize a string so it can be used as a path component. - `plugin_mastodon_save_options()` — line 1917 — Persist plugin options. - `plugin_mastodon_scheduled_window_choices()` — line 2353 — Return the localized admin radio choices for the scheduled synchronization window. - `plugin_mastodon_scheduled_window_start_date()` — line 2577 — Return the FlatPress-local date key that starts the automatic scheduled sync window. @@ -807,58 +811,58 @@ A change in one of these areas often requires corresponding updates in the simul - `plugin_mastodon_should_check_old_thread_replies()` — line 2445 — Check whether known synchronized Mastodon threads should be checked for replies in rotating batches. - `plugin_mastodon_should_run_deletion_sync()` — line 2463 — Check whether the follow-up deletion synchronization is enabled. - `plugin_mastodon_should_update_local_from_remote()` — line 2391 — Check whether remote Mastodon updates may overwrite already existing local FlatPress content. -- `plugin_mastodon_state_comment_key()` — line 3152 — Build the compound state key used for comment mappings. -- `plugin_mastodon_state_get_comment_meta()` — line 3683 — Return mapping metadata for a local comment. -- `plugin_mastodon_state_has_dirty_comment()` — line 3370 — Check whether a comment is queued for synchronization outside the scheduled window. -- `plugin_mastodon_state_has_dirty_entry()` — line 3320 — Check whether an entry is queued for synchronization outside the scheduled window. -- `plugin_mastodon_state_set_comment_tombstone()` — line 3697 — Store a tombstone that blocks stale re-imports of one deleted remote comment. -- `plugin_mastodon_state_has_comment_tombstone()` — line 3717 — Check whether one remote Mastodon comment status was tombstoned locally. -- `plugin_mastodon_protect_locally_deleted_exported_comments()` — line 3728 — Tombstone locally deleted exported FlatPress comment mappings before the next content sync can stale-reimport them from Mastodon thread context. -- `plugin_mastodon_reattach_local_comment_to_entry_status()` — line 3774 — Remove a local imported reply parent link and reattach the surviving reply to the synchronized entry status after its remote parent reply disappeared. -- `plugin_mastodon_state_remove_pending_comment_remote_recheck()` — line 3834 — Remove one pending descendant recheck marker. -- `plugin_mastodon_state_get_pending_comment_remote_recheck()` — line 3848 — Return one pending descendant recheck marker. -- `plugin_mastodon_state_set_pending_comment_remote_recheck()` — line 3863 — Mark one local comment for follow-up verification after an ancestor disappeared remotely. -- `plugin_mastodon_state_set_deletions_pending()` — line 3889 — Persist whether another deletion follow-up request is pending, which scope it should run, and when it may start. -- `plugin_mastodon_deletion_sync_due()` — line 3903 — Check whether the pending deletion synchronization may start after its persisted not-before timestamp. -- `plugin_mastodon_state_has_comment_recheck_scope()` — line 3931 — Check whether the next deletion follow-up request should run only the targeted descendant recheck scope. -- `plugin_mastodon_build_comment_remote_child_index()` — line 3940 — Build a direct-child index for mapped remote reply trees. -- `plugin_mastodon_queue_comment_descendant_remote_rechecks()` — line 3968 — Queue only the direct mapped local children of one deleted remote comment for additional verification passes. -- `plugin_mastodon_process_pending_comment_remote_rechecks()` — line 4007 — Process pending descendant rechecks breadth-first so deeper reply chains can converge within the same targeted follow-up request. -- `plugin_mastodon_state_entry_media_attachment_signature()` — line 3661 — Return the stored attachment-signature for one entry mapping. -- `plugin_mastodon_state_entry_media_description_signature()` — line 3671 — Return the stored description-signature for one entry mapping. -- `plugin_mastodon_state_entry_remote_media()` — line 3631 — Return stored remote media descriptors for one entry mapping. -- `plugin_mastodon_state_get_entry_meta()` — line 3588 — Return mapping metadata for a local entry. -- `plugin_mastodon_normalize_deletions_pending_scope()` — line 3088 — Normalize the targeted deletion-follow-up scope marker. -- `plugin_mastodon_state_normalize()` — line 3101 — Normalize a runtime state array and fill in missing keys. +- `plugin_mastodon_state_comment_key()` — line 3178 — Build the compound state key used for comment mappings. +- `plugin_mastodon_state_get_comment_meta()` — line 3709 — Return mapping metadata for a local comment. +- `plugin_mastodon_state_has_dirty_comment()` — line 3396 — Check whether a comment is queued for synchronization outside the scheduled window. +- `plugin_mastodon_state_has_dirty_entry()` — line 3346 — Check whether an entry is queued for synchronization outside the scheduled window. +- `plugin_mastodon_state_set_comment_tombstone()` — line 3723 — Store a tombstone that blocks stale re-imports of one deleted remote comment. +- `plugin_mastodon_state_has_comment_tombstone()` — line 3743 — Check whether one remote Mastodon comment status was tombstoned locally. +- `plugin_mastodon_protect_locally_deleted_exported_comments()` — line 3754 — Tombstone locally deleted exported FlatPress comment mappings before the next content sync can stale-reimport them from Mastodon thread context. +- `plugin_mastodon_reattach_local_comment_to_entry_status()` — line 3800 — Remove a local imported reply parent link and reattach the surviving reply to the synchronized entry status after its remote parent reply disappeared. +- `plugin_mastodon_state_remove_pending_comment_remote_recheck()` — line 3860 — Remove one pending descendant recheck marker. +- `plugin_mastodon_state_get_pending_comment_remote_recheck()` — line 3874 — Return one pending descendant recheck marker. +- `plugin_mastodon_state_set_pending_comment_remote_recheck()` — line 3889 — Mark one local comment for follow-up verification after an ancestor disappeared remotely. +- `plugin_mastodon_state_set_deletions_pending()` — line 3915 — Persist whether another deletion follow-up request is pending, which scope it should run, and when it may start. +- `plugin_mastodon_deletion_sync_due()` — line 3929 — Check whether the pending deletion synchronization may start after its persisted not-before timestamp. +- `plugin_mastodon_state_has_comment_recheck_scope()` — line 3957 — Check whether the next deletion follow-up request should run only the targeted descendant recheck scope. +- `plugin_mastodon_build_comment_remote_child_index()` — line 3966 — Build a direct-child index for mapped remote reply trees. +- `plugin_mastodon_queue_comment_descendant_remote_rechecks()` — line 3994 — Queue only the direct mapped local children of one deleted remote comment for additional verification passes. +- `plugin_mastodon_process_pending_comment_remote_rechecks()` — line 4033 — Process pending descendant rechecks breadth-first so deeper reply chains can converge within the same targeted follow-up request. +- `plugin_mastodon_state_entry_media_attachment_signature()` — line 3687 — Return the stored attachment-signature for one entry mapping. +- `plugin_mastodon_state_entry_media_description_signature()` — line 3697 — Return the stored description-signature for one entry mapping. +- `plugin_mastodon_state_entry_remote_media()` — line 3657 — Return stored remote media descriptors for one entry mapping. +- `plugin_mastodon_state_get_entry_meta()` — line 3614 — Return mapping metadata for a local entry. +- `plugin_mastodon_normalize_deletions_pending_scope()` — line 3114 — Normalize the targeted deletion-follow-up scope marker. +- `plugin_mastodon_state_normalize()` — line 3127 — Normalize a runtime state array and fill in missing keys. - `plugin_mastodon_scheduler_source_signature()` — line 2931 — Return the current stat-based signature of the full state file. - `plugin_mastodon_scheduler_state_default()` — line 2910 — Return an empty scheduler state derived from the full default state. - `plugin_mastodon_scheduler_state_from_state()` — line 2978 — Build the lightweight scheduler summary from a full runtime state. - `plugin_mastodon_scheduler_state_normalize()` — line 2940 — Normalize a scheduler summary without touching full mapping arrays. -- `plugin_mastodon_scheduler_state_read()` — line 3022 — Load the lightweight scheduler summary and rebuild it conservatively when stale. +- `plugin_mastodon_scheduler_state_read()` — line 3043 — Load the compact scheduler summary through the APCu-capable FlatPress file I/O path, retry with an uncached scheduler-state read if a host returns stale cached content, and rebuild from full state only when missing, invalid, or truly stale. - `plugin_mastodon_scheduler_state_write()` — line 2998 — Persist the lightweight scheduler state. - `plugin_mastodon_state_read()` — line 2872 — Load the persisted runtime state from disk without storing or reading a full-state APCu fallback. -- `plugin_mastodon_state_remove_dirty_comment()` — line 3356 — Remove a comment from the dirty queue. -- `plugin_mastodon_state_remove_dirty_entry()` — line 3307 — Remove an entry from the dirty queue. -- `plugin_mastodon_state_remove_comment_mapping()` — line 3262 — Remove the mapping between a local comment and a remote status. -- `plugin_mastodon_state_remove_entry_mapping()` — line 3240 — Remove the mapping between a local entry and a remote status. -- `plugin_mastodon_state_set_comment_mapping()` — line 3205 — Store the mapping between a local comment and a remote status. -- `plugin_mastodon_state_set_dirty_comment()` — line 3333 — Add an older changed comment to the persistent dirty queue. -- `plugin_mastodon_state_set_dirty_entry()` — line 3287 — Add an older changed entry to the persistent dirty queue. -- `plugin_mastodon_state_set_entry_mapping()` — line 3167 — Store the mapping between a local entry and a remote status. -- `plugin_mastodon_state_write()` — line 3063 — Persist the runtime state to disk and refresh the compact scheduler summary after successful writes, without caching the full state in APCu. -- `plugin_mastodon_status_missing_response()` — line 8491 — Check whether an API response means that the referenced Mastodon status no longer exists. -- `plugin_mastodon_status_text_length()` — line 7099 — Calculate the Mastodon-visible status length with instance URL budgeting. -- `plugin_mastodon_stream_context_request()` — line 7959 — Perform an HTTP request through a stream context fallback. -- `plugin_mastodon_strip_flatpress_tag_bbcode()` — line 4767 — Remove Tag plugin BBCode blocks from entry content. -- `plugin_mastodon_strip_trailing_mastodon_hashtag_footer()` — line 4832 — Remove a trailing Mastodon hashtag footer from imported plain text. -- `plugin_mastodon_subject_line_is_noise()` — line 5111 — Determine whether an extracted line should be ignored as a subject. -- `plugin_mastodon_sync_due()` — line 9802 — Determine whether the scheduled synchronization is currently due. -- `plugin_mastodon_sync_local_to_remote()` — line 9270 — Synchronize local FlatPress content to Mastodon. -- `plugin_mastodon_sync_remote_to_local()` — line 9203 — Synchronize remote Mastodon content into FlatPress. -- `plugin_mastodon_tag_plugin_active()` — line 4580 — Determine whether the Tag plugin is active for the current FlatPress request. +- `plugin_mastodon_state_remove_dirty_comment()` — line 3382 — Remove a comment from the dirty queue. +- `plugin_mastodon_state_remove_dirty_entry()` — line 3333 — Remove an entry from the dirty queue. +- `plugin_mastodon_state_remove_comment_mapping()` — line 3288 — Remove the mapping between a local comment and a remote status. +- `plugin_mastodon_state_remove_entry_mapping()` — line 3266 — Remove the mapping between a local entry and a remote status. +- `plugin_mastodon_state_set_comment_mapping()` — line 3231 — Store the mapping between a local comment and a remote status. +- `plugin_mastodon_state_set_dirty_comment()` — line 3359 — Add an older changed comment to the persistent dirty queue. +- `plugin_mastodon_state_set_dirty_entry()` — line 3313 — Add an older changed entry to the persistent dirty queue. +- `plugin_mastodon_state_set_entry_mapping()` — line 3193 — Store the mapping between a local entry and a remote status. +- `plugin_mastodon_state_write()` — line 3089 — Persist the runtime state to disk and refresh the compact scheduler summary after successful writes, without caching the full state in APCu. +- `plugin_mastodon_status_missing_response()` — line 8576 — Check whether an API response means that the referenced Mastodon status no longer exists. +- `plugin_mastodon_status_text_length()` — line 7149 — Calculate the Mastodon-visible status length with instance URL budgeting. +- `plugin_mastodon_stream_context_request()` — line 8009 — Perform an HTTP request through a stream context fallback. +- `plugin_mastodon_strip_flatpress_tag_bbcode()` — line 4793 — Remove Tag plugin BBCode blocks from entry content. +- `plugin_mastodon_strip_trailing_mastodon_hashtag_footer()` — line 4858 — Remove a trailing Mastodon hashtag footer from imported plain text. +- `plugin_mastodon_subject_line_is_noise()` — line 5137 — Determine whether an extracted line should be ignored as a subject. +- `plugin_mastodon_sync_due()` — line 9887 — Determine whether the scheduled synchronization is currently due. +- `plugin_mastodon_sync_local_to_remote()` — line 9355 — Synchronize local FlatPress content to Mastodon. +- `plugin_mastodon_sync_remote_to_local()` — line 9288 — Synchronize remote Mastodon content into FlatPress. +- `plugin_mastodon_tag_plugin_active()` — line 4606 — Determine whether the Tag plugin is active for the current FlatPress request. - `plugin_mastodon_timestamp_date_key()` — line 2472 — Convert a FlatPress-adjusted timestamp into a stable date key. -- `plugin_mastodon_update_status()` — line 8531 — Update an existing Mastodon status. -- `plugin_mastodon_upload_media_items()` — line 7501 — Upload local media items to Mastodon and collect the created media IDs. -- `plugin_mastodon_verify_credentials()` — line 8377 — Verify the currently configured access token. -- `plugin_mastodon_wait_for_media_attachment()` — line 7452 — Poll an asynchronously processed Mastodon media attachment until it is ready or times out. -- `setup()` — line 10129 — Register the Mastodon admin panel template and assign plugin data to Smarty. +- `plugin_mastodon_update_status()` — line 8616 — Update an existing Mastodon status. +- `plugin_mastodon_upload_media_items()` — line 7551 — Upload local media items to Mastodon and collect the created media IDs. +- `plugin_mastodon_verify_credentials()` — line 8427 — Verify the currently configured access token. +- `plugin_mastodon_wait_for_media_attachment()` — line 7502 — Poll an asynchronously processed Mastodon media attachment until it is ready or times out. +- `setup()` — line 10214 — Register the Mastodon admin panel template and assign plugin data to Smarty. diff --git a/fp-plugins/mastodon/Plugin-Process-Flow.md b/fp-plugins/mastodon/Plugin-Process-Flow.md index 8a3b3527..a41eafa2 100644 --- a/fp-plugins/mastodon/Plugin-Process-Flow.md +++ b/fp-plugins/mastodon/Plugin-Process-Flow.md @@ -1,270 +1,512 @@ # Mastodon Plugin Process Flows -This document describes the current FlatPress Mastodon plugin flow as implemented in `fp-plugins/mastodon/plugin.mastodon.php`, the related post-success hooks in the FlatPress core, and the companion plugin dependencies used to render imported content. +This document describes the current FlatPress Mastodon plugin flow as implemented in +`fp-plugins/mastodon/plugin.mastodon.php`, the related post-success hooks in the FlatPress +core, the large regression harness in `simulate_mastodon_plugin.php`, and the companion +plugin dependencies used to render imported content. + +The diagrams are intentionally implementation-oriented. They are meant to help developers +answer three recurring maintenance questions: + +1. Which state file or mapping is authoritative at a given point? +2. Which Mastodon API endpoint is used, and which compatibility fallback exists? +3. Which path is optimized for ordinary web requests, scheduled runs, or manual repair runs? ## Scope and important state files -The plugin keeps two different state layers: +The plugin keeps a compact scheduler layer and a full synchronization layer. -- `fp-content/plugin_mastodon/scheduler-state.json` is the compact scheduler summary used on ordinary requests to decide whether a scheduled content or deletion synchronization is due. -- `fp-content/plugin_mastodon/state.json` is the full synchronization state. It contains entry/comment mappings, remote identifiers, dirty queues, tombstones, media metadata, cursors, and statistics. It is loaded only when a real sync, deletion sync, admin status view, or manual admin run needs the full state. +- `fp-content/plugin_mastodon/scheduler-state.json` is the compact scheduler summary used on + ordinary requests. It tells the plugin whether a scheduled content sync or a follow-up + deletion sync is due without loading the full mapping state. +- `fp-content/plugin_mastodon/state.json` is the full synchronization state. It contains + entry/comment mappings, reverse remote mappings, dirty queues, tombstones, media metadata, + cursors, statistics, and last-error information. +- `sync.lock` serializes real content/deletion sync runs. +- `sync.guard.json` stores short cooldown markers for content and deletion runs. +- `rate-limit-windows.json` stores persistent cross-request windows for media uploads, + status deletes, and status-page fetches. +- `sync.log` plus rotated `sync.log.1` to `sync.log.3` stores operational diagnostics. ```mermaid flowchart TD Request["FlatPress request"] Init["init hook: plugin_mastodon_maybe_sync"] - SchedulerRead["Read scheduler-state.json through APCu-capable FlatPress I/O"] + Options["Load Mastodon options"] + SchedulerRead["Read scheduler-state.json through FlatPress I/O"] + SchedulerStale{"Summary missing or stale?"} + FullStateForSummary["Load state.json only to rebuild compact summary"] Due{"Content sync due?"} DeleteDue{"Deletion sync due and deletions pending?"} - NoWork["No Mastodon HTTP request and no full state load"] + NoWork["Fast path: no Mastodon HTTP, no full state load"] + Lock["Acquire sync.lock for real work"] + GuardFile["sync.guard.json cooldown check"] + RateGuard["Start local API budget guard"] FullState["Load full state.json"] ContentSync["plugin_mastodon_run_sync"] DeletionSync["plugin_mastodon_run_deletion_sync"] - StateWrite["Write state.json and scheduler-state.json"] - - Request --> Init - Init --> SchedulerRead - SchedulerRead --> Due + StateWrite["Write state.json"] + SchedulerWrite["Write scheduler-state.json"] + RateWindow["Persist rate-limit-windows.json"] + Log["Append and rotate sync.log"] + + Request --> Init --> Options --> SchedulerRead --> SchedulerStale + SchedulerStale -- "Yes" --> FullStateForSummary --> SchedulerWrite --> Due + SchedulerStale -- "No" --> Due Due -- "No" --> DeleteDue DeleteDue -- "No" --> NoWork - Due -- "Yes" --> FullState --> ContentSync --> StateWrite - DeleteDue -- "Yes" --> FullState --> DeletionSync --> StateWrite + Due -- "Yes" --> GuardFile --> Lock --> RateGuard --> FullState --> ContentSync + DeleteDue -- "Yes" --> GuardFile --> Lock --> RateGuard --> FullState --> DeletionSync + ContentSync --> StateWrite --> SchedulerWrite --> RateWindow --> Log + DeletionSync --> StateWrite --> SchedulerWrite --> RateWindow --> Log ``` -## 1. Bidirectional content synchronization +### Full state schema and mapping lifecycle -### 1.1 FlatPress entry to Mastodon status - -A local FlatPress entry is exported as a Mastodon top-level status when it is inside the active synchronization window, or when it has been queued by post-success dirty tracking. A manual full sync keeps the repair behavior and scans all local entries. +The full state is not just a cache. It is the durable reconciliation layer between FlatPress +file IDs and Mastodon status IDs. ```mermaid -sequenceDiagram - autonumber - actor Author as FlatPress author - participant Core as FlatPress Core - participant Hook as entry_saved hook - participant Plugin as Mastodon Plugin - participant State as state.json - participant Media as Local media planner - participant API as Mastodon API +flowchart TD + State["state.json"] - Author->>Core: Save or update entry - Core-->>Hook: entry_saved after successful write - Hook->>Plugin: plugin_mastodon_on_entry_saved - Plugin->>Plugin: Check remote-write guard - alt Guard active - Plugin-->>State: Do not mark dirty - else Local manual change - Plugin->>State: Read mappings for entry - alt Existing local-to-remote mapping changed outside window - Plugin->>State: Add dirty_entries[entry_id] - else Inside active window - Plugin->>State: Remove stale dirty entry marker - else New entry outside active window - Plugin->>State: Keep unqueued until full/manual sync or active window - end - Plugin->>State: Persist scheduler summary + subgraph ForwardMaps["Forward mappings"] + Entries["entries[entry_id]
local entry -> remote top-level status"] + Comments["comments[entry_id/comment_id]
local comment -> remote reply status"] + RemoteMedia["remote_media / media signatures
stored media IDs and alt-text state"] end - Note over Plugin,API: During scheduled or manual sync - Plugin->>Plugin: plugin_mastodon_list_local_entries_for_sync - Plugin->>Plugin: Select active-window entries plus dirty entries - Plugin->>Plugin: plugin_mastodon_build_entry_status_text - Plugin->>Media: plugin_mastodon_collect_local_entry_media - Media-->>Plugin: Media IDs to reuse or local files to upload - alt Entry already has remote_id - Plugin->>API: PUT /api/v1/statuses/{id} - API-->>Plugin: Updated status JSON - Plugin->>State: Update entries mapping and clear dirty marker - else New export - Plugin->>API: POST /api/v1/statuses - API-->>Plugin: Created status JSON - Plugin->>State: Store entries[entry_id] and entries_remote[status_id] + subgraph ReverseMaps["Reverse mappings"] + EntriesRemote["entries_remote[remote_status_id]
remote status -> local entry"] + CommentsRemote["comments_remote[remote_status_id]
remote reply -> local comment"] + end + + subgraph WorkQueues["Work queues and cursors"] + DirtyEntries["dirty_entries"] + DirtyComments["dirty_comments"] + PendingRechecks["pending_comment_remote_rechecks"] + EntryCursor["deletion_cursor_entries"] + CommentCursor["deletion_cursor_comments"] + OldThreadCursor["old_thread_context_cursor"] end + + subgraph Safety["Safety and accounting"] + Tombstones["comment_tombstones"] + ProtectedDeletes["protected local deleted exported comments"] + ContentStats["content_stats"] + DeletionStats["deletion_stats"] + LastError["last_error"] + end + + State --> ForwardMaps + State --> ReverseMaps + State --> WorkQueues + State --> Safety + Entries <--> EntriesRemote + Comments <--> CommentsRemote + DirtyEntries --> Entries + DirtyComments --> Comments + PendingRechecks --> CommentsRemote + Tombstones --> CommentsRemote + RemoteMedia --> Entries ``` -### 1.2 FlatPress comment to Mastodon reply +## 1. Bidirectional content synchronization + +### 1.1 FlatPress entry candidate selection and dirty decision + +A local FlatPress entry is exported as a Mastodon top-level status only when it is inside the +active synchronization window, when a post-success hook queued it as dirty, or when a manual +full sync intentionally scans all entries. -FlatPress comments are exported only after the parent entry has a remote status mapping. The plugin resolves the correct Mastodon `in_reply_to_id` from the entry mapping or from an existing parent-comment mapping. +```mermaid +flowchart TD + EntrySaved["FlatPress entry_save succeeded"] + Hook["entry_saved hook -> plugin_mastodon_on_entry_saved"] + Guard{"plugin-owned local-write guard active?"} + Mapping{"Entry already mapped to remote status?"} + Window{"Entry date inside automatic sync window?"} + Changed{"Hash or relevant media signature changed?"} + Dirty["Add or keep dirty_entries[entry_id]"] + ClearDirty["Clear stale dirty marker"] + Ignore["Do not mark dirty"] + Summary["Persist scheduler summary"] + + EntrySaved --> Hook --> Guard + Guard -- "Yes" --> Ignore --> Summary + Guard -- "No" --> Mapping + Mapping -- "Yes" --> Changed + Changed -- "Yes" --> Dirty --> Summary + Changed -- "No" --> ClearDirty --> Summary + Mapping -- "No" --> Window + Window -- "Yes" --> ClearDirty + Window -- "No" --> Ignore +``` + +```mermaid +flowchart TD + Start["plugin_mastodon_list_local_entries_for_sync"] + DirtyLookup["Build lookup from dirty_entries and dirty_comments"] + CollectFiles["Collect entry files from CONTENT_DIR"] + NextFile["Next entry file"] + Force{"manual full sync force=true?"} + Dirty{"Entry ID in dirty lookup?"} + Window{"Entry ID date inside active content window?"} + SkipWithoutParse["Skip without entry_parse"] + Parse["entry_parse"] + Draft{"Draft category?"} + Add["Add to ordered export list"] + Sort["Sort by local item timestamp"] + Result["Return selected entries"] + + Start --> DirtyLookup --> CollectFiles --> NextFile + NextFile --> Force + Force -- "Yes" --> Parse + Force -- "No" --> Dirty + Dirty -- "Yes" --> Parse + Dirty -- "No" --> Window + Window -- "Yes" --> Parse + Window -- "No" --> SkipWithoutParse --> NextFile + Parse --> Draft + Draft -- "Yes" --> NextFile + Draft -- "No" --> Add --> NextFile + NextFile --> Sort --> Result +``` + +### 1.2 FlatPress entry to Mastodon status + +The export path builds text, tags, media IDs, and update metadata before it decides between +`POST /api/v1/statuses` and `PUT /api/v1/statuses/:id`. ```mermaid sequenceDiagram autonumber - actor Visitor as FlatPress visitor or author - participant Core as FlatPress Core - participant Hook as comment_saved hook - participant Plugin as Mastodon Plugin + participant Sync as plugin_mastodon_sync_local_to_remote participant State as state.json + participant Text as Text and tag builder + participant Media as Media planner participant API as Mastodon API - - Visitor->>Core: Save FlatPress comment - Core-->>Hook: comment_saved after successful write - Hook->>Plugin: plugin_mastodon_on_comment_saved - Plugin->>Plugin: Check remote-write guard - alt Guard active - Plugin-->>State: Do not mark dirty - else Local manual comment - Plugin->>State: Read comment and entry mappings - alt Existing mapped comment changed outside window - Plugin->>State: Add dirty_comments[entry_id/comment_id] - else New comment on unsynchronized local entry inside window - Plugin->>State: Queue entry as dirty so the parent status is created first - else New comment without exportable parent - Plugin->>State: Do not export yet + participant Cleanup as Best-effort cleanup + + Sync->>State: Read entry mapping and dirty marker + Sync->>Text: plugin_mastodon_build_entry_status_text + Text-->>Sync: Plain Mastodon status text within configured URL and character budget + Sync->>Media: collect, validate, and prepare media plan + Media-->>Sync: media_ids, reuse decision, media_attributes, uploaded_media_ids + + alt Existing remote_id + alt Only media descriptions changed and instance supports media_attributes + Sync->>API: PUT /api/v1/statuses/:id with media_attributes + else Unchanged attachments can be reused + Sync->>API: PUT /api/v1/statuses/:id with stored media_ids + else Changed attachments must be uploaded first + Sync->>API: POST /api/v2/media for each changed local file + Sync->>API: PUT /api/v1/statuses/:id with new media_ids end + API-->>Sync: Updated status JSON + Sync->>State: Update entries mapping, remote media meta, signatures, clear dirty marker + else New export + Sync->>API: POST /api/v1/statuses with status, visibility, language, media_ids + API-->>Sync: Created status JSON + Sync->>State: Store entries[entry_id] and entries_remote[status_id] end - Note over Plugin,API: During local-to-remote sync - Plugin->>State: Require entry remote_id - Plugin->>Plugin: Build comment status text - Plugin->>Plugin: Resolve reply target - alt Parent comment has remote mapping - Plugin->>API: POST /api/v1/statuses with in_reply_to_id=parent comment status - else Top-level FlatPress comment - Plugin->>API: POST /api/v1/statuses with in_reply_to_id=entry status + alt Status request fails after new uploads + Sync->>Cleanup: DELETE /api/v1/media/:id for uploaded but unattached media end - API-->>Plugin: Created reply status JSON - Plugin->>State: Store comments and comments_remote mapping - Plugin->>State: Clear dirty comment marker ``` -### 1.3 FlatPress replies to comments become Mastodon replies to replies +### 1.3 FlatPress comment and comment reply export + +FlatPress comments are exported only after the parent entry has a remote status mapping. Nested +comment replies are delayed until their local parent comment has a remote mapping. -FlatPress comments can reply to other FlatPress comments. The plugin delays a child comment export until its parent comment has a remote Mastodon mapping. This avoids creating replies under the entry status when they should actually be replies under another Mastodon reply. +```mermaid +flowchart TD + CommentSaved["FlatPress comment_save succeeded"] + Hook["comment_saved hook -> plugin_mastodon_on_comment_saved"] + Guard{"plugin-owned local-write guard active?"} + EntryMapped{"Parent entry has remote status?"} + CommentMapped{"Comment already has remote status?"} + ParentLocal["Detect local parent comment"] + ParentPending{"Parent comment exists but export still pending?"} + DirtyComment["Add dirty_comments[entry_id/comment_id]"] + DirtyEntry["Queue dirty_entries[entry_id] so parent entry exports first"] + NoExport["Do not export yet"] + Persist["Persist scheduler summary"] + + CommentSaved --> Hook --> Guard + Guard -- "Yes" --> NoExport --> Persist + Guard -- "No" --> EntryMapped + EntryMapped -- "No" --> DirtyEntry --> Persist + EntryMapped -- "Yes" --> CommentMapped + CommentMapped -- "Yes" --> DirtyComment --> Persist + CommentMapped -- "No" --> ParentLocal --> ParentPending + ParentPending -- "Yes" --> DirtyComment + ParentPending -- "No" --> DirtyComment +``` ```mermaid flowchart TD - Comment["FlatPress comment selected for export"] - DetectParent["Detect local parent comment ID"] - ParentPending{"Parent comment exists and export is still pending?"} - Defer["Defer this comment and retry in the same run"] - Resolve["Resolve reply target"] - ParentRemote{"Parent comment has remote_id?"} - ReplyToParent["Create or update Mastodon status in_reply_to_id = parent remote_id"] - ReplyToEntry["Create or update Mastodon status in_reply_to_id = entry remote_id"] - Mapping["Store comment mapping with parent_comment_id and in_reply_to_remote_id"] - Exhausted{"No progress after retry guard?"} - LogDeferred["Log deferred export"] - - Comment --> DetectParent --> ParentPending - ParentPending -- "Yes" --> Defer --> Exhausted - Exhausted -- "No" --> DetectParent - Exhausted -- "Yes" --> LogDeferred - ParentPending -- "No" --> Resolve --> ParentRemote - ParentRemote -- "Yes" --> ReplyToParent --> Mapping - ParentRemote -- "No" --> ReplyToEntry --> Mapping + Candidate["Comment candidate selected for export"] + EntryRemote{"Entry remote_id exists?"} + DetectParent["plugin_mastodon_detect_local_comment_parent_id"] + Resolve["plugin_mastodon_resolve_comment_reply_target"] + ParentRemote{"Parent comment remote_id exists?"} + Defer["Defer and retry later in this run"] + NoProgress{"No progress guard exhausted?"} + LogDeferred["Log deferred export and keep dirty marker"] + ReplyToParent["POST /api/v1/statuses in_reply_to_id=parent comment remote_id"] + ReplyToEntry["POST /api/v1/statuses in_reply_to_id=entry remote_id"] + Mapping["Store comments and comments_remote with parent_comment_id and in_reply_to_remote_id"] + ClearDirty["Clear dirty comment marker"] + + Candidate --> EntryRemote + EntryRemote -- "No" --> LogDeferred + EntryRemote -- "Yes" --> DetectParent --> Resolve --> ParentRemote + ParentRemote -- "Yes" --> ReplyToParent --> Mapping --> ClearDirty + ParentRemote -- "No, parent pending" --> Defer --> NoProgress + NoProgress -- "No" --> DetectParent + NoProgress -- "Yes" --> LogDeferred + ParentRemote -- "No parent or top-level comment" --> ReplyToEntry --> Mapping ``` ### 1.4 Mastodon top-level status to FlatPress entry -The plugin fetches account statuses from Mastodon, filters unsupported or out-of-window statuses, converts Mastodon HTML to FlatPress BBCode, imports remote media, and writes an entry under the remote-write guard. +The remote-to-local path imports top-level statuses owned by the configured Mastodon account, +filters them by visibility/window/source, converts HTML to FlatPress markup, imports remote +media, and writes through the local-write guard. ```mermaid sequenceDiagram autonumber participant Sync as plugin_mastodon_sync_remote_to_local participant API as Mastodon API - participant Converter as HTML and media import - participant Guard as Remote-write guard + participant Filter as Import filters + participant Convert as HTML/media/tag conversion + participant Guard as Local-write guard participant Core as FlatPress Core participant State as state.json Sync->>API: GET /api/v1/accounts/verify_credentials API-->>Sync: Account ID - Sync->>API: GET account statuses since last_remote_status_id + Sync->>API: GET /api/v1/accounts/:id/statuses with paging and local budgets API-->>Sync: Status list loop Each remote top-level status - Sync->>Sync: Check visibility and content window - alt Not importable or outside window - Sync->>Sync: Aggregate skip log + Sync->>Filter: visibility, reblog/reply, sync_start_date, scheduled window, known local-source mapping + alt Not importable + Filter-->>Sync: Aggregate skip reason else Importable - Sync->>Converter: Convert HTML to FlatPress BBCode - Converter->>Converter: Map links, quotes, code, lists, emoji shortcodes - Converter->>Converter: Download media attachments when present - Guard->>Guard: Enter local-write guard - Guard->>Core: entry_save entry - Core-->>Guard: entry_saved hook fires - Guard->>Guard: Hook ignores plugin-owned write - Guard->>Guard: Leave local-write guard - Sync->>State: Store remote-source entry mapping + Sync->>Convert: Mastodon HTML -> FlatPress BBCode + Convert->>Convert: links, quotes, code, lists, mentions, emoji, invisible spans + Convert->>Convert: download remote media and append BBCode + Convert->>Convert: import remote tags as FlatPress [tag] metadata when possible + Sync->>Guard: Enter plugin-owned local-write guard + Guard->>Core: entry_save imported entry + Core-->>Guard: entry_saved hook fires but is ignored by guard + Sync->>Guard: Leave local-write guard + Sync->>State: Store entries, entries_remote, last_remote_status_id, source metadata end end ``` ### 1.5 Mastodon replies in a known imported thread to FlatPress comments -After importing or refreshing a known entry status, the plugin fetches the Mastodon context and walks descendants. It imports only public/importable replies that are not blocked by local tombstones and whose parent relationship can be resolved. +After importing or refreshing a known entry status, the plugin fetches the Mastodon context and +walks descendants. This path must avoid resurrecting locally deleted comments, must not break +thread order, and must cope with temporarily unresolved parents. ```mermaid flowchart TD - EntryMapped["Known entry mapping: local entry <-> remote status"] - FetchContext["Fetch /api/v1/statuses/{id}/context"] - Descendants["Read context descendants"] - BuildIndex["Build remote status lookup"] + EntryMapped["Known imported entry mapping"] + FetchContext["GET /api/v1/statuses/:id/context"] + Descendants["Read descendants"] + BuildIndex["Build remote child index and lookup"] NextReply["Next descendant reply"] - Importable{"Public/importable, in sync start, no tombstone?"} + Importable{"Public/importable and inside sync-start window?"} + Tombstone{"comment_tombstone or protected local delete?"} + AlreadyMapped{"Already mapped to local comment?"} ParentKnown{"Parent is entry status or known/imported reply?"} - Wait["Keep reply pending for another pass"] - Import["plugin_mastodon_import_remote_comment"] - Quote{"Quote imported reply parent enabled?"} + Pending["Keep pending for later pass or pending_comment_remote_rechecks"] + Quote{"quote_imported_reply_parent enabled?"} AddQuote["Prepend FlatPress quote block"] - SaveComment["comment_save under remote-write guard"] - StoreMapping["Store comments and comments_remote mapping"] - Fallback{"No progress in pass?"} - ForceImport["Import remaining with unresolved parent reference"] + Import["plugin_mastodon_import_remote_comment"] + Guard["comment_save under local-write guard"] + Mapping["Store comments and comments_remote mapping"] + Recheck["Queue descendant rechecks when local/remote deletion changed thread"] + Progress{"Progress in this pass?"} + ForceImport["Force remaining import with unresolved parent reference only when safe"] EntryMapped --> FetchContext --> Descendants --> BuildIndex --> NextReply NextReply --> Importable Importable -- "No" --> NextReply - Importable -- "Yes" --> ParentKnown - ParentKnown -- "No" --> Wait --> Fallback - ParentKnown -- "Yes" --> Import --> Quote - Quote -- "Yes" --> AddQuote --> SaveComment - Quote -- "No" --> SaveComment - SaveComment --> StoreMapping --> NextReply - Fallback -- "Progress made" --> NextReply - Fallback -- "No progress" --> ForceImport --> SaveComment + Importable -- "Yes" --> Tombstone + Tombstone -- "Yes" --> NextReply + Tombstone -- "No" --> AlreadyMapped + AlreadyMapped -- "Yes" --> Recheck --> NextReply + AlreadyMapped -- "No" --> ParentKnown + ParentKnown -- "No" --> Pending --> Progress + ParentKnown -- "Yes" --> Quote + Quote -- "Yes" --> AddQuote --> Import + Quote -- "No" --> Import + Import --> Guard --> Mapping --> Recheck --> NextReply + Progress -- "Progress made" --> NextReply + Progress -- "No progress" --> ForceImport --> Guard ``` -## 2. Media, attachments, tags, and hashtags +## 2. Text, URLs, tags, media, and companion plugins + +### 2.1 Local-to-remote status-text pipeline + +The status-text builder intentionally keeps Mastodon-visible length calculation separate from +FlatPress storage. Mastodon counts each URL as the instance's configured URL budget. + +```mermaid +flowchart LR + Source["FlatPress entry or comment body"] + StripMeta["Strip local-only metadata
draft/category/tag BBCode where needed"] + BBCode["Flatten or convert supported BBCode"] + MediaRefs["Remove media-only markup from plain text"] + Links["Normalize public URLs and link text"] + Emoji["Convert FlatPress emoticons or keep shortcode-safe text"] + Tags["Extract and normalize FlatPress tags"] + Footer["Build plugin-owned hashtag footer"] + Join["Join body and footer"] + InstanceLimits["Load /api/v2/instance limits
max_characters and URL reserved length"] + Budget["plugin_mastodon_status_text_length"] + Truncate["plugin_mastodon_limit_status_text"] + Result["Mastodon status text"] + + Source --> StripMeta --> BBCode --> MediaRefs --> Links --> Emoji --> Tags --> Footer --> Join + Join --> InstanceLimits --> Budget + Budget --> Truncate --> Result +``` -### 2.1 FlatPress entry media to Mastodon media attachments +### 2.2 Remote-to-local HTML and BBCode pipeline -The plugin scans entry content for image, gallery, audio, and video BBCode. It validates each local file, respects the Mastodon instance media limits, uploads changed attachments, or reuses existing remote media IDs when possible. +Remote Mastodon content arrives as HTML. The plugin converts only the safe and supported +structures into FlatPress markup and then appends imported media markup. + +```mermaid +flowchart LR + HTML["Mastodon status.content HTML"] + DOM["DOMDocument-based parsing with fallback cleanup"] + Blocks["p, br, blockquote, pre, code, ul, ol, li"] + Links["a tags -> FlatPress URL markup or plain URL"] + Mentions["mentions and hashtags preserved as text/links"] + Invisible["Mastodon invisible/ellipsis spans cleaned"] + Emoji["custom emoji shortcode/text handling"] + Cleanup["Final whitespace and round-trip cleanup"] + Media["Append imported media BBCode"] + Tags["Append [tag] metadata when Tag plugin is active"] + Result["FlatPress entry/comment content"] + + HTML --> DOM --> Blocks --> Links --> Mentions --> Invisible --> Emoji --> Cleanup + Cleanup --> Media --> Tags --> Result +``` + +### 2.3 Local media collection and validation + +The plugin scans entries for image, gallery, audio, and video markup, then validates the +corresponding files against instance capabilities and internal budgets. ```mermaid flowchart TD EntryContent["FlatPress entry content"] Collect["plugin_mastodon_collect_local_entry_media"] - ImageTags["img tags and inline img BBCode"] - GalleryTags["gallery tags and gallery captions"] - AudioVideoTags["audioplayer and videoplayer tags"] - Validate["Validate file exists, MIME type, size limits, media type"] - Plan["plugin_mastodon_prepare_entry_media_sync_plan"] - Reuse{"Existing remote media signature unchanged?"} - Upload["Upload via /api/v2/media or media endpoint"] - Wait["Poll media processing until URL is available"] - ReuseIDs["Reuse previous media IDs"] - Status["Create or update Mastodon status with media_ids"] - Cleanup{"Status request failed after upload?"} - DeleteUpload["Best-effort DELETE uploaded media attachments"] - StoreMeta["Store remote_media and media signatures in state"] + Images["[img] and inline image BBCode"] + Galleries["[gallery] plus gallery captions"] + Audio["AudioVideo audio tags"] + Video["AudioVideo video tags and optional poster"] + Items["plugin_mastodon_prepare_entry_media_items"] + Mime["Guess MIME type and media type"] + Limits["Instance media limits from /api/v2/instance
supported_mime_types, size limits, description_limit"] + Rules{"Uploadable under Mastodon rules?"} + Reject["Skip item and log validation reason"] + Accepted["Accepted local media item"] + AttachmentLimit["Apply max_media_attachments and one audio/video style constraints"] + PlanInput["Input for media sync plan"] EntryContent --> Collect - Collect --> ImageTags --> Validate - Collect --> GalleryTags --> Validate - Collect --> AudioVideoTags --> Validate - Validate --> Plan --> Reuse - Reuse -- "Yes" --> ReuseIDs --> Status - Reuse -- "No" --> Upload --> Wait --> Status - Status --> Cleanup - Cleanup -- "Yes" --> DeleteUpload - Cleanup -- "No" --> StoreMeta + Collect --> Images --> Items + Collect --> Galleries --> Items + Collect --> Audio --> Items + Collect --> Video --> Items + Items --> Mime --> Limits --> Rules + Rules -- "No" --> Reject + Rules -- "Yes" --> Accepted --> AttachmentLimit --> PlanInput +``` + +### 2.4 Media reuse, upload, update, and cleanup lifecycle + +The media plan decides whether existing remote IDs can be reused, whether alt text can be +updated in place through `media_attributes`, or whether files must be uploaded again. + +```mermaid +flowchart TD + Plan["plugin_mastodon_prepare_entry_media_sync_plan"] + Existing{"Existing remote_media metadata?"} + AttachmentSig{"Attachment file signature unchanged?"} + DescriptionSig{"Description signature unchanged?"} + SupportsAttrs{"Instance supports status media_attributes?"} + ReuseIDs["Reuse stored media_ids"] + ReuseWithAttrs["Reuse stored media_ids and send media_attributes on PUT"] + Reupload["Upload local files again"] + Upload["POST /api/v2/media with file, description, optional thumbnail"] + Poll{"Response 202 or media URL missing?"} + Wait["GET /api/v1/media/:id polling with media-type-aware attempts"] + StatusRequest["POST or PUT /api/v1/statuses"] + StatusOK{"Status create/update succeeded?"} + Cleanup["Best-effort DELETE /api/v1/media/:id for unattached uploads"] + Store["Store remote_media, attachment_signature, description_signature"] + + Plan --> Existing + Existing -- "No" --> Reupload + Existing -- "Yes" --> AttachmentSig + AttachmentSig -- "Changed" --> Reupload + AttachmentSig -- "Unchanged" --> DescriptionSig + DescriptionSig -- "Unchanged" --> ReuseIDs --> StatusRequest + DescriptionSig -- "Changed" --> SupportsAttrs + SupportsAttrs -- "Yes" --> ReuseWithAttrs --> StatusRequest + SupportsAttrs -- "No" --> Reupload + Reupload --> Upload --> Poll + Poll -- "Yes" --> Wait --> StatusRequest + Poll -- "No" --> StatusRequest + StatusRequest --> StatusOK + StatusOK -- "Yes" --> Store + StatusOK -- "No and new uploads exist" --> Cleanup +``` + +```mermaid +sequenceDiagram + autonumber + participant Plugin as Mastodon Plugin + participant API as Mastodon API + participant State as state.json + + Plugin->>API: POST /api/v2/media + alt Media is processed synchronously + API-->>Plugin: 200 with media URL + else Media is processed asynchronously + API-->>Plugin: 202 with ID and pending URL + loop Until ready or attempts exhausted + Plugin->>API: GET /api/v1/media/:id + API-->>Plugin: 206 pending, 200 ready, or error + end + end + Plugin->>API: POST or PUT /api/v1/statuses with media_ids + alt Status write succeeds + Plugin->>State: Persist media IDs and signatures + else Status write fails after upload + Plugin->>API: DELETE /api/v1/media/:id for unattached uploads + Plugin->>State: Keep previous mapping/dirty marker for retry + end ``` -### 2.2 Mastodon media attachments to FlatPress media markup +### 2.5 Mastodon media attachments to FlatPress media markup -Remote media is downloaded into FlatPress-managed directories. Images become PhotoSwipe-compatible `[img]` or `[gallery]` markup. Audio and video become AudioVideo plugin player tags. +Remote media import uses a URL fallback order and stores downloaded files in FlatPress-managed +directories before it emits BBCode for the matching companion renderer. ```mermaid flowchart TD @@ -274,57 +516,61 @@ flowchart TD Image["image"] Audio["audio"] Video["video or gifv"] - Download["Download url, remote_url, or preview_url fallback"] + URLFallback["Source URL fallback
url -> remote_url -> preview_url when safe"] + Download["Download with media transfer timeout"] Temp["Write to temporary plugin media directory"] ImageStore["Move images to fp-content/images/mastodon/status-ID"] AttachStore["Move audio/video to fp-content/attachs/mastodon/status-ID"] - Captions["Write .captions.conf when multiple images have descriptions"] + Captions["Write .captions.conf for gallery descriptions"] ImageMarkup{"Image count"} - ImgTag["Single image BBCode with width and title"] - GalleryTag["Multiple image gallery BBCode with width"] - AudioTag["Audio player BBCode with controls and description"] - VideoTag["Video player BBCode with controls, poster, and description"] - EntryContent["Append imported media BBCode to FlatPress entry or comment"] + ImgTag["Single [img] with width and title"] + GalleryTag["[gallery] with width and captions"] + AudioTag["AudioVideo audio player BBCode with controls/description"] + VideoTag["AudioVideo video player BBCode with controls/poster/description"] + Content["Append imported media BBCode to entry or comment"] RemoteStatus --> Attachments --> Classify - Classify -- "image" --> Image --> Download --> Temp --> ImageStore --> Captions --> ImageMarkup - ImageMarkup -- "one" --> ImgTag --> EntryContent - ImageMarkup -- "many" --> GalleryTag --> EntryContent - Classify -- "audio" --> Audio --> Download --> Temp --> AttachStore --> AudioTag --> EntryContent - Classify -- "video/gifv" --> Video --> Download --> Temp --> AttachStore --> VideoTag --> EntryContent + Classify -- "image" --> Image --> URLFallback --> Download --> Temp --> ImageStore --> Captions --> ImageMarkup + ImageMarkup -- "one" --> ImgTag --> Content + ImageMarkup -- "many" --> GalleryTag --> Content + Classify -- "audio" --> Audio --> URLFallback --> Download --> Temp --> AttachStore --> AudioTag --> Content + Classify -- "video/gifv" --> Video --> URLFallback --> Download --> Temp --> AttachStore --> VideoTag --> Content ``` -### 2.3 Tags and hashtags when the FlatPress Tag plugin is active +### 2.6 Tags and hashtags when the FlatPress Tag plugin is active -When the Tag plugin is active, local FlatPress `[tag]` metadata is exported as a Mastodon hashtag footer. Remote Mastodon tags are imported back as FlatPress tag BBCode. The plugin also strips its own hashtag footer during round-trip imports. +When the Tag plugin is active, local FlatPress tag metadata becomes a Mastodon hashtag footer. +On import, plugin-generated footer noise is stripped while real remote hashtags remain usable. ```mermaid flowchart LR subgraph FlatPress_to_Mastodon["FlatPress to Mastodon"] - FPEntry["Entry content with Tag plugin BBCode"] - ExtractTags["Extract and normalize FlatPress tags"] + FPEntry["Entry content with [tag] metadata"] + ExtractTags["Extract, normalize, deduplicate tags"] StripTagBBCode["Remove [tag] markup from plain-text body"] - Footer["Build hashtag footer line"] - StatusText["Append hashtags to Mastodon status text"] + Footer["Build plugin-owned hashtag footer"] + StatusText["Append footer within status budget"] end subgraph Mastodon_to_FlatPress["Mastodon to FlatPress"] - RemoteStatus["Remote status with tags array and content"] + RemoteStatus["Remote status with tags array and HTML content"] ConvertHTML["Convert Mastodon HTML to FlatPress BBCode"] - StripFooter["Strip trailing hashtag footer generated by this plugin"] - BuildTagBBCode["Build [tag] tag1, tag2[/tag]"] - SaveEntry["Save FlatPress entry with tag metadata"] + StripFooter["Strip trailing footer generated by this plugin"] + Preserve["Preserve non-plugin hashtags inside text"] + BuildTagBBCode["Build [tag]tag1, tag2[/tag]"] + SaveEntry["Save FlatPress entry/comment content"] end FPEntry --> ExtractTags --> Footer --> StatusText FPEntry --> StripTagBBCode --> StatusText - RemoteStatus --> ConvertHTML --> StripFooter --> SaveEntry + RemoteStatus --> ConvertHTML --> StripFooter --> Preserve --> SaveEntry RemoteStatus --> BuildTagBBCode --> SaveEntry ``` -### 2.4 Companion plugin dependency overview +### 2.7 Companion plugin dependency overview -The Mastodon plugin can store imported content without all companion plugins, but these plugins determine whether imported markup renders correctly in the FlatPress frontend. +The Mastodon plugin can store imported content without all companion plugins, but these plugins +determine whether imported markup renders correctly in the FlatPress frontend. ```mermaid flowchart TD @@ -344,18 +590,187 @@ flowchart TD Tag -->|"Enables FlatPress tags and Mastodon hashtags in both directions"| ImportedContent Emoticons -->|"Renders imported emoji shortcodes more nicely"| ImportedContent - MastodonPlugin -. "detects plugin_bbcode_startup, do_bbcode_url, do_bbcode_img" .-> BBCode - MastodonPlugin -. "detects PhotoSwipeFunctions" .-> PhotoSwipe - MastodonPlugin -. "detects AudioVideoPlugin" .-> AudioVideo - MastodonPlugin -. "detects plugin_tag_entry and the plugin_tag object" .-> Tag - MastodonPlugin -. "detects plugin_emoticons global map" .-> Emoticons + MastodonPlugin -. "plugin_mastodon_bbcode_plugin_active" .-> BBCode + MastodonPlugin -. "plugin_mastodon_photoswipe_plugin_active" .-> PhotoSwipe + MastodonPlugin -. "plugin_mastodon_audiovideo_plugin_active" .-> AudioVideo + MastodonPlugin -. "plugin_mastodon_tag_plugin_active" .-> Tag + MastodonPlugin -. "plugin_mastodon_emoticons_plugin_active" .-> Emoticons +``` + +## 3. Mastodon API endpoints, capabilities, and fallbacks + +### 3.1 API endpoint map + +The plugin uses Mastodon HTTP APIs through `plugin_mastodon_mastodon_json()` and the multipart +media transport. The FlatPress side is real code; tests usually simulate the remote Mastodon +server. + +```mermaid +flowchart TD + Plugin["Mastodon plugin HTTP layer"] + + subgraph OAuth["OAuth and account setup"] + Discovery["GET /.well-known/oauth-authorization-server"] + Apps["POST /api/v1/apps"] + Authorize["GET /oauth/authorize"] + Token["POST /oauth/token"] + Verify["GET /api/v1/accounts/verify_credentials"] + end + + subgraph Instance["Instance limits and capabilities"] + InstanceV2["GET /api/v2/instance"] + end + + subgraph Statuses["Statuses and contexts"] + AccountStatuses["GET /api/v1/accounts/:id/statuses"] + StatusGet["GET /api/v1/statuses/:id"] + Context["GET /api/v1/statuses/:id/context"] + StatusPost["POST /api/v1/statuses"] + StatusPut["PUT /api/v1/statuses/:id"] + StatusDelete["DELETE /api/v1/statuses/:id"] + StatusDeleteMedia["DELETE /api/v1/statuses/:id?delete_media=1"] + end + + subgraph Media["Media"] + MediaPost["POST /api/v2/media"] + MediaGet["GET /api/v1/media/:id"] + MediaDelete["DELETE /api/v1/media/:id"] + end + + Plugin --> OAuth + Plugin --> Instance + Plugin --> Statuses + Plugin --> Media +``` + +### 3.2 Instance version and capability decisions + +`/api/v2/instance` is the central source for instance limits. If a field is missing, the plugin +uses conservative internal defaults. It does not fall back to `/api/v1/instance`. + +```mermaid +flowchart TD + NeedCaps["Need limits or API capability"] + Cache["Read saved compact instance document / runtime cache"] + HasDoc{"Cached document usable?"} + Fetch["GET /api/v2/instance"] + OK{"Response OK?"} + Store["Store compact instance document"] + Defaults["Use internal defaults
500 chars, 23 URL reserve, 4 media, 1500 desc limit"] + Version["Parse version string"] + MediaAttrs{"version >= 4.1.0?"} + DeleteMedia{"version >= 4.4.0?"} + Unknown{"version unknown?"} + Capabilities["Capability result for current request"] + + NeedCaps --> Cache --> HasDoc + HasDoc -- "No" --> Fetch --> OK + OK -- "Yes" --> Store --> Version + OK -- "No" --> Defaults --> Unknown + HasDoc -- "Yes" --> Version + Version --> MediaAttrs + Version --> DeleteMedia + Version --> Unknown + MediaAttrs --> Capabilities + DeleteMedia --> Capabilities + Unknown --> Capabilities +``` + +### 3.3 OAuth scope compatibility + +The OAuth scope path prefers modern discovery when available and keeps a legacy fallback for +older Mastodon instances. + +```mermaid +flowchart TD + Start["Build OAuth scopes"] + Discovery["GET /.well-known/oauth-authorization-server"] + DiscoveryOK{"Discovery document available?"} + ProfileSupported{"profile scope advertised?"} + Modern["Use profile-capable scope set"] + Legacy["Use legacy read:accounts-compatible scope set"] + Register["POST /api/v1/apps or use saved client"] + Authorize["GET /oauth/authorize"] + Token["POST /oauth/token"] + Verify["GET /api/v1/accounts/verify_credentials"] + + Start --> Discovery --> DiscoveryOK + DiscoveryOK -- "Yes" --> ProfileSupported + ProfileSupported -- "Yes" --> Modern --> Register + ProfileSupported -- "No" --> Legacy --> Register + DiscoveryOK -- "No / 404 / failed" --> Legacy + Register --> Authorize --> Token --> Verify +``` + +### 3.4 Status deletion fallback for Mastodon before 4.4.0 + +Mastodon has long supported deleting a status through `DELETE /api/v1/statuses/:id`. The +`delete_media` query parameter is newer. The plugin therefore omits the parameter when a cached +instance version proves that the server is older than 4.4.0, and it retries once without the +parameter when an unknown server rejects the first request. + +```mermaid +flowchart TD + Delete["plugin_mastodon_delete_status(status_id, deleteMedia=true)"] + Version["plugin_mastodon_instance_supports_status_delete_media"] + KnownOld{"Cached version says older than 4.4.0?"} + KnownNew{"Cached version says 4.4.0 or newer?"} + Unknown{"Version unknown?"} + PlainFirst["DELETE /api/v1/statuses/:id"] + WithParam["DELETE /api/v1/statuses/:id?delete_media=1"] + Response{"Response OK or 404/410 handled by caller?"} + LegacyError{"400, 405, 422, or error mentioning delete_media?"} + RetryPlain["Retry once: DELETE /api/v1/statuses/:id"] + Return["Return final response to deletion sync"] + + Delete --> Version + Version --> KnownOld + KnownOld -- "Yes" --> PlainFirst --> Return + KnownOld -- "No" --> KnownNew + KnownNew -- "Yes" --> WithParam --> Response + KnownNew -- "No" --> Unknown + Unknown -- "Yes" --> WithParam + Response -- "OK / non-legacy error" --> Return + Response -- "Failed" --> LegacyError + LegacyError -- "Yes" --> RetryPlain --> Return + LegacyError -- "No" --> Return ``` -## 3. Scheduled and manual sync flows +### 3.5 Rate-limit and local budget guard -### 3.1 Daily scheduled content synchronization +The plugin protects ordinary web requests and Mastodon servers with a per-run budget and +persistent cross-run windows. -The scheduled content sync is started from the `init` hook. Ordinary requests read only the compact scheduler state first. If the scheduled time has already run on the same day, the request exits without loading the full state and without contacting Mastodon. +```mermaid +stateDiagram-v2 + [*] --> Idle + Idle --> GuardStarted: plugin_mastodon_rate_limit_guard_start + GuardStarted --> RequestAllowed: general request budget available + RequestAllowed --> RequestAllowed: non-budgeted API request + RequestAllowed --> MediaWindow: POST /api/v2/media + RequestAllowed --> DeleteWindow: DELETE status or unreblog + RequestAllowed --> PageWindow: account statuses page fetch + MediaWindow --> RequestAllowed: under 24 uploads / 1800s + DeleteWindow --> RequestAllowed: under 24 deletes / 1800s + PageWindow --> RequestAllowed: under 300 pages / 900s + RequestAllowed --> RemoteLimit: HTTP 429 or X-RateLimit-Remaining <= 10 + MediaWindow --> LocalLimit: media window exhausted + DeleteWindow --> LocalLimit: delete window exhausted + PageWindow --> LocalLimit: page window exhausted + RequestAllowed --> RunBudgetExhausted: 240 request budget exhausted + RemoteLimit --> GuardStopped: stop cleanly with state/log error + LocalLimit --> GuardStopped: stop cleanly with state/log error + RunBudgetExhausted --> GuardStopped: stop cleanly with state/log error + GuardStarted --> GuardStopped: plugin_mastodon_rate_limit_guard_stop + GuardStopped --> [*] +``` + +## 4. Scheduled and manual sync flows + +### 4.1 Daily scheduled content synchronization + +The scheduled content sync is started from the `init` hook. Ordinary POST requests, missing +configuration, active cooldowns, and a not-due scheduler summary all return quickly. ```mermaid flowchart TD @@ -365,17 +780,19 @@ flowchart TD Configured{"Instance URL and access token configured?"} Scheduler["Read scheduler-state.json"] Due{"plugin_mastodon_sync_due?"} - Guard{"Content sync cooldown active?"} - Lock["Open sync.lock and acquire non-blocking exclusive lock"] + Cooldown{"Content sync cooldown active?"} + Lock["Acquire sync.lock non-blocking"] + RateGuard["Start API rate/budget guard"] FullState["Load full state.json"] + Protect["Protect locally deleted exported comments"] Stats["Reset content_stats and last_error"] RemoteToLocal["plugin_mastodon_sync_remote_to_local"] LocalToRemote["plugin_mastodon_sync_local_to_remote"] FlushSkips["Flush aggregated skip-log summaries"] - Write["Write state.json and scheduler-state.json"] - DeletePending{"Deletion sync enabled?"} - Pending["Mark deletions_pending and not-before cooldown"] - Release["Release lock and stop rate-limit guard"] + MarkDeletion{"Deletion sync enabled?"} + Pending["Set deletions_pending and deletions_not_before"] + Write["Write state and scheduler summary"] + Release["Release lock and stop rate guard"] End["Return to normal FlatPress request"] Init --> Method @@ -384,52 +801,20 @@ flowchart TD Configured -- "No" --> End Configured -- "Yes" --> Scheduler --> Due Due -- "No" --> End - Due -- "Yes" --> Guard - Guard -- "Active" --> End - Guard -- "Clear" --> FullState --> Lock - Lock --> Stats --> RemoteToLocal --> LocalToRemote --> FlushSkips --> DeletePending - DeletePending -- "Yes" --> Pending --> Write - DeletePending -- "No" --> Write + Due -- "Yes" --> Cooldown + Cooldown -- "Active" --> End + Cooldown -- "Clear" --> Lock + Lock --> RateGuard --> FullState --> Protect --> Stats --> RemoteToLocal --> LocalToRemote --> FlushSkips --> MarkDeletion + MarkDeletion -- "Yes" --> Pending --> Write + MarkDeletion -- "No" --> Write Write --> Release --> End ``` -### 3.2 Local-to-remote candidate selection during scheduled sync +### 4.2 Follow-up deletion synchronization -Scheduled syncs are optimized for large blogs. They do not parse every old entry every day. Manual full syncs still parse every entry. - -```mermaid -flowchart TD - Start["plugin_mastodon_list_local_entries_for_sync"] - DirtyLookup["Build lookup from dirty_entries and dirty_comments"] - CollectFiles["Collect entry files from CONTENT_DIR"] - NextFile["Next entry file"] - Force{"force == true?"} - Dirty{"Entry ID in dirty lookup?"} - Window{"Entry ID date inside active content window?"} - Skip["Skip file without entry_parse"] - Parse["entry_parse"] - Draft{"Draft category?"} - Add["Add to ordered export list"] - Sort["Sort by local item timestamp"] - Result["Return selected entries"] - - Start --> DirtyLookup --> CollectFiles --> NextFile - NextFile --> Force - Force -- "Yes" --> Parse - Force -- "No" --> Dirty - Dirty -- "Yes" --> Parse - Dirty -- "No" --> Window - Window -- "Yes" --> Parse - Window -- "No" --> Skip --> NextFile - Parse --> Draft - Draft -- "Yes" --> NextFile - Draft -- "No" --> Add --> NextFile - NextFile --> Sort --> Result -``` - -### 3.3 Follow-up deletion synchronization - -The deletion sync is intentionally separate from the content sync. It compares stored mappings against local files and remote statuses. Local deletions are propagated to Mastodon; remote deletions are reflected back into FlatPress under the remote-write guard. +The deletion sync is intentionally separate from the content sync. It compares stored mappings +against local files and remote statuses. Local deletions are propagated to Mastodon; remote +deletions are reflected back into FlatPress under the local-write guard. ```mermaid flowchart TD @@ -439,23 +824,26 @@ flowchart TD Pending{"Force or deletions_pending?"} Due{"Deletion sync due?"} Lock["Acquire sync.lock"] - RecheckOnly{"Pending comment descendant rechecks only?"} + RateGuard["Start API rate/budget guard"] + RecheckOnly{"Only pending descendant rechecks?"} EntryLoop["Iterate entry mappings with cursor"] + EntryScope{"Mapping inside sync_start_date?"} EntryLocal{"Local entry exists?"} - DeleteRemoteEntry["DELETE remote entry status"] + DeleteRemoteEntry["Delete remote entry status"] EntryLookupWindow{"Remote lookup window allows check?"} - FetchRemoteEntry["Fetch remote entry status"] - MissingRemoteEntry{"Remote entry missing?"} - DeleteLocalEntry["entry_delete under remote-write guard"] + FetchRemoteEntry["GET /api/v1/statuses/:id"] + MissingRemoteEntry{"Remote entry missing 404/410?"} + DeleteLocalEntry["entry_delete under local-write guard"] CommentLoop["Iterate comment mappings with cursor"] + CommentScope{"Mapping inside sync_start_date?"} CommentLocal{"Local comment exists?"} - DeleteRemoteComment["DELETE remote comment status"] + DeleteRemoteComment["Delete remote comment status"] QueueDesc["Queue descendant remote rechecks"] CommentLookupWindow{"Remote lookup window allows check?"} - FetchRemoteComment["Fetch remote comment status"] - MissingRemoteComment{"Remote comment missing?"} - DeleteLocalComment["comment_delete under remote-write guard"] - ProcessRechecks["Process pending comment remote rechecks"] + FetchRemoteComment["GET /api/v1/statuses/:id"] + MissingRemoteComment{"Remote comment missing 404/410?"} + DeleteLocalComment["comment_delete under local-write guard"] + ProcessRechecks["Process pending_comment_remote_rechecks"] Complete{"Failures or rate limit?"} ClearPending["Clear pending flags and cursors"] Retry["Keep deletions_pending with cooldown"] @@ -467,16 +855,20 @@ flowchart TD Pending -- "No" --> Write Pending -- "Yes" --> Due Due -- "No" --> Write - Due -- "Yes" --> Lock --> RecheckOnly + Due -- "Yes" --> Lock --> RateGuard --> RecheckOnly RecheckOnly -- "No" --> EntryLoop - EntryLoop --> EntryLocal + EntryLoop --> EntryScope + EntryScope -- "No" --> CommentLoop + EntryScope -- "Yes" --> EntryLocal EntryLocal -- "No" --> DeleteRemoteEntry --> CommentLoop EntryLocal -- "Yes" --> EntryLookupWindow EntryLookupWindow -- "No" --> CommentLoop EntryLookupWindow -- "Yes" --> FetchRemoteEntry --> MissingRemoteEntry MissingRemoteEntry -- "Yes" --> DeleteLocalEntry --> CommentLoop MissingRemoteEntry -- "No" --> CommentLoop - CommentLoop --> CommentLocal + CommentLoop --> CommentScope + CommentScope -- "No" --> ProcessRechecks + CommentScope -- "Yes" --> CommentLocal CommentLocal -- "No" --> DeleteRemoteComment --> QueueDesc --> ProcessRechecks CommentLocal -- "Yes" --> CommentLookupWindow CommentLookupWindow -- "No" --> ProcessRechecks @@ -489,9 +881,41 @@ flowchart TD Complete -- "Failures or rate limit" --> Retry --> Write ``` -### 3.4 Manual full synchronization in the admin area +```mermaid +sequenceDiagram + autonumber + participant Del as Deletion sync + participant State as state.json + participant Caps as Instance capability cache + participant API as Mastodon API + participant Core as FlatPress Core -Manual full synchronization is an explicit admin repair and initial-import/export path. It intentionally loads the full state and scans all entries. It is not optimized away by dirty tracking. + Del->>State: Select mapped entry/comment whose local item disappeared + Del->>Caps: Check cached support for status delete_media + alt Cached Mastodon version before 4.4.0 + Del->>API: DELETE /api/v1/statuses/:id + else Cached version 4.4.0 or newer + Del->>API: DELETE /api/v1/statuses/:id?delete_media=1 + else Version unknown + Del->>API: DELETE /api/v1/statuses/:id?delete_media=1 + alt Server rejects delete_media as legacy parameter + Del->>API: DELETE /api/v1/statuses/:id + end + end + API-->>Del: OK, 404/410 already gone, or failure + Del->>State: Update mapping/tombstone/cursor or keep pending for retry + + Del->>API: GET /api/v1/statuses/:id for still-local mapped item + alt Remote missing + Del->>Core: entry_delete/comment_delete under local-write guard + Del->>State: Queue descendant rechecks and tombstones when needed + end +``` + +### 4.3 Manual full synchronization in the admin area + +Manual admin runs are repair paths. They intentionally load the full state and can bypass the +scheduled due check, but they still use the lock, rate-limit guard, and persisted budgets. ```mermaid sequenceDiagram @@ -503,37 +927,74 @@ sequenceDiagram participant Core as FlatPress Core participant API as Mastodon API - Admin->>AdminUI: Press "Run now" or "Run full synchronization" + Admin->>AdminUI: Press Run now or Run full synchronization AdminUI->>Plugin: plugin_mastodon_run_sync(true, fullWindow) Plugin->>State: Load full state.json - Plugin->>Plugin: Bypass scheduled cooldown guard because force=true - Plugin->>Plugin: Acquire sync.lock - Plugin->>API: Verify credentials - Plugin->>API: Fetch remote statuses and contexts - Plugin->>Core: Import or update entries/comments under remote-write guard - Plugin->>Plugin: Scan local entries + Plugin->>Plugin: Bypass scheduled due/cooldown because force=true + Plugin->>Plugin: Acquire sync.lock and start rate-limit guard + Plugin->>API: Verify credentials and fetch instance/status/context data + Plugin->>Core: Import/update entries/comments under local-write guard alt Full window requested Plugin->>Plugin: Parse every local entry as repair path else Manual non-full run - Plugin->>Plugin: Use configured window and dirty candidates + Plugin->>Plugin: Use configured window plus dirty candidates end - Plugin->>API: Create or update statuses and replies - Plugin->>State: Update mappings, stats, scheduler summary + Plugin->>API: Create/update statuses and replies + Plugin->>State: Update mappings, content_stats, scheduler summary Plugin-->>AdminUI: Return result and diagnostics - Admin->>AdminUI: Press "Run full deletion synchronization" + Admin->>AdminUI: Press Run deletion synchronization AdminUI->>Plugin: plugin_mastodon_run_deletion_sync(true) Plugin->>State: Load full state.json Plugin->>Plugin: Ignore not-before cooldown because force=true - Plugin->>Core: Delete local entries/comments under remote-write guard when remote disappeared - Plugin->>API: Delete remote statuses when local content disappeared - Plugin->>State: Update tombstones, cursors, deletion stats + Plugin->>API: Delete remote statuses for local deletions with delete_media fallback + Plugin->>Core: Delete local entries/comments under local-write guard when remote disappeared + Plugin->>State: Update tombstones, cursors, deletion_stats Plugin-->>AdminUI: Return deletion result ``` -## 4. Core post-success hooks, dirty tracking, and remote-write guard +### 4.4 Admin UI diagnostics + +The admin panel intentionally surfaces operational state so maintainers can distinguish missing +credentials, stale instance information, rate-limit stops, and content/deletion sync failures. + +```mermaid +flowchart TD + AdminAssign["plugin_mastodon_admin_assign"] + Options["Load options and normalize instance URL"] + State["Load state.json"] + Scheduler["Load scheduler-state.json"] + Instance["Build instance info rows"] + OAuth["Evaluate registration, OAuth and scope status"] + Plugins["Detect companion plugin status"] + Content["Expose content_stats and last_run_local"] + Deletion["Expose deletion_stats, deletions_pending, last_deletion_run_local"] + Errors["Expose last_error and log hints"] + Template["admin.plugin.mastodon.tpl"] + + AdminAssign --> Options --> State + AdminAssign --> Scheduler + Options --> Instance + Options --> OAuth + AdminAssign --> Plugins + State --> Content + State --> Deletion + State --> Errors + Scheduler --> Content + Scheduler --> Deletion + Instance --> Template + OAuth --> Template + Plugins --> Template + Content --> Template + Deletion --> Template + Errors --> Template +``` + +## 5. Core post-success hooks, dirty tracking, and local-write guard -The current design depends on post-success hooks in the FlatPress core. These hooks fire after a write or delete operation has succeeded. The Mastodon plugin uses them to queue local manual changes instead of rediscovering every old change by scanning the whole archive daily. +The current design depends on post-success hooks in the FlatPress core. These hooks fire after +a write or delete operation has succeeded. The Mastodon plugin uses them to queue local manual +changes instead of rediscovering every old change by scanning the whole archive daily. ```mermaid flowchart TD @@ -555,6 +1016,7 @@ flowchart TD CommentDirty["plugin_mastodon_on_comment_saved sets dirty_comments or queues parent entry"] CommentDeletion["plugin_mastodon_on_comment_deleted sets deletions_pending when mapped"] StateWrite["Write state.json and scheduler-state.json"] + Stop["Ignore hook to avoid false dirty/deletion markers"] end EntrySave --> EntrySaved --> Guard @@ -562,35 +1024,98 @@ flowchart TD CommentSave --> CommentSaved --> Guard CommentDelete --> CommentDeleted --> Guard - Guard -- "Yes, plugin-owned remote import/delete" --> Stop["Ignore hook to avoid false local dirty markers"] - Guard -- "No, local manual change" --> EntryDirty --> StateWrite - Guard -- "No, local manual deletion" --> EntryDeletion --> StateWrite - Guard -- "No, local manual comment" --> CommentDirty --> StateWrite + Guard -- "Yes, plugin-owned remote import/delete" --> Stop + Guard -- "No, local manual entry save" --> EntryDirty --> StateWrite + Guard -- "No, local manual entry deletion" --> EntryDeletion --> StateWrite + Guard -- "No, local manual comment save" --> CommentDirty --> StateWrite Guard -- "No, local manual comment deletion" --> CommentDeletion --> StateWrite ``` ```mermaid -sequenceDiagram - autonumber - participant RemoteSync as Remote import or remote deletion sync - participant Guard as Local write guard - participant Core as FlatPress Core - participant Hook as Core post-success hook - participant Handler as Mastodon hook handler - participant State as state.json +stateDiagram-v2 + [*] --> LocalManualChange + [*] --> PluginOwnedWrite + + LocalManualChange --> DirtyEntry: entry_saved mapped and changed + LocalManualChange --> DirtyComment: comment_saved mapped or parent pending + LocalManualChange --> DeletionPending: mapped entry/comment deleted + DirtyEntry --> ScheduledOrManualSync + DirtyComment --> ScheduledOrManualSync + DeletionPending --> FollowUpDeletionSync + + PluginOwnedWrite --> GuardEntered: plugin_mastodon_local_write_guard_enter + GuardEntered --> CoreWrite: entry_save/comment_save/entry_delete/comment_delete + CoreWrite --> HookFired: FlatPress post-success hook fires + HookFired --> Ignored: guard active + Ignored --> GuardLeft: plugin_mastodon_local_write_guard_leave + GuardLeft --> AuthoritativeMappingUpdate + + ScheduledOrManualSync --> [*] + FollowUpDeletionSync --> [*] + AuthoritativeMappingUpdate --> [*] +``` + +## 6. Error handling and partial-failure strategy + +A sync run may partially succeed. The plugin therefore distinguishes API failure, local +rate-limit stops, missing remote objects, and state-write failures. + +```mermaid +flowchart TD + Operation["Sync operation"] + API["Mastodon API call"] + Response{"Response category"} + OK["OK: update mapping/statistics"] + Missing["404/410: treat remote object as missing where caller allows it"] + RemoteLimit["429 or X-RateLimit floor reached"] + LegacyFallback["Legacy compatibility fallback, e.g. retry delete without delete_media"] + HardFailure["Hard failure: keep dirty/pending marker"] + UploadFailure["Status failed after media upload"] + Cleanup["Best-effort cleanup uploaded media"] + StateWrite["Write state.json and scheduler summary"] + StateOK{"State write succeeded?"} + LastError["Set last_error and log diagnostics"] + Retry["Keep cooldown/pending marker for future run"] + + Operation --> API --> Response + Response -- "2xx" --> OK --> StateWrite + Response -- "404/410 in lookup/delete context" --> Missing --> StateWrite + Response -- "429 / local budget stop" --> RemoteLimit --> LastError --> Retry + Response -- "legacy delete_media rejection" --> LegacyFallback --> API + Response -- "other error" --> HardFailure --> LastError --> Retry + HardFailure --> UploadFailure + UploadFailure -- "new unattached uploads exist" --> Cleanup --> Retry + StateWrite --> StateOK + StateOK -- "Yes" --> Operation + StateOK -- "No" --> LastError +``` + +## 7. Simulation and regression-test architecture - RemoteSync->>Guard: plugin_mastodon_local_write_guard_enter - RemoteSync->>Core: entry_save, comment_save, entry_delete, or comment_delete - Core-->>Hook: entry_saved, comment_saved, entry_deleted, or comment_deleted - Hook->>Handler: Mastodon handler is called - Handler->>Guard: plugin_mastodon_local_write_guard_active - Guard-->>Handler: true - Handler-->>State: No dirty marker and no deletion-pending marker - RemoteSync->>Guard: plugin_mastodon_local_write_guard_leave - RemoteSync->>State: Store authoritative remote-source mapping changes +`simulate_mastodon_plugin.php` loads the real plugin code from the checked-out tree. It replaces +the external Mastodon side with deterministic fixtures and mock HTTP responses so the regression +suite can verify state transitions, API requests, media handling, and edge-case conversion logic. + +```mermaid +flowchart TD + Script["simulate_mastodon_plugin.php"] + Sandbox["Create FlatPress-like sandbox"] + CoreStubs["Load FlatPress includes and test helpers"] + Plugin["require fp-plugins/mastodon/plugin.mastodon.php"] + Fixtures["Create entries, comments, media files, options, and state fixtures"] + HTTPQueue["Mock Mastodon HTTP response queue"] + RunSync["Call real plugin sync/deletion functions"] + Assertions["test_result assertions"] + InspectState["Inspect state.json, scheduler-state.json, files, and captured API calls"] + OptionalLive["Optional --live-auth credentials smoke test"] + + Script --> Sandbox --> CoreStubs --> Plugin --> Fixtures + Fixtures --> HTTPQueue --> RunSync --> Assertions + Assertions --> InspectState + Script -. "only when explicitly requested" .-> OptionalLive ``` -## 5. Operational guarantees and intentional behavior +## 8. Operational guarantees and intentional behavior ```mermaid flowchart TD @@ -603,20 +1128,31 @@ flowchart TD TargetedScan["Scans active window plus dirty entries/comments"] FullStateNeeded["Loads full state.json because mappings are required"] FullRepair["Full repair scan remains available"] - NoFalseDirty["Remote-write guard prevents plugin-owned writes from becoming local dirty markers"] + Budgets["Local budgets and Mastodon rate-limit headers stop safely"] + NoFalseDirty["Local-write guard prevents plugin-owned writes from becoming local dirty markers"] + Compatibility["Mastodon >= 4.0.0 path with documented delete_media fallback for older than 4.4.0"] Goal --> OrdinaryRequest --> SchedulerOnly Goal --> ScheduledRun --> TargetedScan Goal --> DeletionRun --> FullStateNeeded Goal --> ManualRun --> FullRepair + ScheduledRun --> Budgets + DeletionRun --> Budgets + ManualRun --> Budgets ScheduledRun --> NoFalseDirty DeletionRun --> NoFalseDirty ManualRun --> NoFalseDirty + DeletionRun --> Compatibility ``` Key implications for developers: -- Scheduled content syncs are optimized for large blogs by using post-success dirty queues and date-window selection. +- Scheduled content syncs are optimized for large blogs by using post-success dirty queues and + date-window selection. - Manual full syncs deliberately remain exhaustive and should not be replaced by dirty queues. -- Deletion syncs need the full mapping state because they compare local existence with remote status existence and maintain tombstones and descendant rechecks. -- Companion plugins improve rendering and feature completeness, but the Mastodon plugin still stores importable FlatPress markup even when a companion renderer is currently inactive. +- Deletion syncs need the full mapping state because they compare local existence with remote + status existence and maintain tombstones and descendant rechecks. +- Status deletion uses `delete_media=1` only when it is supported or plausibly supported, and + falls back to plain `DELETE /api/v1/statuses/:id` for older Mastodon behavior. +- Companion plugins improve rendering and feature completeness, but the Mastodon plugin still + stores importable FlatPress markup even when a companion renderer is currently inactive. diff --git a/fp-plugins/mastodon/README.md b/fp-plugins/mastodon/README.md index 236379df..d3054b3b 100644 --- a/fp-plugins/mastodon/README.md +++ b/fp-plugins/mastodon/README.md @@ -50,18 +50,14 @@ The plugin uses: - **DOMDocument / libxml** for the best HTML-to-BBCode conversion - **OpenSSL** to protect stored secrets when available +Mastodon compatibility range: +**Technical:** >= 4.0.0 as of November 2022 +**Recommended:** >= 4.4.0 as of July 2025 + ## Recommended FlatPress plugins For the best result with synchronized Mastodon content, also enable these FlatPress plugins: -- **BBCode** -- **PhotoSwipe** -- **AudioVideo** -- **Tag** -- **Emoticons** - -They improve the result like this: - - **BBCode** renders imported formatting, links, images and galleries properly. - **PhotoSwipe** improves image and gallery display. - **AudioVideo** renders imported and synchronized audio/video attachments as HTML5 media players. @@ -144,6 +140,24 @@ Here you set: The **Synchronization start date** is useful when you do not want to import or export much older content. +I recommend setting the **start date** once and then not changing it. You can change the start date in the following scenarios: + +| **Action** | **Recommendation** | +| ------------------------------------------------------ | ---------------------------------------------------------------------------------------: | +| Historical initial import / backfill | Extend **backward in 7–14-day blocks** | +| Many gallery/video posts in the history | Rather by **number of media items**, not by days: max. approx. 20–24 media items per run | + +**Automatic window for scheduled runs** (specific guidelines). + +| **Usage scenario** | **Recommendation** | +| ------------------------------------------------------ | ---------------------------------------------------------------------------: | +| Normal blog, few media files, few comments | **14 days** | +| Very quiet blog, hardly any media, few old changes | **30 days maximum** | +| Media-heavy: galleries, audio, video | **7 days** | +| Many comments or many old edits | **7–14 days** | + +Tip: If you see error messages indicating that limits have been reached, select a shorter time frame, such as 7 days. + ### 2. More options These switches are especially important: @@ -155,7 +169,16 @@ These switches are especially important: Allows certain synchronized Mastodon replies to also appear as FlatPress entries. Leave this disabled if you want to avoid duplicate content and keep thread replies mainly in the comment area. - **Enable deletion synchronization** - Enables the separate follow-up delete pass. + Enables the separate follow-up delete pass. Deletion synchronization ensures that posts/ statuses, comments/ replies deleted on one side are also deleted on the other. + +**Behavior of deletion synchronizations:** +| **Case** | **Behavior** | +| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Scheduled deletion sync, local content still exists | Remote existence check only within the automatic 7/14/30-day window | +| Scheduled deletion sync, local content has been deleted | Mastodon status/reply will still be deleted, provided it is within the **Synchronization start date** | +| Manual/Force deletion run | May continue to check completely | +| Pending child rechecks | Are not blocked by the automatic window | +| Large mapping sets | Are distributed across multiple runs using a cursor | ### 3. OAuth helper @@ -265,8 +288,11 @@ After the run, also check: For technical details you can additionally inspect: -- `fp-content/plugin_mastodon/state.json` - `fp-content/plugin_mastodon/sync.log` +- `fp-content/plugin_mastodon/sync.guard.json` +- `fp-content/plugin_mastodon/state.json` +- `fp-content/plugin_mastodon/scheduler-state.json` +- `fp-content/plugin_mastodon/rate-limit-windows.json` ## How synchronization works in practice @@ -452,6 +478,73 @@ Check: **Q:** Why can the deletion counters change later than the synchronization counters? **A:** Because the deletion pass runs in a later follow-up request, not in the main synchronization request. +## Notes for Developers: +These protection mechanisms should always apply to both manual and scheduled runs: +| **Protection** | **Manual runs too?** | **Reason** | +| --------------------------------------- | -------------------: | ---------------------------------------- | +| 240 API requests per run | Yes | Mastodon limit protection | +| 24 media uploads per 30 minutes | Yes | protects media limit | +| 24 deletions per 30 minutes | Yes | protects delete limit | +| Paging window | Yes | protects paging bucket | +| Remote remaining floor | Yes | protects against scarce Mastodon headers | +| Progress cursor in delete sync | Yes | prevents repetition of the same mappings | +| Safely process pending child rechecks | Yes | data consistency | + +Fixed runtime and request budgets: +| **Category** | **Limit** | +| -------------------------------------- | ----------------------------------------: | +| Request budget per sync run | `240` | +| Media upload budget per sync run | `24` | +| Delete budget per sync run | `24` | +| Media upload window | `24` uploads / `1800` seconds | +| Delete window | `24` deletes / `1800` seconds | +| Status page window | `300` status pages / `900` seconds | +| Remote Rate Limit Floor | stops at `X-RateLimit-Remaining <= 10` | +| Max. status pages during remote import | `5` | +| Status page limit per API page | `40` | +| Default sync time | `03:00` | +| Sync cooldown | `300` seconds | +| State fallback TTL | `300` seconds | +The plugin thus combines its own internal budgets with Mastodon’s Remote-RateLimit headers. +Throttling occurs upon an HTTP 429 response or if X-RateLimit-Remaining is too low. + +Content, Characters, and Media: +| **Category** | **Primary Source** | **Fallback** | +| --------------------------- | ----------------------------------------------------------------------- | -----------------------------------------------------: | +| Status character limit | `/api/v2/instance → configuration.statuses.max_characters` | `500` | +| Reserved characters per URL | `/api/v2/instance → configuration.statuses.characters_reserved_per_url` | `23` | +| Max. media attachments | `/api/v2/instance → configuration.statuses.max_media_attachments` | `4` | +| Media description limit | `/api/v2/instance → configuration.media_attachments.description_limit` | `1500` | +| Image size limit | `/api/v2/instance → configuration.media_attachments.image_size_limit` | no local limit if unknown | +| Video/audio size limit | `/api/v2/instance → video_size_limit` / `audio_size_limit` | Audio falls back to video, otherwise no local limit | + +Media Processing: +| **Area** | **Limit / Behavior** | +| -------------------------------------------- | ------------------------------------------------------------------ | +| Imported media width in FlatPress BBCode | `320` | +| Image/other media processing attempts | `12` | +| Video/GIFV processing attempts | `60`, from 10 MiB `75`, from 50 MiB `90` | +| Audio processing attempts | `60`, from 50 MiB `75` | +| Image/other upload timeouts | approx. `90` seconds | +| Video/GIFV/audio upload timeouts | approx. `180` seconds, from 50 MiB `300` seconds | +| Media polling | `GET /api/v1/media/:id`, retry wait time roughly `0.1–5.0` seconds | +| cURL redirect limit | `5` | +| cURL connect timeout | `15` seconds | + +Persistence, Logs, and Regression Protection: +| **Category** | **Limit / File** | +| --------------------------- | ------------------------------------------------- | +| State file | `fp-content/plugin_mastodon/state.json` | +| Scheduler state | `fp-content/plugin_mastodon/scheduler-state.json` | +| Sync lock | `sync.lock` | +| Guard file | `sync.guard.json` | +| Rate Limit Window | `rate-limit-windows.json` | +| Sync Log | `sync.log` | +| Max. Log Size | `1 MiB` | +| Rotated Log Files | `3` | +| Pending Comment Rechecks | `3` | +| Old Thread Context Rotation | `3` | + ## Resources for Developers: - [Functional Organization Chart](Function-Organigram.md) - [Process Flows](Plugin-Process-Flow.md) diff --git a/fp-plugins/mastodon/doc_mastodon.txt b/fp-plugins/mastodon/doc_mastodon.txt index 0cd90eb3..d0a077c1 100644 --- a/fp-plugins/mastodon/doc_mastodon.txt +++ b/fp-plugins/mastodon/doc_mastodon.txt @@ -51,22 +51,18 @@ The plugin uses: * **DOMDocument / libxml** for the best HTML-to-BBCode conversion * **OpenSSL** to protect stored secrets when available +Mastodon compatibility range: +**Technical:** >= 4.0.0 as of November 2022 +**Recommended:** >= 4.4.0 as of July 2025 + ===== Recommended FlatPress plugins ===== For the best result with synchronized Mastodon content, also enable these FlatPress plugins: - * **BBCode** - * **PhotoSwipe** - * **AudioVideo** - * **[[tag|Tag]]** - * **Emoticons** - -They improve the result like this: - * **BBCode** renders imported formatting, links, images and galleries properly. * **PhotoSwipe** improves image and gallery display. * **AudioVideo** renders imported and synchronized audio/video attachments as HTML5 media players. - * **Tag** enables tag / hashtag synchronization. + * **[[tag|Tag]]** enables tag / hashtag synchronization. * **Emoticons** renders imported emoji shortcodes more nicely. ===== What the plugin currently does and does not do ===== @@ -144,6 +140,24 @@ Here you set: The **Synchronization start date** is useful when you do not want to import or export much older content. +I recommend setting the start date once and then not changing it. You can change the start date in the following scenarios: + +| **Action** | **Recommendation** | +| ------------------------------------------------------ | ---------------------------------------------------------------------------------------: | +| Historical initial import / backfill | Extend **backward in 7–14-day blocks** | +| Many gallery/video posts in the history | Rather by **number of media items**, not by days: max. approx. 20–24 media items per run | + +Automatic window for scheduled runs (specific guidelines). + +| **Usage scenario** | **Recommendation** | +| ------------------------------------------------------ | ---------------------------------------------------------------------------: | +| Normal blog, few media files, few comments | **14 days** | +| Very quiet blog, hardly any media, few old changes | **30 days maximum** | +| Media-heavy: galleries, audio, video | **7 days** | +| Many comments or many old edits | **7–14 days** | + +Tip: If you see error messages indicating that limits have been reached, select a shorter time frame, such as 7 days. + ==== 2. More options ==== These switches are especially important: @@ -155,7 +169,16 @@ These switches are especially important: Allows certain synchronized Mastodon replies to also appear as FlatPress entries. Leave this disabled if you want to avoid duplicate content and keep thread replies mainly in the comment area. * **Enable deletion synchronization** - Enables the separate follow-up delete pass. + Enables the separate follow-up delete pass. Deletion synchronization ensures that posts/ statuses, comments/ replies deleted on one side are also deleted on the other. + +**Behavior of deletion synchronizations:** +| **Case | **Behavior** | +| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Scheduled deletion sync, local content still exists | Remote existence check only within the automatic 7/14/30-day window | +| Scheduled deletion sync, local content has been deleted | Mastodon status/reply will still be deleted, provided it is within the **Synchronization start date** | +| Manual/Force deletion run | May continue to check completely | +| Pending child rechecks | Are not blocked by the automatic window | +| Large mapping sets | Are distributed across multiple runs using a cursor | ==== 3. OAuth helper ==== @@ -342,8 +365,11 @@ If you mainly want a blog-like discussion structure, leaving the option disabled The plugin stores its working data in: - * ''fp-content/plugin_mastodon/state.json'' * ''fp-content/plugin_mastodon/sync.log'' + * ''fp-content/plugin_mastodon/sync.guard.json'' + * ''fp-content/plugin_mastodon/state.json'' + * ''fp-content/plugin_mastodon/scheduler-state.json'' + * ''fp-content/plugin_mastodon/rate-limit-windows.json'' Imported Mastodon images are stored under: diff --git a/fp-plugins/mastodon/plugin.mastodon.php b/fp-plugins/mastodon/plugin.mastodon.php index 8c9f8b78..28eb947b 100644 --- a/fp-plugins/mastodon/plugin.mastodon.php +++ b/fp-plugins/mastodon/plugin.mastodon.php @@ -3,7 +3,7 @@ * Plugin Name: Mastodon * Plugin URI: https://www.flatpress.org * Description: Synchronizes FlatPress entries and comments with Mastodon. [Instructions] - * Version: 2.4.1 + * Version: 2.4.2 * Author: FlatPress * Author URI: https://www.flatpress.org */ @@ -3015,6 +3015,27 @@ function plugin_mastodon_scheduler_state_write($state) { return $written; } +/** + * Decode a scheduler-state JSON payload only when it matches the current full-state signature. + * @param string|false|null $json + * @param string $sourceSignature + * @return array + */ +function plugin_mastodon_scheduler_state_decode_fresh($json, $sourceSignature) { + if (!is_string($json) || trim($json) === '') { + return array(); + } + $data = json_decode($json, true); + if (!is_array($data)) { + return array(); + } + $schedulerState = plugin_mastodon_scheduler_state_normalize($data); + if ($schedulerState ['source_state_signature'] !== (string) $sourceSignature) { + return array(); + } + return $schedulerState; +} + /** * Load the lightweight scheduler summary and rebuild it conservatively when stale. * @return array @@ -3036,15 +3057,20 @@ function plugin_mastodon_scheduler_state_read() { } $json = plugin_mastodon_io_read_file(PLUGIN_MASTODON_SCHEDULER_STATE_FILE, $prestat); - if (is_string($json) && trim($json) !== '') { - $data = json_decode($json, true); - if (is_array($data)) { - $schedulerState = plugin_mastodon_scheduler_state_normalize($data); - if ($schedulerState ['source_state_signature'] === $sourceSignature) { - plugin_mastodon_runtime_cache_set('scheduler_state', '__signature__', $signature); - plugin_mastodon_runtime_cache_set('scheduler_state', $signature, $schedulerState); - return $schedulerState; - } + $schedulerState = plugin_mastodon_scheduler_state_decode_fresh($json, $sourceSignature); + if ($schedulerState !== array()) { + plugin_mastodon_runtime_cache_set('scheduler_state', '__signature__', $signature); + plugin_mastodon_runtime_cache_set('scheduler_state', $signature, $schedulerState); + return $schedulerState; + } + + $uncachedJson = plugin_mastodon_io_read_file_uncached(PLUGIN_MASTODON_SCHEDULER_STATE_FILE, true); + if ($uncachedJson !== $json) { + $schedulerState = plugin_mastodon_scheduler_state_decode_fresh($uncachedJson, $sourceSignature); + if ($schedulerState !== array()) { + plugin_mastodon_runtime_cache_set('scheduler_state', '__signature__', $signature); + plugin_mastodon_runtime_cache_set('scheduler_state', $signature, $schedulerState); + return $schedulerState; } } } @@ -7041,6 +7067,30 @@ function plugin_mastodon_instance_supports_status_media_attributes($options) { return version_compare($normalized, '4.1.0', '>='); } +/** + * Determine whether cached instance information confirms support for the + * delete_media query parameter on DELETE /api/v1/statuses/:id. + * + * Mastodon added the optional delete_media parameter in 4.4.0. This helper + * intentionally uses cached or stored instance information only, so deletion + * synchronization does not spend an additional network request merely to decide + * which DELETE shape to use. + * + * @param array $options + * @return bool|null True/false when the cached version is known, null when it is unknown. + */ +function plugin_mastodon_instance_supports_status_delete_media($options) { + $document = plugin_mastodon_instance_document($options, false); + if (empty($document ['version']) || !is_string($document ['version'])) { + return null; + } + $normalized = preg_replace('/[^0-9.].*$/', '', trim((string) $document ['version'])); + if (!is_string($normalized) || $normalized === '' || !preg_match('/^\d+(?:\.\d+){0,3}$/', $normalized)) { + return null; + } + return version_compare($normalized, '4.4.0', '>='); +} + /** * Load and cache the Mastodon instance configuration document. * @param array $options @@ -8477,10 +8527,45 @@ function plugin_mastodon_fetch_status($options, $statusId) { */ function plugin_mastodon_delete_status($options, $statusId, $deleteMedia = true) { $path = '/api/v1/statuses/' . rawurlencode((string) $statusId); - if ($deleteMedia) { - $path .= '?delete_media=1'; + $deleteMediaSupported = $deleteMedia ? plugin_mastodon_instance_supports_status_delete_media($options) : false; + $useDeleteMediaParameter = $deleteMedia && $deleteMediaSupported !== false; + $requestPath = $path; + if ($useDeleteMediaParameter) { + $requestPath .= '?delete_media=1'; + } + + $response = plugin_mastodon_mastodon_json($options, 'DELETE', $requestPath, array(), true); + if ($useDeleteMediaParameter && empty($response ['ok']) && plugin_mastodon_delete_status_should_retry_without_delete_media($response)) { + plugin_mastodon_log('Retrying Mastodon status deletion without delete_media for status ' . (string) $statusId . ' after response ' . (isset($response ['code']) ? (string) (int) $response ['code'] : '0')); + $fallback = plugin_mastodon_mastodon_json($options, 'DELETE', $path, array(), true); + $fallback ['delete_media_fallback_attempted'] = true; + $fallback ['delete_media_first_code'] = isset($response ['code']) ? (int) $response ['code'] : 0; + $fallback ['delete_media_first_error'] = plugin_mastodon_response_error_message($response); + return $fallback; } - return plugin_mastodon_mastodon_json($options, 'DELETE', $path, array(), true); + return $response; +} + +/** + * Check whether a failed status deletion may be caused by Mastodon versions + * before 4.4.0 not understanding the optional delete_media query parameter. + * + * @param array $response + * @return bool + */ +function plugin_mastodon_delete_status_should_retry_without_delete_media($response) { + if (plugin_mastodon_rate_limit_state_error() !== '') { + return false; + } + $code = isset($response ['code']) ? (int) $response ['code'] : 0; + if ($code === 400 || $code === 405 || $code === 422) { + return true; + } + if ($code === 0) { + $message = strtolower(plugin_mastodon_response_error_message($response)); + return $message !== '' && strpos($message, 'delete_media') !== false; + } + return false; } /**