Skip to main content






β™² @psych@diaspora.glasswings.com:
Contrast & Compare Department Memo # 47

No wonder the orange toddler is unhinged once again.


"The Obamas are competent, classy, loving, Harvard & Princeton graduates, loved by many, not on the #Epstein files, got the #Nobel Peace Prize, & gave us a steady economy to boot."

Aside from today's #unhinged #WordSalad rants (and yesterday's and day before's) - even while hiding out in Mar-a-Lago....
The old #BrainSpurs seem to be acting up too. Poor Donny #TrumpVirus has zero #ExecutiveFunctioning ability - meaning focus, comprehension, sequencing/planning/reasoning skills - and his repertoire of canned bullying and both-sidesism sound bites, is stale.

#TrumpVirus' florid irrationality is so grotesque & obvious that only his inner circle - the ones wearing oversized clown shoes - think or pretend there's nothing wrong or abnormal. Nothing to see here. Presidents always babble incoherently, start wars without goals or Congress, do 'deals' for the family while starving our children and gutting our society. Right, Donald. Even the #cult is tiring of this.








So, about today…
Lets see, its -6Β°C at the moment, but feels like -14 and they just revised the temperatures down to a high of -3. Thank goodness I just need to be at the chiropractor in an hour, then pick up a cup of coffee (St Louis Bread ain't great coffee, but its free coffee), and come home, no more out for today. Today is my first four week chiro visit, they've been three week visits since I hurt m'self, and the next one will be in four, but after that we might go six and see how things go, these visits are basically a permanent thing.
Lets see how many spoons I have left after taking care of more estate matters, gotta call two investment banks and depending on how things go, might have to tell the lawyer to unfile the probate matter and refile as a large estate, which is a double edged sword, more money for me, but instead of 2-3 months, this can take a year or more to resolve. Should do some cleaning, but… Ehhh, we'll see.
Yesterday wasn't much, grocery shopping and household finances done. Oh, slightly broke the DBA registry for Missouri by trying to register β˜…Generalβ˜… Delivery, while the stars didn't throw an error, they did get replaced by question marks as the system has no idea how to handle that.
Tomorrow will be work of course, thankfully it should be around 5 to 10 degrees warmer than today, but not sure about the wind, I think that'll less so the feels like'll be sane. Unfortunately work won't, Friday they were already making noises about it being so busy they need all drivers. Oh well, long day for me, and I'll be able to do an early start on Wed as they'll likely have routes that never went out.

#Weather #Medical #Today #Life #STL #St-Louis





@gabboman the wafrn dev Next version of Wafrn?


ActivityPub Server in a Single PHP File

shkspr.mobi/blog/2024/02/activ…
Any computer program can be designed to run from a single file if you architect it wrong enough!

I wanted to create the simplest possible Fediverse server which can be used as an educational tool to show how ActivityPub / Mastodon works.

The design goals were:

  • Upload a single PHP file to the server.
  • No databases or separate config files.
  • Single Actor (i.e. not multi-user).
  • Allow the Actor to be followed.
  • Post plain-text messages to followers.
  • Be roughly standards compliant.

And those goals have all been met! Check it out on GitLab. I warn you though, it is the nadir of bad coding. There are no tests, bugger-all security, scalability isn't considered, and it is a mess. But it works.

You can follow the test user @[url=https://example.viii.fi/example]example@example.viii.fi[/url]

Architecture


Firstly, I've slightly cheated on my "single file" stipulation. There's an .htaccess file which turns example.com/whatever into example.com/index.php?path=whatever

The index.php file then takes that path and does stuff. It also contains all the configuration variables which is very bad practice.

Rather than using a database, it saves files to disk.

Again, this is not suitable for any real world use. This is an educational tool to help explain the basics of posting messages to the Fediverse. It requires absolutely no dependencies. You do not need to spin up a dockerised hypervisor to manage your node bundles and re-compile everything to WASM. Just FTP the file up to prod and you're done.

Walkthrough


This is a quick ramble through the code. It is reasonably well documented, I hope.

Preamble


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.
PHP // Set up the Actor's information
$username = rawurlencode("example"); // Encoded as it is often used as part of a URl
$realName = "E. Xample. Jr.";
$summary = "Some text about the user.";
$server = $_SERVER["SERVER_NAME"]; // Domain name this is hosted on

// Generate locally or from 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";

Logging


ActivityPub is a "chatty" protocol. This takes all the requests your server receives and saves them in /logs/ as a datestamped text file.
PHP // Get all headers and requests sent to this server
$headers = print_r( getallheaders(), true );
$postData = print_r( $_POST, true );
$getData = print_r( $_GET, true );
$filesData = print_r( $_FILES, true );
$body = json_decode( file_get_contents( "php://input" ), true );
$bodyData = print_r( $body, true );
$requestData = print_r( $_REQUEST, true );
$serverData = print_r( $_SERVER, true );

// Get the type of request - used in the log filename
if ( isset( $body["type"] ) ) {
$type = " " . $body["type"];
} else {
$type = "";
}

// Create a timestamp in ISO 8601 format for the filename
$timestamp = date( "c" );
// Filename for the log
$filename = "{$timestamp}{$type}.txt";

// Save headers and request data to the timestamped file in the logs directory
if( ! is_dir( "logs" ) ) { mkdir( "logs"); }

file_put_contents( "logs/{$filename}",
"Headers: \n$headers \n\n" .
"Body Data: \n$bodyData \n\n" .
"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"
);

Routing


The .htaccess changes /whatever to /?path=whateverThis runs the function of the path requested.
PHP !empty( $_GET["path"] ) ? $path = $_GET["path"] : die();
switch ($path) {
case ".well-known/webfinger":
webfinger();
case rawurldecode( $username ):
username();
case "following":
following();
case "followers":
followers();
case "inbox":
inbox();
case "write":
write();
case "send":
send();
default:
die();
}

WebFinger


The WebFinger Protocol is used to identify accounts. It is requested with example.com/.well-known/webfinger?resource=acct:username@example.comThis server only has one user, so it ignores the query string and always returns the same details.
PHP function webfinger() {
global $username, $server;

$webfinger = array(
"subject" => "acct:{$username}@{$server}",
"links" => array(
array(
"rel" => "self",
"type" => "application/activity+json",
"href" => "https://{$server}/{$username}"
)
)
);
header( "Content-Type: application/json" );
echo json_encode( $webfinger );
die();
}

Username


Requesting example.com/username returns a JSON document with the user's information.
PHP function username() {
global $username, $realName, $summary, $server, $key_public;

$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",
"preferredUsername" => rawurldecode($username),
"name" => "{$realName}",
"summary" => "{$summary}",
"url" => "https://{$server}",
"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
]
);
header( "Content-Type: application/activity+json" );
echo json_encode( $user );
die();
}

