|
7 | 7 |
|
8 | 8 | namespace Activitypub\Collection; |
9 | 9 |
|
| 10 | +use Activitypub\Signature; |
10 | 11 | use Activitypub\Tombstone; |
11 | 12 |
|
12 | 13 | use function Activitypub\get_remote_metadata_by_actor; |
| 14 | +use function Activitypub\get_rest_url_by_path; |
13 | 15 |
|
14 | 16 | /** |
15 | 17 | * ActivityPub Followers Collection. |
@@ -567,4 +569,108 @@ public static function remove_blocked_actors( $value, $type, $user_id ) { |
567 | 569 |
|
568 | 570 | self::remove( $actor_id, $user_id ); |
569 | 571 | } |
| 572 | + |
| 573 | + /** |
| 574 | + * Compute the partial follower collection digest for a specific instance. |
| 575 | + * |
| 576 | + * Implements FEP-8fcf: Followers collection synchronization. |
| 577 | + * This is a convenience wrapper that filters followers by authority and then |
| 578 | + * computes the digest using the standard FEP-8fcf algorithm. |
| 579 | + * |
| 580 | + * The digest is created by XORing together the individual SHA256 digests |
| 581 | + * of each follower's ID. |
| 582 | + * |
| 583 | + * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md |
| 584 | + * @see Signature::get_collection_digest() for the core digest algorithm |
| 585 | + * |
| 586 | + * @param int $user_id The user ID whose followers to compute. |
| 587 | + * @param string $authority The URI authority (scheme + host) to filter by. |
| 588 | + * |
| 589 | + * @return string|false The hex-encoded digest, or false if no followers. |
| 590 | + */ |
| 591 | + public static function compute_partial_digest( $user_id, $authority ) { |
| 592 | + // Get followers filtered by authority. |
| 593 | + $followers = self::get_by_authority( $user_id, $authority ); |
| 594 | + $follower_ids = \wp_list_pluck( $followers, 'guid' ); |
| 595 | + |
| 596 | + // Delegate to the core digest computation algorithm. |
| 597 | + return Signature::get_collection_digest( $follower_ids ); |
| 598 | + } |
| 599 | + |
| 600 | + /** |
| 601 | + * Get partial followers collection for a specific instance. |
| 602 | + * |
| 603 | + * Returns only followers whose ID shares the specified URI authority. |
| 604 | + * Used for FEP-8fcf synchronization. |
| 605 | + * |
| 606 | + * @param int $user_id The user ID whose followers to get. |
| 607 | + * @param string $authority The URI authority (scheme + host) to filter by. |
| 608 | + * |
| 609 | + * @return \WP_Post[] Array of WP_Post objects. |
| 610 | + */ |
| 611 | + public static function get_by_authority( $user_id, $authority ) { |
| 612 | + $posts = new \WP_Query( |
| 613 | + array( |
| 614 | + 'post_type' => Remote_Actors::POST_TYPE, |
| 615 | + 'posts_per_page' => -1, |
| 616 | + 'orderby' => 'ID', |
| 617 | + 'order' => 'DESC', |
| 618 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query |
| 619 | + 'meta_query' => array( |
| 620 | + 'relation' => 'AND', |
| 621 | + array( |
| 622 | + 'key' => self::FOLLOWER_META_KEY, |
| 623 | + 'value' => $user_id, |
| 624 | + ), |
| 625 | + array( |
| 626 | + 'key' => '_activitypub_inbox', |
| 627 | + 'compare' => 'LIKE', |
| 628 | + 'value' => $authority, |
| 629 | + ), |
| 630 | + ), |
| 631 | + ) |
| 632 | + ); |
| 633 | + |
| 634 | + return $posts->posts ?? array(); |
| 635 | + } |
| 636 | + |
| 637 | + /** |
| 638 | + * Generate the Collection-Synchronization header value for FEP-8fcf. |
| 639 | + * |
| 640 | + * @param int $user_id The user ID whose followers collection to sync. |
| 641 | + * @param string $authority The authority of the receiving instance. |
| 642 | + * |
| 643 | + * @return string|false The header value, or false if cannot generate. |
| 644 | + */ |
| 645 | + public static function generate_sync_header( $user_id, $authority ) { |
| 646 | + $followers = self::get_by_authority( $user_id, $authority ); |
| 647 | + $followers = \wp_list_pluck( $followers, 'guid' ); |
| 648 | + |
| 649 | + // Compute the digest for this specific authority. |
| 650 | + $digest = Signature::get_collection_digest( $followers ); |
| 651 | + |
| 652 | + if ( ! $digest ) { |
| 653 | + return false; |
| 654 | + } |
| 655 | + |
| 656 | + // Build the collection ID (followers collection URL). |
| 657 | + $collection_id = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user_id ) ); |
| 658 | + |
| 659 | + // Build the partial followers URL. |
| 660 | + $url = get_rest_url_by_path( |
| 661 | + sprintf( |
| 662 | + 'actors/%d/followers/sync?authority=%s', |
| 663 | + $user_id, |
| 664 | + rawurlencode( $authority ) |
| 665 | + ) |
| 666 | + ); |
| 667 | + |
| 668 | + // Format as per FEP-8fcf (similar to HTTP Signatures format). |
| 669 | + return sprintf( |
| 670 | + 'collectionId="%s", url="%s", digest="%s"', |
| 671 | + $collection_id, |
| 672 | + $url, |
| 673 | + $digest |
| 674 | + ); |
| 675 | + } |
570 | 676 | } |
0 commit comments