Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Apache.md
Original file line number Diff line number Diff line change
@@ -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]

60 changes: 60 additions & 0 deletions POSTING.md
Original file line number Diff line number Diff line change
@@ -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": "<p>HUNTER Invites you to the Opening Party of the Tom Of Finland Arts &amp; Culture Festival and launch of HUNTER, your new Hardcore Leather F*tish night. </p>\n<p>@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 &amp; Culture London Festival 2023. </p>\n<p>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, cru<em>sing, c</em>ttaging, dungeon and hardcore play.</p>\n<p>9p-10p <strong>FREE ENTY</strong> RELAXED DRESS CODE FOR WELCOME HOUR ONLY (STRICT DRESS CODE REQUIRED ALL OTHER ZONES, ACCESS AT 10p)<br />\n10p-3a <strong>£10 LAUNCH PARTY TICKETS</strong> HUNTER HARDCORE </p>\n<p>More info: <a href=\"/profiles/2316\" title=\"SMOKINLEATHER\">SMOKINLEATHER (2316)</a></p><br><br>Electrowerkz, 7 Torrens St, London, United Kingdom<br><a href='https://bluf.com/e/4662'>View on bluf.com</a>"
}

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);
}
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
137 changes: 87 additions & 50 deletions actor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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" => "<a href='" . $profile[0]['homepage'] . "' rel='me nofollow noopened noreferred' target='_blank'>" . preg_replace('#http(s)?://#', '', $profile[0]['homepage']) . "</a>"
] ;

$actor['attachment'] = [ $homepage ] ;
}
}


response(200, $actor);
} else {
response(400, [ 'error' => 'User missing from Actor request' ]);
response(400, [ 'error' => 'User missing from Actor request' ]);
}
Loading