Following & Followers


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.
PHPfunction following() {
global $server;

$following = array(
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://{$server}/following",
"type" => "Collection",
"totalItems" => 0,
"items" =>
[] );
header( "Content-Type: application/activity+json" );
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" =>
[] );
header( "Content-Type: application/activity+json" );
echo json_encode( $followers );
die();
}

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.
PHP 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"];
$inbox_host = parse_url( $inbox_actor, PHP_URL_HOST );

// 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
// TODO: The path doesn't *always* end with/inbox
$host = $inbox_host;
$path = parse_url( $inbox_actor, PHP_URL_PATH ) . "/inbox";

// Get the signed headers
$headers = generate_signed_headers( $message, $host, $path );

// 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" );
curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode($message) );
curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
$response = curl_exec( $ch );

// Check for errors
if( curl_errno( $ch ) ) {
file_put_contents( "error.txt", curl_error( $ch ) );
}
curl_close($ch);
die();
}

UUID


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.
PHP function uuid() {
return sprintf( "%08x-%04x-%04x-%04x-%012x",
time(),
mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffffffffffff)
);
}

Signing Headers


Every message that your server sends needs to be cryptographically signed with your Private Key. This is a complicated process. Please read "How to make friends and verify requests" for more information.
PHP function generate_signed_headers( $message, $host, $path ) {
global $server, $username, $key_private;

// Encode the message to JSON
$message_json = json_encode( $message );

// Location of the Public Key
$keyId = "https://{$server}/{$username}#main-key";

// Generate signing variables
$hash = hash( "sha256", $message_json, true );
$digest = base64_encode( $hash );
$date = date( "D, d M Y H:i:s \G\M\T" );

// 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
// php.net/manual/en/function.ope…
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;
}

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.
PHP function write() {
// Display an HTML form for the user to enter a message.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.1em;}
</style>
</head>
<body>
<form action="/send" method="post" enctype="multipart/form-data">
<label for="content">Your message:</label><br>
<textarea id="content" name="content" rows="5" cols="32"></textarea><br>
<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();
}

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.
PHP 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"];

