Tidy documentation etc

merge-requests/5/head
Terence Eden 2024-03-01 23:26:10 +00:00
rodzic 69b0df6e87
commit e98623624f
1 zmienionych plików z 60 dodań i 44 usunięć

Wyświetl plik

@ -9,6 +9,7 @@
* The Actor can send messages to followers.
* The message can have linkable URls, hashtags, and mentions.
* An image and alt text can be attached to the message.
* The Actor can follow remote accounts.
* The Server saves logs about requests it receives and sends.
* This code is NOT suitable for production use.
* SPDX-License-Identifier: AGPL-3.0-or-later
@ -36,12 +37,13 @@
// Password for sending messages
$password = "P4ssW0rd";
/** No need to edit anything below here. **/
/** No need to edit anything below here. But please go exploring! **/
// Internal data
$server = $_SERVER["SERVER_NAME"]; // Do not change this!
// Set up where logs and messages go
// Set up where logs and messages go.
// If you want to, you can change these directories to something more suitable for you.
$data = "data";
$directories = array(
"inbox" => "{$data}/inbox",
@ -49,13 +51,15 @@
"following" => "{$data}/following",
"logs" => "{$data}/logs",
"posts" => "posts",
"images" => "images",
);
foreach ( $directories as $directory ) {
if( !is_dir( $directory ) ) { mkdir( $data ); mkdir( $directory ); }
}
// Logging:
// ActivityPub is a "chatty" protocol. This takes all the requests your server receives and saves them as a datestamped text file.
// ActivityPub is a "chatty" protocol.
// This takes all the requests your server receives and saves them as a datestamped text file.
// Get all headers and requests sent to this server
$headers = print_r( getallheaders(), true );
@ -72,19 +76,23 @@
// Get the type of request - used in the log filename
if ( isset( $body["type"] ) ) {
// Sanitise before using it in a filename
$type = "." . urlencode( $body["type"] );
$type = urlencode( $body["type"] );
} else {
// Sanitise the path requested
$type = "." . urlencode( $path );
$type = urlencode( $path );
}
// Create a timestamp for the filename
// Create a timestamp for the filename.
// This format has milliseconds, so should avoid logs being overwritten.
// If you have > 1000 requests per second, please use a different server.
$timestamp = ( new DateTime() )->format( DATE_RFC3339_EXTENDED );
// Alternatively, use a UUID. These retain the date as hex encoded UNIX seconds.
// This means the log files will never clash - but it does make the filenames harder to read.
// $timestamp = uuid();
// Filename for the log
$filename = "{$timestamp}{$type}.txt";
$filename = "{$timestamp}.{$type}.txt";
// Save headers and request data to the timestamped file in the logs directory
file_put_contents( $directories["logs"] . "/{$filename}",
@ -106,11 +114,11 @@
case rawurldecode( $username ):
username(); // Mandatory. Static
case "following":
following(); // Mandatory. Static
following(); // Mandatory. Can be static or dynamic.
case "followers":
followers(); // Mandatory. Can be dynamic
followers(); // Mandatory. Can be static or dynamic.
case "inbox":
inbox(); // Mandatory. Only accepts follow requests.
inbox(); // Mandatory.
case "write":
write(); // User interface for writing posts
case "send":
@ -197,9 +205,10 @@
// Get all the files
$following_files = glob( $directories["following"] . "/*.json");
// Number of posts
// Number of users
$totalItems = count( $following_files );
// Create a list of all the followers
$items = array();
foreach ( $following_files as $following_file ) {
$following = json_decode( file_get_contents( $following_file ),true );
@ -224,9 +233,10 @@
// Get all the files
$follower_files = glob( $directories["followers"] . "/*.json");
// Number of posts
// Number of users
$totalItems = count( $follower_files );
// Create a list of everyone being followed
$items = array();
foreach ( $follower_files as $follower_file ) {
$following = json_decode( file_get_contents( $follower_file ),true );
@ -247,10 +257,6 @@
// Inbox:
// The `/inbox` is the main server. It receives all requests.
// This server only responds to "Follow" requests.
// A remote server sends a follow request which is a JSON file saying who they are.
// The details of the remote user's server is saved to a file so that future messages can be delivered to the follower.
// An accept request is cryptographically signed and POST'd back to the remote server.
function inbox() {
global $body, $server, $username, $key_private, $directories;
@ -274,15 +280,17 @@
file_put_contents( $directories["inbox"] . "/{$inbox_filename}", json_encode( $inbox_message ) );
}
// This inbox only responds to follow requests
// This inbox only responds to follow requests.
// A remote server sends the inbox follow request which is a JSON file saying who they are.
// The details of the remote user's server is saved to a file so that future messages can be delivered to the follower.
// An accept request is cryptographically signed and POST'd back to the remote server.
if ( "Follow" != $inbox_type ) { die(); }
// Get the parameters
$follower_id = $inbox_message["id"]; // E.g. https://mastodon.social/(unique id)
$follower_actor = $inbox_message["actor"]; // E.g. https://mastodon.social/users/Edent
$follower_host = parse_url( $follower_actor, PHP_URL_HOST );
$follower_path = parse_url( $follower_actor, PHP_URL_PATH );
$follower_host = parse_url( $follower_actor, PHP_URL_HOST ); // E.g. mastodon.social
$follower_path = parse_url( $follower_actor, PHP_URL_PATH ); // E.g. /users/Edent
// Get the actor's profile as JSON
// Is the actor an https URl?
@ -305,7 +313,7 @@
// Check for errors
if (curl_errno($ch)) {
// Handle cURL error
// TODO: Handle cURL error
die();
}
@ -345,7 +353,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 );
// Get the signed headers
// 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
@ -430,7 +438,7 @@
"Accept: application/activity+json",
);
} else if ( "GET" == $method ) {
// Sign the path, host, date - NO DIGEST
// Sign the path, host, date - NO DIGEST because there's no message sent.
$stringToSign = "(request-target): get $path\nhost: $host\ndate: $date";
// The signing function returns the variable $signature
@ -459,7 +467,7 @@
return $headers;
}
// User Interface for Homepage:
// User Interface for Homepage.
// This creates a basic HTML page. This content appears when someone visits the root of your site.
function home() {
global $username, $server, $realName, $summary, $directories;
@ -491,10 +499,15 @@ HTML;
// Loop through the posts
foreach ($posts as $post) {
// Get the contents of the file
$postJSON = file_get_contents($post);
$postData = json_decode($postJSON, true);
// Set up some common variables
$postTime = $postData["published"];
$postHTML = $postData["content"];
// If the message has an image attached, display it.
if ( isset($postData["attachment"]) ) {
$postImgUrl = $postData["attachment"]["url"];
$postImgAlt = $postData["attachment"]["name"];
@ -502,6 +515,7 @@ HTML;
} else {
$postImg = "";
}
// Display the post
echo "<li><a href='/{$post}'><time datetime='{$postTime}'>$postTime</time></a><br>{$postHTML}<br>{$postImg}</li>";
}
@ -546,8 +560,8 @@ HTML;
// Send Endpoint:
// This takes the submitted message and checks the password is correct.
// It reads all the followers' data in `data/followers`
// It constructs a list of shared inboxes and unique inboxes
// It reads all the followers' data in `data/followers`.
// It constructs a list of shared inboxes and unique inboxes.
// It sends the message to every server that is following this account.
function send() {
global $password, $server, $username, $key_private, $directories;
@ -572,12 +586,9 @@ HTML;
// Files are stored according to their hash
// A hash of "abc123" is stored in "/images/abc123.jpg"
$sha1 = sha1_file( $image );
$image_path = "images";
$image_full_path = "{$image_path}/{$sha1}.{$image_ext}";
$image_full_path = $directories["images"] . "/{$sha1}.{$image_ext}";
// Move media to the correct location
// Create a directory if it doesn't exist
if( ! is_dir( $image_path ) ) { mkdir( $image_path ); }
move_uploaded_file( $image, $image_full_path );
// Get the alt text
@ -606,7 +617,7 @@ HTML;
$guid = uuid();
// Construct the Note
// contentMap is used to prevent unnecessary "translate this post" pop ups
// `contentMap` is used to prevent unnecessary "translate this post" pop ups
// hardcoded to English
$note = [
"@context" => array(
@ -645,15 +656,16 @@ HTML;
file_put_contents( $directories["posts"] . "/{$guid}.json", print_r( $note_json, true ) );
// Read existing followers
$followers = glob( "data/followers/*.json" );
$followers = glob( $directories["followers"] . "/*.json" );
// Get all the inboxes
$inboxes = [];
foreach ( $followers as $follower ) {
// Get the data about the follower
$follower_info = json_decode( file_get_contents( $follower ), true );
// Some servers have "Shared inboxes"
// If you have lots of followers on a single server, you only need to send the message once
// If you have lots of followers on a single server, you only need to send the message once.
if( isset( $follower_info["endpoints"]["sharedInbox"] ) ) {
$sharedInbox = $follower_info["endpoints"]["sharedInbox"];
if ( !in_array( $sharedInbox, $inboxes ) ) {
@ -680,7 +692,7 @@ HTML;
$inbox_host = parse_url( $inbox, PHP_URL_HOST );
$inbox_path = parse_url( $inbox, PHP_URL_PATH );
// Get the signed headers
// Generate the signed headers
$headers = generate_signed_headers( $message, $inbox_host, $inbox_path, "POST" );
// POST the message and header to the requester's inbox
@ -716,7 +728,7 @@ HTML;
global $server;
// Convert any URls into hyperlinks
$link_pattern = '/\bhttps?:\/\/\S+/iu';
$link_pattern = '/\bhttps?:\/\/\S+/iu'; // Sloppy regex
$replacement = function ( $match ) {
$url = htmlspecialchars( $match[0], ENT_QUOTES, "UTF-8" );
return "<a href=\"$url\">$url</a>";
@ -741,6 +753,7 @@ HTML;
}
// Add HTML links for hashtags into the text
// Todo: Make these links do something.
$content = preg_replace(
$hashtag_pattern,
" <a href='https://{$server}/tag/$1'>#$1</a>",
@ -858,14 +871,16 @@ HTML;
// Get the posted content
$user = $_POST["user"];
// Split the user into username and server
// Split the user (@user@example.com) into username and server
list( , $follow_name, $follow_server ) = explode( "@", $user );
// Get the Webfinger
// This request does not need to be signed.
$webfingerURl = "https://{$follow_server}/.well-known/webfinger?resource=acct:{$follow_name}@{$follow_server}";
$webfingerJSON = file_get_contents( $webfingerURl );
$webfinger = json_decode( $webfingerJSON, true );
// Get the link to the user
foreach( $webfinger["links"] as $link ) {
if ( "self" == $link["rel"] ) {
$profileURl = $link["href"];
@ -959,8 +974,9 @@ HTML;
}
// Validate the Digest
// It is the hash of the raw input string, in binary, encoded as base64
// It is the hash of the raw input string, in binary, encoded as base64.
$digestString = $headers["digest"];
// Usually in the form `SHA-256=Ofv56Jm9rlowLR9zTkfeMGLUG1JYQZj0up3aRPZgT0c=`
// The Base64 encoding may have multiple `=` at the end. So split this at the first `=`
$digestData = explode( "=", $digestString, 2 );