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] + diff --git a/POSTING.md b/POSTING.md new file mode 100644 index 0000000..3007a16 --- /dev/null +++ b/POSTING.md @@ -0,0 +1,60 @@ +# 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" + } + +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); + } diff --git a/README.md b/README.md index f34186f..854fd31 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,24 @@ 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. For details of rewrite rules, have a look at [Apache.md](Apache.md) + +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. + +### 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. 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' ]); } 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); } 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 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 diff --git a/dm_parser.php b/dm_parser.php new file mode 100644 index 0000000..42a5f06 --- /dev/null +++ b/dm_parser.php @@ -0,0 +1,85 @@ + '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 + 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", + '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('%A %d %B %Y', strtotime($e['startdate'])) ; + $content .= "

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

See more details at bluf.com/e/%s", urlencode(strtolower($request)), urlencode(strtolower($request))) ; + + return([ 'type' => 'Note', + 'to' => $activityPub['actor'], + 'published' => strftime('%FT%TZ', time()), + 'content' => $content, + 'tag' => [[ + 'type' => 'Mention', + 'href' => $activityPub['actor'], + 'name' => $actorName + ]], + ]); + } + } + + break ; + + default: + // not one of our handled bots + return false ; + } +} diff --git a/inbox.php b/inbox.php index 21a6519..822c1c5 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,53 @@ 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", + ] ; + } + + $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'; //} 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