// 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}",
"content" => $content,
"contentMap" => ["en" => $content],
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
];

// Construct the Message
$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
$note = [ "@context" => "https://www.w3.org/ns/activitystreams", ...$note ];

// Save the permalink
$note_json = json_encode( $note );
// Check for posts/ directory and create it
if( ! is_dir( "posts" ) ) { mkdir( "posts"); }
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 ) {
$path = "/inbox";

// Get the signed headers
$headers = generate_signed_headers( $message, $host, $path );

// Specify the URL of the remote server
$remoteServerUrl = "https://{$host}{$path}";

// 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" );
curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode($message) );
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();
}

Next Steps


This is not intended to be used in production. Ever. But if you would like to contribute more simple examples of how the protocol works, please come and play on GitLab.

You can follow the test user @[url=https://example.viii.fi/example]example@example.viii.fi[/url]
#ActivityPub #mastodon #php


gabboman the wafrn dev reshared this.


🌴 Seph πŸ’­ πŸ‘Ύ reshared this.


So today's song is an interesting one, this is the version I know from the a NASCAR CD, Runnin' Wide Open, basically a compilation album full of car related songs. What I didn't know and surprised me when SXM played the original is that this is a Bruce Springsteen song. I swear, Springsteen is like Prince, his name turns up in the most unexpected places as a songwriter and/or the original recorder of songs.

youtu.be/9NlLf9_6f_8

#Country #YouTube #March-of-the-Covers #Music







🌴 Seph πŸ’­ πŸ‘Ύ reshared this.


Hi! My name is Anna! I’m a traditional artist from Kyiv πŸ‡ΊπŸ‡¦ . I paint, I adore cats, and I love working out at the gym.

I’m always working on my English, but sometimes I might post in Ukrainian because I don’t always feel like double-checking every word. I hope that’s okay.

I’m not much of a talker, and I’m not quite sure yet what exactly I’ll be posting here, but I wanted to give it a try🫢

#Introduction #Знайомство

reshared this


🌴 Seph πŸ’­ πŸ‘Ύ reshared this.


@TheBreadmonkey

"Our pasta, who art in a colander, draining be your noodles. Thy noodle come, Thy sauce be yum, on top some grated Parmesan. Give us this day, our garlic bread, …and forgive us our trespasses, as we forgive those who trample on our lawns. And lead us not into vegetarianism, but deliver us some pizza, for thine is the meatball, the noodle, and the sauce, forever and ever. R'Amen."

May His noodly appendages touch you

@Ben

reshared this


🌴 Seph πŸ’­ πŸ‘Ύ reshared this.


Look here world!

If you send ships to bail out #Trump in the #StraitOfHormuz, he’ll reduce your tariffs by 1% & stop calling you parasites. Okay?

Would the #TrumpRegime lie?

Alright. He’ll also stop his plans to invade your country AND stop funding agitators in your country as well.

So where are your ships?!

#NeverTrustTrump #USPol #auspol #Iran #TrumpMadeThisMess

reshared this



🌴 Seph πŸ’­ πŸ‘Ύ reshared this.


futurism.com/artificial-intell…

Gee, the Supreme Court actually did a good ruling for once, even if'n its a ruling by not ruling.

#USPol #AI #AI-Art #Supreme-Court #Law




🌴 Seph πŸ’­ πŸ‘Ύ reshared this.


Almost forgot to post today's cover, was laying in bed and remembered, so here it is, day 9. Not much to say about this one, was originally done by Bruce Springsteen, and only a couple words were changed in this, what is prolly the best known version.

youtu.be/E5tJxTZOcHA

#Music #YouTube #March-of-the-Covers #Rock


🌴 Seph πŸ’­ πŸ‘Ύ reshared this.


tumblr.com/onbearfeet/81068339…

🌴 Seph πŸ’­ πŸ‘Ύ reshared this.


supercarblondie.com/video-show…

Honestly don't get why people care all that much about images like this, its funny, but such a thing really won't work, you have the extra drag from the trailer reducing your range, and considering how long it takes to charge my car at 230V/30A, I don't a portable generator is going to give you anything meaningful.

#EV #ElectricCar


🌴 Seph πŸ’­ πŸ‘Ύ reshared this.


Ok, another late one, another almost missed, and tomorrow's will likely be on the late side as I'll be working early and long. Any how, Desperados Waiting For the Train, a song I know from the Highwaymen, the country super group, but was written in 1973, and has been recorded a lot, this particular version I'm not even sure when its from.

youtu.be/mqnl_JBR1Do

#Music #March-of-the-Covers #Country



🌴 Seph πŸ’­ πŸ‘Ύ reshared this.


So, 'bout a week or so ago on Tumblr, a survey came across my dash asking it a pan and a skillet are the same thing. I couldn't resist commenting 'Of course not, have you ever heard of someone calling themselves skillet-sexual?'

#New-Sexuality-Unlocked #Silly

reshared this

in reply to Chuff

On the other hand, they may have some skill-et it…


🌴 Seph πŸ’­ πŸ‘Ύ reshared this.


Oh y'all thought I'd forgotten March of the Covers, or given up due to the lack of feedback? Perish the thought.
Not a lot to say about this song, its a neat reworking of the original song, and in fact Roberta Flack the original singer would perform it with the Fugees, although their version had Lauryn Hill on vocals originally.

youtu.be/oKOtzIo-uYw

#YouTube #Music #March-of-the-Covers


🌴 Seph πŸ’­ πŸ‘Ύ reshared this.


Extra tired today after a 9 hour day, so lets make today's music a quicky. This is actually the version that introduced me to the song, but I have since heard the original by the Band, and like them equally, which is rather a strike against this cover, as in my mind it really doesn't do anything special, its just fine, and nothing else.

youtu.be/Cw5DwLAU8rE

#YouTube #Music #Country #March-of-the-Covers








Ok, might not be leaving


So I might be re-focusing here, Wafrn is getting on my nerves. Its not that I don't like it, @gabboman the wafrn dev has done a great job, but I don't like being forced to be linked to Bluesky. Since I disconnected from it I get error message when trying to like, re-share or comment (I think its all of them) on random posts, not just Bluesky or Wafrn, but randomly anything, mastodon.social posts sometimes will throw an error for example.

And then there's the problem with links to accounts, there's two, one in the profile and another at the bottom of the timeline to go to the user's actual page rather than filtered through my instance. Its a minor problem, but especially on .social domains the bottom one substitutes a number for the user and doesn't work.

Sure, there's features it lacks that I'd like, and if'n someone came along and made something akin to Mastodon/Pleroma/Wafrn/et al with those would be awesome, but I can live with what it has.

I am giving GoToSocial a shot, if'n I can get time to really play with it, but at first glance I can't say I'm impressed by it, there's no obvious additions or improvements to it over the other choices, and the lack of a front end means that I'm using basically the same interface as Mastodon, so it doesn't look nor feel any different.

#WAFRN-Problem #wafrn #gotosocial #Fediverse #socialmedia

#GoToSocial reshared this.

in reply to 🌴 Seph πŸ’­ πŸ‘Ύ

Just my two cents on GoToSocial. I really loved it when I used to be heavily active on fedi, but its promises of being "lightweight" are somewhat… incorrect. While that's true, it eventually chews through I/O like its no-one's business. Suppose it you keep to a small circle it probably won't be that bad, but letting it run wild federating with a huge amount of servers really chokes it.
in reply to gabboman the wafrn dev

Ours was connected to Postgres. I must point out our GTS server was federating with an insane amount of servers (like, getting way past 10k+). While that probably wasn't within its usecase, it was entirely the reason I stopped self-hosting fedi in the end: I'd already obliterated our self-hosted Mastodon instance, and I just could not be bothered with doing yet-another-migration-and-starting-afresh-again.
in reply to Ducky πŸ¦†πŸ‡Ό

@Ducky πŸ¦†πŸ‡Ό @gabboman the wafrn dev Yeah, that was its one claim to fame that I could see, and if'n you're just doing a one user, there's lots of alternatives, though my pet peeve with those is mostly that they're all identical in features with the only difference being what programming language they're in.





L'Amérique et Israël ont normalisé le meurtre d'enfants. Les plus innocents, les plus vulnérables, sont massacrés et le monde continue comme si de rien n'était. Ça suffit ! Combien de temps encore le sang des enfants sera-t-il le « problème » de quelqu'un d'autre ?





#uspol
⇧