Tidy documentation etc
rodzic
69b0df6e87
commit
e98623624f
90
index.php
90
index.php
|
@ -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 );
|
||||
|
|
Ładowanie…
Reference in New Issue