Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b83c6ec
feat(standard-site): publish posts as site.standard.document records
danielpost May 31, 2026
c51b36f
feat(settings): make publication overrides explicit via Update button
danielpost May 31, 2026
2abef7e
feat(standard-site): try app.bsky.embed.record for the bsky post
danielpost May 31, 2026
2ce2130
revert(standard-site): drop the app.bsky.embed.record experiment
danielpost May 31, 2026
5cab0c1
feat(standard-site): re-enable app.bsky.embed.record for the bsky post
danielpost May 31, 2026
ece0eb4
revert(standard-site): go back to external link card embed
danielpost May 31, 2026
83a97dc
feat(standard-site): add basicTheme RGB colors to publication
danielpost May 31, 2026
95f1d18
feat(standard-site): auto-adopt existing publication on connect
danielpost May 31, 2026
9721e5b
feat(standard-site): Records admin tab
danielpost May 31, 2026
ea4e83b
style(records): match Settings tab layout; render thumbnails
danielpost May 31, 2026
81b1731
feat(standard-site): publication icon picker
danielpost May 31, 2026
288f7dc
fix(settings): use wp.media() directly for the icon picker
danielpost May 31, 2026
154a5c0
feat(records): pdsls links, View on Bluesky, Refresh, relative dates
danielpost May 31, 2026
e3472a3
feat(records): match Settings width, one card per document, Tooltip date
danielpost May 31, 2026
0ec25d6
style(records): match Settings card padding (20px / 10px mobile)
danielpost May 31, 2026
9dd8169
style(records): match Logs page refresh button
danielpost May 31, 2026
707dd32
feat(settings): use PaletteEdit for publication theme colors
danielpost May 31, 2026
49c1b58
style(settings): pass raw draft theme colors (empty when unset)
danielpost May 31, 2026
2124586
style(settings): use transparent as the empty-color sentinel
danielpost May 31, 2026
6d0fba6
style(settings): move theme-colors help text below the palette
danielpost May 31, 2026
a9b4223
feat(settings): gate publication theme colors behind a toggle
danielpost May 31, 2026
44340b6
feat(records,settings): show basicTheme as ColorIndicators; label the…
danielpost May 31, 2026
770178f
style(settings): drop the 'Theme colors' label inside PaletteEdit
danielpost May 31, 2026
4af33dd
style(settings): URL above icon, icon + colors side-by-side 50/50
danielpost May 31, 2026
0116da2
style(settings): hide PaletteEdit header row, drop AT-URI field
danielpost May 31, 2026
2167616
feat(standard-site): attach document + publication strongRefs to bsky…
danielpost May 31, 2026
b12f71f
style(editor): drop 'Also' from the standard.site toggle label
danielpost May 31, 2026
576e93f
fix(standard-site): correct basicTheme schema discriminators
danielpost May 31, 2026
9c6ca52
feat(standard-site): back-fill bskyPostRef on document after bsky pos…
danielpost May 31, 2026
01d7e19
revert(standard-site): don't back-fill bskyPostRef on the document
danielpost May 31, 2026
c15364a
fix(standard-site): restore basicTheme $type per the spec; re-enable …
danielpost May 31, 2026
aa52152
fix(standard-site): publish document atomically with bsky post
danielpost May 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions includes/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,43 @@ public function register_settings(): void {
],
);

register_setting(
'autoblue',
'autoblue_publish_documents_enabled',
[
'type' => 'boolean',
'description' => __( 'Whether to publish posts as standard.site documents.', 'autoblue' ),
'show_in_rest' => true,
'default' => false,
'sanitize_callback' => 'rest_sanitize_boolean',
],
);

register_setting(
'autoblue',
'autoblue_publication_overrides',
[
'type' => 'object',
'description' => __( 'Per-field overrides for the standard.site publication record.', 'autoblue' ),
'show_in_rest' => [
'schema' => [
'type' => 'object',
'properties' => [
'name' => [ 'type' => [ 'string', 'null' ] ],
'description' => [ 'type' => [ 'string', 'null' ] ],
'icon_id' => [ 'type' => [ 'integer', 'null' ] ],
'theme_background' => [ 'type' => [ 'string', 'null' ] ],
'theme_foreground' => [ 'type' => [ 'string', 'null' ] ],
'theme_accent' => [ 'type' => [ 'string', 'null' ] ],
'theme_accent_foreground' => [ 'type' => [ 'string', 'null' ] ],
],
'additionalProperties' => false,
],
],
'default' => [],
],
);

