diff --git a/index.php b/index.php
index 0ca1193..4632878 100644
--- a/index.php
+++ b/index.php
@@ -43,7 +43,7 @@
$server = $_SERVER["SERVER_NAME"]; // Do not change this!
// Set up where logs and messages go.
- // If you want to, you can change these directories to something more suitable for you.
+ // You can change these directories to something more suitable if you like.
$data = "data";
$directories = array(
"inbox" => "{$data}/inbox",
@@ -53,6 +53,7 @@
"posts" => "posts",
"images" => "images",
);
+ // Create the directories if they don't already exist.
foreach ( $directories as $directory ) {
if( !is_dir( $directory ) ) { mkdir( $data ); mkdir( $directory ); }
}
@@ -71,7 +72,8 @@
$bodyData = print_r( $body, true );
$requestData = print_r( $_REQUEST, true );
$serverData = print_r( $_SERVER, true );
- !empty( $_GET["path"] ) ? $path = $_GET["path"] : $path = "/";
+ // If the root has been requested, manually set the path to `/`
+ !empty( $_GET["path"] ) ? $path = $_GET["path"] : $path = "/";
// Get the type of request - used in the log filename
if ( isset( $body["type"] ) ) {
@@ -123,21 +125,21 @@
case "outbox":
outbox(); // Optional. Dynamic.
case "write":
- write(); // User interface for writing posts
+ write(); // User interface for writing posts.
case "send":
- send(); // API for posting content to the Fediverse
+ send(); // API for posting content to the Fediverse.
case "follow":
- follow(); // User interface for following an external user
+ follow(); // User interface for following an external user.
case "follow_user":
- follow_user(); // API for following a user
+ follow_user(); // API for following a user.
case "read":
- view( "read" ); // User interface for reading posts
+ view( "read" );// User interface for reading posts.
case ".well-known/nodeinfo":
- wk_nodeinfo(); // Optional. Static.
+ wk_nodeinfo(); // Optional. Static.
case "nodeinfo/2.1":
- nodeinfo(); // Optional. Static.
+ nodeinfo(); // Optional. Static.
case "/":
- view( "home" ); // Optional. Can be dynamic
+ view( "home" );// Optional. Can be dynamic
default:
die();
}
@@ -221,7 +223,7 @@
// Create a list of all the followers
$items = array();
foreach ( $following_files as $following_file ) {
- $following = json_decode( file_get_contents( $following_file ),true );
+ $following = json_decode( file_get_contents( $following_file ), true );
$items[] = $following["id"];
}
@@ -238,8 +240,8 @@
}
function followers() {
global $server, $directories;
- // The number of followers is self-reported
- // You can set this to any number you like
+ // The number of followers is self-reported.
+ // You can set this to any number you like.
// Get all the files
$follower_files = glob( $directories["followers"] . "/*.json");
@@ -322,13 +324,13 @@
$inbox_actor_json = curl_exec( $ch );
// Check for errors
- if (curl_errno($ch)) {
+ if ( curl_errno( $ch ) ) {
// TODO: Handle cURL error
die();
}
// Close cURL session
- curl_close($ch);
+ curl_close( $ch );
// Save the actor's data in `/data/followers/`
$follower_filename = urlencode( $follower_actor );
@@ -362,24 +364,7 @@
];
// The Accept is POSTed to the inbox on the server of the user who requested the follow
- $follower_inbox_path = parse_url( $follower_inbox, PHP_URL_PATH );
- // Generate the signed headers
- $headers = generate_signed_headers( $message, $follower_host, $follower_inbox_path, "POST" );
-
- // POST the message and header to the requester's inbox
- $ch = curl_init( $follower_inbox );
- 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 ) ) {
- $timestamp = ( new DateTime() )->format( DATE_RFC3339_EXTENDED );
- file_put_contents( $directories["logs"] . "/{$timestamp}.Error.txt", curl_error( $ch ) );
- }
- curl_close( $ch );
+ sentMessageToSingle( $follower_inbox, $message );
die();
}
@@ -405,7 +390,7 @@
global $server, $username, $key_private;
// Location of the Public Key
- $keyId = "https://{$server}/{$username}#main-key";
+ $keyId = "https://{$server}/{$username}#main-key";
// Get the Private Key
$signer = openssl_get_privatekey( $key_private );
@@ -413,7 +398,7 @@
// Timestamp this message was sent
$date = date( "D, d M Y H:i:s \G\M\T" );
- // There are subtly different signing requirements for POST and GET
+ // There are subtly different signing requirements for POST and GET.
if ( "POST" == $method ) {
// Encode the message object to JSON
$message_json = json_encode( $message );
@@ -497,64 +482,64 @@
}
// Counters for followers, following, and posts
- $follower_files = glob( $directories["followers"] . "/*.json");
+ $follower_files = glob( $directories["followers"] . "/*.json" );
$totalFollowers = count( $follower_files );
- $following_files = glob( $directories["following"] . "/*.json");
+ $following_files = glob( $directories["following"] . "/*.json" );
$totalFollowing = count( $following_files );
// Show the HTML page
echo <<< HTML
-
-
-
HTML;
// Get all the files in the directory
$message_files = array_reverse( glob( $directories[$directory] . "/*.json") );
@@ -612,16 +597,21 @@ HTML;
// Set up the common components
$object = $message["object"];
+
+ // Get the message's ID.
+ // Set up the HTML representation
if ( isset( $message["object"]["id"] ) ) {
$id = $message["object"]["id"];
- $timeHTML = "";
+ $publishedHTML = "{$published}";
} else if ( isset( $message["id"] ) ) {
$id = $message["id"];
- $timeHTML = "";
+ $publishedHTML = "{$published}";
} else {
$id = "";
- $timeHTML = "";
+ $publishedHTML = $published;
}
+ // For displaying the post's information
+ $timeHTML = "";
// Get the actor who authored the message
if ( isset( $message["actor"] ) ) {
@@ -656,10 +646,11 @@ HTML;
// Render the message according to type
if ( "Create" == $type || "Update" == $type || "Note" == $type ) {
- // Get the HTML content and sanitise it.
+ // Get the HTML content
// There is a slight difference between the formatting of sent and received messages
"Note" == $type ? $content = $message["content"] : $content = $object["content"];
- $content = strip_tags( $content, $allowed_elements );
+ // Sanitise the HTML
+ $content = strip_tags( $content, $allowed_elements );
// Is this a reply to something?
if ( isset( $object["inReplyTo"] ) ) {
@@ -680,7 +671,7 @@ HTML;
if ( isset( $object["summary"] ) ) {
$summary = $object["summary"];
$summary = strip_tags( $summary, $allowed_elements );
-
+ // Hide the content until the user interacts with it.
$content = "{$summary}{$content}";
}
@@ -712,16 +703,20 @@ HTML;
// Only use things which have a MIME Type set
if ( isset( $attachment["mediaType"] ) ) {
- $mediaType = explode( "/", $attachment["mediaType"])[0];
+ $mediaURl = $attachment["url"];
+ $mime = $attachment["mediaType"];
+ // Use the first half of the MIME Type.
+ // For example `image/png` or `video/mp4`
+ $mediaType = explode( "/", $mime )[0];
if ( "image" == $mediaType ) {
// Get the alt text
isset( $attachment["name"] ) ? $alt = $attachment["name"] : $alt = "";
- $content .= "";
+ $content .= "";
} else if ( "video" == $mediaType ) {
- $content .= "";
+ $content .= "";
}else if ( "audio" == $mediaType ) {
- $content .= "";
+ $content .= "";
}
}
}
@@ -767,7 +762,6 @@ HTML;
}
echo <<< HTML
-
@@ -866,7 +860,7 @@ HTML;
$type = $_POST["type"];
// Likes and Announces have an identical message structure
- if ( $type == "Like" || $type == "Announce" ) {
+ if ( "Like" == $type || "Announce" == $type ) {
// Was a URl sent?
if ( isset( $_POST["postURl"] ) && filter_var( $_POST["postURl"], FILTER_VALIDATE_URL ) ) {
$postURl = $_POST["postURl"];
@@ -875,6 +869,11 @@ HTML;
die();
}
+ if ( "Like" == $type ) {
+ // The message will need to be sent to the inbox of the author of the message
+ $inbox_single = getInboxFromMessageURl( $postURl );
+ }
+
// Outgoing Message ID
$guid = uuid();
@@ -888,16 +887,14 @@ HTML;
"object" => $postURl
];
- // Annouces are sent to an audience
+ // Announces are sent to an audience
// The audience is public and it is sent to all followers
- if ( $type == "Announce") {
+ // TODO: Let the original poster know we boosted them
+ if ( $type == "Announce" ) {
$message = array_merge( $message,
- array("to" => [
- "https://www.w3.org/ns/activitystreams#Public"
- ],
- "cc" => [
- "https://{$server}/followers"
- ])
+ array(
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => ["https://{$server}/followers"])
);
}
@@ -920,7 +917,7 @@ HTML;
list( "HTML" => $content, "TagArray" => $tags ) = process_content( $content );
// Is there an image attached?
- if ( isset( $_FILES['image']['tmp_name'] ) && ("" != $_FILES['image']['tmp_name'] ) ) {
+ if ( isset( $_FILES['image']['tmp_name'] ) && ( "" != $_FILES['image']['tmp_name'] ) ) {
// Get information about the image
$image = $_FILES['image']['tmp_name'];
$image_info = getimagesize( $image );
@@ -948,8 +945,7 @@ HTML;
"mediaType" => "{$image_mime}",
"url" => "https://{$server}/{$image_full_path}",
"name" => $alt
- ];
-
+ ];
} else {
$attachment = [];
}
@@ -1000,6 +996,54 @@ HTML;
$note_json = json_encode( $note );
file_put_contents( $directories["posts"] . "/{$guid}.json", print_r( $note_json, true ) );
+ // Is this message going to one user? (Usually a Like)
+ if ( isset( $inbox_single ) ) {
+ $messageSent = sentMessageToSingle( $inbox_single, $message );
+ } else { // Send to all the user's followers
+ $messageSent = sendMessageToFollowers( $message );
+ }
+
+ // Render the JSON so the user can see the POST has worked
+ if ( $messageSent ) {
+ header( "Location: https://{$server}/posts/{$guid}.json" );
+ die();
+ } else {
+ echo "ERROR!";
+ die();
+ }
+ }
+
+ // POST a signed message to a single inbox
+ function sentMessageToSingle( $inbox, $message ) {
+ global $directories;
+
+ $inbox_host = parse_url( $inbox, PHP_URL_HOST );
+ $inbox_path = parse_url( $inbox, PHP_URL_PATH );
+
+ // Generate 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( $inbox );
+ 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 ) ) {
+ $timestamp = ( new DateTime() )->format( DATE_RFC3339_EXTENDED );
+ file_put_contents( $directories["logs"] . "/{$timestamp}.Error.txt", curl_error( $ch ) );
+ return false;
+ }
+ curl_close($ch);
+ return true;
+ }
+
+ // POST a signed message to the inboxes of all followers
+ function sendMessageToFollowers( $message ) {
+ global $directories;
// Read existing followers
$followers = glob( $directories["followers"] . "/*.json" );
@@ -1062,9 +1106,7 @@ HTML;
// Close the multi-handle
curl_multi_close( $mh );
- // Render the JSON so the user can see the POST has worked
- header( "Location: https://{$server}/posts/{$guid}.json" );
- die();
+ return true;
}
// Content can be plain text. But to add clickable links and hashtags, it needs to be turned into HTML.
@@ -1115,7 +1157,7 @@ HTML;
// Construct the mentions value for the note object
// This goes in the generic "tag" property
- // TODO: Add this to the CC field
+ // TODO: Add this to the CC field & appropriate inbox
foreach ( $usernames as $username ) {
list( , $user, $domain ) = explode( "@", $username );
$tags[] = array(
@@ -1137,11 +1179,69 @@ HTML;
$content = "
{$content}
";
return [
- "HTML" => $content,
+ "HTML" => $content,
"TagArray" => $tags
];
}
+ // When given the URl of a post, this looks up the post, finds the user, then returns their inbox or shared inbox
+ function getInboxFromMessageURl( $url ) {
+
+ // Get details about the message
+ $messageData = getDataFromURl( $url );
+
+ // The author is the user who the message is attributed to
+ if ( isset ( $messageData["attributedTo"] ) && filter_var( $messageData["attributedTo"], FILTER_VALIDATE_URL) ) {
+ $profileData = getDataFromURl( $messageData["attributedTo"] );
+ } else {
+ return null;
+ }
+
+ // Get the shared inbox or personal inbox
+ if( isset( $profileData["endpoints"]["sharedInbox"] ) ) {
+ $inbox = $profileData["endpoints"]["sharedInbox"];
+ } else {
+ // If not, use the individual inbox
+ $inbox = $profileData["inbox"];
+ }
+
+ // Return the destination inbox if it is valid
+ if ( filter_var( $inbox, FILTER_VALIDATE_URL) ) {
+ return $inbox;
+ } else {
+ return null;
+ }
+ }
+
+ // GET a request to a URl and returns structured data
+ function getDataFromURl ( $url ) {
+ // Split the URL
+ $url_host = parse_url( $url, PHP_URL_HOST );
+ $url_path = parse_url( $url, PHP_URL_PATH );
+
+ // Generate signed headers for this request
+ $headers = generate_signed_headers( null, $url_host, $url_path, "GET" );
+
+ // Set cURL options
+ $ch = curl_init( $url );
+ curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
+
+ // Execute the cURL session
+ $urlJSON = curl_exec( $ch );
+
+ // Check for errors
+ if (curl_errno( $ch )) {
+ // Handle cURL error
+ die();
+ }
+
+ // Close cURL session
+ curl_close( $ch );
+
+ return json_decode( $urlJSON, true );
+ }
+
// The Outbox contains a date-ordered list (newest first) of all the user's posts
// This is optional.
function outbox() {
@@ -1239,42 +1339,11 @@ HTML;
if ( !isset( $profileURl ) ) { echo "No profile"; die(); }
// Get the user's details
- // This request does not need to be signed normally.
- // Some servers will only respond to signed requests.
- // It need to specify that it wants a JSON response
-
- $profileURl_host = parse_url( $profileURl, PHP_URL_HOST );
- $profileURl_path = parse_url( $profileURl, PHP_URL_PATH );
-
- // Request the JSON representation of the the user
- $ch = curl_init( $profileURl );
-
- // Generate signed headers for this request
- $headers = generate_signed_headers( null, $profileURl_host, $profileURl_path, "GET" );
-
- // Set cURL options
- curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true);
- curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
-
- // Execute the cURL session
- $profileJSON = curl_exec( $ch );
-
- // Check for errors
- if (curl_errno($ch)) {
- // Handle cURL error
- die();
- }
-
- // Close cURL session
- curl_close($ch);
-
- $profileData = json_decode( $profileJSON, true );
+ $profileData = getDataFromURl( $profileURl );
// 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 = [
@@ -1287,27 +1356,11 @@ HTML;
// Sign a request to 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 ) ) {
- $timestamp = ( new DateTime() )->format( DATE_RFC3339_EXTENDED );
- file_put_contents( $directories["logs"] . "/{$timestamp}.Error.txt", curl_error( $ch ) );
- }
- curl_close($ch);
+ sentMessageToSingle( $profileInbox, $message );
// Save the user's details
$following_filename = urlencode( $profileURl );
- file_put_contents( $directories["following"] . "/{$following_filename}.json", $profileJSON );
+ file_put_contents( $directories["following"] . "/{$following_filename}.json", json_encode( $profileData ) );
// Render the JSON so the user can see the POST has worked
header( "Location: https://{$server}/data/following/" . urlencode( $following_filename ) . ".json" );
@@ -1422,36 +1475,8 @@ HTML;
// This is usually in the form `https://example.com/user/username#main-key`
// This is to differentiate if the user has multiple keys
// TODO: Check the actual key
- // This request does not need to be signed normally.
- // Some servers will only respond to signed requests.
- // It need to specify that it wants a JSON response
- $publicKeyURL_host = parse_url( $publicKeyURL, PHP_URL_HOST );
- $publicKeyURL_path = parse_url( $publicKeyURL, PHP_URL_PATH );
-
- // Request the JSON representation of the the user
- $ch = curl_init( $publicKeyURL );
-
- // Generate signed headers for this request
- $headers = generate_signed_headers( null, $publicKeyURL_host, $publicKeyURL_path, "GET" );
-
- // Set cURL options
- curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true);
- curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
-
- // Execute the cURL session
- $userJSON = curl_exec( $ch );
-
- // Check for errors
- if (curl_errno($ch)) {
- // Handle cURL error
- die();
- }
-
- // Close cURL session
- curl_close($ch);
-
- $userData = json_decode( $userJSON, true );
+ $userData = getDataFromURl( $publicKeyURL );
$publicKey = $userData["publicKey"]["publicKeyPem"];
// Get the remaining parts