Skip to content

Commit 0f22c7e

Browse files
kraftbjpfefferle
andauthored
Resolve Bluesky reactions on non-post post types (#185)
Co-authored-by: Matthias Pfefferle <pfefferle@users.noreply.github.com>
1 parent a9a7954 commit 0f22c7e

3 files changed

Lines changed: 200 additions & 13 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
Import Bluesky likes, reposts, and replies on all your published content types. Previously these interactions were only brought back for standard posts, so likes and replies on pages and other content published to Bluesky were quietly missed.

includes/class-reaction-sync.php

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,12 @@ private static function quoted_post_web_url( string $at_uri ): string {
10591059
* fallback, a like/repost targeting a reply post would silently
10601060
* fail to resolve back to the originating WordPress post.
10611061
*
1062+
* Both lookups are scoped to `get_supported_post_types()` so the
1063+
* resolver covers exactly the types the Publisher federates — see
1064+
* {@see self::find_post_by_uri_meta()} for why the explicit scope
1065+
* matters. When no post types are supported there is nothing to
1066+
* resolve, so bail before running any query.
1067+
*
10621068
* @param string $uri AT-URI.
10631069
* @return int|false
10641070
*/
@@ -1067,25 +1073,49 @@ private static function find_post_by_bsky_uri( string $uri ): int|false {
10671073
return false;
10681074
}
10691075

1070-
$posts = \get_posts(
1071-
array(
1072-
'meta_key' => BskyPost::META_URI, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
1073-
'meta_value' => $uri, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
1074-
'posts_per_page' => 1,
1075-
'post_status' => 'publish',
1076-
'has_password' => false,
1077-
'fields' => 'ids',
1078-
)
1079-
);
1076+
$post_types = get_supported_post_types();
10801077

1081-
if ( ! empty( $posts ) ) {
1082-
return (int) $posts[0];
1078+
/*
1079+
* An empty supported-types list means nothing is federated, so
1080+
* nothing should resolve. Bail before get_posts(), whose empty
1081+
* `post_type` array silently falls back to WordPress's `post`
1082+
* default and would reintroduce the exact miss this scoping fixes.
1083+
*/
1084+
if ( empty( $post_types ) ) {
1085+
return false;
1086+
}
1087+
1088+
$post_id = self::find_post_by_uri_meta( BskyPost::META_URI, $uri, $post_types );
1089+
1090+
if ( false !== $post_id ) {
1091+
return $post_id;
10831092
}
10841093

1094+
return self::find_post_by_uri_meta( BskyPost::META_URI_INDEX, $uri, $post_types );
1095+
}
1096+
1097+
/**
1098+
* Resolve a published, federated post from a URI-valued meta key.
1099+
*
1100+
* Scoped to the supported post types rather than left to
1101+
* `get_posts()`'s `post_type => 'post'` default. Without the explicit
1102+
* scope, reactions targeting a custom post type we publish (an
1103+
* `aside`/`status`-style CPT, a note type, etc.) resolve to nothing
1104+
* and are dropped silently — no comment row, no error. Passing the
1105+
* types explicitly also picks up supported CPTs registered with
1106+
* `exclude_from_search`, which the `'any'` shortcut would miss.
1107+
*
1108+
* @param string $meta_key Meta key to match ({@see BskyPost::META_URI} or META_URI_INDEX).
1109+
* @param string $uri AT-URI to match.
1110+
* @param string[] $post_types Supported post types to scope the query to.
1111+
* @return int|false
1112+
*/
1113+
private static function find_post_by_uri_meta( string $meta_key, string $uri, array $post_types ): int|false {
10851114
$posts = \get_posts(
10861115
array(
1087-
'meta_key' => BskyPost::META_URI_INDEX, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
1116+
'meta_key' => $meta_key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
10881117
'meta_value' => $uri, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
1118+
'post_type' => $post_types,
10891119
'posts_per_page' => 1,
10901120
'post_status' => 'publish',
10911121
'has_password' => false,

tests/phpunit/tests/class-test-reaction-sync.php

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@
1818
*/
1919
class Test_Reaction_Sync extends WP_UnitTestCase {
2020

21+
/**
22+
* Reset post-type support state leaked by the resolver tests.
23+
*/
24+
public function tear_down(): void {
25+
\delete_option( 'atmosphere_support_post_types' );
26+
27+
if ( \post_type_exists( 'atmos_hidden_cpt' ) ) {
28+
\unregister_post_type( 'atmos_hidden_cpt' );
29+
}
30+
31+
parent::tear_down();
32+
}
33+
2134
/**
2235
* Test that find_post_by_bsky_uri returns the correct post.
2336
*/
@@ -75,6 +88,146 @@ public function test_find_post_by_bsky_uri_not_found() {
7588
$this->assertFalse( $method->invoke( null, 'at://did:plc:unknown/app.bsky.feed.post/xyz' ) );
7689
}
7790

91+
/**
92+
* A reaction targeting a supported non-`post` post type must resolve.
93+
*
94+
* Regression: `find_post_by_bsky_uri()` called `get_posts()` without a
95+
* `post_type`, so WordPress defaulted the query to `post_type => 'post'`.
96+
* Every like/repost/reply on a supported page or custom post type then
97+
* resolved to nothing and was dropped silently — no comment row, no
98+
* error, no log. Fails before the resolver is scoped to the federated
99+
* post types.
100+
*/
101+
public function test_find_post_by_bsky_uri_resolves_supported_non_default_post_type() {
102+
\update_option( 'atmosphere_support_post_types', array( 'post', 'page' ) );
103+
104+
$post_id = self::factory()->post->create(
105+
array(
106+
'post_type' => 'page',
107+
'post_status' => 'publish',
108+
)
109+
);
110+
$uri = 'at://did:plc:test123/app.bsky.feed.post/page123';
111+
112+
\update_post_meta( $post_id, BskyPost::META_URI, $uri );
113+
114+
$method = new \ReflectionMethod( Reaction_Sync::class, 'find_post_by_bsky_uri' );
115+
116+
$this->assertSame( $post_id, $method->invoke( null, $uri ) );
117+
}
118+
119+
/**
120+
* A supported CPT registered with `exclude_from_search` must resolve too.
121+
*
122+
* This is why the resolver scopes to `get_supported_post_types()` rather
123+
* than the `post_type => 'any'` shortcut: `'any'` omits post types flagged
124+
* `exclude_from_search`, so a naive `'any'` fix would still drop reactions
125+
* on such a type. Fails both before the fix and under an `'any'`-based fix.
126+
*/
127+
public function test_find_post_by_bsky_uri_resolves_supported_exclude_from_search_cpt() {
128+
\register_post_type(
129+
'atmos_hidden_cpt',
130+
array(
131+
'public' => true,
132+
'exclude_from_search' => true,
133+
)
134+
);
135+
\update_option( 'atmosphere_support_post_types', array( 'post', 'atmos_hidden_cpt' ) );
136+
137+
$post_id = self::factory()->post->create(
138+
array(
139+
'post_type' => 'atmos_hidden_cpt',
140+
'post_status' => 'publish',
141+
)
142+
);
143+
$uri = 'at://did:plc:test123/app.bsky.feed.post/hidden123';
144+
145+
\update_post_meta( $post_id, BskyPost::META_URI, $uri );
146+
147+
$method = new \ReflectionMethod( Reaction_Sync::class, 'find_post_by_bsky_uri' );
148+
149+
$this->assertSame( $post_id, $method->invoke( null, $uri ) );
150+
}
151+
152+
/**
153+
* The thread-index fallback must also resolve a supported non-`post` type.
154+
*
155+
* `find_post_by_bsky_uri()` scopes BOTH lookups — the single-record
156+
* `META_URI` key and the `META_URI_INDEX` thread fallback Publisher
157+
* populates for every teaser-thread reply. The other regression tests
158+
* only seed `META_URI`, so they never reach the second query. This one
159+
* seeds only the index key on a `page` to guard the fallback branch
160+
* against a future change that drops its `post_type` scope.
161+
*/
162+
public function test_find_post_by_bsky_uri_thread_index_resolves_supported_non_default_post_type() {
163+
\update_option( 'atmosphere_support_post_types', array( 'post', 'page' ) );
164+
165+
$post_id = self::factory()->post->create(
166+
array(
167+
'post_type' => 'page',
168+
'post_status' => 'publish',
169+
)
170+
);
171+
$reply_uri = 'at://did:plc:test123/app.bsky.feed.post/pagereply123';
172+
173+
\add_post_meta( $post_id, BskyPost::META_URI_INDEX, $reply_uri );
174+
175+
$method = new \ReflectionMethod( Reaction_Sync::class, 'find_post_by_bsky_uri' );
176+
177+
$this->assertSame( $post_id, $method->invoke( null, $reply_uri ) );
178+
}
179+
180+
/**
181+
* A reaction on an unsupported post type must NOT resolve.
182+
*
183+
* Pins the upper bound of the scoping: the resolver covers exactly the
184+
* federated types, no more. Without this, a future widening back to
185+
* `post_type => 'any'` would silently start importing reactions onto
186+
* content the site never federated, and every positive test would still
187+
* pass. Here `page` carries matching meta but is absent from the
188+
* supported list, so the lookup must miss.
189+
*/
190+
public function test_find_post_by_bsky_uri_ignores_unsupported_post_type() {
191+
\update_option( 'atmosphere_support_post_types', array( 'post' ) );
192+
193+
$post_id = self::factory()->post->create(
194+
array(
195+
'post_type' => 'page',
196+
'post_status' => 'publish',
197+
)
198+
);
199+
$uri = 'at://did:plc:test123/app.bsky.feed.post/unsupported123';
200+
201+
\update_post_meta( $post_id, BskyPost::META_URI, $uri );
202+
203+
$method = new \ReflectionMethod( Reaction_Sync::class, 'find_post_by_bsky_uri' );
204+
205+
$this->assertFalse( $method->invoke( null, $uri ) );
206+
}
207+
208+
/**
209+
* An empty supported-types list must resolve nothing, not fall back to `post`.
210+
*
211+
* When a site owner unticks every type, the option is stored as an empty
212+
* array. Passing `post_type => array()` to `get_posts()` silently defaults
213+
* to `post_type => 'post'`, which would reintroduce the original miss (and
214+
* resolve reactions onto content the Publisher no longer federates). The
215+
* resolver must short-circuit to false instead: a matching `post` exists
216+
* here, yet nothing is federated, so nothing should resolve.
217+
*/
218+
public function test_find_post_by_bsky_uri_returns_false_when_no_supported_types() {
219+
\update_option( 'atmosphere_support_post_types', array() );
220+
221+
$post_id = self::factory()->post->create( array( 'post_status' => 'publish' ) );
222+
$uri = 'at://did:plc:test123/app.bsky.feed.post/notypes123';
223+
224+
\update_post_meta( $post_id, BskyPost::META_URI, $uri );
225+
226+
$method = new \ReflectionMethod( Reaction_Sync::class, 'find_post_by_bsky_uri' );
227+
228+
$this->assertFalse( $method->invoke( null, $uri ) );
229+
}
230+
78231
/**
79232
* Test that find_comment_by_source_id returns the correct comment.
80233
*/

0 commit comments

Comments
 (0)