From 6b609a487def52a3830ed5878fc84ac1fef4c1be Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:31:42 +0100 Subject: [PATCH 01/19] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index f34186f..75d0b2a 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,8 @@ creates a Note (the default) with the text "Hello, world!" ``` etc. + +# Nigel Whitfield updates, July 2023 +This fork provides an additional profile page within admin, allowing youto populate some of the information that will be displayed when people search for your actor. + +It additionally addresses a couple of minor issues relevant to my own installation, viz database creation, and an Apache re-write. From 69ed4ceaa603448de251913be3d36cd08f26353a Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:32:55 +0100 Subject: [PATCH 02/19] Add profile management page --- admin/profile.php | 104 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 admin/profile.php diff --git a/admin/profile.php b/admin/profile.php new file mode 100644 index 0000000..fa3f3d3 --- /dev/null +++ b/admin/profile.php @@ -0,0 +1,104 @@ +close() ; + + + header('Location: index.php') ; +} + +$profile = query($db, 'SELECT summary, url, name, icon, homepage FROM profile WHERE user=?', $_REQUEST['user']); +$db->close() ; + +?> + + + + phpAP profile for <?=$_REQUEST['user']; ?> + + + +

phpAP profile for

+
+ + 0) { + ?> +

+

+

+

+

+ + +

+

+

+

+

+ + + + +
+

+ Back to index +

+ + + \ No newline at end of file From 6b88dcd22852b31d5a539a4561ae093b1fb38fa9 Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:34:01 +0100 Subject: [PATCH 03/19] Add files via upload Add additional profile fields to repsonse, if found in database --- actor.php | 137 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 50 deletions(-) diff --git a/actor.php b/actor.php index c9788dd..023eae3 100644 --- a/actor.php +++ b/actor.php @@ -50,59 +50,96 @@ // check query parameters and extract userpart if (! empty($_GET['user'])) { - if (strtolower($_GET['user']) === $_SERVER['SERVER_NAME']) { - // instance actor - always served without needing signature - $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); - $info = query($db, 'SELECT rsa_public FROM instance'); - $db->close(); - - if (count($info) > 0) { - // The instance actor has been set up properly. - $user = $_SERVER['SERVER_NAME']; - $type = 'Application'; - $rsa_public = $info[0]['rsa_public']; - } else { - response(500, [ 'error' => 'Instance actor has not been properly configured.' ]); - } + if (strtolower($_GET['user']) === $_SERVER['SERVER_NAME']) { + // instance actor - always served without needing signature + $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); + $info = query($db, 'SELECT rsa_public FROM instance'); + $db->close(); + + if (count($info) > 0) { + // The instance actor has been set up properly. + $user = $_SERVER['SERVER_NAME']; + $type = 'Application'; + $rsa_public = $info[0]['rsa_public']; + } else { + response(500, [ 'error' => 'Instance actor has not been properly configured.' ]); + } + } else { + // TODO: Verify signature + + // connect to db, look up user info (verify the actor exists) + $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); + $info = query($db, 'SELECT user, type, rsa_public FROM acct WHERE user=?', $_GET['user']); + $db->close(); + + if (count($info) > 0) { + // Found an account by this name. + $user = $info[0]['user']; + $type = $info[0]['type']; + $rsa_public = $info[0]['rsa_public']; } else { - // TODO: Verify signature - - // connect to db, look up user info (verify the actor exists) - $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); - $info = query($db, 'SELECT user, type, rsa_public FROM acct WHERE user=?', $_GET['user']); - $db->close(); - - if (count($info) > 0) { - // Found an account by this name. - $user = $info[0]['user']; - $type = $info[0]['type']; - $rsa_public = $info[0]['rsa_public']; - } else { - response(404, [ 'error' => 'No such user ' . $_GET['user'] . ' in Actor request' ]); - } + response(404, [ 'error' => 'No such user ' . $_GET['user'] . ' in Actor request' ]); } + } // Build the complete Actor doc and send it out. - $actor = [ - "@context" => ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"], - "id" => $phpActivityPub_root . 'actor.php?user=' . $user, - "type" => $type, - - "inbox" => $phpActivityPub_root . 'inbox.php?user=' . $user, - "outbox" => $phpActivityPub_root . 'outbox.php?user=' . $user, - - "preferredUsername" => $user, - "followers" => $phpActivityPub_root . 'followers.php?user=' . $user, - "following" => $phpActivityPub_root . 'following.php?user=' . $user, - - "publicKey" => [ - "id" => $phpActivityPub_root . 'actor.php?user=' . $user . '#main-key', - "owner" => $phpActivityPub_root . 'actor.php?user=' . $user, - "publicKeyPem" => $rsa_public - ] - ]; - - response(200, $actor); + $actor = [ + "@context" => ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"], + "id" => $phpActivityPub_root . 'actor.php?user=' . $user, + "type" => $type, + + "inbox" => $phpActivityPub_root . 'inbox.php?user=' . $user, + "outbox" => $phpActivityPub_root . 'outbox.php?user=' . $user, + + "preferredUsername" => $user, + + "followers" => $phpActivityPub_root . 'followers.php?user=' . $user, + "following" => $phpActivityPub_root . 'following.php?user=' . $user, + + "publicKey" => [ + "id" => $phpActivityPub_root . 'actor.php?user=' . $user . '#main-key', + "owner" => $phpActivityPub_root . 'actor.php?user=' . $user, + "publicKeyPem" => $rsa_public + ] + ]; + + // NW: check for extended profile information + $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); + $profile = query($db, 'SELECT summary, url, name, icon, homepage FROM profile WHERE user=?', $_GET['user']); + $db->close() ; + + if (count($profile) > 0) { + // there is profile info for this user + if (strlen($profile[0]['summary']) > 0) { + $actor['summary'] = $profile[0]['summary'] ; + } + if (strlen($profile[0]['url']) > 0) { + $actor['url'] = $profile[0]['url'] ; + } + if (strlen($profile[0]['name']) > 0) { + $actor['name'] = $profile[0]['name'] ; + } + if (strlen($profile[0]['icon']) > 0) { + $icon = [ + "type" => "Image", + "mediaType" => "image/png", + "url" => $profile[0]['icon'] + ] ; + $actor['icon'] = $icon ; + } + if (strlen($profile[0]['homepage']) > 0) { + $homepage = [ + "type" => "PropertyValue", + "name" => "Homepage", + "value" => "" . preg_replace('#http(s)?://#', '', $profile[0]['homepage']) . "" + ] ; + + $actor['attachment'] = [ $homepage ] ; + } + } + + + response(200, $actor); } else { - response(400, [ 'error' => 'User missing from Actor request' ]); + response(400, [ 'error' => 'User missing from Actor request' ]); } From fa18edd4fd32b7fede955b49068485b7a731b7da Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:35:46 +0100 Subject: [PATCH 04/19] Handle Apache Rewrite rule Return the correct document path when webfinger is handled via an Apache re-write rule --- admin/functions.php | 219 +++++++++++++++++++++++--------------------- 1 file changed, 115 insertions(+), 104 deletions(-) diff --git a/admin/functions.php b/admin/functions.php index c568d85..193c0aa 100644 --- a/admin/functions.php +++ b/admin/functions.php @@ -10,12 +10,23 @@ // SERVER_NAME is only defined when invoked through CGI. // You may set a value here that be used for CLI calls. if (empty($_SERVER['SERVER_NAME'])) { - $_SERVER['SERVER_NAME'] = 'example.com'; + $_SERVER['SERVER_NAME'] = 'example.com'; } // Sets the "root" of phpActivityPub - that is, the hostname plus path to the // PHP scripts (but not the scripts themselves) -$phpActivityPub_root = 'https://' . $_SERVER['SERVER_NAME'] . preg_replace('#[^/]+\.php$#', '', $_SERVER['SCRIPT_NAME']); + +// NW; check to handle webfinger being redirected via Apache +// for example, we have a rewrite rule like this: +// +// RewriteRule ^/.well-known/webfinger /activitypub/webfinger.php [L,QSA] +// +if (preg_match('#/webfinger.php$#', $_SERVER['SCRIPT_FILENAME'])) { + $filepath = preg_replace('#[^/]+\.php$#', '', $_SERVER['SCRIPT_FILENAME']); + $phpActivityPub_root = 'https://' . $_SERVER['SERVER_NAME'] . preg_replace('#' . $_SERVER['DOCUMENT_ROOT'] .'#', '', $filepath); +} else { + $phpActivityPub_root = 'https://' . $_SERVER['SERVER_NAME'] . preg_replace('#[^/]+\.php$#', '', $_SERVER['SCRIPT_NAME']); +} // //////////////// // TEMPLATE @@ -23,12 +34,12 @@ // Send a JSON response and a status code, then exit function response($code = 204, $object = null) { - http_response_code($code); - if ($object) { - header('Content-Type: application/json'); - echo json_encode($object, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - } - exit; + http_response_code($code); + if ($object) { + header('Content-Type: application/json'); + echo json_encode($object, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + exit; } // //////////////// @@ -38,33 +49,33 @@ function response($code = 204, $object = null) function query($db, $sql, ...$params) { // prepare query and bind parameters - $stmt = $db->prepare($sql); + $stmt = $db->prepare($sql); // check that the right number of params were passed in - if ($stmt->paramCount() != count($params)) { - throw new Exception("Error in query '$sql': expected " . $stmt->paramCount() . " but got " . count($params)); - } + if ($stmt->paramCount() != count($params)) { + throw new Exception("Error in query '$sql': expected " . $stmt->paramCount() . " but got " . count($params)); + } - for ($i = 0; $i < count($params); $i++) { - $stmt->bindParam($i + 1, $params[$i], gettype($params[$i]) == 'integer' ? SQLITE3_INTEGER : SQLITE3_TEXT); - } + for ($i = 0; $i < count($params); $i++) { + $stmt->bindParam($i + 1, $params[$i], gettype($params[$i]) == 'integer' ? SQLITE3_INTEGER : SQLITE3_TEXT); + } - $result = $stmt->execute(); + $result = $stmt->execute(); - $response = array(); + $response = array(); // a FALSE value here might indicate a db error but we should not explode // e.g. in case of duplicate key or something non-fatal - if ($result) { - // retrieve all responses - while ($info = $result->fetchArray(SQLITE3_ASSOC)) { - array_push($response, $info); - } - $result->finalize(); + if ($result) { + // retrieve all responses + while ($info = $result->fetchArray(SQLITE3_ASSOC)) { + array_push($response, $info); } + $result->finalize(); + } // close everything and return - $stmt->close(); + $stmt->close(); - return $response; + return $response; } // //////////////// @@ -78,96 +89,96 @@ function request($user, $url, $body = null) { // fetch the private key: we need these to encode the message headers - $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); - $result = query($db, 'SELECT rsa_private FROM acct WHERE user=?', $user); - if (count($result) == 1) { - $keypair = $result[0]['rsa_private']; - } else { - throw new Exception("Failed to get rsa_private for user $user"); - } - $db->close(); + $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); + $result = query($db, 'SELECT rsa_private FROM acct WHERE user=?', $user); + if (count($result) == 1) { + $keypair = $result[0]['rsa_private']; + } else { + throw new Exception("Failed to get rsa_private for user $user"); + } + $db->close(); // Break the URL into components for putting into headers - $components = parse_url($url); + $components = parse_url($url); // Build headers and signature string // get today's date as well - $date = gmdate('D, d M Y H:i:s T'); + $date = gmdate('D, d M Y H:i:s T'); // concatenate it all together - $signed_string = "(request-target): " . ($body ? 'post' : 'get') . ' ' . $components['path'] . - "\nhost: " . $components['host'] . - "\ndate: " . $date; - if ($body) { - // create digest of the document - $digest = 'SHA-256=' . base64_encode(hash('sha256', $body, true)); - $signed_string .= "\ndigest: " . $digest; - } + $signed_string = "(request-target): " . ($body ? 'post' : 'get') . ' ' . $components['path'] . + "\nhost: " . $components['host'] . + "\ndate: " . $date; + if ($body) { + // create digest of the document + $digest = 'SHA-256=' . base64_encode(hash('sha256', $body, true)); + $signed_string .= "\ndigest: " . $digest; + } // SIGN THE HEADERS - openssl_sign($signed_string, $signature, $keypair, OPENSSL_ALGO_SHA256); + openssl_sign($signed_string, $signature, $keypair, OPENSSL_ALGO_SHA256); // Create the final header with signature - $header = 'keyId="' . $GLOBALS['phpActivityPub_root'] . 'actor.php?user=' . $user . '#main-key",headers="(request-target) host date' . ($body ? ' digest' : '') . '",signature="' . base64_encode($signature) . '"'; + $header = 'keyId="' . $GLOBALS['phpActivityPub_root'] . 'actor.php?user=' . $user . '#main-key",headers="(request-target) host date' . ($body ? ' digest' : '') . '",signature="' . base64_encode($signature) . '"'; // READY TO POST! //open connection - $ch = curl_init($url); - - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_MAXREDIRS, 50); - - #'Host: ' . $components['host'], - $headers = array( - 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'Date: ' . $date, - 'Signature: ' . $header + $ch = curl_init($url); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_MAXREDIRS, 50); + + #'Host: ' . $components['host'], + $headers = array( + 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'Date: ' . $date, + 'Signature: ' . $header + ); + + if ($body) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + array_push( + $headers, + 'Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'Digest: ' . $digest ); + }; - if ($body) { - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - array_push( - $headers, - 'Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'Digest: ' . $digest - ); - }; - - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - - error_log("REQUEST: $url, headers:" . print_r($headers, true)); - if ($body) { - error_log("body: $body"); - } - - //execute the request! - $result = curl_exec($ch); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - if ($result === false) { - $e = curl_errno($ch); - $err_str = "cURL error: ($e: " . curl_strerror($e) . "): " . curl_error($ch); - curl_close($ch); + error_log("REQUEST: $url, headers:" . print_r($headers, true)); + if ($body) { + error_log("body: $body"); + } - throw new Exception($err_str); - } + //execute the request! + $result = curl_exec($ch); - $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + if ($result === false) { + $e = curl_errno($ch); + $err_str = "cURL error: ($e: " . curl_strerror($e) . "): " . curl_error($ch); curl_close($ch); - if ($responseCode < 200 || $responseCode >= 300) { - throw new Exception("cURL error: Server responded $responseCode: $result"); - } - if ($result) { - error_log("Received result: $result"); - $json_response = json_decode($result, true); - if (! $json_response) { - throw new Exception("json_decode error: Failed to decode '$result'"); - } - return $json_response; + + throw new Exception($err_str); + } + + $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + curl_close($ch); + if ($responseCode < 200 || $responseCode >= 300) { + throw new Exception("cURL error: Server responded $responseCode: $result"); + } + if ($result) { + error_log("Received result: $result"); + $json_response = json_decode($result, true); + if (! $json_response) { + throw new Exception("json_decode error: Failed to decode '$result'"); } - return array(); + return $json_response; + } + return array(); } // Given an Actor URL, get some info we need to post to them @@ -177,13 +188,13 @@ function request($user, $url, $body = null) function get_inbox($user, $url) { // use the instance actor for this req - $json_content = request($user, $url); + $json_content = request($user, $url); - if ($json_content['inbox']) { - return $json_content['inbox']; - } + if ($json_content['inbox']) { + return $json_content['inbox']; + } - throw new Exception("Failed to look up actor for " . $url . ": " . $json_content); + throw new Exception("Failed to look up actor for " . $url . ": " . $json_content); } // Send an Activity to a remote server. @@ -192,12 +203,12 @@ function sendActivity($user, $dest, $activity) { // right FIRST of all, we need the destination inbox - $dest = get_inbox($user, $dest); + $dest = get_inbox($user, $dest); - $document = json_encode(array_merge([ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'actor' => $GLOBALS['phpActivityPub_root'] . 'actor.php?user=' . $user - ], $activity), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $document = json_encode(array_merge([ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'actor' => $GLOBALS['phpActivityPub_root'] . 'actor.php?user=' . $user + ], $activity), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - return request($user, $dest, $document); + return request($user, $dest, $document); } From 729cfcb4cb19a6f9048c389d8165e63f5abb6fa5 Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:36:22 +0100 Subject: [PATCH 05/19] Add profile link to index page --- admin/index.php | 87 ++++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/admin/index.php b/admin/index.php index 795868d..fbf1733 100644 --- a/admin/index.php +++ b/admin/index.php @@ -1,17 +1,21 @@ - - phpAP Admin - + phpAP Admin + close(); ?> - - -

phpAP Admin

-
-

Instance Actor

- Public key: -
-
-

User List

- - - + + +

phpAP Admin

+
+

Instance Actor

+ Public key: +
+
+

User List

+
UserTypePost Key
+ + + + + + \n"; + // NW - added link to profile script + echo '\n"; } ?> -
UserTypePost Key
', $a['user'], '', $a['type'], '', $a['key'], "
', $a['user'], '', $a['type'], '', $a['key'], "Profile
-
-

Add User

-
- - - -
- - + +
+

Add User

+
+ + + +
+ + + \ No newline at end of file From b92a185f2b95670a8f049f8f78a474dd823f3278 Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:52:09 +0100 Subject: [PATCH 06/19] Create POSTING.md Add example JSON --- POSTING.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 POSTING.md diff --git a/POSTING.md b/POSTING.md new file mode 100644 index 0000000..27a4874 --- /dev/null +++ b/POSTING.md @@ -0,0 +1,17 @@ +# Mastodon post example + +This is an example to show how a post can be formatted so that it displays with a summary text, an image, and a link in the text body. +Our use case is posting details of forthcoming events from our calendar. The formatting is aimed at Mastodon, but should work find on +other fedi platforms. On Mastodon, the summary (which includes event name and date) will appear as a content warning, with the rest +of the text collapsed; this avoids cluttering up timelines with long event descriptions if people don't want to read them. + + { + "type": "Note", + "summary": "Tom of Finland Art & Culture Festival and HUNTER - Launch Party, 21 July 2023", + "attachment": { + "summary": "HUNTER, Electrowerkz, Islington", + "url": "https://bluf.com/photos/events/4662/f5255291-056f-45a9-840d-6723665b79a8_1_201_a.jpg" + }, + "content": "

HUNTER Invites you to the Opening Party of the Tom Of Finland Arts & Culture Festival and launch of HUNTER, your new Hardcore Leather F*tish night.

\n

@electrowerkz_ teams up with infamous @brewhunter.dom (who reignited London’s Leather scene with his now-fabled night MASTERY) to present an explosive night on the London scene. HUNTER is also proud to be the Opening Party for Tom Of Finland Art & Culture London Festival 2023.

\n

The Launch will feature a welcome hour zone, but as darkness falls, the CIGAR ZONE, MASTERY ZONE and DARK ZONE (hardcore dress code only) will open for The Hunt. Vintage Cinema, cigars, crusing, cttaging, dungeon and hardcore play.

\n

9p-10p FREE ENTY RELAXED DRESS CODE FOR WELCOME HOUR ONLY (STRICT DRESS CODE REQUIRED ALL OTHER ZONES, ACCESS AT 10p)
\n10p-3a £10 LAUNCH PARTY TICKETS HUNTER HARDCORE

\n

More info: SMOKINLEATHER (2316)



Electrowerkz, 7 Torrens St, London, United Kingdom
View on bluf.com" + } + From f083a9458c2a082998ff72285a1b9b6a62b1e601 Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:54:46 +0100 Subject: [PATCH 07/19] Update POSTING.md --- POSTING.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/POSTING.md b/POSTING.md index 27a4874..80655d1 100644 --- a/POSTING.md +++ b/POSTING.md @@ -5,13 +5,17 @@ Our use case is posting details of forthcoming events from our calendar. The for other fedi platforms. On Mastodon, the summary (which includes event name and date) will appear as a content warning, with the rest of the text collapsed; this avoids cluttering up timelines with long event descriptions if people don't want to read them. - { - "type": "Note", - "summary": "Tom of Finland Art & Culture Festival and HUNTER - Launch Party, 21 July 2023", - "attachment": { - "summary": "HUNTER, Electrowerkz, Islington", - "url": "https://bluf.com/photos/events/4662/f5255291-056f-45a9-840d-6723665b79a8_1_201_a.jpg" - }, - "content": "

HUNTER Invites you to the Opening Party of the Tom Of Finland Arts & Culture Festival and launch of HUNTER, your new Hardcore Leather F*tish night.

\n

@electrowerkz_ teams up with infamous @brewhunter.dom (who reignited London’s Leather scene with his now-fabled night MASTERY) to present an explosive night on the London scene. HUNTER is also proud to be the Opening Party for Tom Of Finland Art & Culture London Festival 2023.

\n

The Launch will feature a welcome hour zone, but as darkness falls, the CIGAR ZONE, MASTERY ZONE and DARK ZONE (hardcore dress code only) will open for The Hunt. Vintage Cinema, cigars, crusing, cttaging, dungeon and hardcore play.

\n

9p-10p FREE ENTY RELAXED DRESS CODE FOR WELCOME HOUR ONLY (STRICT DRESS CODE REQUIRED ALL OTHER ZONES, ACCESS AT 10p)
\n10p-3a £10 LAUNCH PARTY TICKETS HUNTER HARDCORE

\n

More info: SMOKINLEATHER (2316)



Electrowerkz, 7 Torrens St, London, United Kingdom
View on bluf.com" - } + { + "type": "Note", + "summary": "Tom of Finland Art & Culture Festival and HUNTER - Launch Party, 21 July 2023", + "attachment": { + "summary": "HUNTER, Electrowerkz, Islington", + "url": "https://bluf.com/photos/events/4662/f5255291-056f-45a9-840d-6723665b79a8_1_201_a.jpg" + }, + "content": "

HUNTER Invites you to the Opening Party of the Tom Of Finland Arts & Culture Festival and launch of HUNTER, your new Hardcore Leather F*tish night.

\n

@electrowerkz_ teams up with infamous @brewhunter.dom (who reignited London’s Leather scene with his now-fabled night MASTERY) to present an explosive night on the London scene. HUNTER is also proud to be the Opening Party for Tom Of Finland Art & Culture London Festival 2023.

\n

The Launch will feature a welcome hour zone, but as darkness falls, the CIGAR ZONE, MASTERY ZONE and DARK ZONE (hardcore dress code only) will open for The Hunt. Vintage Cinema, cigars, crusing, cttaging, dungeon and hardcore play.

\n

9p-10p FREE ENTY RELAXED DRESS CODE FOR WELCOME HOUR ONLY (STRICT DRESS CODE REQUIRED ALL OTHER ZONES, ACCESS AT 10p)
\n10p-3a £10 LAUNCH PARTY TICKETS HUNTER HARDCORE

\n

More info: SMOKINLEATHER (2316)



Electrowerkz, 7 Torrens St, London, United Kingdom
View on bluf.com" + } +You can post an item like this - assuming it's saved in a file called event.json with curl from the command line: + + curl -X POST -H 'X-API-KEY: {actor POST key}' https://example.com/{install path}/post.php -d @event.json + From bbeac62c08fe7b8a2e2e2c61f30a07c7667d9df0 Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Fri, 7 Jul 2023 12:23:28 +0100 Subject: [PATCH 08/19] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 75d0b2a..93bc647 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,7 @@ etc. This fork provides an additional profile page within admin, allowing youto populate some of the information that will be displayed when people search for your actor. It additionally addresses a couple of minor issues relevant to my own installation, viz database creation, and an Apache re-write. + +The latest commit creates a user page, which displays a barebones profile, inlcuding list of followers. You can use the profile admin +page to set the URL of this for each user you create. This solves the problem where, for example, a follower on a remote Mastodon instance can't see who's following your bot, because that's not stored locally. Mastodon displays the 'url' of the actor as link to +the original profile. From 95c04a6f4ef54dcdf9c7ffbe6c168a91a55f5561 Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Fri, 7 Jul 2023 12:24:53 +0100 Subject: [PATCH 09/19] Add user.php This file provides a basic display of profile information and followers, for those who click the profile link on a remote instance --- user.php | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 user.php diff --git a/user.php b/user.php new file mode 100644 index 0000000..71773bd --- /dev/null +++ b/user.php @@ -0,0 +1,80 @@ + 0) { + // Found an account by this name. + + $profile = query($db, 'SELECT summary, url, name, icon, homepage FROM profile WHERE user=?', $_GET['user']) ; + + $followers = query($db, 'SELECT dest FROM sub WHERE user=?', $_GET['user']) ; + $db->close(); +} else { + $db->close(); ?> + + + + No user found + + + +

No such user

+ + + + + + + + ActivityPub Profile for @<?= $_GET['user']; ?> + + + +

Profile information for @

+ 0) { + ?> +

profile icon

+

+

+

More info

+

Followers:

+ + +

Sorry, can't find profile information for this user

+ + + + \ No newline at end of file From 27ff2c3ee27f95a184b9ef5beea78a0b5617f2ff Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Fri, 7 Jul 2023 20:08:59 +0100 Subject: [PATCH 10/19] Update POSTING.md Add code snippet to show how we use the bot on BLUF.com --- POSTING.md | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/POSTING.md b/POSTING.md index 80655d1..3007a16 100644 --- a/POSTING.md +++ b/POSTING.md @@ -18,4 +18,43 @@ of the text collapsed; this avoids cluttering up timelines with long event descr You can post an item like this - assuming it's saved in a file called event.json with curl from the command line: curl -X POST -H 'X-API-KEY: {actor POST key}' https://example.com/{install path}/post.php -d @event.json - + +## Automated posting +We use this on BLUF.com, where our site has an event object, to which we have added method asActivityPub, which returns JSON formatted as +above for an event with an image, or a simple note, for events without an image. Automating posts via the phpActivityPub is simple. You can see our bot in action as @events@bluf.com from your favourite Fediverse platform. + +Here's the code-snippets: + + define('FEDI_URL','https://example.com//path/to/activityPub/post.php') ; + define('APIKEY','POST_KEY for your actor') ; + + while ($event = $events->fetch_assoc()) { + $e = new \BLUF\Calendar\event($event['id']) ; + fedi_post($e->asActivityPub()) ; + + sleep(rand(5, 20)) ; // avoid flooding + } + + function fedi_post($note) + { + $curl = curl_init() ; + + curl_setopt_array($curl, [ + CURLOPT_URL => FEDI_URL, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_POSTFIELDS => $note, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_HTTPHEADER => [ + "Content-Type: application/json;charset=utf-8", + "X-API-KEY: " . APIKEY + ], + ]); + + $response = curl_exec($curl); + $err = curl_error($curl); + + curl_close($curl); + } From 0107ab34881b68d1c76a9f26f2c45682e4f122bf Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Fri, 7 Jul 2023 20:24:35 +0100 Subject: [PATCH 11/19] Create Apache.md Information about ReWrite rules for Apache --- Apache.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Apache.md diff --git a/Apache.md b/Apache.md new file mode 100644 index 0000000..21d0acd --- /dev/null +++ b/Apache.md @@ -0,0 +1,17 @@ +# Using this bot with Apache webserver + +To aid anyone else who wants to use this simple bot system on a site using Apache, here are the ReWrite rules +that we use. + +The files from this code are installed in DOCUMENT_ROOT/activitypub. We want to provide a users link that doesn't +necessarily reveal the install path, and we also need to make sure webfinger can be found under .well-known + +(Using these rules is why we also have a change in functions.php from the original, to detect the correct path +when webfinger is called). + +Assuming you have RewriteEngine On already in your site config, add these two rules + + # For ActivityPub + RewriteRule ^/.well-known/webfinger /activitypub/webfinger.php [L,QSA] + RewriteRule ^/users/(.*) /activitypub/user.php?user=$1 [L] + From adedb6b599287db458c6343f16697274efdbab11 Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Sat, 8 Jul 2023 12:51:17 +0100 Subject: [PATCH 12/19] Update README.md --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 93bc647..9888405 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,20 @@ creates a Note (the default) with the text "Hello, world!" etc. # Nigel Whitfield updates, July 2023 -This fork provides an additional profile page within admin, allowing youto populate some of the information that will be displayed when people search for your actor. +This fork provides some additional functionality for bots. It additionally addresses a couple of minor issues relevant to my own installation, viz database creation, and an Apache re-write. -It additionally addresses a couple of minor issues relevant to my own installation, viz database creation, and an Apache re-write. +## Additonal features: -The latest commit creates a user page, which displays a barebones profile, inlcuding list of followers. You can use the profile admin +### Admin profile page +In [admin/profile.php](admin/profile.php) you can populate some of the information that will be displayed when people search for your actor. + +### User profile page +Created by [user.php](user.php), which displays a barebones profile, inlcuding list of followers. You can use the profile admin page to set the URL of this for each user you create. This solves the problem where, for example, a follower on a remote Mastodon instance can't see who's following your bot, because that's not stored locally. Mastodon displays the 'url' of the actor as link to the original profile. + +### Inbox detection of DMs +The latest update to [inbox.php](inbox.php) detects when a DM is sent to one of your actors, and then hands the content off to +a parse_content function in the include file [dm_parser.php](dm_parser.php). This is intended to keep some site specific code +separate from the rest of the functionality. The example included allows followers to request a summary of events in a particular +city by sending a DM to our bot with the name of the city. From 905d63ff1264407f2e619d4b6b58a0ad7f350c90 Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Sat, 8 Jul 2023 12:52:44 +0100 Subject: [PATCH 13/19] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9888405..c41674a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ etc. # Nigel Whitfield updates, July 2023 This fork provides some additional functionality for bots. It additionally addresses a couple of minor issues relevant to my own installation, viz database creation, and an Apache re-write. -## Additonal features: +Additonal features: ### Admin profile page In [admin/profile.php](admin/profile.php) you can populate some of the information that will be displayed when people search for your actor. From e12a72479f15671fe563a8074f49cac79f2958eb Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Sat, 8 Jul 2023 12:55:21 +0100 Subject: [PATCH 14/19] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c41674a..854fd31 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,14 @@ creates a Note (the default) with the text "Hello, world!" etc. +See [POSTING.md](POSTING.md) for a more detailed example. + # Nigel Whitfield updates, July 2023 -This fork provides some additional functionality for bots. It additionally addresses a couple of minor issues relevant to my own installation, viz database creation, and an Apache re-write. +This fork provides some additional functionality for bots. It additionally addresses a couple of minor issues relevant to my own installation, viz database creation, and an Apache re-write. For details of rewrite rules, have a look at [Apache.md](Apache.md) Additonal features: -### Admin profile page +### Admin profile page In [admin/profile.php](admin/profile.php) you can populate some of the information that will be displayed when people search for your actor. ### User profile page From fe9a4a19d28ee153ec27cbd1f93007b03ce5d49f Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Sat, 8 Jul 2023 12:56:14 +0100 Subject: [PATCH 15/19] Add files via upload Detect incoming DMs and hand off to a function in the dm_parser module --- inbox.php | 58 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/inbox.php b/inbox.php index 21a6519..0968ac5 100644 --- a/inbox.php +++ b/inbox.php @@ -26,6 +26,8 @@ include_once 'admin/functions.php'; +date_default_timezone_set('UTC') ; + // Attempt to json_decode the body, in whatever form it arrives. // (multipart/form-data is not supported and just returns an empty array) $sContentType = $_SERVER["CONTENT_TYPE"] ?? 'text/plain'; @@ -34,13 +36,14 @@ // Required parameters: user if (! empty($_GET['user'])) { if (strtolower($_GET['user']) === $_SERVER['SERVER_NAME']) { - // the instance actor does not actually have a working inbox + // the instance actor does not actually have a working inbox response(405, [ 'error' => 'Instance actor rejects all inbox posts' ]); } else { - // TODO: Verify signature + // TODO: Verify signature - // verify the Object matches our Actor URL... if not, this request was sent to the wrong inbox! - //if ($content['object'] === $phpActivityPub_root . 'actor.php?user=' . $_GET['user']) { + // verify the Object matches our Actor URL... if not, this request was sent to the wrong inbox! + //if ($content['object'] === $phpActivityPub_root . 'actor.php?user=' . $_GET['user']) { + // debug // switch based on received activity type if ($content['type'] === 'Follow') { @@ -48,7 +51,7 @@ query($db, 'INSERT OR IGNORE INTO sub(user, dest) VALUES(?, ?)', $_GET['user'], $content['actor']); $db->close(); - // create and send Accept reply + // create and send Accept reply sendActivity($_GET['user'], $content['actor'], [ 'type' => 'Accept', 'object' => $content['id'] @@ -60,13 +63,54 @@ query($db, 'DELETE FROM sub WHERE user=? AND dest=?', $_GET['user'], $content['object']['actor']); $db->close(); - // Undo is unilateral and does not expect an Accept response + // Undo is unilateral and does not expect an Accept response response(204); + } elseif (($content['type'] == 'Create') && ! in_array('https://www.w3.org/ns/activitystreams#Public', $content['to'])) { + // Nigel Whitfield, July 2023 + // this is a direct message + // we put all the code in our dm_parser library + // it will return either the Note we're sending back, or false + + require_once 'dm_parser.php' ; + + $response = parse_content($_GET['user'], $content) ; + + if ($response === false) { + // send back a basic instruction or error message + $response = [ 'type' => 'Note', + 'to' => $content['actor'], + 'published' => strftime('%FT%TZ', time()), + 'content' => "We didn't understand that, or couldn't find any results. Try sending the word HELP", + //'inReplyTo' => $content['id'] + ] ; + } + + $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READWRITE); + + query( + $db, + 'INSERT INTO post (user, content) VALUES (?, ?)', + $_GET['user'], + json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); + $id = $db->lastInsertRowId(); + $db->close(); + + $response['id'] = $phpActivityPub_root . 'activity.php?create=1&id=' . $id ; + + sendActivity($_GET['user'], $content['actor'], [ + //"id" => $response['id'], + 'type' => 'Create', + 'to' => $response['to'], + 'object' => $response + ]); + + response(200) ; } else { response(405, [ 'error' => 'Unsupported request type ' . $content['type'] ]); } - //} else { + //} else { //http_response_code(400); //echo 'Wrong inbox'; //} From 8e99e554a98eaf16e64e7a57b4d07ad4c69a8e9f Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Sat, 8 Jul 2023 12:58:05 +0100 Subject: [PATCH 16/19] Add files via upload dm_parser is an example of how we can extract a command from an incoming DM, and use it to build a response to send back to the original actor --- dm_parser.php | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 dm_parser.php diff --git a/dm_parser.php b/dm_parser.php new file mode 100644 index 0000000..37994ca --- /dev/null +++ b/dm_parser.php @@ -0,0 +1,67 @@ + 'Note', + 'to' => $activityPub['actor'], + 'published' => strftime('%FT%TZ', time()), + 'content' => "Send the name of a city to see what events we have listed in the next 30 days, eg Berlin", + ]); + } else { + // look up info in the database and build a response + require_once('common/v4database.php') ; + + $sql = sprintf("SELECT title, startdate FROM events WHERE private = 'n' AND ( city LIKE '%%%s%%' OR localisedcity LIKE '%%%s%%') AND startdate BETWEEN CURRENT_DATE() AND DATE_ADD(CURRENT_DATE(), INTERVAL +30 DAY) ORDER BY startdate ASC", $v4read->real_escape_string($request), $v4read->real_escape_string($request)) ; + $events = $v4read->query($sql) ; + + if ($events->num_rows == 0) { + return([ 'type' => 'Note', + 'to' => $activityPub['actor'], + 'published' => strftime('%FT%TZ', time()), + 'content' => "Sorry, we have can't find any events matching your request. Try sending the name of a city" + ]); + } else { + $content = sprintf("Here's your list of what's coming up in the next 30 days in %s

", $request) ; + + while ($e = $events->fetch_assoc()) { + $when = strftime('%d %B %Y', strtotime($e['startdate'])) ; + $content .= "

" . $e['title'] . " on " . $when ; + } + + $content .= sprintf("

See more details at bluf.com/e/%s", strtolower($request), strtolower($request)) ; + + return([ 'type' => 'Note', + 'to' => $activityPub['actor'], + 'published' => strftime('%FT%TZ', time()), + 'content' => $content + ]); + } + } + + break ; + + default: + // not one of our handled bots + return false ; + } +} From 470fb0419953e9a55eb6ce175eb1ac1ce2cec8c6 Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Sat, 8 Jul 2023 13:33:40 +0100 Subject: [PATCH 17/19] Add files via upload Minor tidying up --- dm_parser.php | 24 +++++++++++++++++++++--- inbox.php | 3 +-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/dm_parser.php b/dm_parser.php index 37994ca..c9d818b 100644 --- a/dm_parser.php +++ b/dm_parser.php @@ -21,11 +21,19 @@ function parse_content($user, $activityPub) $request = trim(preg_replace('#@events#', '', strip_tags($activityPub['object']['content']))) ; + preg_match('#://([^/]*)/#', $activityPub['actor'], $matches) ; + $actorName = '@' . basename($activityPub['actor']) . '@' . $matches[1] ; // turn into @user@host.format + if (strtolower($request) == 'help') { return([ 'type' => 'Note', 'to' => $activityPub['actor'], 'published' => strftime('%FT%TZ', time()), 'content' => "Send the name of a city to see what events we have listed in the next 30 days, eg Berlin", + 'tag' => [[ + 'type' => 'Mention', + 'href' => $activityPub['actor'], + 'name' => $actorName + ]], ]); } else { // look up info in the database and build a response @@ -38,13 +46,18 @@ function parse_content($user, $activityPub) return([ 'type' => 'Note', 'to' => $activityPub['actor'], 'published' => strftime('%FT%TZ', time()), - 'content' => "Sorry, we have can't find any events matching your request. Try sending the name of a city" + 'content' => "Sorry, we have can't find any events matching your request. Try sending the name of a city", + 'tag' => [[ + 'type' => 'Mention', + 'href' => $activityPub['actor'], + 'name' => $actorName + ]], ]); } else { $content = sprintf("Here's your list of what's coming up in the next 30 days in %s

", $request) ; while ($e = $events->fetch_assoc()) { - $when = strftime('%d %B %Y', strtotime($e['startdate'])) ; + $when = strftime('%A %d %B %Y', strtotime($e['startdate'])) ; $content .= "

" . $e['title'] . " on " . $when ; } @@ -53,7 +66,12 @@ function parse_content($user, $activityPub) return([ 'type' => 'Note', 'to' => $activityPub['actor'], 'published' => strftime('%FT%TZ', time()), - 'content' => $content + 'content' => $content, + 'tag' => [[ + 'type' => 'Mention', + 'href' => $activityPub['actor'], + 'name' => $actorName + ]], ]); } } diff --git a/inbox.php b/inbox.php index 0968ac5..af6af8e 100644 --- a/inbox.php +++ b/inbox.php @@ -82,7 +82,6 @@ 'to' => $content['actor'], 'published' => strftime('%FT%TZ', time()), 'content' => "We didn't understand that, or couldn't find any results. Try sending the word HELP", - //'inReplyTo' => $content['id'] ] ; } @@ -103,7 +102,7 @@ //"id" => $response['id'], 'type' => 'Create', 'to' => $response['to'], - 'object' => $response + 'object' => $response, ]); response(200) ; From 2be57159bda826414bc194ab8dd8621a23adef3a Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Sun, 9 Jul 2023 14:41:45 +0100 Subject: [PATCH 18/19] Add files via upload Small tweak to the links returned in response to a search --- dm_parser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dm_parser.php b/dm_parser.php index c9d818b..42a5f06 100644 --- a/dm_parser.php +++ b/dm_parser.php @@ -61,7 +61,7 @@ function parse_content($user, $activityPub) $content .= "

" . $e['title'] . " on " . $when ; } - $content .= sprintf("

See more details at bluf.com/e/%s", strtolower($request), strtolower($request)) ; + $content .= sprintf("

See more details at bluf.com/e/%s", urlencode(strtolower($request)), urlencode(strtolower($request))) ; return([ 'type' => 'Note', 'to' => $activityPub['actor'], From 3219d456de62b1afba51d19f9189122d4d421498 Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Sun, 9 Jul 2023 14:42:40 +0100 Subject: [PATCH 19/19] Add files via upload Ensure 'to' field is an array in the Create activity used to send DM responses --- inbox.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inbox.php b/inbox.php index af6af8e..822c1c5 100644 --- a/inbox.php +++ b/inbox.php @@ -79,7 +79,7 @@ if ($response === false) { // send back a basic instruction or error message $response = [ 'type' => 'Note', - 'to' => $content['actor'], + 'to' => [ $content['actor'] ], 'published' => strftime('%FT%TZ', time()), 'content' => "We didn't understand that, or couldn't find any results. Try sending the word HELP", ] ; @@ -101,7 +101,7 @@ sendActivity($_GET['user'], $content['actor'], [ //"id" => $response['id'], 'type' => 'Create', - 'to' => $response['to'], + 'to' => [ $response['to']] , 'object' => $response, ]);