diff --git a/README.md b/README.md index efd2ef1419..fca76b758c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -A fork of Pleroma/Rebased. More information soon. +`pl`. A fork of Pleroma/Rebased. More information soon. --- diff --git a/app.json b/app.json index b0b28b9db4..c612a3ee75 100644 --- a/app.json +++ b/app.json @@ -1,10 +1,10 @@ { - "name": "Rebased", - "description": "Rebased, the recommended backend for Soapbox written in Elixir.", + "name": "pl", + "description": "Federated social media software, a fork of Pleroma/Rebased", "keywords": [ "fediverse" ], - "website": "https://soapbox.pub", + "website": "https://github.com/mkljczk/pl", "dokku": { "plugins": [ "postgres" diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md index e50ececa16..f1a8f23b30 100644 --- a/docs/development/API/admin_api.md +++ b/docs/development/API/admin_api.md @@ -1907,3 +1907,52 @@ Note that this differs from the Mastodon API variant: Mastodon API only returns ```json {} ``` + +## `GET /api/v1/pleroma/admin/rules` + +### List rules + +- Response: JSON, list of rules + +```json +[ + { + "id": "1", + "priority": 1, + "text": "There are no rules", + "hint": null + } +] +``` + +## `POST /api/v1/pleroma/admin/rules` + +### Create a rule + +- Params: + - `text`: string, required, rule content + - `hint`: string, optional, rule description + - `priority`: integer, optional, rule ordering priority + +- Response: JSON, a single rule + +## `PATCH /api/v1/pleroma/admin/rules/:id` + +### Update a rule + +- Params: + - `text`: string, optional, rule content + - `hint`: string, optional, rule description + - `priority`: integer, optional, rule ordering priority + +- Response: JSON, a single rule + +## `DELETE /api/v1/pleroma/admin/rules/:id` + +### Delete a rule + +- Response: JSON, empty object + +```json +{} +``` diff --git a/lib/pleroma/rule.ex b/lib/pleroma/rule.ex index 067d071112..f59294a006 100644 --- a/lib/pleroma/rule.ex +++ b/lib/pleroma/rule.ex @@ -16,13 +16,14 @@ defmodule Pleroma.Rule do schema "rules" do field(:priority, :integer, default: 0) field(:text, :string) + field(:hint, :string) timestamps() end def changeset(%Rule{} = rule, params \\ %{}) do rule - |> cast(params, [:priority, :text]) + |> cast(params, [:priority, :text, :hint]) |> validate_required([:text]) end @@ -39,6 +40,11 @@ def get(ids) when is_list(ids) do def get(id), do: Repo.get(__MODULE__, id) + def exists?(id) do + from(r in __MODULE__, where: r.id == ^id) + |> Repo.exists?() + end + def create(params) do {:ok, rule} = %Rule{} diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index d9b907ecfe..fb8e36bbfd 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1340,6 +1340,15 @@ defp restrict_unauthenticated(query, nil) do defp restrict_unauthenticated(query, _), do: query + defp restrict_rule(query, %{rule_id: rule_id}) do + from( + activity in query, + where: fragment("(?)->'rules' \\? (?)", activity.data, ^rule_id) + ) + end + + defp restrict_rule(query, _), do: query + defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query defp exclude_poll_votes(query, _) do @@ -1507,6 +1516,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_object(opts) |> restrict_filtered(opts) |> restrict_quote_url(opts) + |> restrict_rule(opts) |> maybe_restrict_deactivated_users(opts) |> exclude_poll_votes(opts) |> exclude_chat_messages(opts) diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index 9b386fa0c0..c60a5436a3 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -10,8 +10,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do alias Pleroma.User alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.Report + alias Pleroma.Web.AdminAPI.RuleView alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.MastodonAPI.InstanceView alias Pleroma.Web.MastodonAPI.StatusView defdelegate merge_account_views(user), to: AdminAPI.AccountView @@ -92,8 +92,10 @@ defp rules(nil) do end defp rules(rule_ids) do - rule_ids - |> Rule.get() - |> render_many(InstanceView, "rule.json", as: :rule) + rules = + rule_ids + |> Rule.get() + + render(RuleView, "index.json", rules: rules) end end diff --git a/lib/pleroma/web/admin_api/views/rule_view.ex b/lib/pleroma/web/admin_api/views/rule_view.ex index f291452483..606443f051 100644 --- a/lib/pleroma/web/admin_api/views/rule_view.ex +++ b/lib/pleroma/web/admin_api/views/rule_view.ex @@ -13,9 +13,10 @@ def render("index.json", %{rules: rules} = _opts) do def render("show.json", %{rule: rule} = _opts) do %{ - id: rule.id, + id: to_string(rule.id), priority: rule.priority, - text: rule.text + text: rule.text, + hint: rule.hint } end end diff --git a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex index 47d3bb9360..d3b2b1ad4d 100644 --- a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex @@ -30,6 +30,12 @@ def index_operation do report_state(), "Filter by report state" ), + Operation.parameter( + :rule_id, + :query, + %Schema{type: :string}, + "Filter by selected rule id" + ), Operation.parameter( :limit, :query, @@ -200,8 +206,9 @@ defp report do items: %Schema{ type: :object, properties: %{ - id: %Schema{type: :integer}, - text: %Schema{type: :string} + id: %Schema{type: :string}, + text: %Schema{type: :string}, + hint: %Schema{type: :string, nullable: true} } } } diff --git a/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex b/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex index ed0d9eaf6d..c3a3ecc7c9 100644 --- a/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex @@ -84,7 +84,8 @@ defp create_request do required: [:text], properties: %{ priority: %Schema{type: :integer}, - text: %Schema{type: :string} + text: %Schema{type: :string}, + hint: %Schema{type: :string} } } end @@ -94,7 +95,8 @@ defp update_request do type: :object, properties: %{ priority: %Schema{type: :integer}, - text: %Schema{type: :string} + text: %Schema{type: :string}, + hint: %Schema{type: :string} } } end @@ -103,10 +105,10 @@ defp rule do %Schema{ type: :object, properties: %{ - id: %Schema{type: :integer}, + id: %Schema{type: :string}, priority: %Schema{type: :integer}, text: %Schema{type: :string}, - created_at: %Schema{type: :string, format: :"date-time"} + hint: %Schema{type: :string, nullable: true} } } end diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index 27ce85caac..c83cbfd1de 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -485,8 +485,9 @@ defp array_of_rules do items: %Schema{ type: :object, properties: %{ - id: %Schema{type: :integer}, - text: %Schema{type: :string} + id: %Schema{type: :string}, + text: %Schema{type: :string}, + hint: %Schema{type: :string} } } } diff --git a/lib/pleroma/web/api_spec/operations/report_operation.ex b/lib/pleroma/web/api_spec/operations/report_operation.ex index fd68f67a2e..f5f88974c6 100644 --- a/lib/pleroma/web/api_spec/operations/report_operation.ex +++ b/lib/pleroma/web/api_spec/operations/report_operation.ex @@ -57,7 +57,7 @@ defp create_request do rule_ids: %Schema{ type: :array, nullable: true, - items: %Schema{type: :number}, + items: %Schema{type: :string}, description: "Array of rules" } }, @@ -67,7 +67,7 @@ defp create_request do "status_ids" => ["1337"], "comment" => "bad status!", "forward" => "false", - "rule_ids" => [3] + "rule_ids" => ["3"] } } end diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 3df2a7bdd7..02490c0563 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -675,8 +675,7 @@ defp get_report_rules(nil) do defp get_report_rules(rule_ids) do rule_ids - |> Rule.get() - |> Enum.map(& &1.id) + |> Enum.filter(&Rule.exists?/1) end def update_report_state(activity_ids, state) when is_list(activity_ids) do diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 7db46648d6..1bd75dec8a 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -91,8 +91,9 @@ def render("rules.json", _) do def render("rule.json", %{rule: rule}) do %{ - id: rule.id, - text: rule.text + id: to_string(rule.id), + text: rule.text, + hint: rule.hint || "" } end @@ -187,10 +188,7 @@ defp common_information(instance) do title: Keyword.get(instance, :name), version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.compat_version()})", languages: Keyword.get(instance, :languages, ["en"]), - rules: render(__MODULE__, "rules.json"), - soapbox: %{ - version: Soapbox.version() - } + rules: render(__MODULE__, "rules.json") } end diff --git a/lib/pleroma/web/rich_media/parser/card.ex b/lib/pleroma/web/rich_media/parser/card.ex new file mode 100644 index 0000000000..5bd5823d1d --- /dev/null +++ b/lib/pleroma/web/rich_media/parser/card.ex @@ -0,0 +1,155 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser.Card do + alias Pleroma.Web.RichMedia.Parser.Card + alias Pleroma.Web.RichMedia.Parser.Embed + + @types ["link", "photo", "video", "rich"] + + # https://docs.joinmastodon.org/entities/card/ + defstruct url: nil, + title: nil, + description: "", + type: "link", + author_name: "", + author_url: "", + provider_name: "", + provider_url: "", + html: "", + width: 0, + height: 0, + image: nil, + image_description: "", + embed_url: "", + blurhash: nil + + def parse(%Embed{url: url, oembed: %{"type" => type, "title" => title} = oembed} = embed) + when type in @types and is_binary(url) do + uri = URI.parse(url) + + %Card{ + url: url, + title: title, + description: get_description(embed), + type: oembed["type"], + author_name: oembed["author_name"], + author_url: oembed["author_url"], + provider_name: oembed["provider_name"] || uri.host, + provider_url: oembed["provider_url"] || "#{uri.scheme}://#{uri.host}", + html: sanitize_html(oembed["html"]), + width: oembed["width"], + height: oembed["height"], + image: get_image(oembed) |> fix_uri(url) |> proxy(), + image_description: get_image_description(embed), + embed_url: oembed["url"] |> fix_uri(url) |> proxy() + } + |> IO.inspect + |> validate() + end + + def parse(%Embed{url: url} = embed) when is_binary(url) do + uri = URI.parse(url) + + %Card{ + url: url, + title: get_title(embed), + description: get_description(embed), + type: "link", + provider_name: uri.host, + provider_url: "#{uri.scheme}://#{uri.host}", + image: get_image(embed) |> fix_uri(url) |> proxy(), + image_description: get_image_description(embed), + } + |> validate() + end + + def parse(card), do: {:error, {:invalid_metadata, card}} + + defp get_title(embed) do + case embed do + %{meta: %{"twitter:title" => title}} when is_binary(title) and title != "" -> title + %{meta: %{"og:title" => title}} when is_binary(title) and title != "" -> title + %{title: title} when is_binary(title) and title != "" -> title + _ -> nil + end + end + + defp get_description(%{meta: meta}) do + case meta do + %{"twitter:description" => desc} when is_binary(desc) and desc != "" -> desc + %{"og:description" => desc} when is_binary(desc) and desc != "" -> desc + %{"description" => desc} when is_binary(desc) and desc != "" -> desc + _ -> "" + end + end + + defp get_image(%{meta: meta}) do + case meta do + %{"twitter:image" => image} when is_binary(image) and image != "" -> image + %{"og:image" => image} when is_binary(image) and image != "" -> image + _ -> "" + end + end + + defp get_image(%{"thumbnail_url" => image}) when is_binary(image) and image != "", do: image + defp get_image(%{"type" => "photo", "url" => image}), do: image + defp get_image(_), do: "" + + defp get_image_description(%{meta: %{"og:image:alt" => image_description}}), do: image_description + defp get_image_description(_), do: "" + + defp sanitize_html(html) do + with {:ok, html} <- FastSanitize.Sanitizer.scrub(html, Pleroma.HTML.Scrubber.OEmbed), + {:ok, [{"iframe", _, _}]} <- Floki.parse_fragment(html) do + html + else + _ -> "" + end + end + + def to_map(%Card{} = card) do + card + |> Map.from_struct() + |> stringify_keys() + end + + def to_map(%{} = card), do: stringify_keys(card) + + defp stringify_keys(%{} = map), do: Map.new(map, fn {k, v} -> {Atom.to_string(k), v} end) + + def fix_uri("http://" <> _ = uri, _base_uri), do: uri + def fix_uri("https://" <> _ = uri, _base_uri), do: uri + def fix_uri("/" <> _ = uri, base_uri), do: URI.merge(base_uri, uri) |> URI.to_string() + def fix_uri("", _base_uri), do: nil + + def fix_uri(uri, base_uri) when is_binary(uri), + do: URI.merge(base_uri, "/#{uri}") |> URI.to_string() + + def fix_uri(_uri, _base_uri), do: nil + + defp proxy(url) when is_binary(url), do: Pleroma.Web.MediaProxy.url(url) + defp proxy(_), do: nil + + def validate(%Card{type: type, html: html} = card) + when type in ["video", "rich"] and (is_binary(html) == false or html == "") do + card + |> Map.put(:type, "link") + |> validate() + end + + def validate(%Card{type: type, title: title} = card) + when type in @types and is_binary(title) and title != "" do + {:ok, card} + end + + def validate(%Embed{} = embed) do + case Card.parse(embed) do + {:ok, %Card{} = card} -> validate(card) + card -> {:error, {:invalid_metadata, card}} + end + end + + def validate(card), do: {:error, {:invalid_metadata, card}} +end diff --git a/lib/soapbox.ex b/lib/soapbox.ex deleted file mode 100644 index 88b597147d..0000000000 --- a/lib/soapbox.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Soapbox do - @version "3.0.0" - - def version, do: @version -end diff --git a/mix.exs b/mix.exs index f3b1cc3222..3610e38ea5 100644 --- a/mix.exs +++ b/mix.exs @@ -1,12 +1,12 @@ defmodule Pleroma.Mixfile do use Mix.Project - @build_name "soapbox" + @build_name "pl" def project do [ app: :pleroma, - name: "Rebased", + name: "pl", compat_name: "Pleroma", version: version("2.6.52"), elixir: "~> 1.11", diff --git a/priv/repo/migrations/20240406000000_add_hint_to_rules.exs b/priv/repo/migrations/20240406000000_add_hint_to_rules.exs new file mode 100644 index 0000000000..2732905602 --- /dev/null +++ b/priv/repo/migrations/20240406000000_add_hint_to_rules.exs @@ -0,0 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.AddHintToRules do + use Ecto.Migration + + def change do + alter table(:rules) do + add_if_not_exists(:hint, :text) + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/report_controller_test.exs b/test/pleroma/web/admin_api/controllers/report_controller_test.exs index 81632c9e9a..9fbb608c42 100644 --- a/test/pleroma/web/admin_api/controllers/report_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/report_controller_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do alias Pleroma.ModerationLog alias Pleroma.Repo alias Pleroma.ReportNote + alias Pleroma.Rule alias Pleroma.Web.CommonAPI setup do @@ -468,6 +469,34 @@ test "returns 403 when requested by anonymous" do "error" => "Invalid credentials." } end + + test "returns reports with specified role_id", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + + %{id: rule_id} = Rule.create(%{text: "Example rule"}) + + rule_id = to_string(rule_id) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "", + rule_ids: [rule_id] + }) + + {:ok, _report} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "" + }) + + response = + conn + |> get("/api/pleroma/admin/reports?rule_id=#{rule_id}") + |> json_response_and_validate_schema(:ok) + + assert %{"reports" => [%{"id" => ^report_id}]} = response + end end describe "POST /api/pleroma/admin/reports/assign_account" do diff --git a/test/pleroma/web/admin_api/controllers/rule_controller_test.exs b/test/pleroma/web/admin_api/controllers/rule_controller_test.exs index c5c72d293a..96b52b2722 100644 --- a/test/pleroma/web/admin_api/controllers/rule_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/rule_controller_test.exs @@ -27,6 +27,10 @@ test "sorts rules by priority", %{conn: conn} do %{id: id2} = Rule.create(%{text: "Second rule", priority: 2}) %{id: id3} = Rule.create(%{text: "Third rule", priority: 1}) + id1 = to_string(id1) + id2 = to_string(id2) + id3 = to_string(id3) + response = conn |> get("/api/pleroma/admin/rules") diff --git a/test/pleroma/web/admin_api/views/report_view_test.exs b/test/pleroma/web/admin_api/views/report_view_test.exs index f70ef4f865..6e155ef586 100644 --- a/test/pleroma/web/admin_api/views/report_view_test.exs +++ b/test/pleroma/web/admin_api/views/report_view_test.exs @@ -178,15 +178,17 @@ test "renders included rules" do user = insert(:user) other_user = insert(:user) - %{id: id, text: text} = Rule.create(%{text: "Example rule"}) + %{id: rule_id, text: text} = Rule.create(%{text: "Example rule"}) + + rule_id = to_string(rule_id) {:ok, activity} = CommonAPI.report(user, %{ account_id: other_user.id, - rule_ids: [id] + rule_ids: [rule_id] }) - assert %{rules: [%{id: ^id, text: ^text}]} = + assert %{rules: [%{id: ^rule_id, text: ^text}]} = ReportView.render("show.json", Report.extract_report_info(activity)) end 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 index 2617c93d5a..cde7941282 100644 --- a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs @@ -58,7 +58,6 @@ test "get instance information", %{conn: conn} do assert result["pleroma"]["vapid_public_key"] assert result["pleroma"]["stats"]["mau"] == 0 assert result["pleroma"]["oauth_consumer_strategies"] == [] - assert result["soapbox"]["version"] =~ "." assert email == from_config_email assert thumbnail == from_config_thumbnail @@ -105,17 +104,28 @@ test "get peers", %{conn: conn} do end test "get instance rules", %{conn: conn} do - Rule.create(%{text: "Example rule"}) - Rule.create(%{text: "Second rule"}) - Rule.create(%{text: "Third rule"}) + Rule.create(%{text: "Example rule", hint: "Rule description", priority: 1}) + Rule.create(%{text: "Third rule", priority: 2}) + Rule.create(%{text: "Second rule", priority: 1}) conn = get(conn, "/api/v1/instance") assert result = json_response_and_validate_schema(conn, 200) - rules = result["rules"] - - assert length(rules) == 3 + assert [ + %{ + "text" => "Example rule", + "hint" => "Rule description" + }, + %{ + "text" => "Second rule", + "hint" => "" + }, + %{ + "text" => "Third rule", + "hint" => "" + } + ] = result["rules"] end test "get instance configuration", %{conn: conn} do diff --git a/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs index da9c46ec69..4ab5d07715 100644 --- a/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs @@ -88,6 +88,8 @@ test "submit a report with rule_ids", %{ } do %{id: rule_id} = Rule.create(%{text: "There are no rules"}) + rule_id = to_string(rule_id) + assert %{"action_taken" => false, "id" => id} = conn |> put_req_header("content-type", "application/json") @@ -101,6 +103,23 @@ test "submit a report with rule_ids", %{ assert %Activity{data: %{"rules" => [^rule_id]}} = Activity.get_report(id) end + test "rules field is empty if provided wrong rule id", %{ + conn: conn, + target_user: target_user + } do + assert %{"id" => id} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{ + "account_id" => target_user.id, + "forward" => "false", + "rule_ids" => ["-1"] + }) + |> json_response_and_validate_schema(200) + + assert %Activity{data: %{"rules" => []}} = Activity.get_report(id) + end + test "account_id is required", %{ conn: conn, activity: activity