From 88fddc81d730978f2a4d8a671936b54041e3fe45 Mon Sep 17 00:00:00 2001
From: Yassine Doghri <yassine@doghri.fr>
Date: Thu, 6 Jan 2022 14:26:03 +0000
Subject: [PATCH] feat(nodeinfo2): add .well-known route for nodeinfo2
 containing metadata about the castopod instance

---
 modules/Fediverse/Config/Routes.php           |  5 ++
 .../Controllers/NodeInfo2Controller.php       | 53 +++++++++++++++
 modules/Fediverse/Models/ActorModel.php       | 67 +++++++++++++++++++
 modules/Fediverse/Models/PostModel.php        | 26 +++++++
 modules/Fediverse/Objects/ActorObject.php     |  3 +
 5 files changed, 154 insertions(+)
 create mode 100644 modules/Fediverse/Controllers/NodeInfo2Controller.php

diff --git a/modules/Fediverse/Config/Routes.php b/modules/Fediverse/Config/Routes.php
index 5df9c97d8d..c3bf55bc7c 100644
--- a/modules/Fediverse/Config/Routes.php
+++ b/modules/Fediverse/Config/Routes.php
@@ -29,6 +29,11 @@ $routes->group('', [
         'as' => 'webfinger',
     ]);
 
+    // nodeInfo2
+    $routes->get('.well-known/x-nodeinfo2', 'NodeInfo2Controller', [
+        'as' => 'nodeInfo2',
+    ]);
+
     // Actor
     $routes->group('@(:actorUsername)', function ($routes): void {
         // Actor
diff --git a/modules/Fediverse/Controllers/NodeInfo2Controller.php b/modules/Fediverse/Controllers/NodeInfo2Controller.php
new file mode 100644
index 0000000000..d1ea326d25
--- /dev/null
+++ b/modules/Fediverse/Controllers/NodeInfo2Controller.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright  2021 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace Modules\Fediverse\Controllers;
+
+use CodeIgniter\Controller;
+use CodeIgniter\HTTP\ResponseInterface;
+
+class NodeInfo2Controller extends Controller
+{
+    public function index(): ResponseInterface
+    {
+        $totalUsers = model('ActorModel')
+            ->getTotalLocalActors();
+        $totalPosts = model('PostModel')
+            ->getTotalLocalPosts();
+        $activeMonth = model('ActorModel')
+            ->getActiveLocalActors(1);
+        $activeHalfyear = model('ActorModel')
+            ->getActiveLocalActors(6);
+
+        $nodeInfo2 = [
+            'version' => '1.0',
+            'server' => [
+                'baseUrl' => base_url(),
+                'name' => service('settings')
+                    ->get('App.siteName'),
+                'software' => 'Castopod Host',
+                'version' => CP_VERSION,
+            ],
+            'protocols' => ['activitypub'],
+            'openRegistrations' => config('Auth')
+                ->allowRegistration,
+            'usage' => [
+                'users' => [
+                    'total' => $totalUsers,
+                    'activeMonth' => $activeMonth,
+                    'activeHalfyear' => $activeHalfyear,
+                ],
+                'localPosts' => $totalPosts,
+            ],
+        ];
+
+        return $this->response->setJSON($nodeInfo2);
+    }
+}
diff --git a/modules/Fediverse/Models/ActorModel.php b/modules/Fediverse/Models/ActorModel.php
index 3f0592ed23..628cca0560 100644
--- a/modules/Fediverse/Models/ActorModel.php
+++ b/modules/Fediverse/Models/ActorModel.php
@@ -205,6 +205,73 @@ class ActorModel extends BaseModel
         Events::trigger('on_unblock_actor', $actorId);
     }
 
+    public function getTotalLocalActors(): int
+    {
+        helper('fediverse');
+
+        $cacheName = config('Fediverse')
+            ->cachePrefix . 'blocked_actors';
+        if (! ($found = cache($cacheName))) {
+            $result = $this->select('COUNT(*) as total_local_actors')
+                ->where('domain', get_current_domain())
+                ->get()
+                ->getResultArray();
+
+            $found = (int) $result[0]['total_local_actors'];
+
+            cache()
+                ->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
+    }
+
+    public function getActiveLocalActors(int $lastNumberOfMonths = 1): int
+    {
+        helper('fediverse');
+
+        $cacheName = config('Fediverse')
+            ->cachePrefix . 'blocked_actors';
+        if (! ($found = cache($cacheName))) {
+            $tablePrefix = config('Database')
+                ->default['DBPrefix'] . config('Fediverse')
+                ->tablesPrefix;
+            $result = $this->select('COUNT(DISTINCT `cp_fediverse_actors`.`id`) as `total_active_actors`', false)
+                ->join(
+                    $tablePrefix . 'posts',
+                    $tablePrefix . 'actors.id = ' . $tablePrefix . 'posts.actor_id',
+                    'left outer'
+                )
+                ->join(
+                    $tablePrefix . 'favourites',
+                    $tablePrefix . 'actors.id = ' . $tablePrefix . 'favourites.actor_id',
+                    'left outer'
+                )
+                ->where($tablePrefix . 'actors.domain', get_current_domain())
+                ->groupStart()
+                ->where(
+                    "`{$tablePrefix}posts`.`created_at` >= NOW() - INTERVAL {$lastNumberOfMonths} month",
+                    null,
+                    false
+                )
+                ->orWhere(
+                    "`{$tablePrefix}favourites`.`created_at` >= NOW() - INTERVAL {$lastNumberOfMonths} month",
+                    null,
+                    false
+                )
+                ->groupEnd()
+                ->get()
+                ->getResultArray();
+
+            $found = (int) $result[0]['total_active_actors'];
+
+            cache()
+                ->save($cacheName, $found, DAY);
+        }
+
+        return $found;
+    }
+
     public function clearCache(Actor $actor): void
     {
         $cachePrefix = config('Fediverse')
diff --git a/modules/Fediverse/Models/PostModel.php b/modules/Fediverse/Models/PostModel.php
index 2d390eb3dc..b3f162f8e7 100644
--- a/modules/Fediverse/Models/PostModel.php
+++ b/modules/Fediverse/Models/PostModel.php
@@ -470,6 +470,7 @@ class PostModel extends BaseUuidModel
             'actor_id' => $actor->id,
             'reblog_of_id' => $post->id,
             'published_at' => Time::now(),
+            'created_by' => user_id(),
         ]);
 
         // add reblog
@@ -593,6 +594,31 @@ class PostModel extends BaseUuidModel
         }
     }
 
