support for nodeinfo
This commit is contained in:
		
							parent
							
								
									073fba5312
								
							
						
					
					
						commit
						fed5224f46
					
				
					 15 changed files with 368 additions and 13 deletions
				
			
		|  | @ -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. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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: *"); | ||||||
|  |  | ||||||
|  | @ -51,7 +51,15 @@ 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 '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(); | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
							
								
								
									
										77
									
								
								php/federator/api/wellknown/nodeinfo.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								php/federator/api/wellknown/nodeinfo.php
									
										
									
									
									
										Normal 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; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								php/federator/cache/cache.php
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								php/federator/cache/cache.php
									
										
									
									
										vendored
									
									
								
							|  | @ -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 | ||||||
|      * |      * | ||||||
|  |  | ||||||
|  | @ -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(); | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										70
									
								
								php/federator/data/stats.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								php/federator/data/stats.php
									
										
									
									
									
										Normal 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) | ''; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								php/federator/dio/stats.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								php/federator/dio/stats.php
									
										
									
									
									
										Normal 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; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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 | ||||||
|      * |      * | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -4,3 +4,4 @@ port = 6379 | ||||||
| username = federator | username = federator | ||||||
| password = redis*change*password | password = redis*change*password | ||||||
| userttl = 10 | userttl = 10 | ||||||
|  | statsttl = 60 | ||||||
|  |  | ||||||
							
								
								
									
										8
									
								
								templates/federator/nodeinfo.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								templates/federator/nodeinfo.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | ||||||
|  | {ldelim} | ||||||
|  |     "links": [ | ||||||
|  |         { | ||||||
|  |             "rel": "http://nodeinfo.diaspora.software/ns/schema/2.1", | ||||||
|  |             "href": "https://{$fqdn}/nodeinfo/2.1" | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  |  {rdelim}} | ||||||
							
								
								
									
										29
									
								
								templates/federator/nodeinfo2.0.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								templates/federator/nodeinfo2.0.json
									
										
									
									
									
										Normal 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} | ||||||
							
								
								
									
										30
									
								
								templates/federator/nodeinfo2.1.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								templates/federator/nodeinfo2.1.json
									
										
									
									
									
										Normal 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} | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Sascha Nitsch
						Sascha Nitsch