Skip to content

Commit 39d2ba7

Browse files
authored
Add: Controls for publishing WordPress comments to Bluesky (#199)
1 parent 9abfb0a commit 39d2ba7

16 files changed

Lines changed: 714 additions & 54 deletions
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Add a setting — and a filter for plugins — to keep WordPress comment changes from being published to Bluesky while leaving incoming reactions enabled.

docs/developer-docs.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- [Extending Content Formats](#extending-content-formats)
99
- [Custom Post Type Support](#custom-post-type-support)
1010
- [Publishing Programmatically](#publishing-programmatically)
11+
- [Outgoing Comment Controls](#outgoing-comment-controls)
1112
- [Token Encryption](#token-encryption)
1213
- [Templates and Admin UI](#templates-and-admin-ui)
1314

@@ -352,6 +353,38 @@ landing page, so their `source_url` is intentionally empty and
352353
Integrations can react to each via
353354
[`atmosphere_reaction_synced`](#public-hooks).
354355

356+
## Outgoing Comment Controls
357+
358+
ATmosphere publishes eligible WordPress comments as Bluesky replies.
359+
Administrators can turn these writes off under
360+
**Settings → ATmosphere → Reactions** ("Outgoing replies"). The underlying
361+
`atmosphere_publish_comments` option defaults to enabled so existing sites
362+
keep their current behavior.
363+
364+
Host plugins can enforce the boundary with a behavior filter that runs
365+
*after* the stored preference and has the final say:
366+
367+
```php
368+
add_filter( 'atmosphere_should_publish_comments', '__return_false' );
369+
```
370+
371+
The override is on effective behavior, not the option — the saved
372+
preference stays untouched (and the settings form keeps editing it), so
373+
removing the filter restores whatever the site had configured. Because the
374+
filter runs last, it can also force the lane back *on* while the option is
375+
off.
376+
377+
While comment publishing is disabled, ATmosphere does not create, update,
378+
or delete Bluesky reply records for WordPress comments, including work that
379+
was already queued in WP-Cron. Replies that were previously published
380+
remain unchanged. Post and standard.site document publishing continues
381+
normally, as does inbound syncing of Bluesky replies, likes, and reposts.
382+
383+
Direct calls to the `Publisher` comment methods return a WP_Error with the
384+
`atmosphere_comment_publishing_disabled` code while the control is off. Use
385+
`\Atmosphere\is_comment_publishing_enabled()` when an integration needs to inspect
386+
the effective state.
387+
355388
## Token Encryption
356389

357390
OAuth tokens are encrypted at rest with a key derived from the site's `AUTH_KEY` and `AUTH_SALT`. That keeps the key out of the database, but it also means the stored tokens become unreadable when the salts change — after a migration, a regenerated `wp-config.php`, or a security plugin that rotates salts on a schedule. ATmosphere detects that case, flags the connection, and asks the user to reconnect.

docs/php-coding-standards.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ use function Atmosphere\is_connected;
225225
```php
226226
\apply_filters( 'atmosphere_syncable_post_types', array( 'post' ) );
227227
\apply_filters( 'atmosphere_should_publish_comment', $bool, $comment );
228+
\apply_filters( 'atmosphere_should_publish_comments', $bool ); // Feature switch for publishing WP comments to Bluesky; runs after the stored setting, final say. NOT the per-comment `_comment` filter above.
228229
\apply_filters( 'atmosphere_should_sync_reply', $bool, $notification, $post_id );
229230
\apply_filters( 'atmosphere_backfill_query_chunk_size', 500 );
230231
\apply_filters( 'atmosphere_publish_retry_delays', array( 60, 300, 900 ) ); // Backoff ladder for failed publish/update cron workers; length = retry budget; empty array disables retries.

includes/class-atmosphere.php

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -981,21 +981,21 @@ private static function clear_visibility_cleanup_marker( \WP_Post $post ): void
981981
/**
982982
* Schedule AT Protocol record deletion before a post is permanently deleted.
983983
*
984-
* Captures every Bluesky TID (post root + thread replies + outbound
985-
* comment replies) and the document TID from post meta, then
986-
* schedules a single async batch delete via cron. Thread-strategy
987-
* posts read every TID from `Post::META_THREAD_RECORDS`; outbound
988-
* comment replies come from `Publisher::collect_published_comment_tids()`.
984+
* Captures every Bluesky post TID (root + thread replies) and the
985+
* document TID from post meta, then schedules an async batch delete
986+
* via cron. When comment publishing is enabled, outbound comment
987+
* replies are also collected through
988+
* `Publisher::collect_published_comment_tids()`.
989989
*
990-
* Comment TIDs must be collected here, while WP still has the
990+
* Enabled comment TIDs must be collected here, while WP still has the
991991
* comment rows: `wp_delete_post( $id, true )` fires `before_delete_post`
992992
* first and only then iterates child comments, so this is the last
993993
* opportunity to read them.
994994
*
995-
* The trash path (`Publisher::delete_post()`) already cascades
996-
* comment deletes; this keeps the permanent-delete path symmetric
997-
* so unpublishing or hard-deleting a post does not orphan its
998-
* outbound replies on the PDS.
995+
* While comment publishing is enabled, the trash path
996+
* (`Publisher::delete_post()`) also cascades comment deletes. This
997+
* keeps permanent deletion symmetric. When they are disabled, both
998+
* paths preserve existing outbound replies instead.
999999
*
10001000
* @param int $post_id Post ID being deleted.
10011001
*/
@@ -1038,10 +1038,9 @@ public function on_before_delete( int $post_id ): void {
10381038

10391039
$doc_tid = (string) \get_post_meta( $post_id, Transformer\Document::META_TID, true );
10401040

1041-
$comment_tids = \array_column(
1042-
Publisher::collect_published_comment_tids( $post_id ),
1043-
'tid'
1044-
);
1041+
$comment_tids = is_comment_publishing_enabled()
1042+
? \array_column( Publisher::collect_published_comment_tids( $post_id ), 'tid' )
1043+
: array();
10451044

10461045
if ( ! empty( $bsky_tids ) || '' !== $doc_tid || ! empty( $comment_tids ) ) {
10471046
\wp_schedule_single_event(
@@ -1135,7 +1134,7 @@ public function on_comment_edit( int $comment_id ): void {
11351134
* @param int $comment_id Comment ID.
11361135
*/
11371136
public function on_comment_before_delete( int $comment_id ): void {
1138-
if ( ! is_connected() ) {
1137+
if ( ! is_comment_publishing_enabled() || ! is_connected() ) {
11391138
return;
11401139
}
11411140

@@ -1168,6 +1167,15 @@ public function on_comment_before_delete( int $comment_id ): void {
11681167
* @return bool
11691168
*/
11701169
public static function should_publish_comment( \WP_Comment $comment ): bool {
1170+
/*
1171+
* Checked first: a disabled site skips the eligibility work (which
1172+
* busts the parent post's cache on every comment event) and the
1173+
* per-comment filter entirely.
1174+
*/
1175+
if ( ! is_comment_publishing_enabled() ) {
1176+
return false;
1177+
}
1178+
11711179
$should = self::is_comment_eligible( $comment );
11721180

11731181
/**
@@ -1273,7 +1281,7 @@ private function schedule_comment_publish( \WP_Comment $comment ): void {
12731281
* @param \WP_Comment $comment Comment object.
12741282
*/
12751283
private function schedule_comment_delete( \WP_Comment $comment ): void {
1276-
if ( ! is_connected() ) {
1284+
if ( ! is_comment_publishing_enabled() || ! is_connected() ) {
12771285
return;
12781286
}
12791287

@@ -1686,6 +1694,10 @@ static function (): void {
16861694
\add_action(
16871695
'atmosphere_delete_records',
16881696
static function ( $bsky_tids, string $doc_tid, $comment_tids = array() ): void {
1697+
/*
1698+
* delete_post_by_tids() drops the comment TIDs itself when
1699+
* comment publishing is disabled at execution time.
1700+
*/
16891701
$comment_tids = \is_array( $comment_tids ) ? $comment_tids : array();
16901702
$result = Publisher::delete_post_by_tids( $bsky_tids, $doc_tid, $comment_tids );
16911703

@@ -1784,6 +1796,10 @@ static function ( int $comment_id ): void {
17841796
\add_action(
17851797
'atmosphere_delete_comment',
17861798
static function ( int $comment_id ): void {
1799+
if ( ! is_comment_publishing_enabled() ) {
1800+
return;
1801+
}
1802+
17871803
$comment = \get_comment( $comment_id );
17881804
if ( ! $comment instanceof \WP_Comment ) {
17891805
return;
@@ -1802,7 +1818,7 @@ static function ( int $comment_id ): void {
18021818
\add_action(
18031819
'atmosphere_delete_comment_record',
18041820
static function ( string $tid ): void {
1805-
if ( '' === $tid ) {
1821+
if ( ! is_comment_publishing_enabled() || '' === $tid ) {
18061822
return;
18071823
}
18081824

@@ -2208,6 +2224,15 @@ private static function is_transient_publish_error( \WP_Error $error ): bool {
22082224
* @param int $comment_id Comment ID just published.
22092225
*/
22102226
private static function reconcile_comment_after_publish( int $comment_id ): void {
2227+
/*
2228+
* A switch flipped while applyWrites was in flight cannot undo the
2229+
* completed request. Keep the returned record metadata intact and do
2230+
* not enqueue a compensating delete while outgoing writes are off.
2231+
*/
2232+
if ( ! is_comment_publishing_enabled() ) {
2233+
return;
2234+
}
2235+
22112236
/*
22122237
* Drop the in-process `WP_Comment` cache so a concurrent web
22132238
* request that just unapproved or deleted this comment is

includes/class-options.php

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,32 +50,47 @@ public static function register_settings(): void {
5050
'atmosphere',
5151
'atmosphere_auto_publish',
5252
array(
53-
'type' => 'boolean',
54-
'description' => 'Whether new posts are automatically published to AT Protocol.',
55-
'default' => '1',
56-
'show_in_rest' => true,
53+
'type' => 'boolean',
54+
'description' => 'Whether new posts are automatically published to AT Protocol.',
55+
'default' => '1',
56+
'show_in_rest' => true,
57+
'sanitize_callback' => array( Sanitize::class, 'boolean_option' ),
58+
)
59+
);
60+
61+
\register_setting(
62+
'atmosphere',
63+
'atmosphere_publish_comments',
64+
array(
65+
'type' => 'boolean',
66+
'description' => 'Whether eligible WordPress comments are published as Bluesky replies.',
67+
'default' => '1',
68+
'show_in_rest' => true,
69+
'sanitize_callback' => array( Sanitize::class, 'boolean_option' ),
5770
)
5871
);
5972

6073
\register_setting(
6174
'atmosphere',
6275
'atmosphere_sync_reactions',
6376
array(
64-
'type' => 'boolean',
65-
'description' => 'Whether Bluesky likes and reposts are imported.',
66-
'default' => '1',
67-
'show_in_rest' => true,
77+
'type' => 'boolean',
78+
'description' => 'Whether Bluesky likes and reposts are imported.',
79+
'default' => '1',
80+
'show_in_rest' => true,
81+
'sanitize_callback' => array( Sanitize::class, 'boolean_option' ),
6882
)
6983
);
7084

7185
\register_setting(
7286
'atmosphere',
7387
'atmosphere_sync_replies',
7488
array(
75-
'type' => 'boolean',
76-
'description' => 'Whether Bluesky replies are imported as comments.',
77-
'default' => '1',
78-
'show_in_rest' => true,
89+
'type' => 'boolean',
90+
'description' => 'Whether Bluesky replies are imported as comments.',
91+
'default' => '1',
92+
'show_in_rest' => true,
93+
'sanitize_callback' => array( Sanitize::class, 'boolean_option' ),
7994
)
8095
);
8196

0 commit comments

Comments
 (0)