From fed5224f465e36731b94d4ee492f1bbc95f2941f Mon Sep 17 00:00:00 2001 From: Sascha Nitsch Date: Mon, 22 Jul 2024 17:27:48 +0200 Subject: [PATCH] support for nodeinfo --- README.md | 2 +- php/federator/api.php | 14 ++--- php/federator/api/wellknown.php | 14 ++++- php/federator/api/wellknown/nodeinfo.php | 77 ++++++++++++++++++++++++ php/federator/cache/cache.php | 8 +++ php/federator/connector/connector.php | 7 +++ php/federator/data/stats.php | 70 +++++++++++++++++++++ php/federator/dio/stats.php | 45 ++++++++++++++ plugins/federator/contentnation.php | 24 ++++++++ plugins/federator/dummyconnector.php | 14 +++++ plugins/federator/rediscache.php | 38 +++++++++++- rediscache.ini | 1 + templates/federator/nodeinfo.json | 8 +++ templates/federator/nodeinfo2.0.json | 29 +++++++++ templates/federator/nodeinfo2.1.json | 30 +++++++++ 15 files changed, 368 insertions(+), 13 deletions(-) create mode 100644 php/federator/api/wellknown/nodeinfo.php create mode 100644 php/federator/data/stats.php create mode 100644 php/federator/dio/stats.php create mode 100644 templates/federator/nodeinfo.json create mode 100644 templates/federator/nodeinfo2.0.json create mode 100644 templates/federator/nodeinfo2.1.json diff --git a/README.md b/README.md index 2661184..4598b36 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ This will be changed, but works for the current develop verison. If the include redis cache is enabled, create a users.acl for redis with the content: - user federator on ~u_* +get +set >redis*change*password + user federator on ~u_* +get +set ~s_* +get +setex ~m_* +get +setex >redis*change*password change password in the rediscache.ini to match your given password. diff --git a/php/federator/api.php b/php/federator/api.php index 931771b..5134850 100644 --- a/php/federator/api.php +++ b/php/federator/api.php @@ -94,6 +94,10 @@ class Api extends Main } } switch ($this->paths[0]) { + case '.well-known': + case 'nodeinfo': + $handler = new Api\WellKnown($this); + break; case 'v1': switch ($this->paths[1]) { case 'dummy': @@ -101,9 +105,6 @@ class Api extends Main break; } break; - case '.well-known': - $handler = new Api\WellKnown($this); - break; } $printresponse = true; if ($handler !== null) { @@ -126,14 +127,13 @@ class Api extends Main header($name . ': ' . $value); } } + if ($this->responseCode != 200) { + http_response_code($this->responseCode); + } if ($printresponse) { if ($this->redirect !== null) { header("Location: $this->redirect"); } - // @phan-suppress-next-line PhanSuspiciousValueComparison - if ($this->responseCode != 200) { - http_response_code($this->responseCode); - } if ($this->responseCode != 404) { header("Content-type: " . $this->contentType); header("Access-Control-Allow-Origin: *"); diff --git a/php/federator/api/wellknown.php b/php/federator/api/wellknown.php index 95bccdd..c2525fa 100644 --- a/php/federator/api/wellknown.php +++ b/php/federator/api/wellknown.php @@ -51,9 +51,17 @@ class WellKnown implements APIInterface case 'GET': switch (sizeof($paths)) { case 2: - if ($paths[1] === 'webfinger') { - $wf = new WellKnown\WebFinger($this, $this->main); - return $wf->exec(); + if ($paths[0] === 'nodeinfo') { + $ni = new WellKnown\NodeInfo($this, $this->main); + return $ni->exec($paths); + } + switch ($paths[1]) { + case 'nodeinfo': + $ni = new WellKnown\NodeInfo($this, $this->main); + return $ni->exec($paths); + case 'webfinger': + $wf = new WellKnown\WebFinger($this, $this->main); + return $wf->exec(); } } break; diff --git a/php/federator/api/wellknown/nodeinfo.php b/php/federator/api/wellknown/nodeinfo.php new file mode 100644 index 0000000..0d5b251 --- /dev/null +++ b/php/federator/api/wellknown/nodeinfo.php @@ -0,0 +1,77 @@ +wellKnown = $wellKnown; + $this->main = $main; + } + + /** + * handle nodeinfo request + * + * @param string[] $paths path of us + * @return bool true on success + */ + public function exec($paths) + { + $data = [ + 'fqdn' => $_SERVER['SERVER_NAME'] + ]; + $template = null; + if (sizeof($paths) == 2 && $paths[0] === '.well-known' && $paths[1] === 'nodeinfo') { + $template = 'nodeinfo.json'; + } else { + if ($paths[0] !== 'nodeinfo') { + throw new \Federator\Exceptions\FileNotFound(); + } + } + if ($template === null) { + switch ($paths[1]) { + case '2.1': + $template = 'nodeinfo2.1.json'; + break; + default: + $template = 'nodeinfo2.0.json'; + } + $stats = \Federator\DIO\Stats::getStats($this->main); + echo "fetch usercount via connector\n"; + $data['usercount'] = $stats->userCount; + $data['postcount'] = $stats->postCount; + $data['commentcount'] = $stats->commentCount; + } + $tpl = $this->main->renderTemplate($template, $data); + $this->wellKnown->setResponse($tpl); + return true; + } +} diff --git a/php/federator/cache/cache.php b/php/federator/cache/cache.php index 2678705..fa1fae8 100644 --- a/php/federator/cache/cache.php +++ b/php/federator/cache/cache.php @@ -13,6 +13,14 @@ namespace Federator\Cache; */ interface Cache extends \Federator\Connector\Connector { + /** + * save remote stats + * + * @param \Federator\Data\Stats $stats stats to save + * @return void + */ + public function saveRemoteStats($stats); + /** * save remote user by given name * diff --git a/php/federator/connector/connector.php b/php/federator/connector/connector.php index 17474e5..e877111 100644 --- a/php/federator/connector/connector.php +++ b/php/federator/connector/connector.php @@ -29,4 +29,11 @@ interface Connector * @return \Federator\Data\User | false */ public function getRemoteUserBySession(string $_session, string $_user); + + /** + * get statistics from remote system + * + * @return \Federator\Data\Stats|false + */ + public function getRemoteStats(); } diff --git a/php/federator/data/stats.php b/php/federator/data/stats.php new file mode 100644 index 0000000..f122bce --- /dev/null +++ b/php/federator/data/stats.php @@ -0,0 +1,70 @@ +userCount = $data['userCount']; + $stats->postCount = $data['postCount']; + $stats->commentCount = $data['commentCount']; + return $stats; + } + + /** + * convert internal data to json string + * + * @return string + */ + public function toJson() + { + $data = [ + 'userCount' => $this->userCount, + 'postCount' => $this->postCount, + 'commentCount' => $this->commentCount + ]; + return json_encode($data) | ''; + } +} diff --git a/php/federator/dio/stats.php b/php/federator/dio/stats.php new file mode 100644 index 0000000..ac905cf --- /dev/null +++ b/php/federator/dio/stats.php @@ -0,0 +1,45 @@ +getCache(); + // ask cache + if ($cache !== null) { + $stats = $cache->getRemoteStats(); + if ($stats !== false) { + return $stats; + } + } + $connector = $main->getConnector(); + // ask connector for stats + $stats = $connector->getRemoteStats(); + if ($cache !== null && $stats !== false) { + $cache->saveRemoteStats($stats); + } + if ($stats === false) { + $stats = new \Federator\Data\Stats(); + } + return $stats; + } +} diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php index 1e34b19..05855d6 100644 --- a/plugins/federator/contentnation.php +++ b/plugins/federator/contentnation.php @@ -37,6 +37,30 @@ class ContentNation implements Connector $this->service = $config['service-uri']; } + /** + * 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) { + print_r($info); + 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 * diff --git a/plugins/federator/dummyconnector.php b/plugins/federator/dummyconnector.php index 7abb589..9aef028 100644 --- a/plugins/federator/dummyconnector.php +++ b/plugins/federator/dummyconnector.php @@ -19,6 +19,20 @@ class DummyConnector implements Connector { } + /** + * get statistics from remote system + * + * @return \Federator\Data\Stats|false + */ + public function getRemoteStats() + { + $stats = new \Federator\Data\Stats(); + $stats->userCount = 9; + $stats->postCount = 11; + $stats->commentCount = 13; + return $stats; + } + /** * get remote user by name * @param string $_name user or profile name diff --git a/plugins/federator/rediscache.php b/plugins/federator/rediscache.php index bec8b19..9e838da 100644 --- a/plugins/federator/rediscache.php +++ b/plugins/federator/rediscache.php @@ -77,6 +77,25 @@ class RedisCache implements Cache return $prefix . '_' . md5($input); } + /** + * get statistics from remote system + * + * @return \Federator\Data\Stats|false + */ + public function getRemoteStats() + { + if (!$this->connected) { + $this->connect(); + } + $key = 'm_stats'; + $data = $this->redis->get($key); + if ($data === false) { + return false; + } + $stats = \Federator\Data\Stats::createFromJson($data); + return $stats; + } + /** * get remote user by given name * @@ -119,7 +138,22 @@ class RedisCache implements Cache } /** - * save remote user by namr + * save remote stats + * + * @param \Federator\Data\Stats $stats stats to save + * @return void + */ + public function saveRemoteStats($stats) + { + if (!$this->connected) { + $this->connect(); + } + $key = 'm_stats'; + $serialized = $stats->toJson(); + $this->redis->setEx($key, $this->config['statsttl'], $serialized); + } + /** + * save remote user by name * * @param string $_name user/profile name * @param \Federator\Data\User $user user data @@ -129,7 +163,7 @@ class RedisCache implements Cache { $key = self::createKey('u', $_name); $serialized = $user->toJson(); - $this->redis->setEx($key, $this->userTTL, $serialized,); + $this->redis->setEx($key, $this->userTTL, $serialized); } /** diff --git a/rediscache.ini b/rediscache.ini index 2b9f0d5..5d55955 100644 --- a/rediscache.ini +++ b/rediscache.ini @@ -4,3 +4,4 @@ port = 6379 username = federator password = redis*change*password userttl = 10 +statsttl = 60 diff --git a/templates/federator/nodeinfo.json b/templates/federator/nodeinfo.json new file mode 100644 index 0000000..9eb2357 --- /dev/null +++ b/templates/federator/nodeinfo.json @@ -0,0 +1,8 @@ +{ldelim} + "links": [ + { + "rel": "http://nodeinfo.diaspora.software/ns/schema/2.1", + "href": "https://{$fqdn}/nodeinfo/2.1" + } + ] + {rdelim}} \ No newline at end of file diff --git a/templates/federator/nodeinfo2.0.json b/templates/federator/nodeinfo2.0.json new file mode 100644 index 0000000..e5f85fb --- /dev/null +++ b/templates/federator/nodeinfo2.0.json @@ -0,0 +1,29 @@ +{ldelim} + "version": "2.0", + "software": {ldelim} + "name": "federator", + "version": "1.0.0", + {rdelim}, + "protocols": [ + "activitypub" + ], + "relay": "none", + "services": {ldelim} + "inbound": [], + "outbound": [ + "atom1.0", + "rss2.0" + ] + {rdelim}, + "openRegistrations": true, + "usage": {ldelim} + "users": {ldelim} + "total": {$usercount} +{* "activeMonth": activemonth, + "activeHalfyear": activehalfyear *} + {rdelim}, + "localPosts": {$postcount}, + "localComments": {$commentcount} + {rdelim}, + "metadata": {ldelim}{rdelim} +{rdelim} diff --git a/templates/federator/nodeinfo2.1.json b/templates/federator/nodeinfo2.1.json new file mode 100644 index 0000000..c515ddd --- /dev/null +++ b/templates/federator/nodeinfo2.1.json @@ -0,0 +1,30 @@ +{ldelim} + "version": "2.1", + "software": {ldelim} + "name": "federator", + "version": "1.0.0", + "repository": "https://git.contentnation.net/grumpydevelop/federator" + {rdelim}, + "protocols": [ + "activitypub" + ], + "relay": "none", + "services": {ldelim} + "inbound": [], + "outbound": [ + "atom1.0", + "rss2.0" + ] + {rdelim}, + "openRegistrations": true, + "usage": {ldelim} + "users": {ldelim} + "total": {$usercount} +{* "activeMonth": activemonth, + "activeHalfyear": activehalfyear *} + {rdelim}, + "localPosts": {$postcount}, + "localComments": {$commentcount} + {rdelim}, + "metadata": {ldelim}{rdelim} +{rdelim}