diff --git a/index.php b/index.php index 1ba614a..9cce61d 100644 --- a/index.php +++ b/index.php @@ -106,6 +106,10 @@ send(); // API for posting content to the Fediverse case "outbox": outbox(); // Optional. Dynamic. + case "follow": + follow(); // User interface for following an external user + case "follow_user": + follow_user(); // API for following a user case "/": home(); // Optional. Can be dynamic default: @@ -265,10 +269,10 @@ // Close cURL session curl_close($ch); - // Save the actor's data in `/data/followers/` - if( ! is_dir( "data/followers" ) ) { mkdir( "data"); mkdir( "data/followers"); } - $follower_filename = urlencode( $follower_actor ); - file_put_contents( "data/followers/{$follower_filename}.json", $inbox_actor_json ); + // Save the actor's data in `/data/followers/` + if( ! is_dir( "data/followers" ) ) { mkdir( "data"); mkdir( "data/followers"); } + $follower_filename = urlencode( $follower_actor ); + file_put_contents( "data/followers/{$follower_filename}.json", $inbox_actor_json ); } else { die(); @@ -713,7 +717,7 @@ HTML; // This goes in the generic "tag" property // TODO: Add this to the CC field foreach ( $usernames as $username ) { - list( $null, $user, $domain ) = explode( "@", $username ); + list( , $user, $domain ) = explode( "@", $username ); $tags[] = array( "type" => "Mention", "href" => "https://{$domain}/@{$user}", @@ -773,6 +777,113 @@ HTML; die(); } + // This creates a UI for the user to follow another user + function follow() { + echo <<< HTML + + + + + Follow + + + +
+ +
+
+
+ +
+ + +HTML; + die(); + } + + // This receives a request to follow an external user + // It looks up the external user's details + // Then it sends a follow request + // If the request is accepted, it saves the details in `data/following/` as a JSON file + function follow_user() { + global $password, $server, $username, $key_private; + + // Does the posted password match the stored password? + if( $password != $_POST["password"] ) { echo "Wrong Password!"; die(); } + + // Get the posted content + $user = $_POST["user"]; + + // Split the user into username and server + list( , $follow_name, $follow_server ) = explode( "@", $user ); + + // Get the Webfinger + $webfingerURl = "https://{$follow_server}/.well-known/webfinger?resource=acct:{$follow_name}@{$follow_server}"; + $webfingerJSON = file_get_contents( $webfingerURl ); + $webfinger = json_decode( $webfingerJSON, true ); + + foreach( $webfinger["links"] as $link ) { + if ( "self" == $link["rel"] ) { + $profileURl = $link["href"]; + } + } + if ( !isset( $profileURl ) ) { echo "No profile"; die(); } + + // Get the user's details + // This request does not need to be signed. But it does need to specify that it wants a JSON response + $context = stream_context_create( + [ "http" => [ "header" => "Accept: application/activity+json" ] ] + ); + $profileJSON = file_get_contents( $profileURl, false, $context ); + $profileData = json_decode( $profileJSON, true ); + + // Get the user's inbox + $profileInbox = $profileData["inbox"]; + $inbox_host = parse_url( $profileInbox, PHP_URL_HOST ); + $inbox_path = parse_url( $profileInbox, PHP_URL_PATH ); + + // Create a follow request + $guid = uuid(); + $message = [ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "https://{$server}/{$guid}", + "type" => "Follow", + "actor" => "https://{$server}/{$username}", + "object" => $profileURl + ]; + + // Sign a request follow + // The Accept is POSTed to the inbox on the server of the user who requested the follow + // Get the signed headers + $headers = generate_signed_headers( $message, $inbox_host, $inbox_path, "POST" ); + + // POST the message and header to the requester's inbox + $ch = curl_init( $profileInbox ); + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "POST" ); + curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( $message ) ); + curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers ); + curl_exec( $ch ); + + // Check for errors + if( curl_errno( $ch ) ) { + file_put_contents( "error.txt", curl_error( $ch ) ); + } + curl_close($ch); + + // Save the user's details + // Save headers and request data to the timestamped file in the logs directory + if( ! is_dir( "data/following" ) ) { mkdir( "data"); mkdir( "data/following"); } + $following_filename = urlencode( $profileURl ); + file_put_contents( "data/following/{$following_filename}.json", $profileJSON ); + + // Render the JSON so the user can see the POST has worked + header( "Location: https://{$server}/data/following/" . urlencode( $following_filename ) . ".json" ); + die(); + } + // Verify the signature sent with the message. // This is optional // It is very confusing