+    public function getTotalLocalPosts(): int
+    {
+        helper('fediverse');
+
+        $cacheName = config('Fediverse')
+            ->cachePrefix . 'blocked_actors';
+        if (! ($found = cache($cacheName))) {
+            $tablePrefix = config('Fediverse')
+                ->tablesPrefix;
+            $result = $this->select('COUNT(*) as total_local_posts')
+                ->join($tablePrefix . 'actors', $tablePrefix . 'actors.id = ' . $tablePrefix . 'posts.actor_id')
+                ->where($tablePrefix . 'actors.domain', get_current_domain())
+                ->where('`published_at` <= NOW()', null, false)
+                ->get()
+                ->getResultArray();
+
+            $found = (int) $result[0]['total_local_posts'];
+
+            cache()
+                ->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
+    }
+
     public function clearCache(Post $post): void
     {
         $cachePrefix = config('Fediverse')
diff --git a/modules/Fediverse/Objects/ActorObject.php b/modules/Fediverse/Objects/ActorObject.php
index 0914f3094e..ee1c389030 100644
--- a/modules/Fediverse/Objects/ActorObject.php
+++ b/modules/Fediverse/Objects/ActorObject.php
@@ -36,6 +36,8 @@ class ActorObject extends ObjectType
 
     protected string $url;
 
+    protected string $nodeInfo2Url;
+
     /**
      * @var array<string, string>
      */
@@ -59,6 +61,7 @@ class ActorObject extends ObjectType
         $this->preferredUsername = $actor->username;
         $this->summary = $actor->summary;
         $this->url = $actor->uri;
+        $this->nodeInfo2Url = url_to('nodeInfo2');
 
         $this->inbox = $actor->inbox_url;
         $this->outbox = $actor->outbox_url;
-- 
GitLab