Compare commits

...

2 Commits

Author SHA1 Message Date
Sascha Nitsch ebf3d05620 support for host-meta file 2024-07-22 17:38:14 +02:00
Sascha Nitsch fed5224f46 support for nodeinfo 2024-07-22 17:27:48 +02:00
16 changed files with 387 additions and 13 deletions

View File

@ -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: 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. change password in the rediscache.ini to match your given password.

View File

@ -94,6 +94,10 @@ class Api extends Main
} }
} }
switch ($this->paths[0]) { switch ($this->paths[0]) {
case '.well-known':
case 'nodeinfo':
$handler = new Api\WellKnown($this);
break;
case 'v1': case 'v1':
switch ($this->paths[1]) { switch ($this->paths[1]) {
case 'dummy': case 'dummy':
@ -101,9 +105,6 @@ class Api extends Main
break; break;
} }
break; break;
case '.well-known':
$handler = new Api\WellKnown($this);
break;
} }
$printresponse = true; $printresponse = true;
if ($handler !== null) { if ($handler !== null) {
@ -126,14 +127,13 @@ class Api extends Main
header($name . ': ' . $value); header($name . ': ' . $value);
} }
} }
if ($this->responseCode != 200) {
http_response_code($this->responseCode);
}
if ($printresponse) { if ($printresponse) {
if ($this->redirect !== null) { if ($this->redirect !== null) {
header("Location: $this->redirect"); header("Location: $this->redirect");
} }
// @phan-suppress-next-line PhanSuspiciousValueComparison
if ($this->responseCode != 200) {
http_response_code($this->responseCode);
}
if ($this->responseCode != 404) { if ($this->responseCode != 404) {
header("Content-type: " . $this->contentType); header("Content-type: " . $this->contentType);
header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Origin: *");

View File

@ -37,6 +37,19 @@ class WellKnown implements APIInterface
$this->main = $main; $this->main = $main;
} }
/**
* return host-meta information
*
* @return true
*/
private function hostMeta()
{
$data = [
'fqdn' => $_SERVER['SERVER_NAME']
];
$this->response = $this->main->renderTemplate('host-meta.xml', $data);
return true;
}
/** /**
* run given url path * run given url path
* *
@ -51,7 +64,17 @@ class WellKnown implements APIInterface
case 'GET': case 'GET':
switch (sizeof($paths)) { switch (sizeof($paths)) {
case 2: case 2:
if ($paths[1] === 'webfinger') { if ($paths[0] === 'nodeinfo') {
$ni = new WellKnown\NodeInfo($this, $this->main);
return $ni->exec($paths);
}
switch ($paths[1]) {
case 'host-meta':
return $this->hostMeta();
case 'nodeinfo':
$ni = new WellKnown\NodeInfo($this, $this->main);
return $ni->exec($paths);
case 'webfinger':
$wf = new WellKnown\WebFinger($this, $this->main); $wf = new WellKnown\WebFinger($this, $this->main);
return $wf->exec(); return $wf->exec();
} }

View File

@ -0,0 +1,77 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
* SPDX-License-Identifier: GPL-3.0-or-later
*
* @author Sascha Nitsch (grumpydeveloper)
**/
namespace Federator\Api\WellKnown;
class NodeInfo
{
/**
* parent instance
*
* @var \Federator\Api\WellKnown $wellKnown
*/
private $wellKnown;
/**
* main instance
*
* @var \Federator\Main $main
*/
private $main;
/**
* constructor
*
* @param \Federator\Api\WellKnown $wellKnown parent instance
* @param \Federator\Main $main main instance
* @return void
*/
public function __construct($wellKnown, $main)
{
$this->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;
}
}

View File

@ -13,6 +13,14 @@ namespace Federator\Cache;
*/ */
interface Cache extends \Federator\Connector\Connector 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 * save remote user by given name
* *

View File

@ -29,4 +29,11 @@ interface Connector
* @return \Federator\Data\User | false * @return \Federator\Data\User | false
*/ */
public function getRemoteUserBySession(string $_session, string $_user); public function getRemoteUserBySession(string $_session, string $_user);
/**
* get statistics from remote system
*
* @return \Federator\Data\Stats|false
*/
public function getRemoteStats();
} }

View File

@ -0,0 +1,70 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
* SPDX-License-Identifier: GPL-3.0-or-later
*
* @author Sascha Nitsch (grumpydeveloper)
**/
namespace Federator\Data;
/**
* storage class for statistics
*/
class Stats
{
/**
* user count on the external system
*
* @var int $userCount
*/
public $userCount;
/**
* post count on the external system
*
* @var int $postCount
*/
public $postCount;
/**
* comment count on the external system
*
* @var int $commentCount
*/
public $commentCount;
/**
* create new user object from json string
*
* @param string $input input string
* @return Stats
*/
public static function createFromJson($input)
{
$stats = new Stats();
$data = json_decode($input, true);
if ($data === null) {
return $stats;
}
$stats->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) | '';
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
* SPDX-License-Identifier: GPL-3.0-or-later
*
* @author Sascha Nitsch (grumpydeveloper)
**/
namespace Federator\DIO;
/**
* IO functions related to stats
*/
class Stats
{
/**
* get remote stats
*
* @param \Federator\Main $main
* main instance
* @return \Federator\Data\Stats
*/
public static function getStats($main)
{
$cache = $main->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;
}
}

View File

@ -37,6 +37,30 @@ class ContentNation implements Connector
$this->service = $config['service-uri']; $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 * get remote user by given name
* *

View File

@ -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 * get remote user by name
* @param string $_name user or profile name * @param string $_name user or profile name

View File

@ -77,6 +77,25 @@ class RedisCache implements Cache
return $prefix . '_' . md5($input); 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 * 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 string $_name user/profile name
* @param \Federator\Data\User $user user data * @param \Federator\Data\User $user user data
@ -129,7 +163,7 @@ class RedisCache implements Cache
{ {
$key = self::createKey('u', $_name); $key = self::createKey('u', $_name);
$serialized = $user->toJson(); $serialized = $user->toJson();
$this->redis->setEx($key, $this->userTTL, $serialized,); $this->redis->setEx($key, $this->userTTL, $serialized);
} }
/** /**

View File

@ -4,3 +4,4 @@ port = 6379
username = federator username = federator
password = redis*change*password password = redis*change*password
userttl = 10 userttl = 10
statsttl = 60

View File

@ -0,0 +1,4 @@
version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" template="https://{$fqdn}/.well-known/webfinger?resource={ldelim}uri{rdelim}"/>
</XRD>

View File

@ -0,0 +1,8 @@
{ldelim}
"links": [
{
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
"href": "https://{$fqdn}/nodeinfo/2.1"
}
]
{rdelim}}

View File

@ -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}

View File

@ -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}