diff --git a/.gitignore b/.gitignore
index d222587..4d8a7bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ php-docs
.phpdoc
phpdoc
html
+/cache
diff --git a/config.ini b/config.ini
index dcf9ba1..7411f48 100644
--- a/config.ini
+++ b/config.ini
@@ -14,6 +14,8 @@ compiledir = '../cache'
[plugins]
rediscache = 'rediscache.php'
dummy = 'dummyconnector.php'
+contentnation = 'contentnation.php'
+mastodon = 'mastodon.php'
[maintenance]
username = 'federatoradmin'
diff --git a/contentnation.ini b/contentnation.ini
new file mode 100644
index 0000000..17de2d5
--- /dev/null
+++ b/contentnation.ini
@@ -0,0 +1,6 @@
+[contentnation]
+service-uri = https://contentnation.net
+
+[userdata]
+path = '/home/net/contentnation/userdata/htdocs/' // need to download local copy of image and put img-path here
+url = 'https://userdata.contentnation.net'
\ No newline at end of file
diff --git a/htdocs/index.html b/htdocs/index.html
new file mode 100644
index 0000000..a3e7f67
--- /dev/null
+++ b/htdocs/index.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+ API Request UI
+
+
+
+
+
+
API Request UI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Response:
+
Waiting for response
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/htdocs/info.php b/htdocs/info.php
new file mode 100644
index 0000000..79e7d9d
--- /dev/null
+++ b/htdocs/info.php
@@ -0,0 +1,3 @@
+setPath((string)$_REQUEST['_call']);
+ $this->setPath((string) $_REQUEST['_call']);
$this->openDatabase();
$this->loadPlugins();
+
+ $host = 'dummy'; // fallback
+
+ // Check if path matches something like fedusers/username@domain.tld
+ if (preg_match("#^fedusers/([^@]+)@([^/]+)$#", $this->path, $matches) === 1) {
+ $host = strtolower($matches[2]); // extract domain
+ } else {
+ $host = 'dummy';
+ echo "using dummy host in API::run\n";
+ }
+
+ $connector = $this->getConnectorForHost($host);
$retval = "";
$handler = null;
- if ($this->connector === null) {
+ if ($connector === null) {
http_response_code(500);
return;
}
@@ -85,7 +97,7 @@ class Api extends Main
$this->dbh,
$_SERVER['HTTP_X_SESSION'],
$_SERVER['HTTP_X_PROFILE'],
- $this->connector,
+ $connector,
$this->cache
);
if ($this->user === false) {
@@ -112,7 +124,7 @@ class Api extends Main
$printresponse = true;
if ($handler !== null) {
try {
- $printresponse = $handler->exec($this->paths, $this->user);
+ $printresponse = $handler->exec($this->paths, $this->user, $connector);
if ($printresponse) {
$retval = $handler->toJson();
}
@@ -168,7 +180,7 @@ class Api extends Main
* @param string $message optional message
* @throws Exceptions\PermissionDenied
*/
- public function checkPermission($permission, $exception = "\Exceptions\PermissionDenied", $message = null) : void
+ public function checkPermission($permission, $exception = "\Exceptions\PermissionDenied", $message = null): void
{
// generic check first
if ($this->user === false) {
@@ -198,7 +210,7 @@ class Api extends Main
* input to strip
* @return string stripped input
*/
- public static function stripHTML(string $_input) : string
+ public static function stripHTML(string $_input): string
{
$out = preg_replace('/<(script[^>]*)>/i', '<${1}>', $_input);
$out = preg_replace('/<\/(script)>/i', '</${1};>', $out);
@@ -212,7 +224,7 @@ class Api extends Main
* parameter to check
* @return bool true if in
*/
- public static function hasPost(string $_key) : bool
+ public static function hasPost(string $_key): bool
{
return array_key_exists($_key, $_POST);
}
@@ -228,13 +240,13 @@ class Api extends Main
*/
public function escapePost(string $key, $int = false)
{
- if (! array_key_exists($key, $_POST)) {
+ if (!array_key_exists($key, $_POST)) {
return $int ? 0 : "";
}
if ($int === true) {
return intval($_POST[$key]);
}
- $ret = $this->dbh->escape_string($this->stripHTML((string)$_POST[$key]));
+ $ret = $this->dbh->escape_string($this->stripHTML((string) $_POST[$key]));
return $ret;
}
diff --git a/php/federator/api/apiinterface.php b/php/federator/api/apiinterface.php
index b8cf7fe..a12ee74 100644
--- a/php/federator/api/apiinterface.php
+++ b/php/federator/api/apiinterface.php
@@ -19,9 +19,10 @@ interface APIInterface
* @param array $paths path array split by /
*
* @param \Federator\Data\User|false $user user who is calling us
+ * @param \Federator\Connector\Connector $connector connector to use
* @return bool true on success
*/
- public function exec($paths, $user);
+ public function exec($paths, $user, $connector);
/**
* get internal represenation as json string
diff --git a/php/federator/api/fedusers.php b/php/federator/api/fedusers.php
index 77958e7..401c5be 100644
--- a/php/federator/api/fedusers.php
+++ b/php/federator/api/fedusers.php
@@ -43,9 +43,10 @@ class FedUsers implements APIInterface
*
* @param array $paths path array split by /
* @param \Federator\Data\User|false $user user who is calling us @unused-param
+ * @param \Federator\Connector\Connector $connector connector to use
* @return bool true on success
*/
- public function exec($paths, $user)
+ public function exec($paths, $user, $connector)
{
$method = $_SERVER["REQUEST_METHOD"];
$handler = null;
@@ -53,7 +54,7 @@ class FedUsers implements APIInterface
case 2:
if ($method === 'GET') {
// /users/username or /@username
- return $this->returnUserProfile($paths[1]);
+ return $this->returnUserProfile($paths[1], $connector);
}
break;
case 3:
@@ -82,10 +83,10 @@ class FedUsers implements APIInterface
$ret = false;
switch ($method) {
case 'GET':
- $ret = $handler->get($paths[1]);
+ $ret = $handler->get($paths[1], $connector);
break;
case 'POST':
- $ret = $handler->post($paths[1]);
+ $ret = $handler->post($paths[1], $connector);
break;
}
if ($ret !== false) {
@@ -102,14 +103,15 @@ class FedUsers implements APIInterface
* return user profile
*
* @param string $_name
+ * @param \Federator\Connector\Connector $connector connector to use
* @return boolean true on success
*/
- private function returnUserProfile($_name)
+ private function returnUserProfile($_name, $connector)
{
$user = \Federator\DIO\User::getUserByName(
$this->main->getDatabase(),
$_name,
- $this->main->getConnector(),
+ $connector,
$this->main->getCache()
);
if ($user === false || $user->id === null) {
@@ -123,7 +125,7 @@ class FedUsers implements APIInterface
'fqdn' => $_SERVER['SERVER_NAME'],
'name' => $user->name,
'username' => $user->id,
- 'publickey' => str_replace("\n", "\\n", $user->publicKey),
+ 'publickey' => $user->publicKey,
'registered' => gmdate('Y-m-d\TH:i:s\Z', $user->registered), // 2021-03-25T00:00:00Z
'summary' => $user->summary,
'type' => $user->type
diff --git a/php/federator/api/fedusers/fedusersinterface.php b/php/federator/api/fedusers/fedusersinterface.php
index 1782323..934f95c 100644
--- a/php/federator/api/fedusers/fedusersinterface.php
+++ b/php/federator/api/fedusers/fedusersinterface.php
@@ -14,15 +14,17 @@ interface FedUsersInterface
* get call for user
*
* @param string $_user user to fetch data for
+ * @param \Federator\Connector\Connector $connector connector to use
* @return string|false response or false in case of error
*/
- public function get($_user);
+ public function get($_user, $connector);
/**
* post call for user
*
* @param string $_user user to add data to
+ * @param \Federator\Connector\Connector $connector connector to use
* @return string|false response or false in case of error
*/
- public function post($_user);
+ public function post($_user, $connector);
}
diff --git a/php/federator/api/fedusers/outbox.php b/php/federator/api/fedusers/outbox.php
index a18fae9..16ea175 100644
--- a/php/federator/api/fedusers/outbox.php
+++ b/php/federator/api/fedusers/outbox.php
@@ -33,13 +33,13 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
* handle get call
*
* @param string $_user user to fetch outbox for
+ * @param \Federator\Connector\Connector $connector connector to use
* @return string|false response
*/
- public function get($_user)
+ public function get($_user, $connector)
{
$dbh = $this->main->getDatabase();
$cache = $this->main->getCache();
- $connector = $this->main->getConnector();
// get user
$user = \Federator\DIO\User::getUserByName(
$dbh,
@@ -50,6 +50,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
if ($user->id === null) {
return false;
}
+
// get posts from user
$outbox = new \Federator\Data\ActivityPub\Common\Outbox();
$min = $this->main->extractFromURI("min", "");
@@ -79,16 +80,17 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
$outbox->setPrev($id . '&min=' . $oldestId);
}
$obj = $outbox->toObject();
- return json_encode($obj);
+ return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
}
/**
* handle post call
*
* @param string $_user user to add data to outbox @unused-param
+ * @param \Federator\Connector\Connector $connector connector to use
* @return string|false response
*/
- public function post($_user)
+ public function post($_user, $connector)
{
return false;
}
diff --git a/php/federator/api/v1/dummy.php b/php/federator/api/v1/dummy.php
index b70a5ac..97a9e0a 100644
--- a/php/federator/api/v1/dummy.php
+++ b/php/federator/api/v1/dummy.php
@@ -23,7 +23,7 @@ class Dummy implements \Federator\Api\APIInterface
/**
* internal message to output
*
- * @var Array $message
+ * @var array $message
*/
private $message = [];
@@ -42,9 +42,10 @@ class Dummy implements \Federator\Api\APIInterface
*
* @param array $paths path array split by /
* @param \Federator\Data\User|false $user user who is calling us
+ * @param \Federator\Connector\Connector $connector connector to use
* @return bool true on success
*/
- public function exec($paths, $user) : bool
+ public function exec($paths, $user, $connector) : bool
{
// only for user with the 'publish' permission
if ($user === false || $user->hasPermission('publish') === false) {
diff --git a/php/federator/api/wellknown.php b/php/federator/api/wellknown.php
index 6b1e76d..752f33b 100644
--- a/php/federator/api/wellknown.php
+++ b/php/federator/api/wellknown.php
@@ -57,7 +57,7 @@ class WellKnown implements APIInterface
* @param \Federator\Data\User|false $user user who is calling us @unused-param
* @return bool true on success
*/
- public function exec($paths, $user)
+ public function exec($paths, $user, $connector)
{
$method = $_SERVER["REQUEST_METHOD"];
switch ($method) {
@@ -66,14 +66,14 @@ class WellKnown implements APIInterface
case 2:
if ($paths[0] === 'nodeinfo') {
$ni = new WellKnown\NodeInfo($this, $this->main);
- return $ni->exec($paths);
+ return $ni->exec($paths, $connector);
}
switch ($paths[1]) {
case 'host-meta':
return $this->hostMeta();
case 'nodeinfo':
$ni = new WellKnown\NodeInfo($this, $this->main);
- return $ni->exec($paths);
+ return $ni->exec($paths, $connector);
case 'webfinger':
$wf = new WellKnown\WebFinger($this, $this->main);
return $wf->exec();
diff --git a/php/federator/api/wellknown/nodeinfo.php b/php/federator/api/wellknown/nodeinfo.php
index 0d5b251..5bcef42 100644
--- a/php/federator/api/wellknown/nodeinfo.php
+++ b/php/federator/api/wellknown/nodeinfo.php
@@ -41,9 +41,10 @@ class NodeInfo
* handle nodeinfo request
*
* @param string[] $paths path of us
+ * @param \Federator\Connector\Connector $connector connector to use
* @return bool true on success
*/
- public function exec($paths)
+ public function exec($paths, $connector)
{
$data = [
'fqdn' => $_SERVER['SERVER_NAME']
@@ -64,7 +65,7 @@ class NodeInfo
default:
$template = 'nodeinfo2.0.json';
}
- $stats = \Federator\DIO\Stats::getStats($this->main);
+ $stats = \Federator\DIO\Stats::getStats($this->main, $connector);
echo "fetch usercount via connector\n";
$data['usercount'] = $stats->userCount;
$data['postcount'] = $stats->postCount;
diff --git a/php/federator/api/wellknown/webfinger.php b/php/federator/api/wellknown/webfinger.php
index 72555e3..b817401 100644
--- a/php/federator/api/wellknown/webfinger.php
+++ b/php/federator/api/wellknown/webfinger.php
@@ -46,18 +46,18 @@ class WebFinger
{
$_resource = $this->main->extractFromURI('resource');
$matches = [];
- $config = $this->main->getConfig();
- $domain = $config['generic']['externaldomain'];
- if (preg_match("/^acct:([^@]+)@(.*)$/", $_resource, $matches) != 1 || $matches[2] !== $domain) {
+ if (preg_match("/^acct:([^@]+)@(.*)$/", $_resource, $matches) != 1) {
throw new \Federator\Exceptions\InvalidArgument();
- }
+ }
+ $domain = $matches[2];
$user = \Federator\DIO\User::getUserByName(
$this->main->getDatabase(),
$matches[1],
- $this->main->getConnector(),
+ $this->main->getConnectorForHost($domain),
$this->main->getCache()
);
if ($user->id == 0) {
+ echo "not found";
throw new \Federator\Exceptions\FileNotFound();
}
$data = [
diff --git a/php/federator/connector/connector.php b/php/federator/connector/connector.php
index aab10e7..d1f41ac 100644
--- a/php/federator/connector/connector.php
+++ b/php/federator/connector/connector.php
@@ -13,6 +13,12 @@ namespace Federator\Connector;
*/
interface Connector
{
+ /**
+ * get the host this connector is dedicated to
+ *
+ * @return string
+ */
+ public function getHost();
/**
* get posts by given user
*
diff --git a/php/federator/data/activitypub/common/apobject.php b/php/federator/data/activitypub/common/apobject.php
index 735f3d6..62c2886 100644
--- a/php/federator/data/activitypub/common/apobject.php
+++ b/php/federator/data/activitypub/common/apobject.php
@@ -773,7 +773,7 @@ class APObject implements \JsonSerializable
* {@inheritDoc}
* @see JsonSerializable::jsonSerialize()
*/
- public function jsonSerialize()
+ public function jsonSerialize(): mixed
{
return $this->toObject();
}
diff --git a/php/federator/data/user.php b/php/federator/data/user.php
index efb829d..ef255e3 100644
--- a/php/federator/data/user.php
+++ b/php/federator/data/user.php
@@ -145,7 +145,7 @@ class User
*/
public function hasPermission(string $p)
{
- return in_array($p, $this->permissions, false);
+ return in_array(strtolower($p), array_map('strtolower', $this->permissions), true);
}
/**
diff --git a/php/federator/dio/stats.php b/php/federator/dio/stats.php
index ac905cf..9aa3fd4 100644
--- a/php/federator/dio/stats.php
+++ b/php/federator/dio/stats.php
@@ -17,11 +17,11 @@ class Stats
/**
* get remote stats
*
- * @param \Federator\Main $main
- * main instance
+ * @param \Federator\Main $main main instance
+ * @param \Federator\Connector\Connector $connector connector to use
* @return \Federator\Data\Stats
*/
- public static function getStats($main)
+ public static function getStats($main, $connector)
{
$cache = $main->getCache();
// ask cache
@@ -31,7 +31,6 @@ class Stats
return $stats;
}
}
- $connector = $main->getConnector();
// ask connector for stats
$stats = $connector->getRemoteStats();
if ($cache !== null && $stats !== false) {
diff --git a/php/federator/dio/user.php b/php/federator/dio/user.php
index 24ab009..9470198 100644
--- a/php/federator/dio/user.php
+++ b/php/federator/dio/user.php
@@ -145,6 +145,7 @@ class User
public static function getUserByName($dbh, $_name, $connector, $cache)
{
$user = false;
+
// ask cache
if ($cache !== null) {
$user = $cache->getRemoteUserByName($_name);
@@ -179,6 +180,7 @@ class User
$stmt->fetch();
}
$stmt->close();
+
if ($user->id === null) {
// ask connector for user-id
$ruser = $connector->getRemoteUserByName($_name);
@@ -190,7 +192,7 @@ class User
if ($user->id === null && $user->externalid !== null) {
self::addLocalUser($dbh, $user, $_name);
}
- $cache->saveRemoteUserByName($_name, $user);
+ $cache->saveRemoteUserByName($_name, user: $user);
}
return $user;
}
diff --git a/php/federator/main.php b/php/federator/main.php
index 456c57f..6ec0637 100644
--- a/php/federator/main.php
+++ b/php/federator/main.php
@@ -28,11 +28,11 @@ class Main
*/
protected $config;
/**
- * remote connector
+ * remote connectors
*
- * @var Connector\Connector $connector
+ * @var array $connectors
*/
- protected $connector = null;
+ protected $connectors = [];
/**
* response content type
*
@@ -93,7 +93,7 @@ class Main
public static function extractFromURI($param, $fallback = '')
{
$uri = $_SERVER['REQUEST_URI'];
- $params = substr($uri, (int)(strpos($uri, '?') + 1));
+ $params = substr($uri, (int) (strpos($uri, '?') + 1));
$params = explode('&', $params);
foreach ($params as $p) {
$tokens = explode('=', $p);
@@ -141,18 +141,20 @@ class Main
}
/**
- * get connector
- *
- * @return \Federator\Connector\Connector
+ * Get the connector for a given remote host
+ *
+ * @param string $remoteHost The host from the actor URL (e.g. mastodon.social)
+ * @return Connector\Connector|null
*/
- public function getConnector()
+ public function getConnectorForHost(string $remoteHost): ?Connector\Connector
{
- return $this->connector;
+ $host = strtolower(parse_url($remoteHost, PHP_URL_HOST) ?? $remoteHost);
+ return $this->connectors[$host] ?? null;
}
/**
* get config
- * @return Array
+ * @return array
*/
public function getConfig()
{
@@ -172,7 +174,7 @@ class Main
/**
* load plugins
*/
- public function loadPlugins() : void
+ public function loadPlugins(): void
{
if (array_key_exists('plugins', $this->config)) {
$basepath = $_SERVER['DOCUMENT_ROOT'] . '../plugins/federator/';
@@ -199,8 +201,8 @@ class Main
$dbconf = $this->config["database"];
$this->dbh = new \mysqli(
$dbconf['host'],
- $usernameOverride ?? (string)$dbconf['username'],
- $passwordOverride ?? (string)$dbconf['password'],
+ $usernameOverride ?? (string) $dbconf['username'],
+ $passwordOverride ?? (string) $dbconf['password'],
$dbconf['database']
);
if ($this->dbh->connect_error !== null) {
@@ -221,7 +223,7 @@ class Main
$smarty = new \Smarty\Smarty();
$root = $_SERVER['DOCUMENT_ROOT'];
$smarty->setCompileDir($root . $this->config['templates']['compiledir']);
- $smarty->setTemplateDir((string)realpath($root . $this->config['templates']['path']));
+ $smarty->setTemplateDir((string) realpath($root . $this->config['templates']['path']));
$smarty->assign('database', $this->dbh);
$smarty->assign('maininstance', $this);
foreach ($data as $key => $value) {
@@ -233,17 +235,22 @@ class Main
/**
* set cache
*/
- public function setCache(Cache\Cache $cache) : void
+ public function setCache(Cache\Cache $cache): void
{
$this->cache = $cache;
}
/**
- * set connector
+ * Set a connector for a specific remote host
+ *
+ * @param string $remoteURL The remote host (like mastodon.social or contentnation.net)
+ * @param Connector\Connector $connector The connector instance
*/
- public function setConnector(Connector\Connector $connector) : void
+ public function addConnector(string $remoteURL, Connector\Connector $connector): void
{
- $this->connector = $connector;
+ // Normalize the host (no scheme, lowercase)
+ $host = strtolower(parse_url($remoteURL, PHP_URL_HOST) ?? $remoteURL);
+ $this->connectors[$host] = $connector;
}
/**
@@ -252,7 +259,7 @@ class Main
* @param int $code
* new response code
*/
- public function setResponseCode(int $code) : void
+ public function setResponseCode(int $code): void
{
$this->responseCode = $code;
}
@@ -270,7 +277,7 @@ class Main
* optional parameters
* @return string translation
*/
- public static function translate(?string $lang, string $group, string $key, array $parameters = array()) : string
+ public static function translate(?string $lang, string $group, string $key, array $parameters = array()): string
{
$l = new Language($lang);
return $l->printlang($group, $key, $parameters);
@@ -281,7 +288,7 @@ class Main
*
* @param ?string $lang
*/
- public static function validLanguage(?string $lang) : bool
+ public static function validLanguage(?string $lang): bool
{
$language = new Language($lang);
if ($language->getLang() === $lang) {
diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php
index 3b871de..a9051b6 100644
--- a/plugins/federator/contentnation.php
+++ b/plugins/federator/contentnation.php
@@ -6,11 +6,11 @@
* @author Sascha Nitsch (grumpydeveloper)
**/
- namespace Federator\Connector;
+namespace Federator\Connector;
- /**
- * Connector to ContentNation.net
- */
+/**
+ * Connector to ContentNation.net
+ */
class ContentNation implements Connector
{
/**
@@ -49,6 +49,15 @@ class ContentNation implements Connector
$this->main = $main;
}
+ /**
+ * get the host this connector is dedicated to
+ *
+ * @return string
+ */
+ public function getHost() {
+ return "contentnation.net";
+ }
+
/**
* get posts by given user
*
@@ -83,63 +92,66 @@ class ContentNation implements Connector
$userdata = $this->config['userdata']['url'];
foreach ($activities as $activity) {
$create = new \Federator\Data\ActivityPub\Common\Create();
- $create->setAActor('https://' . $host .'/' . $userId);
+ $create->setAActor('https://' . $host . '/' . $userId);
$create->setID($activity['id'])
->setPublished($activity['timestamp'])
->addTo("https://www.w3.org/ns/activitystreams#Public")
->addCC('https://' . $host . '/' . $userId . '/followers.json');
switch ($activity['type']) {
case 'Article':
- $create->setURL('https://'.$host . '/' . $activity['language'] . '/' . $userId . '/'
- . $activity['name']);
+ $create->setURL('https://' . $host . '/' . $activity['language'] . '/' . $userId . '/'
+ . $activity['name']);
$apArticle = new \Federator\Data\ActivityPub\Common\Article();
if (array_key_exists('tags', $activity)) {
foreach ($activity['tags'] as $tag) {
$href = 'https://' . $host . '/' . $activity['language']
- . '/search.htm?tagsearch=' . urlencode($tag);
+ . '/search.htm?tagsearch=' . urlencode($tag);
$tagObj = new \Federator\Data\ActivityPub\Common\Tag();
$tagObj->setHref($href)
- ->setName('#' . urlencode(str_replace(' ', '', $tag)))
- ->setType('Hashtag');
+ ->setName('#' . urlencode(str_replace(' ', '', $tag)))
+ ->setType('Hashtag');
$apArticle->addTag($tagObj);
}
}
$apArticle->setPublished($activity['published'])
- ->setName($activity['title'])
- ->setAttributedTo('https://' . $host .'/' . $activity['profilename'])
- ->setContent(
- $activity['teaser'] ??
- $this->main->translate(
- $activity['language'],
- 'article',
- 'newarticle'
- )
- )
- ->addTo("https://www.w3.org/ns/activitystreams#Public")
- ->addCC('https://' . $host . '/' . $userId . '/followers.json');
+ ->setName($activity['title'])
+ ->setAttributedTo('https://' . $host . '/' . $activity['profilename'])
+ ->setContent(
+ $activity['teaser'] ??
+ $this->main->translate(
+ $activity['language'],
+ 'article',
+ 'newarticle'
+ )
+ )
+ ->addTo("https://www.w3.org/ns/activitystreams#Public")
+ ->addCC('https://' . $host . '/' . $userId . '/followers.json');
$articleimage = $activity['imagealt'] ??
$this->main->translate($activity['language'], 'article', 'image');
$idurl = 'https://' . $host . '/' . $activity['language']
- . '/' . $userId . '/'. $activity['name'];
+ . '/' . $userId . '/' . $activity['name'];
$apArticle->setID($idurl)
- ->setURL($idurl);
+ ->setURL($idurl);
$image = $activity['image'] ?? $activity['profileimg'];
- $mediaType = @mime_content_type($imgpath . $activity['profile'] . '/' . $image) | 'text/plain';
+ // $mediaType = @mime_content_type($imgpath . $activity['profile'] . '/' . $image) | 'text/plain'; // old approach, using local copy of images
+ $imgUrl = $userdata . '/' . $activity['profile'] . $image;
+ $mediaType = $this->getRemoteMimeType($imgUrl) ?? 'text/plain';
$img = new \Federator\Data\ActivityPub\Common\Image();
$img->setMediaType($mediaType)
- ->setName($articleimage)
- ->setURL($userdata . '/' . $activity['profile'] . $image);
+ ->setName($articleimage)
+ ->setURL($userdata . '/' . $activity['profile'] . $image);
$apArticle->addImage($img);
- $create->setObject($apArticle);
+ $create->setObject(object: $apArticle);
$posts[] = $create;
break; // Article
case 'Comment':
-// echo "comment\n";
-// print_r($activity);
+ $comment = new \Federator\Data\ActivityPub\Common\Activity('Comment');
+ $create->setObject($comment);
+ $posts[] = $create;
break; // Comment
case 'Vote':
- $url = 'https://'.$host . '/' . $activity['articlelang'] . $userId . '/'
- . $activity['articlename'];
+ $url = 'https://' . $host . '/' . $activity['articlelang'] . $userId . '/'
+ . $activity['articlename'];
$url .= '/vote/' . $activity['id'];
$create->setURL($url);
if ($activity['upvote'] === true) {
@@ -167,7 +179,7 @@ class ContentNation implements Connector
$actor->setName($activity['username']);
$like->setActor($actor);
$url = 'https://' . $host . '/' . $activity['articlelang']
- . '/' . $userId . '/'. $activity['articlename'];
+ . '/' . $userId . '/' . $activity['articlename'];
if ($activity['comment'] !== '') {
$url .= '/comment/' . $activity['comment'];
}
@@ -188,6 +200,18 @@ class ContentNation implements Connector
}
return $posts;
}
+
+ /**
+ * Get the MIME type of a remote file by its URL.
+ *
+ * @param string $_url The URL of the remote file.
+ * @return string|false The MIME type if found, or false on failure.
+ */
+ public function getRemoteMimeType($url)
+ {
+ $headers = get_headers($url, 1);
+ return $headers['Content-Type'] ?? 'unknown';
+ }
/**
* get statistics from remote system
@@ -212,7 +236,7 @@ class ContentNation implements Connector
return $stats;
}
- /**
+ /**
* get remote user by given name
*
* @param string $_name user/profile name
@@ -220,6 +244,9 @@ class ContentNation implements Connector
*/
public function getRemoteUserByName(string $_name)
{
+ if (preg_match("#^([^@]+)@([^/]+)$#", $_name, $matches) === 1) {
+ $_name = $matches[1];
+ }
// validate name
if (preg_match("/^[a-zA-Z0-9_\-]+$/", $_name) != 1) {
return false;
@@ -271,7 +298,7 @@ class ContentNation implements Connector
return false;
}
$r = json_decode($response, true);
- if ($r === false || !is_array($r) || !array_key_exists($_user, $r)) {
+ if ($r === false || !is_array($r) || !array_key_exists($_user, $r)) {
return false;
}
$user = $this->getRemoteUserByName($_user);
@@ -299,5 +326,5 @@ namespace Federator;
function contentnation_load($main)
{
$cn = new Connector\ContentNation($main);
- $main->setConnector($cn);
+ $main->addConnector($cn->getHost(), $cn);
}
diff --git a/plugins/federator/dummyconnector.php b/plugins/federator/dummyconnector.php
index ee0e6c3..d5acad4 100644
--- a/plugins/federator/dummyconnector.php
+++ b/plugins/federator/dummyconnector.php
@@ -18,6 +18,15 @@ class DummyConnector implements Connector
public function __construct()
{
}
+
+ /**
+ * get the host this connector is dedicated to
+ *
+ * @return string
+ */
+ public function getHost() {
+ return "dummy";
+ }
/**
* get posts by given user
@@ -87,5 +96,5 @@ namespace Federator;
function dummy_load($main)
{
$dummy = new Connector\DummyConnector();
- $main->setConnector($dummy);
+ $main->addConnector($dummy->getHost(), $dummy);
}
diff --git a/plugins/federator/mastodon.php b/plugins/federator/mastodon.php
new file mode 100644
index 0000000..96bfdd1
--- /dev/null
+++ b/plugins/federator/mastodon.php
@@ -0,0 +1,302 @@
+ $config
+ */
+ private $config;
+
+ /**
+ * main instance
+ *
+ * @var \Federator\Main $main
+ */
+ private $main;
+
+ /**
+ * service-URL
+ *
+ * @var string $service
+ */
+ private $service;
+
+ /**
+ * constructor
+ *
+ * @param \Federator\Main $main
+ */
+ public function __construct($main)
+ {
+ $config = parse_ini_file($_SERVER['DOCUMENT_ROOT'] . '../mastodon.ini', true);
+ if ($config !== false) {
+ $this->config = $config;
+ }
+ $this->service = $config['mastodon']['service-uri'];
+ $this->main = $main;
+ }
+
+ /**
+ * get the host this connector is dedicated to
+ *
+ * @return string
+ */
+ public function getHost() {
+ return "mastodon.social";
+ }
+
+ /**
+ * get posts by given user
+ *
+ * @param string $userId user id
+ * @param string $min min date
+ * @param string $max max date
+ * @return \Federator\Data\ActivityPub\Common\APObject[]|false
+ */
+ public function getRemotePostsByUser($userId, $min = null, $max = null)
+ {
+ $remoteURL = $this->service . '/users/' . $userId . '/outbox';
+
+ $items = [];
+
+ do {
+ if ($min !== '') {
+ $remoteURL .= '&minTS=' . urlencode($min);
+ }
+ if ($max !== '') {
+ $remoteURL .= '&maxTS=' . urlencode($max);
+ }
+ // Fetch the current page of items (first or subsequent pages)
+ [$outboxResponse, $outboxInfo] = \Federator\Main::getFromRemote($remoteURL, ['Accept: application/activity+json']);
+
+ if ($outboxInfo['http_code'] !== 200) {
+ echo "aborting";
+ return false;
+ }
+
+ $outbox = json_decode($outboxResponse, true);
+
+ // Extract orderedItems from the current page
+ if (isset($outbox['orderedItems'])) {
+ $items = array_merge($items, $outbox['orderedItems']);
+ }
+
+ // Step 4: Use 'last' URL to determine pagination
+ if (isset($outbox['last'])) {
+ // The 'last' URL will usually have a query string that includes min_id for the next set of results
+ $remoteURL = $outbox['last']; // Update to the last URL for the next page of items
+ } else {
+ break; // No more pages, exit pagination
+ }
+
+ } while (!empty($outbox['last'])); // Continue fetching until no 'last' URL
+
+
+ $items = [];
+
+ // Follow `first` page (or get orderedItems directly)
+ if (isset($outbox['orderedItems'])) {
+ $items = $outbox['orderedItems'];
+ } elseif (isset($outbox['first'])) {
+ $firstURL = is_array($outbox['first']) ? $outbox['first']['id'] : $outbox['first'];
+ [$pageResponse, $pageInfo] = \Federator\Main::getFromRemote($firstURL, ['Accept: application/activity+json']);
+ if ($pageInfo['http_code'] !== 200) {
+ return false;
+ }
+ $page = json_decode($pageResponse, true);
+ $items = $page['orderedItems'] ?? [];
+ }
+
+ // Convert to internal representation
+ $posts = [];
+ foreach ($items as $activity) {
+ if (!isset($activity['type']) || $activity['type'] !== 'Create' || !isset($activity['object'])) {
+ continue; // Skip non-Create activities
+ }
+
+ $obj = $activity['object'];
+ $create = new \Federator\Data\ActivityPub\Common\Create();
+ $create->setID($activity['id'])
+ ->setPublished(strtotime($activity['published'] ?? $obj['published'] ?? 'now'))
+ ->setAActor($activity['actor'])
+ ->addTo("https://www.w3.org/ns/activitystreams#Public");
+
+ // Handle main Note content
+ if ($obj['type'] === 'Note') {
+ $apNote = new \Federator\Data\ActivityPub\Common\Note();
+ $apNote->setID($obj['id'])
+ ->setPublished(strtotime($obj['published'] ?? 'now'))
+ ->setContent($obj['content'] ?? '')
+ ->setAttributedTo($obj['attributedTo'] ?? $activity['actor'])
+ ->addTo("https://www.w3.org/ns/activitystreams#Public");
+
+ // Handle attachments
+ if (!empty($obj['attachment']) && is_array($obj['attachment'])) {
+ foreach ($obj['attachment'] as $media) {
+ if (!isset($media['type'], $media['url']))
+ continue;
+ $mediaObj = new \Federator\Data\ActivityPub\Common\APObject($media['type']);
+ $mediaObj->setURL($media['url']);
+ $apNote->addAttachment($mediaObj);
+ }
+ }
+
+ $create->setObject($apNote);
+ }
+
+ $posts[] = $create;
+ }
+
+ return $posts;
+ }
+
+ /**
+ * Get the MIME type of a remote file by its URL.
+ *
+ * @param string $_url The URL of the remote file.
+ * @return string|false The MIME type if found, or false on failure.
+ */
+ public function getRemoteMimeType($url)
+ {
+ $headers = get_headers($url, 1);
+ return $headers['Content-Type'] ?? 'unknown';
+ }
+
+ /**
+ * get statistics from remote system
+ *
+ * @return \Federator\Data\Stats|false
+ */
+ public function getRemoteStats()
+ {
+ $remoteURL = $this->service . '/api/stats';
+ [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
+ if ($info['http_code'] != 200) {
+ return false;
+ }
+ $r = json_decode($response, true);
+ if ($r === false || $r === null || !is_array($r)) {
+ return false;
+ }
+ $stats = new \Federator\Data\Stats();
+ $stats->userCount = array_key_exists('userCount', $r) ? $r['userCount'] : 0;
+ $stats->postCount = array_key_exists('pageCount', $r) ? $r['pageCount'] : 0;
+ $stats->commentCount = array_key_exists('commentCount', $r) ? $r['commentCount'] : 0;
+ return $stats;
+ }
+
+ /**
+ * get remote user by given name
+ *
+ * @param string $_name user/profile name
+ * @return \Federator\Data\User | false
+ */
+ public function getRemoteUserByName(string $_name)
+ {
+ // Validate username (Mastodon usernames can include @ and domain parts)
+ if (preg_match("/^[a-zA-Z0-9_\-@.]+$/", $_name) !== 1) {
+ return false;
+ }
+
+ // Mastodon lookup API endpoint
+ $remoteURL = $this->service . '/api/v1/accounts/lookup?acct=' . urlencode($_name);
+
+ // Set headers
+ $headers = ['Accept: application/json'];
+
+ // Fetch data from Mastodon instance
+ [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
+
+ // Handle HTTP errors
+ if ($info['http_code'] !== 200) {
+ return false;
+ }
+
+ // Decode response
+ $r = json_decode($response, true);
+ if ($r === false || $r === null || !is_array($r)) {
+ return false;
+ }
+
+ // Map response to User object
+ $user = new \Federator\Data\User();
+ $user->externalid = (string) $r['id']; // Mastodon uses numeric IDs
+ $user->iconMediaType = 'image/png'; // Mastodon doesn't explicitly return this, assume PNG
+ $user->iconURL = $r['avatar'] ?? null;
+ $user->imageMediaType = 'image/png';
+ $user->imageURL = $r['header'] ?? null;
+ $user->name = $r['display_name'] ?: $r['username'];
+ $user->summary = $r['note'];
+ $user->type = 'Person'; // Mastodon profiles are ActivityPub "Person" objects
+ $user->registered = strtotime($r['created_at']);
+
+ return $user;
+ }
+
+ /**
+ * get remote user by given session
+ *
+ * @param string $_session session id
+ * @param string $_user user or profile name
+ * @return \Federator\Data\User | false
+ */
+ public function getRemoteUserBySession(string $_session, string $_user)
+ {
+ // validate $_session and $user
+ if (preg_match("/^[a-z0-9]{16}$/", $_session) != 1) {
+ return false;
+ }
+ if (preg_match("/^[a-zA-Z0-9_\-]+$/", $_user) != 1) {
+ return false;
+ }
+ $remoteURL = $this->service . '/api/users/permissions?profile=' . urlencode($_user);
+ $headers = ['Cookie: session=' . $_session, 'Accept: application/json'];
+ [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
+
+ if ($info['http_code'] != 200) {
+ return false;
+ }
+ $r = json_decode($response, true);
+ if ($r === false || !is_array($r) || !array_key_exists($_user, $r)) {
+ return false;
+ }
+ $user = $this->getRemoteUserByName($_user);
+ if ($user === false) {
+ return false;
+ }
+ // extend with permissions
+ $user->permissions = [];
+ $user->session = $_session;
+ foreach ($r[$_user] as $p) {
+ $user->permissions[] = $p;
+ }
+ return $user;
+ }
+}
+
+namespace Federator;
+
+/**
+ * Function to initialize plugin
+ *
+ * @param \Federator\Main $main main instance
+ * @return void
+ */
+function mastodon_load($main)
+{
+ $mast = new Connector\Mastodon($main);
+ $main->addConnector($mast->getHost(), $mast);
+}
diff --git a/plugins/federator/rediscache.php b/plugins/federator/rediscache.php
index 5c96eeb..78b16d4 100644
--- a/plugins/federator/rediscache.php
+++ b/plugins/federator/rediscache.php
@@ -53,6 +53,15 @@ class RedisCache implements Cache
}
}
+ /**
+ * get the host this connector is dedicated to
+ *
+ * @return string
+ */
+ public function getHost() {
+ return "redis";
+ }
+
/**
* connect to redis
* @return void