commit: 9effa24f308917f70276c41f91fb204b7684d942
parent b729a8b140306fd67817442bdde1299e05aca5b2
Author: marcin mikołajczak <git@mkljczk.pl>
Date:   Thu, 10 Aug 2023 22:52:38 +0200
Implement api/v2/instance route
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
Diffstat:
7 files changed, 288 insertions(+), 23 deletions(-)
diff --git a/changelog.d/instance-v2.add b/changelog.d/instance-v2.add
@@ -0,0 +1 @@
+Implement /api/v2/instance route
+\ No newline at end of file
diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex
@@ -23,6 +23,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
     }
   end
 
+  def show2_operation do
+    %Operation{
+      tags: ["Instance misc"],
+      summary: "Retrieve instance information",
+      description: "Information about the server",
+      operationId: "InstanceController.show2",
+      responses: %{
+        200 => Operation.response("Instance", "application/json", instance2())
+      }
+    }
+  end
+
   def peers_operation do
     %Operation{
       tags: ["Instance misc"],
@@ -165,6 +177,166 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
     }
   end
 
+  defp instance2 do
+    %Schema{
+      type: :object,
+      properties: %{
+        domain: %Schema{type: :string, description: "The domain name of the instance"},
+        title: %Schema{type: :string, description: "The title of the website"},
+        version: %Schema{
+          type: :string,
+          description: "The version of Pleroma installed on the instance"
+        },
+        source_url: %Schema{
+          type: :string,
+          description: "The version of Pleroma installed on the instance"
+        },
+        description: %Schema{
+          type: :string,
+          description: "Admin-defined description of the Pleroma site"
+        },
+        usage: %Schema{
+          type: :object,
+          description: "Instance usage statistics",
+          properties: %{
+            users: %Schema{
+              type: :object,
+              description: "User count statistics",
+              properties: %{
+                active_month: %Schema{
+                  type: :integer,
+                  description: "Monthly active users"
+                }
+              }
+            }
+          }
+        },
+        email: %Schema{
+          type: :string,
+          description: "An email that may be contacted for any inquiries",
+          format: :email
+        },
+        urls: %Schema{
+          type: :object,
+          description: "URLs of interest for clients apps",
+          properties: %{}
+        },
+        stats: %Schema{
+          type: :object,
+          description: "Statistics about how much information the instance contains",
+          properties: %{
+            user_count: %Schema{
+              type: :integer,
+              description: "Users registered on this instance"
+            },
+            status_count: %Schema{
+              type: :integer,
+              description: "Statuses authored by users on instance"
+            },
+            domain_count: %Schema{
+              type: :integer,
+              description: "Domains federated with this instance"
+            }
+          }
+        },
+        thumbnail: %Schema{
+          type: :object,
+          properties: %{
+            url: %Schema{
+              type: :string,
+              description: "Banner image for the website",
+              nullable: true
+            }
+          }
+        },
+        languages: %Schema{
+          type: :array,
+          items: %Schema{type: :string},
+          description: "Primary langauges of the website and its staff"
+        },
+        registrations: %Schema{
+          type: :object,
+          description: "Registrations-related configuration",
+          properties: %{
+            enabled: %Schema{
+              type: :boolean,
+              description: "Whether registrations are enabled"
+            },
+            approval_required: %Schema{
+              type: :boolean,
+              description: "Whether users need to be manually approved by admin"
+            }
+          }
+        },
+        configuration: %Schema{
+          type: :object,
+          description: "Instance configuration",
+          properties: %{
+            urls: %Schema{
+              type: :object,
+              properties: %{
+                streaming: %Schema{
+                  type: :string,
+                  description: "Websockets address for push streaming"
+                }
+              }
+            },
+            statuses: %Schema{
+              type: :object,
+              description: "A map with poll limits for local statuses",
+              properties: %{
+                max_characters: %Schema{
+                  type: :integer,
+                  description: "Posts character limit (CW/Subject included in the counter)"
+                },
+                max_media_attachments: %Schema{
+                  type: :integer,
+                  description: "Media attachment limit"
+                }
+              }
+            },
+            media_attachments: %Schema{
+              type: :object,
+              description: "A map with poll limits for media attachments",
+              properties: %{
+                image_size_limit: %Schema{
+                  type: :integer,
+                  description: "File size limit of uploaded images"
+                },
+                video_size_limit: %Schema{
+                  type: :integer,
+                  description: "File size limit of uploaded videos"
+                }
+              }
+            },
+            polls: %Schema{
+              type: :object,
+              description: "A map with poll limits for local polls",
+              properties: %{
+                max_options: %Schema{
+                  type: :integer,
+                  description: "Maximum number of options."
+                },
+                max_characters_per_option: %Schema{
+                  type: :integer,
+                  description: "Maximum number of characters per option."
+                },
+                min_expiration: %Schema{
+                  type: :integer,
+                  description: "Minimum expiration time (in seconds)."
+                },
+                max_expiration: %Schema{
+                  type: :integer,
+                  description: "Maximum expiration time (in seconds)."
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  end
+
   defp array_of_domains do
     %Schema{
       type: :array,
diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -16,6 +16,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
     render(conn, "show.json")
   end
 
+  @doc "GET /api/v2/instance"
+  def show2(conn, _params) do
+    render(conn, "show2.json")
+  end
+
   @doc "GET /api/v1/instance/peers"
   def peers(conn, _params) do
     json(conn, Pleroma.Stats.get_peers())
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -14,7 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
     instance = Config.get(:instance)
 
     %{
-      uri: Pleroma.Web.Endpoint.url(),
+      uri: Pleroma.Web.WebFinger.domain(),
       title: Keyword.get(instance, :name),
       description: Keyword.get(instance, :description),
       short_description: Keyword.get(instance, :short_description),
@@ -30,6 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
       languages: Keyword.get(instance, :languages, ["en"]),
       registrations: Keyword.get(instance, :registrations_open),
       approval_required: Keyword.get(instance, :account_approval_required),
+      configuration: configuration(),
       # Extra (not present in Mastodon):
       max_toot_chars: Keyword.get(instance, :limit),
       max_media_attachments: Keyword.get(instance, :max_media_attachments),
@@ -41,19 +42,38 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
       background_image: Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image),
       shout_limit: Config.get([:shout, :limit]),
       description_limit: Keyword.get(instance, :description_limit),
-      pleroma: %{
-        metadata: %{
-          account_activation_required: Keyword.get(instance, :account_activation_required),
-          features: features(),
-          federation: federation(),
-          fields_limits: fields_limits(),
-          post_formats: Config.get([:instance, :allowed_post_formats]),
-          birthday_required: Config.get([:instance, :birthday_required]),
-          birthday_min_age: Config.get([:instance, :birthday_min_age])
-        },
-        stats: %{mau: Pleroma.User.active_user_count()},
-        vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
-      }
+      pleroma: pleroma_configuration(instance)
+    }
+  end
+
+  def render("show2.json", _) do
+    instance = Config.get(:instance)
+
+    %{
+      domain: Pleroma.Web.WebFinger.domain(),
+      title: Keyword.get(instance, :name),
+      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
+      source_url: Pleroma.Application.repository(),
+      description: Keyword.get(instance, :short_description),
+      usage: %{users: %{active_month: Pleroma.User.active_user_count()}},
+      thumbnail: %{
+        url:
+          URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail))
+          |> to_string
+      },
+      languages: Keyword.get(instance, :languages, ["en"]),
+      configuration: configuration2(),
+      registrations: %{
+        enabled: Keyword.get(instance, :registrations_open),
+        approval_required: Keyword.get(instance, :account_approval_required),
+        message: nil
+      },
+      contact: %{
+        email: Keyword.get(instance, :email),
+        account: nil
+      },
+      # Extra (not present in Mastodon):
+      pleroma: pleroma_configuration2(instance)
     }
   end
 
@@ -68,6 +88,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
       "shareable_emoji_packs",
       "multifetch",
       "pleroma:api/v1/notifications:include_types_filter",
+      "quote_posting",
       "editing",
       if Config.get([:activitypub, :blockers_visible]) do
         "blockers_visible"
@@ -78,13 +99,6 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
       if Config.get([:gopher, :enabled]) do
         "gopher"
       end,
-      # backwards compat
-      if Config.get([:shout, :enabled]) do
-        "chat"
-      end,
-      if Config.get([:shout, :enabled]) do
-        "shout"
-      end,
       if Config.get([:instance, :allow_relay]) do
         "relay"
       end,
@@ -94,6 +108,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
       "pleroma_emoji_reactions",
       "pleroma_custom_emoji_reactions",
       "pleroma_chat_messages",
+      "email_list",
       if Config.get([:instance, :show_reactions]) do
         "exposable_reactions"
       end,
@@ -132,7 +147,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
     |> Map.put(:enabled, Config.get([:instance, :federating]))
   end
 
-  def fields_limits do
+  defp fields_limits do
     %{
       max_fields: Config.get([:instance, :max_account_fields]),
       max_remote_fields: Config.get([:instance, :max_remote_account_fields]),
@@ -140,4 +155,65 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
       value_length: Config.get([:instance, :account_field_value_length])
     }
   end
+
+  defp configuration do
+    %{
+      statuses: %{
+        max_characters: Config.get([:instance, :limit]),
+        max_media_attachments: Config.get([:instance, :max_media_attachments])
+      },
+      media_attachments: %{
+        image_size_limit: Config.get([:instance, :upload_limit]),
+        video_size_limit: Config.get([:instance, :upload_limit])
+      },
+      polls: %{
+        max_options: Config.get([:instance, :poll_limits, :max_options]),
+        max_characters_per_option: Config.get([:instance, :poll_limits, :max_option_chars]),
+        min_expiration: Config.get([:instance, :poll_limits, :min_expiration]),
+        max_expiration: Config.get([:instance, :poll_limits, :max_expiration])
+      }
+    }
+  end
+
+  defp configuration2 do
+    configuration()
+    |> Map.merge(%{
+      urls: %{streaming: Pleroma.Web.Endpoint.websocket_url()}
+    })
+  end
+
+  defp pleroma_configuration(instance) do
+    %{
+      metadata: %{
+        account_activation_required: Keyword.get(instance, :account_activation_required),
+        features: features(),
+        federation: federation(),
+        fields_limits: fields_limits(),
+        post_formats: Config.get([:instance, :allowed_post_formats]),
+        birthday_required: Config.get([:instance, :birthday_required]),
+        birthday_min_age: Config.get([:instance, :birthday_min_age])
+      },
+      stats: %{mau: Pleroma.User.active_user_count()},
+      vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
+    }
+  end
+
+  defp pleroma_configuration2(instance) do
+    configuration = pleroma_configuration(instance)
+
+    configuration
+    |> Map.merge(%{
+      metadata:
+        configuration.metadata
+        |> Map.merge(%{
+          avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
+          background_upload_limit: Keyword.get(instance, :background_upload_limit),
+          banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
+          background_image:
+            Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image),
+          description_limit: Keyword.get(instance, :description_limit),
+          shout_limit: Config.get([:shout, :limit])
+        })
+    })
+  end
 end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
@@ -774,11 +774,14 @@ defmodule Pleroma.Web.Router do
 
   scope "/api/v2", Pleroma.Web.MastodonAPI do
     pipe_through(:api)
+
     get("/search", SearchController, :search2)
 
     post("/media", MediaController, :create2)
 
     get("/suggestions", SuggestionController, :index2)
+
+    get("/instance", InstanceController, :show2)
   end
 
   scope "/api", Pleroma.Web do
diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex
@@ -96,7 +96,7 @@ defmodule Pleroma.Web.WebFinger do
     |> XmlBuilder.to_doc()
   end
 
-  defp domain do
+  def domain do
     Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host()
   end
 
diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
@@ -106,4 +106,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
              |> get("/api/v1/instance")
              |> json_response_and_validate_schema(200)
   end
+
+  test "get instance information v2", %{conn: conn} do
+    clear_config([:auth, :oauth_consumer_strategies], [])
+
+    assert get(conn, "/api/v2/instance")
+           |> json_response_and_validate_schema(200)
+  end
 end