2024-02-12 21:21:05 +00:00
< ? php
/*
* " This code is not a code of honour... no highly esteemed code is commemorated here... nothing valued is here. "
* " What is here is dangerous and repulsive to us. This message is a warning about danger. "
* This is a rudimentary , single - file , low complexity , minimum functionality , ActivityPub server .
* For educational purposes only .
2024-02-17 18:17:07 +00:00
* The Server produces an Actor who can be followed .
* The Actor can send messages to followers .
2024-02-18 15:48:33 +00:00
* The message can have linkable URls , hashtags , and mentions .
* An image and alt text can be attached to the message .
2024-02-17 18:17:07 +00:00
* 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
* This code is also " licenced " under CRAPL v0 - https :// matt . might . net / articles / crapl /
2024-02-12 21:21:05 +00:00
* " Any appearance of design in the Program is purely coincidental and should not in any way be mistaken for evidence of thoughtful software construction. "
2024-02-17 18:17:07 +00:00
* For more information , please re - read .
2024-02-12 21:21:05 +00:00
*/
2024-02-15 09:29:21 +00:00
// Preamble: Set your details here
// This is where you set up your account's name and bio. You also need to provide a public/private keypair. The posting page is protected with a password that also needs to be set here.
2024-02-12 21:21:05 +00:00
// Set up the Actor's information
2024-02-18 20:58:11 +00:00
// Edit these:
$username = rawurlencode ( " example " ); // Type the @ username that you want. Do not include an "@".
$realName = " E. Xample. Jr. " ; // This is the user's "real" name.
$summary = " Some text about the user. " ; // This is the bio of your user.
2024-02-12 21:21:05 +00:00
// Generate locally or from https://cryptotools.net/rsagen
// Newlines must be replaced with "\n"
$key_private = " -----BEGIN RSA PRIVATE KEY----- \n ... \n -----END RSA PRIVATE KEY----- " ;
$key_public = " -----BEGIN PUBLIC KEY----- \n ... \n -----END PUBLIC KEY----- " ;
// Password for sending messages
$password = " P4ssW0rd " ;
2024-02-18 20:58:11 +00:00
/** No need to edit anything below here. **/
// Internal data
$server = $_SERVER [ " SERVER_NAME " ]; // Do not change this!
2024-02-15 09:29:21 +00:00
// Logging:
// ActivityPub is a "chatty" protocol. This takes all the requests your server receives and saves them in `/logs/` as a datestamped text file.
2024-02-12 21:21:05 +00:00
// Get all headers and requests sent to this server
$headers = print_r ( getallheaders (), true );
2024-02-13 13:30:36 +00:00
$postData = print_r ( $_POST , true );
$getData = print_r ( $_GET , true );
$filesData = print_r ( $_FILES , true );
$body = json_decode ( file_get_contents ( " php://input " ), true );
2024-02-15 09:29:21 +00:00
$bodyData = print_r ( $body , true );
2024-02-12 21:21:05 +00:00
$requestData = print_r ( $_REQUEST , true );
2024-02-13 13:30:36 +00:00
$serverData = print_r ( $_SERVER , true );
2024-02-12 21:21:05 +00:00
2024-02-13 13:30:36 +00:00
// Get the type of request - used in the log filename
2024-02-12 21:21:05 +00:00
if ( isset ( $body [ " type " ] ) ) {
2024-02-18 14:32:12 +00:00
// Sanitise type to only include letter
$type = " " . preg_replace ( '/[^a-zA-Z]/' , '' , $body [ " type " ] );
2024-02-12 21:21:05 +00:00
} else {
$type = " " ;
}
2024-02-19 10:46:18 +00:00
// 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 );
2024-02-12 21:21:05 +00:00
// Filename for the log
$filename = " { $timestamp } { $type } .txt " ;
2024-02-13 13:30:36 +00:00
// Save headers and request data to the timestamped file in the logs directory
if ( ! is_dir ( " logs " ) ) { mkdir ( " logs " ); }
file_put_contents ( " logs/ { $filename } " ,
2024-02-12 21:21:05 +00:00
" Headers: \n $headers \n \n " .
2024-02-13 13:30:36 +00:00
" Body Data: \n $bodyData \n \n " .
2024-02-12 21:21:05 +00:00
" POST Data: \n $postData \n \n " .
" GET Data: \n $getData \n \n " .
" Files Data: \n $filesData \n \n " .
" Request Data: \n $requestData\n\n " .
" Server Data: \n $serverData \n \n "
);
2024-02-15 09:29:21 +00:00
// Routing:
2024-02-12 21:21:05 +00:00
// The .htaccess changes /whatever to /?path=whatever
2024-02-15 09:29:21 +00:00
// This runs the function of the path requested.
2024-02-18 14:10:01 +00:00
! empty ( $_GET [ " path " ] ) ? $path = $_GET [ " path " ] : home ();
2024-02-12 21:21:05 +00:00
switch ( $path ) {
case " .well-known/webfinger " :
2024-02-18 14:29:06 +00:00
webfinger (); // Mandatory. Static.
2024-02-13 13:30:36 +00:00
case rawurldecode ( $username ) :
2024-02-18 14:29:06 +00:00
username (); // Mandatory. Static
2024-02-12 21:21:05 +00:00
case " following " :
2024-02-18 14:29:06 +00:00
following (); // Mandatory. Static
2024-02-12 21:21:05 +00:00
case " followers " :
2024-02-18 14:29:06 +00:00
followers (); // Mandatory. Could be dynamic
2024-02-12 21:21:05 +00:00
case " inbox " :
2024-02-18 14:29:06 +00:00
inbox (); // Mandatory. Only accepts follow requests.
2024-02-12 21:21:05 +00:00
case " write " :
2024-02-18 14:29:06 +00:00
write (); // User interface for writing posts
case " send " : // API for posting content to the Fediverse
2024-02-12 21:21:05 +00:00
send ();
2024-02-18 14:44:57 +00:00
case " outbox " : // Optional. Dynamic.
outbox ();
2024-02-12 21:21:05 +00:00
default :
die ();
}
2024-02-15 09:29:21 +00:00
// The [WebFinger Protocol](https://docs.joinmastodon.org/spec/webfinger/) is used to identify accounts.
// It is requested with `example.com/.well-known/webfinger?resource=acct:username@example.com`
// This server only has one user, so it ignores the query string and always returns the same details.
2024-02-12 21:21:05 +00:00
function webfinger () {
global $username , $server ;
$webfinger = array (
" subject " => " acct: { $username } @ { $server } " ,
" links " => array (
array (
" rel " => " self " ,
" type " => " application/activity+json " ,
" href " => " https:// { $server } / { $username } "
)
)
);
2024-02-13 13:30:36 +00:00
header ( " Content-Type: application/json " );
2024-02-12 21:21:05 +00:00
echo json_encode ( $webfinger );
die ();
}
2024-02-15 09:29:21 +00:00
// User:
// Requesting `example.com/username` returns a JSON document with the user's information.
2024-02-12 21:21:05 +00:00
function username () {
2024-02-15 09:29:21 +00:00
global $username , $realName , $summary , $server , $key_public ;
2024-02-12 21:21:05 +00:00
$user = array (
" @context " => [
" https://www.w3.org/ns/activitystreams " ,
" https://w3id.org/security/v1 "
],
" id " => " https:// { $server } / { $username } " ,
" type " => " Person " ,
" following " => " https:// { $server } /following " ,
" followers " => " https:// { $server } /followers " ,
" inbox " => " https:// { $server } /inbox " ,
2024-02-18 14:44:57 +00:00
" outbox " => " https:// { $server } /outbox " ,
2024-02-12 21:21:05 +00:00
" preferredUsername " => rawurldecode ( $username ),
" name " => " { $realName } " ,
2024-02-13 13:30:36 +00:00
" summary " => " { $summary } " ,
2024-02-18 20:58:11 +00:00
" url " => " https:// { $server } / { $username } " ,
2024-02-12 21:21:05 +00:00
" manuallyApprovesFollowers " => true ,
" discoverable " => true ,
" published " => " 2024-02-12T11:51:00Z " ,
" icon " => [
" type " => " Image " ,
" mediaType " => " image/png " ,
" url " => " https:// { $server } /icon.png "
],
" publicKey " => [
" id " => " https:// { $server } / { $username } #main-key " ,
" owner " => " https:// { $server } / { $username } " ,
" publicKeyPem " => $key_public
]
);
2024-02-13 13:30:36 +00:00
header ( " Content-Type: application/activity+json " );
2024-02-12 21:21:05 +00:00
echo json_encode ( $user );
die ();
}
2024-02-15 09:29:21 +00:00
// Follower / Following:
// These JSON documents show how many users are following / followers-of this account.
// The information here is self-attested. So you can lie and use any number you want.
2024-02-12 21:21:05 +00:00
function following () {
global $server ;
$following = array (
" @context " => " https://www.w3.org/ns/activitystreams " ,
" id " => " https:// { $server } /following " ,
" type " => " Collection " ,
" totalItems " => 0 ,
" items " => []
);
2024-02-13 13:30:36 +00:00
header ( " Content-Type: application/activity+json " );
2024-02-12 21:21:05 +00:00
echo json_encode ( $following );
die ();
}
function followers () {
global $server ;
$followers = array (
" @context " => " https://www.w3.org/ns/activitystreams " ,
" id " => " https:// { $server } /followers " ,
" type " => " Collection " ,
" totalItems " => 0 ,
" items " => []
);
2024-02-13 13:30:36 +00:00
header ( " Content-Type: application/activity+json " );
2024-02-12 21:21:05 +00:00
echo json_encode ( $followers );
die ();
}
2024-02-15 09:29:21 +00:00
// 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.
// This code does not cryptographically validate the headers of the received message.
// The name of the remote user's server is saved to a file so that future messages can be delivered to it.
// An accept request is cryptographically signed and POST'd back to the remote server.
2024-02-12 21:21:05 +00:00
function inbox () {
global $body , $server , $username , $key_private ;
// Get the message and type
$inbox_message = $body ;
$inbox_type = $inbox_message [ " type " ];
// This inbox only responds to follow requests
if ( " Follow " != $inbox_type ) { die (); }
// Get the parameters
$inbox_id = $inbox_message [ " id " ];
$inbox_actor = $inbox_message [ " actor " ];
2024-02-13 13:30:36 +00:00
$inbox_host = parse_url ( $inbox_actor , PHP_URL_HOST );
2024-02-12 21:21:05 +00:00
// Does this account have any followers?
if ( file_exists ( " followers.json " ) ) {
$followers_file = file_get_contents ( " followers.json " );
$followers_json = json_decode ( $followers_file , true );
} else {
$followers_json = array ();
}
// Add user to list. Don't care about duplicate users, server is what's important
$followers_json [ $inbox_host ][ " users " ][] = $inbox_actor ;
// Save the new followers file
file_put_contents ( " followers.json " , print_r ( json_encode ( $followers_json ), true ) );
// Response Message ID
// This isn't used for anything important so could just be a random number
$guid = uuid ();
// Create the Accept message
$message = [
" @context " => " https://www.w3.org/ns/activitystreams " ,
" id " => " https:// { $server } / { $guid } " ,
" type " => " Accept " ,
" actor " => " https:// { $server } / { $username } " ,
" object " => [
" @context " => " https://www.w3.org/ns/activitystreams " ,
" id " => $inbox_id ,
" type " => $inbox_type ,
" actor " => $inbox_actor ,
" object " => " https:// { $server } / { $username } " ,
]
];
// The Accept is sent to the server of the user who requested the follow
2024-02-18 15:09:41 +00:00
// TODO: The path doesn't *always* end with /inbox
2024-02-12 21:21:05 +00:00
$host = $inbox_host ;
2024-02-13 13:30:36 +00:00
$path = parse_url ( $inbox_actor , PHP_URL_PATH ) . " /inbox " ;
2024-02-12 21:21:05 +00:00
2024-02-14 09:45:04 +00:00
// Get the signed headers
$headers = generate_signed_headers ( $message , $host , $path );
2024-02-12 21:21:05 +00:00
// Specify the URL of the remote server's inbox
// TODO: The path doesn't *always* end with /inbox
$remoteServerUrl = $inbox_actor . " /inbox " ;
// POST the message and header to the requester's inbox
$ch = curl_init ( $remoteServerUrl );
curl_setopt ( $ch , CURLOPT_RETURNTRANSFER , true );
curl_setopt ( $ch , CURLOPT_CUSTOMREQUEST , " POST " );
2024-02-14 09:45:04 +00:00
curl_setopt ( $ch , CURLOPT_POSTFIELDS , json_encode ( $message ) );
2024-02-12 21:21:05 +00:00
curl_setopt ( $ch , CURLOPT_HTTPHEADER , $headers );
2024-02-16 09:34:09 +00:00
curl_exec ( $ch );
2024-02-12 21:21:05 +00:00
// Check for errors
if ( curl_errno ( $ch ) ) {
file_put_contents ( " error.txt " , curl_error ( $ch ) );
}
curl_close ( $ch );
die ();
}
2024-02-15 09:29:21 +00:00
// Unique ID:
// Every message sent should have a unique ID.
// This can be anything you like. Some servers use a random number.
// I prefer a date-sortable string.
2024-02-12 21:21:05 +00:00
function uuid () {
2024-02-14 09:51:14 +00:00
return sprintf ( " %08x-%04x-%04x-%04x-%012x " ,
2024-02-12 21:21:05 +00:00
time (),
mt_rand ( 0 , 0xffff ),
mt_rand ( 0 , 0xffff ),
mt_rand ( 0 , 0x3fff ) | 0x8000 ,
mt_rand ( 0 , 0xffffffffffff )
);
}
2024-02-15 09:29:21 +00:00
// Headers:
// Every message that your server sends needs to be cryptographically signed with your Private Key.
// This is a complicated process. Please read https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/ for more information.
2024-02-14 09:45:04 +00:00
function generate_signed_headers ( $message , $host , $path ) {
global $server , $username , $key_private ;
2024-02-15 09:29:21 +00:00
// Encode the message object to JSON
2024-02-14 09:45:04 +00:00
$message_json = json_encode ( $message );
// Location of the Public Key
$keyId = " https:// { $server } / { $username } #main-key " ;
// Generate signing variables
2024-02-14 09:51:14 +00:00
$hash = hash ( " sha256 " , $message_json , true );
2024-02-14 09:45:04 +00:00
$digest = base64_encode ( $hash );
2024-02-14 09:51:14 +00:00
$date = date ( " D, d M Y H:i:s \ G \ M \T " );
2024-02-14 09:45:04 +00:00
// Get the Private Key
$signer = openssl_get_privatekey ( $key_private );
// Sign the path, host, date, and digest
$stringToSign = " (request-target): post $path\nhost : $host\ndate : $date\ndigest : SHA-256= $digest " ;
// The signing function returns the variable $signature
// https://www.php.net/manual/en/function.openssl-sign.php
openssl_sign (
$stringToSign ,
$signature ,
$signer ,
OPENSSL_ALGO_SHA256
);
// Encode the signature
$signature_b64 = base64_encode ( $signature );
// Full signature header
$signature_header = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"' ;
// Header for POST reply
$headers = array (
" Host: { $host } " ,
" Date: { $date } " ,
" Digest: SHA-256= { $digest } " ,
" Signature: { $signature_header } " ,
" Content-Type: application/activity+json " ,
" Accept: application/activity+json " ,
);
return $headers ;
}
2024-02-18 14:10:01 +00:00
// User Interface for Homepage:
// This creates a basic HTML page. This content appears when someone visits the root of your site.
function home () {
2024-02-18 14:29:06 +00:00
global $username , $server , $realName , $summary ;
echo <<< HTML
<! DOCTYPE html >
< html lang = " en-GB " >
< head >
< meta charset = " UTF-8 " >
< title > { $realName } </ title >
< style >
body { text - align : center ; font - family : sans - serif ; font - size : 1.1 em ; }
</ style >
</ head >
< body >
< span class = " h-card " >
< img src = " icon.png " alt = " icon " class = " u-photo " width = " 140px " />
< h1 >< span class = " p-name " > { $realName } </ span ></ h1 >
< h2 >< a class = " p-nickname u-url " href = " https:// { $server } / { $username } " >@ { $username } @ { $server } </ a ></ h2 >
< p class = " note " > { $summary } </ p >
</ span >
< p >< a href = " https://gitlab.com/edent/activitypub-single-php-file/ " > This software is licenced under AGPL 3.0 </ a >.</ p >
< p > This site is a basic < a href = " https://www.w3.org/TR/activitypub/ " > ActivityPub </ a > server designed to be < a href = " https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/ " > a lightweight educational tool </ a >.</ p >
</ body >
</ html >
HTML ;
die ();
2024-02-18 14:10:01 +00:00
}
2024-02-15 09:29:21 +00:00
// User Interface for Writing:
// This creates a basic HTML form. Type in your message and your password. It then POSTs the data to the `/send` endpoint.
2024-02-12 21:21:05 +00:00
function write () {
echo <<< HTML
<! DOCTYPE html >
< html lang = " en-GB " >
< head >
< meta charset = " UTF-8 " >
< title > Send Message </ title >
< style >
* { font - family : sans - serif ; font - size : 1.1 em ;}
</ style >
</ head >
< body >
< form action = " /send " method = " post " enctype = " multipart/form-data " >
< label for = " content " > Your message :</ label >< br >
2024-02-16 09:34:09 +00:00
< textarea id = " content " name = " content " rows = " 5 " cols = " 32 " ></ textarea >< br >
2024-02-18 15:48:33 +00:00
< label for = " image " > Attach an image </ label >< br >
< input type = " file " name = " image " id = " image " accept = " image/* " >< br >
< label for = " alt " > Alt Text </ label >
< input type = " text " name = " alt " id = " alt " size = " 32 " />< br >
2024-02-12 21:21:05 +00:00
< label for = " password " > Password </ label >< br >
< input type = " password " name = " password " id = " password " size = " 32 " >< br >
< input type = " submit " value = " Post Message " >
</ form >
</ body >
</ html >
HTML ;
die ();
}
2024-02-15 09:29:21 +00:00
// Send Endpoint:
// This takes the submitted message and checks the password is correct.
// It reads the `followers.json` file and sends the message to every server that is following this account.
2024-02-12 21:21:05 +00:00
function send () {
global $password , $server , $username , $key_private ;
// Does the posted password match the stored password?
if ( $password != $_POST [ " password " ] ) { die (); }
// Get the posted content
$content = $_POST [ " content " ];
2024-02-16 16:09:05 +00:00
// Process the content into HTML to get hashtags etc
list ( " HTML " => $content , " TagArray " => $tags ) = process_content ( $content );
2024-02-18 15:48:33 +00:00
// Is there an image attached?
if ( isset ( $_FILES [ 'image' ][ 'tmp_name' ] ) && ( " " != $_FILES [ 'image' ][ 'tmp_name' ] ) ) {
// Get information about the image
$image = $_FILES [ 'image' ][ 'tmp_name' ];
$image_info = getimagesize ( $image );
$image_ext = image_type_to_extension ( $image_info [ 2 ] );
$image_mime = $image_info [ " mime " ];
// 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 } " ;
// 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
if ( isset ( $_POST [ " alt " ] ) ) {
$alt = $_POST [ " alt " ];
} else {
$alt = " " ;
}
// Construct the attachment value for the post
$attachment = [
" type " => " Image " ,
" mediaType " => " { $image_mime } " ,
" url " => " https:// { $server } / { $image_full_path } " ,
" name " => $alt
];
} else {
$attachment = [];
}
2024-02-13 13:30:36 +00:00
// Current time - ISO8601
$timestamp = date ( " c " );
2024-02-12 21:21:05 +00:00
// Outgoing Message ID
$guid = uuid ();
// Construct the Note
2024-02-13 13:30:36 +00:00
// contentMap is used to prevent unnecessary "translate this post" pop ups
// hardcoded to English
2024-02-12 21:21:05 +00:00
$note = [
" @context " => array (
" https://www.w3.org/ns/activitystreams "
),
" id " => " https:// { $server } /posts/ { $guid } .json " ,
" type " => " Note " ,
" published " => $timestamp ,
" attributedTo " => " https:// { $server } / { $username } " ,
" content " => $content ,
" contentMap " => [ " en " => $content ],
2024-02-16 16:09:05 +00:00
" to " => [ " https://www.w3.org/ns/activitystreams#Public " ],
2024-02-18 15:48:33 +00:00
" tag " => $tags ,
" attachment " => $attachment
2024-02-12 21:21:05 +00:00
];
// Construct the Message
2024-02-16 09:34:09 +00:00
// The audience is public and it is sent to all followers
2024-02-12 21:21:05 +00:00
$message = [
" @context " => " https://www.w3.org/ns/activitystreams " ,
" id " => " https:// { $server } /posts/ { $guid } .json " ,
" type " => " Create " ,
" actor " => " https:// { $server } / { $username } " ,
" to " => [
" https://www.w3.org/ns/activitystreams#Public "
],
" cc " => [
" https:// { $server } /followers "
],
" object " => $note
];
// Create the context for the permalink
2024-02-13 13:30:36 +00:00
$note = [ " @context " => " https://www.w3.org/ns/activitystreams " , ... $note ];
2024-02-12 21:21:05 +00:00
// Save the permalink
$note_json = json_encode ( $note );
2024-02-13 13:30:36 +00:00
// Check for posts/ directory and create it
if ( ! is_dir ( " posts " ) ) { mkdir ( " posts " ); }
2024-02-12 21:21:05 +00:00
file_put_contents ( " posts/ { $guid } .json " , print_r ( $note_json , true ) );
// Read existing users and get their hosts
$followers_file = file_get_contents ( " followers.json " );
$followers_json = json_decode ( $followers_file , true );
$hosts = array_keys ( $followers_json );
// Prepare to use the multiple cURL handle
$mh = curl_multi_init ();
// Loop through all the severs of the followers
// Each server needs its own cURL handle
// Each POST to an inbox needs to be signed separately
foreach ( $hosts as $host ) {
2024-02-16 09:34:09 +00:00
// TODO: Not every host uses /inbox
2024-02-14 09:51:14 +00:00
$path = " /inbox " ;
2024-02-14 09:45:04 +00:00
// Get the signed headers
$headers = generate_signed_headers ( $message , $host , $path );
2024-02-12 21:21:05 +00:00
// Specify the URL of the remote server
$remoteServerUrl = " https:// { $host } { $path } " ;
// POST the message and header to the requester's inbox
2024-02-16 09:34:09 +00:00
$ch = curl_init ( $remoteServerUrl );
2024-02-12 21:21:05 +00:00
curl_setopt ( $ch , CURLOPT_RETURNTRANSFER , true );
curl_setopt ( $ch , CURLOPT_CUSTOMREQUEST , " POST " );
2024-02-14 09:45:04 +00:00
curl_setopt ( $ch , CURLOPT_POSTFIELDS , json_encode ( $message ) );
2024-02-12 21:21:05 +00:00
curl_setopt ( $ch , CURLOPT_HTTPHEADER , $headers );
// Add the handle to the multi-handle
curl_multi_add_handle ( $mh , $ch );
}
// Execute the multi-handle
do {
$status = curl_multi_exec ( $mh , $active );
if ( $active ) {
curl_multi_select ( $mh );
}
} while ( $active && $status == CURLM_OK );
// 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 ();
}
2024-02-16 16:09:05 +00:00
// Content can be plain text. But to add clickable links and hashtags, it needs to be turned into HTML.
2024-02-17 09:30:50 +00:00
// Tags are also included separately in the note
2024-02-16 16:09:05 +00:00
function process_content ( $content ) {
global $server ;
2024-02-17 09:30:50 +00:00
// Convert any URls into hyperlinks
$link_pattern = '/\bhttps?:\/\/\S+/iu' ;
$replacement = function ( $match ) {
$url = htmlspecialchars ( $match [ 0 ], ENT_QUOTES , " UTF-8 " );
return " <a href= \" $url\ " > $url </ a > " ;
};
$content = preg_replace_callback ( $link_pattern , $replacement , $content );
2024-02-16 16:09:05 +00:00
// Get any hashtags
$hashtags = [];
2024-02-17 09:30:50 +00:00
$hashtag_pattern = '/(?:^|\s)\#(\w+)/' ; // Beginning of string, or whitespace, followed by #
2024-02-17 20:48:03 +00:00
preg_match_all ( $hashtag_pattern , $content , $hashtag_matches );
foreach ( $hashtag_matches [ 1 ] as $match ) {
2024-02-16 16:09:05 +00:00
$hashtags [] = $match ;
}
// Construct the tag value for the note object
$tags = [];
foreach ( $hashtags as $hashtag ) {
$tags [] = array (
" type " => " Hashtag " ,
" name " => " # { $hashtag } " ,
);
}
// Add HTML links for hashtags into the text
$content = preg_replace (
2024-02-17 20:48:03 +00:00
$hashtag_pattern ,
2024-02-17 21:00:36 +00:00
" <a href='https:// { $server } /tag/ $ 1'># $ 1</a> " ,
2024-02-16 16:09:05 +00:00
$content
);
2024-02-17 20:48:03 +00:00
// Detect user mentions
$usernames = [];
$usernames_pattern = '/@(\S+)@(\S+)/' ; // This is a *very* sloppy regex
preg_match_all ( $usernames_pattern , $content , $usernames_matches );
foreach ( $usernames_matches [ 0 ] as $match ) {
$usernames [] = $match ;
}
// Construct the mentions value for the note object
// This goes in the generic "tag" property
// TODO: Add this to the CC field
foreach ( $usernames as $username ) {
list ( $null , $user , $domain ) = explode ( " @ " , $username );
$tags [] = array (
" type " => " Mention " ,
" href " => " https:// { $domain } /@ { $user } " ,
" name " => " { $username } "
);
// Add HTML links to usernames
$username_link = " <a href= \" https:// { $domain } /@ { $user } \" > $username </a> " ;
$content = str_replace ( $username , $username_link , $content );
}
2024-02-16 16:09:05 +00:00
// Construct the content
$content = " <p> { $content } </p> " ;
2024-02-17 20:48:03 +00:00
return [
" HTML " => $content ,
" TagArray " => $tags
];
2024-02-16 16:09:05 +00:00
}
2024-02-18 14:44:57 +00:00
// The Outbox contains a date-ordered list (newest first) of all the user's posts
// This is optional.
function outbox () {
global $server , $username ;
// Get all posts
$posts = array_reverse ( glob ( " posts/ " . " *.json " ) );
// Number of posts
$totalItems = count ( $posts );
// Create an ordered list
$orderedItems = [];
foreach ( $posts as $post ) {
$orderedItems [] = array (
" type " => " Create " ,
" actor " => " https:// { $server } / { $username } " ,
" object " => " https:// { $server } / { $post } "
);
}
// Create User's outbox
$outbox = array (
" @context " => " https://www.w3.org/ns/activitystreams " ,
" id " => " https:// { $server } /outbox " ,
" type " => " OrderedCollection " ,
" totalItems " => $totalItems ,
" summary " => " All the user's posts " ,
" orderedItems " => $orderedItems
);
// Render the page
header ( " Content-Type: application/activity+json " );
echo json_encode ( $outbox );
die ();
}
2024-02-14 09:45:04 +00:00
// "One to stun, two to kill, three to make sure"
2024-02-13 13:30:36 +00:00
die ();
die ();
die ();