PluginProbe
ActivityPub / trunk
ActivityPub vtrunk
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / handler / class-follow.php

class-follow.php in ActivityPub trunk, at includes/handler/class-follow.php

183 lines 5.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Follow handler file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Handler;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Application;
12 use Activitypub\Collection\Actors;
13 use Activitypub\Collection\Followers;
14 use Activitypub\Collection\Remote_Actors;
15 use Activitypub\Http;
16
17 use function Activitypub\add_to_outbox;
18 use function Activitypub\object_to_uri;
19
20 /**
21 * Handle Follow requests.
22 */
23 class Follow {
24 /**
25 * Initialize the class, registering WordPress hooks.
26 */
27 public static function init() {
28 \add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow' ), 10, 2 );
29 \add_action( 'activitypub_handled_follow', array( self::class, 'queue_accept' ), 10, 4 );
30
31 // The Application actor cannot be followed; explicitly reject such Follows so they don't sit "pending" on the remote instance forever.
32 \add_action( 'activitypub_inbox_shared_follow', array( self::class, 'reject_application_follow' ), 10, 2 );
33 }
34
35 /**
36 * Handle "Follow" requests.
37 *
38 * @param array $activity The activity object.
39 * @param int|int[] $user_ids The user ID(s).
40 */
41 public static function handle_follow( $activity, $user_ids ) {
42 // Extract the user ID (follow requests are always for a single user).
43 $user_id = \is_array( $user_ids ) ? \reset( $user_ids ) : $user_ids;
44
45 // Check if the actor already follows the user.
46 $already_following = false;
47 $remote_actor = Remote_Actors::get_by_uri( $activity['actor'] );
48 if ( ! \is_wp_error( $remote_actor ) ) {
49 $already_following = Followers::follows( $remote_actor->ID, $user_id );
50 }
51
52 // Save follower if not already following.
53 if ( $already_following ) {
54 $success = false;
55 } else {
56 $remote_actor = Followers::add( $user_id, $activity['actor'] );
57 $success = ! \is_wp_error( $remote_actor );
58
59 if ( $success ) {
60 $remote_actor = \get_post( $remote_actor );
61 }
62 }
63
64 /**
65 * Fires after a new follower has been added.
66 *
67 * @deprecated 7.5.0 Use "activitypub_handled_follow" instead.
68 *
69 * @param string $actor The URL of the actor (follower) who initiated the follow.
70 * @param array $activity The complete activity data of the follow request.
71 * @param int $user_id The ID of the WordPress user being followed.
72 * @param \WP_Post|\WP_Error $remote_actor The Actor object containing the new follower's data.
73 */
74 \do_action_deprecated( 'activitypub_followers_post_follow', array( $activity['actor'], $activity, $user_id, $remote_actor ), '7.5.0', 'activitypub_handled_follow' );
75
76 /**
77 * Fires after a Follow activity has been handled.
78 *
79 * @param array $activity The ActivityPub activity data.
80 * @param int[] $user_ids The local user IDs.
81 * @param bool $success True on success, false otherwise.
82 * @param \WP_Post|\WP_Error $remote_actor The remote actor/follower, or WP_Error if failed.
83 */
84 \do_action( 'activitypub_handled_follow', $activity, (array) $user_ids, $success, $remote_actor );
85 }
86
87 /**
88 * Send Accept response.
89 *
90 * @param array $activity_object The ActivityPub activity data.
91 * @param int|int[] $user_ids The local user IDs.
92 * @param bool $success True on success, false otherwise.
93 * @param \WP_Post|\WP_Error $remote_actor The remote actor/follower, or WP_Error if failed.
94 */
95 public static function queue_accept( $activity_object, $user_ids, $success, $remote_actor ) {
96 if ( \is_wp_error( $remote_actor ) ) {
97 // Impossible to send a "Reject" because we can not get the Remote-Inbox.
98 return;
99 }
100
101 // Extract the user ID from the array (follow requests are always for a single user).
102 $user_id = \is_array( $user_ids ) ? \reset( $user_ids ) : $user_ids;
103
104 $actor = $activity_object['actor'];
105
106 // Only send minimal data.
107 $activity_object = \array_intersect_key(
108 $activity_object,
109 array(
110 'id' => 1,
111 'type' => 1,
112 'actor' => 1,
113 'object' => 1,
114 )
115 );
116
117 $activity = new Activity();
118 $activity->set_type( 'Accept' );
119 $activity->set_actor( Actors::get_by_id( $user_id )->get_id() );
120 $activity->set_object( $activity_object );
121 $activity->set_to( array( $actor ) );
122
123 add_to_outbox( $activity, null, $user_id, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE );
124 }
125
126 /**
127 * Reject Follow requests aimed at the Application actor.
128 *
129 * The Application advertises `manuallyApprovesFollowers` but is not followable,
130 * so an explicit Reject is the only way a remote follow request gets resolved.
131 * The Reject is sent directly instead of through the Outbox, which only
132 * dispatches for real actors, and is signed with the Application key.
133 *
134 * @since 9.1.0
135 *
136 * @param array $activity The Follow activity data.
137 * @param int[] $user_ids The local recipient IDs the inbox resolved.
138 */
139 public static function reject_application_follow( $activity, $user_ids ) {
140 // A resolved recipient means the Follow targets a real actor, not the Application.
141 if ( ! empty( $user_ids ) ) {
142 return;
143 }
144
145 if ( empty( $activity['object'] ) || ! Application::is_application_resource( object_to_uri( $activity['object'] ) ) ) {
146 return;
147 }
148
149 $actor = object_to_uri( $activity['actor'] );
150 $remote_actor = Remote_Actors::fetch_by_uri( $actor );
151
152 if ( \is_wp_error( $remote_actor ) ) {
153 return;
154 }
155
156 $inbox = \get_post_meta( $remote_actor->ID, '_activitypub_inbox', true );
157
158 if ( ! $inbox ) {
159 return;
160 }
161
162 // Only send minimal data.
163 $origin_activity = \array_intersect_key(
164 $activity,
165 array(
166 'id' => 1,
167 'type' => 1,
168 'actor' => 1,
169 'object' => 1,
170 )
171 );
172
173 $reject = new Activity();
174 $reject->set_type( 'Reject' );
175 $reject->set_id( Application::get_id() . '#reject-' . \md5( \wp_json_encode( $origin_activity ) ) );
176 $reject->set_actor( Application::get_id() );
177 $reject->set_object( $origin_activity );
178 $reject->set_to( array( $actor ) );
179
180 Http::post( $inbox, $reject->to_json(), null );
181 }
182 }
183