Add likes and announces. Rationalise homepage and read page
rodzic
db46472041
commit
8c93c008e0
623
index.php
623
index.php
|
@ -131,13 +131,13 @@
|
|||
case "follow_user":
|
||||
follow_user(); // API for following a user
|
||||
case "read":
|
||||
read(); // User interface for reading posts
|
||||
view( "read" ); // User interface for reading posts
|
||||
case ".well-known/nodeinfo":
|
||||
wk_nodeinfo(); // Optional. Static.
|
||||
case "nodeinfo/2.1":
|
||||
nodeinfo(); // Optional. Static.
|
||||
case "/":
|
||||
home(); // Optional. Can be dynamic
|
||||
view( "home" ); // Optional. Can be dynamic
|
||||
default:
|
||||
die();
|
||||
}
|
||||
|
@ -477,11 +477,24 @@
|
|||
return $headers;
|
||||
}
|
||||
|
||||
|
||||
// User Interface for Homepage.
|
||||
// This creates a basic HTML page. This content appears when someone visits the root of your site.
|
||||
function home() {
|
||||
function view( $style ) {
|
||||
global $username, $server, $realName, $summary, $directories;
|
||||
$rawUsername = rawurldecode( $username );
|
||||
|
||||
switch ( $style ) {
|
||||
case "home":
|
||||
$h1 = "HomePage";
|
||||
$directory = "posts";
|
||||
break;
|
||||
case "read":
|
||||
$h1 = "InBox";
|
||||
$directory = "inbox";
|
||||
break;
|
||||
}
|
||||
|
||||
echo <<< HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-GB">
|
||||
|
@ -493,7 +506,7 @@ echo <<< HTML
|
|||
<meta property="og:title" content="{$realName}">
|
||||
<meta property="og:description" content="{$summary}">
|
||||
<meta property="og:image" content="https://{$server}/banner.png">
|
||||
<title>{$realName}</title>
|
||||
<title>{$h1} {$realName}</title>
|
||||
<style>
|
||||
body { margin:0; padding: 0; font-family:sans-serif; }
|
||||
@media screen and (max-width: 800px) { body { width: 100%; }}
|
||||
|
@ -519,8 +532,8 @@ echo <<< HTML
|
|||
<main class="h-feed">
|
||||
<header>
|
||||
<div class="banner">
|
||||
<img src="banner.png" alt="" class="u-feature" /><br/>
|
||||
<img src="icon.png" alt="icon" class="u-photo" />
|
||||
<img src="banner.png" alt="" class="u-feature"><br>
|
||||
<img src="icon.png" alt="icon" class="u-photo">
|
||||
</div>
|
||||
<address>
|
||||
<h1 class="p-name p-author">{$realName}</h1>
|
||||
|
@ -534,31 +547,161 @@ echo <<< HTML
|
|||
</header>
|
||||
<ul>
|
||||
HTML;
|
||||
// Get all posts, most recent first
|
||||
$posts = array_reverse( glob( $directories["posts"] . "/*.json") );
|
||||
// Get all the files in the directory
|
||||
$message_files = array_reverse( glob( $directories[$directory] . "/*.json") );
|
||||
// Keep the most recent 200
|
||||
$message_files = array_slice( $message_files, 0, 200 );
|
||||
|
||||
// Loop through the posts
|
||||
foreach ($posts as $post) {
|
||||
// Get the contents of the file
|
||||
$postJSON = file_get_contents($post);
|
||||
$postData = json_decode($postJSON, true);
|
||||
// Sometimes messages are received out of order.
|
||||
// This sorts them by their published time or, if there is none, the received time.
|
||||
$messages_ordered = [];
|
||||
foreach ( $message_files as $message_file ) {
|
||||
// Get the contents of the JSON
|
||||
$message = json_decode( file_get_contents( $message_file ), true );
|
||||
|
||||
// Use the timestamp of the message. If there is none, use the date in the filename
|
||||
if ( isset( $message["published"] ) ) {
|
||||
$published = $message["published"];
|
||||
} else {
|
||||
$published_hexstamp = end( explode( "/", explode( "-", $message_file)[0] ) ) ;
|
||||
$published_time = hexdec( $published_hexstamp );
|
||||
$published = date( "c", $published_time );
|
||||
|
||||
// Set up some common variables
|
||||
$postTime = $postData["published"];
|
||||
$postHTML = $postData["content"];
|
||||
}
|
||||
// Place in an array where the key is the timestamp
|
||||
$messages_ordered[$published] = $message;
|
||||
}
|
||||
|
||||
// If the message has an image attached, display it.
|
||||
if ( isset($postData["attachment"]["url"]) ) {
|
||||
$postImgUrl = $postData["attachment"]["url"];
|
||||
$postImgAlt = $postData["attachment"]["name"];
|
||||
$postImg = "<br/><img class='u-photo' src='{$postImgUrl}' alt='$postImgAlt'>";
|
||||
// Sort with newest on top
|
||||
krsort( $messages_ordered );
|
||||
|
||||
// HTML is *probably* sanitised by the sender. But let's not risk it, eh?
|
||||
// Using the allow-list from https://docs.joinmastodon.org/spec/activitypub/#sanitization
|
||||
$allowed_elements = ["p", "span", "br", "a", "del", "pre", "code", "em", "strong", "b", "i", "u", "ul", "ol", "li", "blockquote"];
|
||||
|
||||
// Print the items in a list
|
||||
foreach ( $messages_ordered as $published=>$message ) {
|
||||
|
||||
// Set up the common components
|
||||
$object = $message["object"];
|
||||
if ( isset( $message["object"]["id"] ) ) {
|
||||
$id = $message["object"]["id"];
|
||||
$timeHTML = "<time datetime=\"{$published}\" class=\"u-url\" rel=\"bookmark\"><a href=\"{$id}\">{$published}</a></time>";
|
||||
} else {
|
||||
$id = "";
|
||||
$timeHTML = "<time datetime=\"{$published}\" class=\"u-url\" rel=\"bookmark\">{$published}</time>";
|
||||
}
|
||||
$actor = $message["actor"];
|
||||
$actorName = end( explode("/", $actor ) );
|
||||
$actorHTML = "<a href=\"$actor\">@{$actorName}</a>";
|
||||
|
||||
// What type of message is this?
|
||||
$type = $message["type"];
|
||||
|
||||
// Render the message according to type
|
||||
if ( "Create" == $type || "Update" == $type || "Note" == $type ) {
|
||||
// Get the HTML content and sanitise it.
|
||||
// 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 );
|
||||
|
||||
// Is this a reply to something?
|
||||
if ( isset( $object["inReplyTo"] ) ) {
|
||||
$replyToURl = $object["inReplyTo"];
|
||||
$replyTo = " in reply to <a href=\"{$replyToURl}\">$replyToURl</a>";
|
||||
} else {
|
||||
$postImg = "";
|
||||
$replyTo = "";
|
||||
}
|
||||
|
||||
// Display the post
|
||||
echo "<li><article class='h-entry'><a class='u-url' rel='bookmark' href='/{$post}'><time datetime='{$postTime}'>$postTime</time></a><br><span class='e-content'>{$postHTML}{$postImg}</span></article></li>";
|
||||
// Has the user has been specifically CC'd?
|
||||
if ( isset( $object["cc"] ) ) {
|
||||
$reply = in_array( "https://{$server}/{$username}", $object["cc"] );
|
||||
} else {
|
||||
$reply = false;
|
||||
}
|
||||
|
||||
// Is there is a Content Warning?
|
||||
if ( isset( $object["summary"] ) ) {
|
||||
$summary = $object["summary"];
|
||||
$summary = strip_tags( $summary, $allowed_elements );
|
||||
|
||||
$content = "<details><summary>{$summary}</summary>{$content}</details>";
|
||||
}
|
||||
|
||||
// Is there a poll?
|
||||
// ActivityPub specification - https://www.w3.org/TR/activitystreams-vocabulary/#questions
|
||||
// Mastodon documentation - https://docs.joinmastodon.org/spec/activitypub/#Question
|
||||
if ( isset( $object["oneOf"] ) ) {
|
||||
$content .= "<h3>Poll Results</h3>";
|
||||
foreach ( $object["oneOf"] as $pollOption ) {
|
||||
$pollOptionName = htmlspecialchars( $pollOption["name"] );
|
||||
$pollOptionValue = htmlspecialchars( $pollOption["replies"]["totalItems"] );
|
||||
|
||||
$content .= "<br>{$pollOptionName}: $pollOptionValue";
|
||||
}
|
||||
}
|
||||
if ( isset( $object["anyOf"] ) ) {
|
||||
$content .= "<h3>Poll Results</h3>";
|
||||
foreach ( $object["anyOf"] as $pollOption ) {
|
||||
$pollOptionName = htmlspecialchars( $pollOption["name"] );
|
||||
$pollOptionValue = htmlspecialchars( $pollOption["replies"]["totalItems"] );
|
||||
|
||||
$content .= "<br>{$pollOptionName}: $pollOptionValue";
|
||||
}
|
||||
}
|
||||
|
||||
// Add any images
|
||||
if ( isset( $object["attachment"] ) ) {
|
||||
foreach ( $object["attachment"] as $attachment ) {
|
||||
|
||||
// Only use things which have a MIME Type set
|
||||
if ( isset( $attachment["mediaType"] ) ) {
|
||||
$mediaType = explode( "/", $attachment["mediaType"])[0];
|
||||
|
||||
if ( "image" == $mediaType ) {
|
||||
// Get the alt text
|
||||
isset( $attachment["name"] ) ? $alt = $attachment["name"] : $alt = "";
|
||||
$content .= "<img src='" . $attachment["url"] . "' alt='{$alt}'>";
|
||||
} else if ( "video" == $mediaType ) {
|
||||
$content .= "<video controls><source src='" . $attachment["url"] . "' type='" . $attachment["mediaType"] . "' /></video>";
|
||||
}else if ( "audio" == $mediaType ) {
|
||||
$content .= "<audio controls src='" . $attachment["url"] . "' type='" . $attachment["mediaType"] . "'></audio>";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What sort of message is this?
|
||||
switch ( $type ) {
|
||||
case "Create":
|
||||
case "Note":
|
||||
$verb = "wrote";
|
||||
break;
|
||||
case "Update":
|
||||
$verb = "updated";
|
||||
break;
|
||||
default:
|
||||
$verb = "said";
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $reply ) {
|
||||
// Highlight that this is a reply
|
||||
echo "<li><article class=\"h-entry\"><mark>{$timeHTML} {$actorHTML} {$verb}{$replyTo}:</mark> <blockquote class=\"e-content\">{$content}</blockquote></article></li>";
|
||||
} else {
|
||||
echo "<li><article class=\"h-entry\">{$timeHTML} {$actorHTML} {$verb}{$replyTo}: <blockquote class=\"e-content\">{$content}</blockquote></article></li>";
|
||||
}
|
||||
|
||||
} else if ( "Like" == $type ) {
|
||||
$objectHTML = "<a href=\"$object\">{$object}</a>";
|
||||
echo "<li><article class=\"h-entry\">{$timeHTML} {$actorHTML} liked {$objectHTML}</article></li>";
|
||||
} else if ( "Follow" == $type ) {
|
||||
echo "<li><article class=\"h-entry\">{$timeHTML} {$actorHTML} followed you</article></li>";
|
||||
} else if ( "Announce" == $type ) {
|
||||
$objectHTML = "<a href=\"$object\">{$object}</a>";
|
||||
echo "<li><article class=\"h-entry\">{$timeHTML} {$actorHTML} boosted {$objectHTML}</article></li>";
|
||||
}
|
||||
}
|
||||
echo <<< HTML
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -586,6 +729,7 @@ echo <<< HTML
|
|||
<fieldset>
|
||||
<legend>Send a message</legend>
|
||||
<form action="/send" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" id="type" name="type" value="Create">
|
||||
<label for="content">Your message:</label><br>
|
||||
<textarea id="content" name="content" rows="5" cols="32"></textarea><br>
|
||||
<label for="inReplyTo">Reply to URl:</label>
|
||||
|
@ -599,6 +743,28 @@ echo <<< HTML
|
|||
<input type="submit" value="Post Message">
|
||||
</form>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Like a post</legend>
|
||||
<form action="/send" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" id="type" name="type" value="Like">
|
||||
<label for="postURl">URl of post to like:</label>
|
||||
<input type="url" name="postURl" id="postURl" size="32" /><br>
|
||||
<label for="password">Password</label><br>
|
||||
<input type="password" name="password" id="password" size="32"><br><br>
|
||||
<input type="submit" value="Like the message">
|
||||
</form>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Boost a post</legend>
|
||||
<form action="/send" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" id="type" name="type" value="Announce">
|
||||
<label for="postURl">URl of post to boost:</label>
|
||||
<input type="url" name="postURl" id="postURl" size="32" /><br>
|
||||
<label for="password">Password</label><br>
|
||||
<input type="password" name="password" id="password" size="32"><br><br>
|
||||
<input type="submit" value="Boost the Message">
|
||||
</form>
|
||||
</fieldset>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
|
@ -616,98 +782,137 @@ HTML;
|
|||
// Does the posted password match the stored password?
|
||||
if( $password != $_POST["password"] ) { die(); }
|
||||
|
||||
// Get the posted content
|
||||
$content = $_POST["content"];
|
||||
// What sort of message is being sent?
|
||||
$type = $_POST["type"];
|
||||
|
||||
// Is this a reply?
|
||||
if ( isset( $_POST["inReplyTo"] ) && filter_var( $_POST["inReplyTo"], FILTER_VALIDATE_URL ) ) {
|
||||
$inReplyTo = $_POST["inReplyTo"];
|
||||
} else {
|
||||
$inReplyTo = null;
|
||||
}
|
||||
|
||||
// Process the content into HTML to get hashtags etc
|
||||
list( "HTML" => $content, "TagArray" => $tags ) = process_content( $content );
|
||||
|
||||
// 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_full_path = $directories["images"] . "/{$sha1}.{$image_ext}";
|
||||
|
||||
// Move media to the correct location
|
||||
move_uploaded_file( $image, $image_full_path );
|
||||
|
||||
// Get the alt text
|
||||
if ( isset( $_POST["alt"] ) ) {
|
||||
$alt = $_POST["alt"];
|
||||
// Likes and Announces have an identical message structure
|
||||
if ( $type == "Like" || $type == "Announce" ) {
|
||||
// Was a URl sent?
|
||||
if ( isset( $_POST["postURl"] ) && filter_var( $_POST["postURl"], FILTER_VALIDATE_URL ) ) {
|
||||
$postURl = $_POST["postURl"];
|
||||
} else {
|
||||
$alt = "";
|
||||
echo "No valid URl sent.";
|
||||
die();
|
||||
}
|
||||
|
||||
// Construct the attachment value for the post
|
||||
$attachment = [
|
||||
"type" => "Image",
|
||||
"mediaType" => "{$image_mime}",
|
||||
"url" => "https://{$server}/{$image_full_path}",
|
||||
"name" => $alt
|
||||
];
|
||||
// Outgoing Message ID
|
||||
$guid = uuid();
|
||||
|
||||
} else {
|
||||
$attachment = [];
|
||||
// Construct the Message
|
||||
// The audience is public and it is sent to all followers
|
||||
// TODO: This will also need to be sent to the server of the user whose post it is
|
||||
$message = [
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"id" => "https://{$server}/posts/{$guid}.json",
|
||||
"type" => $type,
|
||||
"actor" => "https://{$server}/{$username}",
|
||||
"published"=> date( "c" ),
|
||||
"to" => [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"cc" => [
|
||||
"https://{$server}/followers"
|
||||
],
|
||||
"object" => $postURl
|
||||
];
|
||||
|
||||
// Construct the Note
|
||||
// This is for saving in the logs
|
||||
$note = $message;
|
||||
|
||||
} else if ( $type == "Create" ) {
|
||||
// Get the posted content
|
||||
$content = $_POST["content"];
|
||||
|
||||
// Is this a reply?
|
||||
if ( isset( $_POST["inReplyTo"] ) && filter_var( $_POST["inReplyTo"], FILTER_VALIDATE_URL ) ) {
|
||||
$inReplyTo = $_POST["inReplyTo"];
|
||||
} else {
|
||||
$inReplyTo = null;
|
||||
}
|
||||
|
||||
// Process the content into HTML to get hashtags etc
|
||||
list( "HTML" => $content, "TagArray" => $tags ) = process_content( $content );
|
||||
|
||||
// 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_full_path = $directories["images"] . "/{$sha1}.{$image_ext}";
|
||||
|
||||
// Move media to the correct location
|
||||
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 = [];
|
||||
}
|
||||
|
||||
// Current time - ISO8601
|
||||
$timestamp = date( "c" );
|
||||
|
||||
// Outgoing Message ID
|
||||
$guid = uuid();
|
||||
|
||||
// Construct the Note
|
||||
// `contentMap` is used to prevent unnecessary "translate this post" pop ups
|
||||
// hardcoded to English
|
||||
$note = [
|
||||
"@context" => array(
|
||||
"https://www.w3.org/ns/activitystreams"
|
||||
),
|
||||
"id" => "https://{$server}/posts/{$guid}.json",
|
||||
"type" => "Note",
|
||||
"published" => $timestamp,
|
||||
"attributedTo" => "https://{$server}/{$username}",
|
||||
"inReplyTo" => $inReplyTo,
|
||||
"content" => $content,
|
||||
"contentMap" => ["en" => $content],
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"tag" => $tags,
|
||||
"attachment" => $attachment
|
||||
];
|
||||
|
||||
// Construct the Message
|
||||
// The audience is public and it is sent to all followers
|
||||
$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
|
||||
];
|
||||
}
|
||||
|
||||
// Current time - ISO8601
|
||||
$timestamp = date( "c" );
|
||||
|
||||
// Outgoing Message ID
|
||||
$guid = uuid();
|
||||
|
||||
// Construct the Note
|
||||
// `contentMap` is used to prevent unnecessary "translate this post" pop ups
|
||||
// hardcoded to English
|
||||
$note = [
|
||||
"@context" => array(
|
||||
"https://www.w3.org/ns/activitystreams"
|
||||
),
|
||||
"id" => "https://{$server}/posts/{$guid}.json",
|
||||
"type" => "Note",
|
||||
"published" => $timestamp,
|
||||
"attributedTo" => "https://{$server}/{$username}",
|
||||
"inReplyTo" => $inReplyTo,
|
||||
"content" => $content,
|
||||
"contentMap" => ["en" => $content],
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"tag" => $tags,
|
||||
"attachment" => $attachment
|
||||
];
|
||||
|
||||
// Construct the Message
|
||||
// The audience is public and it is sent to all followers
|
||||
$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
|
||||
];
|
||||
|
||||
// Save the permalink
|
||||
$note_json = json_encode( $note );
|
||||
// Check for posts/ directory and create it
|
||||
file_put_contents( $directories["posts"] . "/{$guid}.json", print_r( $note_json, true ) );
|
||||
|
||||
// Read existing followers
|
||||
|
@ -1200,210 +1405,6 @@ HTML;
|
|||
return $verified;
|
||||
}
|
||||
|
||||
// Displays the most recent 200 messages in the inbox
|
||||
function read() {
|
||||
global $server, $username, $realName, $directories;
|
||||
$rawUsername = rawurldecode( $username );
|
||||
|
||||
// Get all the files in the inbox
|
||||
$inbox_files = array_reverse( glob( $directories["inbox"] . "/*.json") );
|
||||
// Keep the most recent 200
|
||||
$inbox_files = array_slice( $inbox_files, 0, 200 );
|
||||
|
||||
// Sometimes messages are received out of order.
|
||||
// This sorts them by their published time or, if there is none, the received time.
|
||||
$inbox_ordered = [];
|
||||
foreach ( $inbox_files as $inbox_file ) {
|
||||
// Get the contents of the JSON
|
||||
$inbox_message = json_decode( file_get_contents( $inbox_file ), true );
|
||||
|
||||
// Use the timestamp of the message. If there is none, use the date in the filename
|
||||
if ( isset( $inbox_message["published"] ) ) {
|
||||
$published = $inbox_message["published"];
|
||||
} else {
|
||||
$published_hexstamp = end( explode( "/", explode( "-", $inbox_file)[0] ) ) ;
|
||||
$published_time = hexdec( $published_hexstamp );
|
||||
$published = date( "c", $published_time );
|
||||
|
||||
}
|
||||
// Place in an array where the key is the timestamp
|
||||
$inbox_ordered[$published] = $inbox_message;
|
||||
}
|
||||
|
||||
// Sort with newest on top
|
||||
krsort( $inbox_ordered );
|
||||
|
||||
// Show a basic HTML interface
|
||||
echo <<< HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-GB">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Reader</title>
|
||||
<style>
|
||||
body { margin:0; padding: 0; font-family:sans-serif; }
|
||||
@media screen and (max-width: 800px) { body { width: 100%; }}
|
||||
@media screen and (min-width: 799px) { body { width: 800px; margin: 0 auto; }}
|
||||
address { font-style: normal; }
|
||||
img { max-width: 50%; }
|
||||
.h-feed { margin:auto; width: 100%; }
|
||||
.h-feed > header { text-align: center; margin: 0 auto; }
|
||||
.h-feed .banner { text-align: center; margin:0 auto; max-width: 650px; }
|
||||
.h-feed > h1, .h-feed > h2 { margin-top: 10px; margin-bottom: 0; }
|
||||
.h-feed > header > h1:has(span.p-author), h2:has(a.p-nickname) { word-wrap: break-word; max-width: 90%; padding-left:20px; }
|
||||
.h-feed .u-feature:first-child { margin-top: 10px; margin-bottom: -150px; max-width: 100%;}
|
||||
.h-feed .u-photo { max-height: 8vw; max-width:100%; min-height: 120px; }
|
||||
.h-feed .about { font-size: smaller; background-color: #F5F5F5; padding: 10px; border-top: dotted 1px #808080; border-bottom: dotted 1px #808080; }
|
||||
.h-feed > ul { padding-left: 0; list-style-type: none; }
|
||||
.h-feed > ul > li { padding: 10px; border-bottom: dotted 1px #808080; }
|
||||
.h-entry { padding-right: 10px; }
|
||||
.h-entry time { font-weight: bold; }
|
||||
.h-entry .e-content a { word-wrap: break-word; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="h-feed">
|
||||
<header>
|
||||
<div class="banner">
|
||||
<img src="banner.png" alt="" class="u-feature" /><br/>
|
||||
<img src="icon.png" alt="icon" class="u-photo" />
|
||||
</div>
|
||||
<h1>Reader view for <span class="p-name p-author">{$realName}</span></h1>
|
||||
<h2><a class="p-nickname u-url" rel="author" href="https://{$server}/{$username}">@{$rawUsername}@{$server}</a></h2>
|
||||
<div class="about">
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
<ul>
|
||||
HTML;
|
||||
|
||||
// HTML is *probably* sanitised by the sender. But let's not risk it, eh?
|
||||
// Using the allow-list from https://docs.joinmastodon.org/spec/activitypub/#sanitization
|
||||
$allowed_elements = ["p", "span", "br", "a", "del", "pre", "code", "em", "strong", "b", "i", "u", "ul", "ol", "li", "blockquote"];
|
||||
|
||||
// Print the items in a list
|
||||
foreach ( $inbox_ordered as $published=>$inbox_message ) {
|
||||
|
||||
// Set up the common components
|
||||
$object = $inbox_message["object"];
|
||||
if ( isset( $inbox_message["object"]["id"] ) ) {
|
||||
$id = $inbox_message["object"]["id"];
|
||||
$timeHTML = "<time datetime=\"{$published}\" class=\"u-url\" rel=\"bookmark\"><a href=\"{$id}\">{$published}</a></time>";
|
||||
} else {
|
||||
$id = "";
|
||||
$timeHTML = "<time datetime=\"{$published}\" class=\"u-url\" rel=\"bookmark\">{$published}</time>";
|
||||
}
|
||||
$actor = $inbox_message["actor"];
|
||||
$actorName = end( explode("/", $actor ) );
|
||||
$actorHTML = "<a href=\"$actor\">@{$actorName}</a>";
|
||||
|
||||
|
||||
// What type of message is this?
|
||||
$type = $inbox_message["type"];
|
||||
|
||||
// Render the message according to type
|
||||
if ( "Create" == $type || "Update" == $type ) {
|
||||
// Get the HTML content and sanitise it.
|
||||
$content = $object["content"];
|
||||
$content = strip_tags( $content, $allowed_elements );
|
||||
|
||||
// Is this a reply to something?
|
||||
if ( isset( $object["inReplyTo"] ) ) {
|
||||
$replyToURl = $object["inReplyTo"];
|
||||
$replyTo = " in reply to <a href=\"{$replyToURl}\">$replyToURl</a>";
|
||||
} else {
|
||||
$replyTo = "";
|
||||
}
|
||||
|
||||
// Has the user has been specifically CC'd?
|
||||
if ( isset( $object["cc"] ) ) {
|
||||
$reply = in_array( "https://{$server}/{$username}", $object["cc"] );
|
||||
} else {
|
||||
$reply = false;
|
||||
}
|
||||
|
||||
// Is there is a Content Warning?
|
||||
if ( isset( $object["summary"] ) ) {
|
||||
$summary = $object["summary"];
|
||||
$summary = strip_tags( $summary, $allowed_elements );
|
||||
|
||||
$content = "<details><summary>{$summary}</summary>{$content}</details>";
|
||||
}
|
||||
|
||||
// Is there a poll?
|
||||
// ActivityPub specification - https://www.w3.org/TR/activitystreams-vocabulary/#questions
|
||||
// Mastodon documentation - https://docs.joinmastodon.org/spec/activitypub/#Question
|
||||
if ( isset( $object["oneOf"] ) ) {
|
||||
$content .= "<h3>Poll Results</h3>";
|
||||
foreach ( $object["oneOf"] as $pollOption ) {
|
||||
$pollOptionName = htmlspecialchars( $pollOption["name"] );
|
||||
$pollOptionValue = htmlspecialchars( $pollOption["replies"]["totalItems"] );
|
||||
|
||||
$content .= "<br>{$pollOptionName}: $pollOptionValue";
|
||||
}
|
||||
}
|
||||
if ( isset( $object["anyOf"] ) ) {
|
||||
$content .= "<h3>Poll Results</h3>";
|
||||
foreach ( $object["anyOf"] as $pollOption ) {
|
||||
$pollOptionName = htmlspecialchars( $pollOption["name"] );
|
||||
$pollOptionValue = htmlspecialchars( $pollOption["replies"]["totalItems"] );
|
||||
|
||||
$content .= "<br>{$pollOptionName}: $pollOptionValue";
|
||||
}
|
||||
}
|
||||
|
||||
// Add any images
|
||||
if ( isset( $object["attachment"] ) ) {
|
||||
foreach ( $object["attachment"] as $attachment ) {
|
||||
|
||||
// Only use things which have a MIME Type set
|
||||
if ( isset( $attachment["mediaType"] ) ) {
|
||||
$mediaType = explode( "/", $attachment["mediaType"])[0];
|
||||
|
||||
if ( "image" == $mediaType ) {
|
||||
// Get the alt text
|
||||
isset( $attachment["name"] ) ? $alt = $attachment["name"] : $alt = "";
|
||||
$content .= "<img src='" . $attachment["url"] . "' alt='{$alt}'>";
|
||||
} else if ( "video" == $mediaType ) {
|
||||
$content .= "<video controls><source src='" . $attachment["url"] . "' type='" . $attachment["mediaType"] . "' /></video>";
|
||||
}else if ( "audio" == $mediaType ) {
|
||||
$content .= "<audio controls src='" . $attachment["url"] . "' type='" . $attachment["mediaType"] . "'></audio>";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What sort of message is this?
|
||||
"Create" == $type ? $verb = "wrote" : $verb = "updated";
|
||||
if ( $reply ) {
|
||||
// Highlight that this is a reply
|
||||
echo "<li><article class=\"h-entry\"><mark>{$timeHTML} {$actorHTML} {$verb}{$replyTo}:</mark> <blockquote class=\"e-content\">{$content}</blockquote></article></li>";
|
||||
} else {
|
||||
echo "<li><article class=\"h-entry\">{$timeHTML} {$actorHTML} {$verb}{$replyTo}: <blockquote class=\"e-content\">{$content}</blockquote></article></li>";
|
||||
}
|
||||
|
||||
} else if ( "Like" == $type ) {
|
||||
$objectHTML = "<a href=\"$object\">{$object}</a>";
|
||||
echo "<li><article class=\"h-entry\">{$timeHTML} {$actorHTML} liked {$objectHTML}</article></li>";
|
||||
} else if ( "Follow" == $type ) {
|
||||
echo "<li><article class=\"h-entry\">{$timeHTML} {$actorHTML} followed you</article></li>";
|
||||
} else if ( "Announce" == $type ) {
|
||||
$objectHTML = "<a href=\"$object\">{$object}</a>";
|
||||
echo "<li><article class=\"h-entry\">{$timeHTML} {$actorHTML} boosted {$objectHTML}</article></li>";
|
||||
}
|
||||
}
|
||||
echo <<< HTML
|
||||
</ul>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
die();
|
||||
}
|
||||
|
||||
// The NodeInfo Protocol is used to identify servers.
|
||||
// It is looked up with `example.com/.well-known/nodeinfo`
|
||||
// See https://nodeinfo.diaspora.software/
|
||||
|
|
Ładowanie…
Reference in New Issue