Skip to main content




friendica (DFRN) - Link to source

@Fedilab Apps Hey, crazy question, but since Holos is up and running, would it be possible to make something more traditional?

I was rolling around the idea of running Wafrn server from a spare phone, and then it occurred to me how useful that'd be, I mean lots of people have a spare phone, and older phones, even older premium phones can be had cheaply, right? So what if we could install a front end-less server on one? Leave it at home on WiFi, and connect to it via FediLab or another Mastodon compatible client.

You prolly don't need another project, but I figured I'd toss that out there as it seems like its got some similarities to #holos

in reply to Kāpitan

Yes because the server already runs directly on the phone, so adding a Mastodon-compatible API on top so Fedilab or other clients can connect to it locally is possible. It could be another app that just brings the server part.
in reply to Fedilab Apps

@Fedilab Apps Nods, "That's what I was thinking, basically the idea popped into my head that such a thing would make self-hosting more than just one account easy, after all there's a reason why I have a hosted instance, setting up this stuff is beyond me, but an app I can download, start and have my own instance up and running? I'll need a domain and a relay, but that's far easier than setting up my own typical instance, no? I wouldn't expect to be able to host too many accounts, but here I have 5 accounts, 3 that don't post much, one that posts in batches (I so wish there was something with a queue function), and my regular personal account."




friendica (DFRN) - Link to source



Busy, busy today, got up at 6, pulled all the donations out, now at Discount Tire to swap out the tires and wheels for the set that'd been on Doppel. After that, Walmart for insoles, and might as well get the groceries done. Then home to get things ready for tomorrow's flea market

#Today #life







friendica (DFRN) - Link to source






Lady Laura on Mastodon
Pack of Dogs by John Littleboy (they also have cat cards )
Lady Laura on Mastodon
artiphany.com/collections/play…













Revealed: a #crypto #billionaire’s political base hosting ‘anti-woke’ and #rightwing activists in Westminster | #Politics | The Guardian


Convicted man moves abroad and then tries to disrupt his native country from afar on #immigration (hypocrite much?) and #abortion.

theguardian.com/politics/2026/…

#UK


tend2wobble reshared this.



friendica (DFRN) - Link to source






"Encyclopedia Britannica and Merriam-Webster have filed a lawsuit against OpenAI, alleging in its complaint that the AI giant has committed 'massive copyright infringement.'" techcrunch.com/2026/03/16/merr…


friendica (DFRN) - Link to source

Andrew Pam reshared this.





friendica (DFRN) - Link to source



I hate hold music, on the phone with the second investment firm, with a good hour so far between the two of them. Of the two, T Rowe Price has the worst hold music.

Vik was so disorganized, so far since he passed in July, I've had to deal with Fidelity, Morgan Stanley (I hate them with a passion), Merril Lynch (not impressed by them, despite knowing there was an account there, likely inherited from his mom, they can't find any record of either of them having an account there ever), and T Rowe Price. Definitely glad to be consolidating this down to a single financial planner who uses Charles Schwab. Not sure how good CS is, but if I had to choose one of the ones I've dealt with, it'd be Fidelity hands down, they've been incredibly easy to deal with.

#Life #Investments #Estate



friendica (DFRN) - Link to source

Wow, what a trainwreck#AI #law #lawfailure #ca #california

Chuff² reshared this.








@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.





friendica (DFRN) - Link to source



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




friendica (DFRN) - Link to source

@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.