register_setting(
'autoblue',
'autoblue_log_level',
Expand Down
18 changes: 18 additions & 0 deletions includes/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public function register_admin_assets( string $hook_suffix ): void {
return;
}

wp_enqueue_media();
wp_enqueue_style( 'wp-components' );
wp_enqueue_style( 'dataviews' );
$this->enqueue_script(
Expand Down Expand Up @@ -108,13 +109,30 @@ private function get_initial_state(): array {
],
];

$publication_record = get_option( 'autoblue_publication_record', [] );
$overrides = get_option( 'autoblue_publication_overrides', [] );
$override_icon_id = is_array( $overrides ) && ! empty( $overrides['icon_id'] ) ? (int) $overrides['icon_id'] : 0;
$override_icon_url = $override_icon_id > 0 ? (string) wp_get_attachment_image_url( $override_icon_id, 'thumbnail' ) : '';
$site_icon_url = (string) get_site_icon_url( 128 );

return [
'accounts' => [
'items' => $connections,
],
'settings' => [
'enabled' => get_option( 'autoblue_enabled', false ),
'availablePostTypes' => Utils::get_available_post_types(),
'standardSite' => [
'isRootInstall' => Utils::is_root_install(),
'publication' => [
'url' => untrailingslashit( home_url( '/' ) ),
'siteName' => get_bloginfo( 'name' ),
'siteDesc' => get_bloginfo( 'description' ),
'siteIconUrl' => $site_icon_url,
'iconOverrideUrl' => $override_icon_url,
'publishedUri' => isset( $publication_record['uri'] ) ? (string) $publication_record['uri'] : null,
],
],
],
'logs' => [
'items' => $logs['data'],
Expand Down
112 changes: 111 additions & 1 deletion includes/Bluesky.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,17 @@ public function share_to_bluesky( int $post_id ) {
$body['record']['embed']['external']['thumb'] = $image_blob;
}

$response = $this->api_client->create_record( $body, $connection['access_jwt'], $connection['did'] );
if ( $this->should_publish_document_for( $post_id ) ) {
$response = $this->create_post_with_document(
$post_id,
$post,
$connection,
$body['record'],
is_array( $image_blob ) ? $image_blob : null
);
} else {
$response = $this->api_client->create_record( $body, $connection['access_jwt'], $connection['did'] );
}

if ( is_wp_error( $response ) ) {
$this->log->error(
Expand Down Expand Up @@ -219,4 +229,104 @@ public function share_to_bluesky( int $post_id ) {

return $share;
}

/**
* Whether this post should also produce a standard.site document record.
*/
private function should_publish_document_for( int $post_id ): bool {
if ( ! Utils::is_standard_site_enabled() ) {
return false;
}

return (bool) get_post_meta( $post_id, 'autoblue_publish_document', true );
}

/**
* Create the Bluesky post and standard.site document in one atomic write.
*
* The initial document cannot include bskyPostRef because the Bluesky post
* CID is only known after the write. Once the atomic create succeeds, we
* update the document with the document-side strongRef. The Bluesky post does
* not point back at the document, avoiding an unstable CID cycle.
*
* @param array<string,mixed> $connection
* @param array<string,mixed> $bsky_record
* @param array<string,mixed>|null $image_blob
* @return array<string,mixed>|\WP_Error
*/
private function create_post_with_document( int $post_id, \WP_Post $post, array $connection, array $bsky_record, ?array $image_blob ) {
$publication = new Standard\Publication( $this->api_client, $this->log );
$publication_uri = $publication->ensure_exists_for_connection( $connection );
if ( is_wp_error( $publication_uri ) ) {
return $publication_uri;
}

$document = new Standard\Document( $this->api_client, $this->log, $publication );
$prepared = $document->prepare_record( $post_id, (string) $publication_uri, null, $image_blob );
if ( is_wp_error( $prepared ) ) {
return $prepared;
}

$result = $this->api_client->apply_writes(
[
[
'$type' => 'com.atproto.repo.applyWrites#create',
'collection' => 'app.bsky.feed.post',
'rkey' => $this->generate_rkey(),
'value' => $bsky_record,
],
[
'$type' => 'com.atproto.repo.applyWrites#create',
'collection' => 'site.standard.document',
'rkey' => $prepared['rkey'],
'value' => $prepared['record'],
],
],
$connection['access_jwt'],
$connection['did']
);

if ( is_wp_error( $result ) ) {
return $result;
}

$bsky_result = $result['results'][0] ?? [];
$doc_result = $result['results'][1] ?? [];
if ( empty( $bsky_result['uri'] ) || empty( $bsky_result['cid'] ) || empty( $doc_result['uri'] ) || empty( $doc_result['cid'] ) ) {
return new \WP_Error( 'autoblue_invalid_apply_writes_response', __( 'The PDS did not return complete Bluesky and document record refs.', 'autoblue' ) );
}

$document->store_meta( $post_id, $doc_result, $prepared['rkey'] );

$update = $document->update_bsky_ref(
$post_id,
[
'uri' => (string) $bsky_result['uri'],
'cid' => (string) $bsky_result['cid'],
],
$image_blob,
$connection
);

if ( is_wp_error( $update ) ) {
$this->log->error(
__( 'Failed to add bskyPostRef to standard.site document for post `{post_title}` with ID `{post_id}`: {message}', 'autoblue' ),
[
'post_id' => $post_id,
'post_title' => $post->post_title,
'message' => $update->get_error_message(),
]
);
}

return [
'uri' => (string) $bsky_result['uri'],
'cid' => (string) $bsky_result['cid'],
'applyWrites' => $result,
];
}

private function generate_rkey(): string {
return 'ab' . strtolower( str_replace( '-', '', wp_generate_uuid4() ) );
}
}
102 changes: 102 additions & 0 deletions includes/Bluesky/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,108 @@ public function create_record( array $record, string $access_token, string $did
);
}

/**
* List records in a collection via com.atproto.repo.listRecords.
*
* @return array<string,mixed>|\WP_Error
*/
public function list_records( string $repo, string $collection, string $access_token, ?string $cursor = null, int $limit = 100 ) {
if ( ! $repo || ! $collection || ! $access_token ) {
return new \WP_Error( 'autoblue_invalid_list_records_args', __( 'Repo, collection, and access token are required.', 'autoblue' ) );
}

$pds_endpoint = $this->resolve_pds_endpoint( $repo );

if ( is_wp_error( $pds_endpoint ) ) {
return $pds_endpoint;
}

$body = [
'repo' => $repo,
'collection' => $collection,
'limit' => $limit,
];

if ( $cursor ) {
$body['cursor'] = $cursor;
}

return $this->send_request(
[
'endpoint' => 'com.atproto.repo.listRecords',
'method' => 'GET',
'headers' => [
'Authorization' => 'Bearer ' . $access_token,
],
'body' => $body,
'base_url' => $pds_endpoint,
]
);
}

/**
* Create-or-update a record at a known rkey via com.atproto.repo.putRecord.
*
* @param array<string, mixed> $record Must include repo, collection, rkey, record.
* @return array<string,mixed>|\WP_Error
*/
public function put_record( array $record, string $access_token, string $did ) {
if ( ! $record || ! $access_token ) {
return new \WP_Error( 'autoblue_invalid_record_or_access_token', __( 'Invalid record or access token.', 'autoblue' ) );
}

$pds_endpoint = $this->resolve_pds_endpoint( $did );

if ( is_wp_error( $pds_endpoint ) ) {
return $pds_endpoint;
}

return $this->send_request(
[
'endpoint' => 'com.atproto.repo.putRecord',
'method' => 'POST',
'headers' => [
'Authorization' => 'Bearer ' . $access_token,
],
'body' => $record,
'base_url' => $pds_endpoint,
]
);
}

/**
* Apply a batch of writes atomically via com.atproto.repo.applyWrites.
*
* @param array<int,array<string,mixed>> $writes
* @return array<string,mixed>|\WP_Error
*/
public function apply_writes( array $writes, string $access_token, string $did ) {
if ( empty( $writes ) || ! $access_token || ! $did ) {
return new \WP_Error( 'autoblue_invalid_apply_writes_args', __( 'Writes, access token, and DID are required.', 'autoblue' ) );
}

$pds_endpoint = $this->resolve_pds_endpoint( $did );

if ( is_wp_error( $pds_endpoint ) ) {
return $pds_endpoint;
}

return $this->send_request(
[
'endpoint' => 'com.atproto.repo.applyWrites',
'method' => 'POST',
'headers' => [
'Authorization' => 'Bearer ' . $access_token,
],
'body' => [
'repo' => $did,
'writes' => $writes,
],
'base_url' => $pds_endpoint,
]
);
}

/**
* @return array<string,mixed>|\WP_Error
*/
Expand Down
8 changes: 8 additions & 0 deletions includes/ConnectionsManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ public function add_connection( string $did, string $app_password ) {
$this->log->success( __( 'Connection added.', 'autoblue' ), [ 'did' => $did ] );
}

/**
* Fires after a Bluesky connection is added or refreshed via reconnect.
*
* @param array<string,mixed> $new_connection The freshly stored connection.
* @param bool $is_reconnect Whether this was a reconnect of an existing DID.
*/
do_action( 'autoblue/connection_added', $new_connection, $is_reconnect );

return $new_connection;
}

Expand Down
Loading
Loading