diff options
Diffstat (limited to 'lib')
24 files changed, 737 insertions, 451 deletions
| diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex index 4cc634727..84dccf7f3 100644 --- a/lib/mix/tasks/pleroma/benchmark.ex +++ b/lib/mix/tasks/pleroma/benchmark.ex @@ -27,7 +27,7 @@ defmodule Mix.Tasks.Pleroma.Benchmark do      })    end -  def run(["render_timeline", nickname]) do +  def run(["render_timeline", nickname | _] = args) do      start_pleroma()      user = Pleroma.User.get_by_nickname(nickname) @@ -37,33 +37,37 @@ defmodule Mix.Tasks.Pleroma.Benchmark do        |> Map.put("blocking_user", user)        |> Map.put("muting_user", user)        |> Map.put("user", user) -      |> Map.put("limit", 80) +      |> Map.put("limit", 4096)        |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()        |> Enum.reverse()      inputs = %{ -      "One activity" => Enum.take_random(activities, 1), -      "Ten activities" => Enum.take_random(activities, 10), -      "Twenty activities" => Enum.take_random(activities, 20), -      "Forty activities" => Enum.take_random(activities, 40), -      "Eighty activities" => Enum.take_random(activities, 80) +      "1 activity" => Enum.take_random(activities, 1), +      "10 activities" => Enum.take_random(activities, 10), +      "20 activities" => Enum.take_random(activities, 20), +      "40 activities" => Enum.take_random(activities, 40), +      "80 activities" => Enum.take_random(activities, 80)      } +    inputs = +      if Enum.at(args, 2) == "extended" do +        Map.merge(inputs, %{ +          "200 activities" => Enum.take_random(activities, 200), +          "500 activities" => Enum.take_random(activities, 500), +          "2000 activities" => Enum.take_random(activities, 2000), +          "4096 activities" => Enum.take_random(activities, 4096) +        }) +      else +        inputs +      end +      Benchee.run(        %{ -        "Parallel rendering" => fn activities -> -          Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ -            activities: activities, -            for: user, -            as: :activity -          }) -        end,          "Standart rendering" => fn activities ->            Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{              activities: activities,              for: user, -            as: :activity, -            parallel: false +            as: :activity            })          end        }, diff --git a/lib/mix/tasks/pleroma/docs.ex b/lib/mix/tasks/pleroma/docs.ex new file mode 100644 index 000000000..0d2663648 --- /dev/null +++ b/lib/mix/tasks/pleroma/docs.ex @@ -0,0 +1,42 @@ +defmodule Mix.Tasks.Pleroma.Docs do +  use Mix.Task +  import Mix.Pleroma + +  @shortdoc "Generates docs from descriptions.exs" +  @moduledoc """ +  Generates docs from `descriptions.exs`. + +  Supports two formats: `markdown` and `json`. + +  ## Generate Markdown docs + +  `mix pleroma.docs` + +  ## Generate JSON docs + +  `mix pleroma.docs json` +  """ + +  def run(["json"]) do +    do_run(Pleroma.Docs.JSON) +  end + +  def run(_) do +    do_run(Pleroma.Docs.Markdown) +  end + +  defp do_run(implementation) do +    start_pleroma() + +    with {descriptions, _paths} <- Mix.Config.eval!("config/description.exs"), +         {:ok, file_path} <- +           Pleroma.Docs.Generator.process( +             implementation, +             descriptions[:pleroma][:config_description] +           ) do +      type = if implementation == Pleroma.Docs.Markdown, do: "Markdown", else: "JSON" + +      Mix.shell().info([:green, "#{type} docs successfully generated to #{file_path}."]) +    end +  end +end diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index a7844c36b..ec558168a 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Activity do    use Ecto.Schema    alias Pleroma.Activity +  alias Pleroma.Activity.Queries    alias Pleroma.ActivityExpiration    alias Pleroma.Bookmark    alias Pleroma.Notification @@ -65,8 +66,8 @@ defmodule Pleroma.Activity do      timestamps()    end -  def with_joined_object(query) do -    join(query, :inner, [activity], o in Object, +  def with_joined_object(query, join_type \\ :inner) do +    join(query, join_type, [activity], o in Object,        on:          fragment(            "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", @@ -78,10 +79,10 @@ defmodule Pleroma.Activity do      )    end -  def with_preloaded_object(query) do +  def with_preloaded_object(query, join_type \\ :inner) do      query      |> has_named_binding?(:object) -    |> if(do: query, else: with_joined_object(query)) +    |> if(do: query, else: with_joined_object(query, join_type))      |> preload([activity, object: object], object: object)    end @@ -107,12 +108,9 @@ defmodule Pleroma.Activity do    def with_set_thread_muted_field(query, _), do: query    def get_by_ap_id(ap_id) do -    Repo.one( -      from( -        activity in Activity, -        where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id)) -      ) -    ) +    ap_id +    |> Queries.by_ap_id() +    |> Repo.one()    end    def get_bookmark(%Activity{} = activity, %User{} = user) do @@ -133,21 +131,10 @@ defmodule Pleroma.Activity do    end    def get_by_ap_id_with_object(ap_id) do -    Repo.one( -      from( -        activity in Activity, -        where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id)), -        left_join: o in Object, -        on: -          fragment( -            "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", -            o.data, -            activity.data, -            activity.data -          ), -        preload: [object: o] -      ) -    ) +    ap_id +    |> Queries.by_ap_id() +    |> with_preloaded_object(:left) +    |> Repo.one()    end    def get_by_id(id) do @@ -158,66 +145,34 @@ defmodule Pleroma.Activity do    end    def get_by_id_with_object(id) do -    from(activity in Activity, -      where: activity.id == ^id, -      inner_join: o in Object, -      on: -        fragment( -          "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", -          o.data, -          activity.data, -          activity.data -        ), -      preload: [object: o] -    ) +    Activity +    |> where(id: ^id) +    |> with_preloaded_object()      |> Repo.one()    end -  def by_object_ap_id(ap_id) do -    from( -      activity in Activity, -      where: -        fragment( -          "coalesce((?)->'object'->>'id', (?)->>'object') = ?", -          activity.data, -          activity.data, -          ^to_string(ap_id) -        ) -    ) -  end - -  def create_by_object_ap_id(ap_ids) when is_list(ap_ids) do -    from( -      activity in Activity, -      where: -        fragment( -          "coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)", -          activity.data, -          activity.data, -          ^ap_ids -        ), -      where: fragment("(?)->>'type' = 'Create'", activity.data) -    ) +  def all_by_ids_with_object(ids) do +    Activity +    |> where([a], a.id in ^ids) +    |> with_preloaded_object() +    |> Repo.all()    end -  def create_by_object_ap_id(ap_id) when is_binary(ap_id) do -    from( -      activity in Activity, -      where: -        fragment( -          "coalesce((?)->'object'->>'id', (?)->>'object') = ?", -          activity.data, -          activity.data, -          ^to_string(ap_id) -        ), -      where: fragment("(?)->>'type' = 'Create'", activity.data) -    ) +  @doc """ +  Accepts `ap_id` or list of `ap_id`. +  Returns a query. +  """ +  @spec create_by_object_ap_id(String.t() | [String.t()]) :: Ecto.Queryable.t() +  def create_by_object_ap_id(ap_id) do +    ap_id +    |> Queries.by_object_id() +    |> Queries.by_type("Create")    end -  def create_by_object_ap_id(_), do: nil -    def get_all_create_by_object_ap_id(ap_id) do -    Repo.all(create_by_object_ap_id(ap_id)) +    ap_id +    |> create_by_object_ap_id() +    |> Repo.all()    end    def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do @@ -228,54 +183,17 @@ defmodule Pleroma.Activity do    def get_create_by_object_ap_id(_), do: nil -  def create_by_object_ap_id_with_object(ap_ids) when is_list(ap_ids) do -    from( -      activity in Activity, -      where: -        fragment( -          "coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)", -          activity.data, -          activity.data, -          ^ap_ids -        ), -      where: fragment("(?)->>'type' = 'Create'", activity.data), -      inner_join: o in Object, -      on: -        fragment( -          "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", -          o.data, -          activity.data, -          activity.data -        ), -      preload: [object: o] -    ) -  end - -  def create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do -    from( -      activity in Activity, -      where: -        fragment( -          "coalesce((?)->'object'->>'id', (?)->>'object') = ?", -          activity.data, -          activity.data, -          ^to_string(ap_id) -        ), -      where: fragment("(?)->>'type' = 'Create'", activity.data), -      inner_join: o in Object, -      on: -        fragment( -          "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", -          o.data, -          activity.data, -          activity.data -        ), -      preload: [object: o] -    ) +  @doc """ +  Accepts `ap_id` or list of `ap_id`. +  Returns a query. +  """ +  @spec create_by_object_ap_id_with_object(String.t() | [String.t()]) :: Ecto.Queryable.t() +  def create_by_object_ap_id_with_object(ap_id) do +    ap_id +    |> create_by_object_ap_id() +    |> with_preloaded_object()    end -  def create_by_object_ap_id_with_object(_), do: nil -    def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do      ap_id      |> create_by_object_ap_id_with_object() @@ -299,7 +217,8 @@ defmodule Pleroma.Activity do    def normalize(_), do: nil    def delete_by_ap_id(id) when is_binary(id) do -    by_object_ap_id(id) +    id +    |> Queries.by_object_id()      |> select([u], u)      |> Repo.delete_all()      |> elem(1) @@ -308,10 +227,19 @@ defmodule Pleroma.Activity do        %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id        _ -> nil      end) +    |> purge_web_resp_cache()    end    def delete_by_ap_id(_), do: nil +  defp purge_web_resp_cache(%Activity{} = activity) do +    %{path: path} = URI.parse(activity.data["id"]) +    Cachex.del(:web_resp_cache, path) +    activity +  end + +  defp purge_web_resp_cache(nil), do: nil +    for {ap_type, type} <- @mastodon_notification_types do      def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),        do: unquote(type) @@ -334,31 +262,10 @@ defmodule Pleroma.Activity do    end    def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do -    from( -      a in Activity, -      where: -        fragment( -          "? ->> 'type' = 'Follow'", -          a.data -        ), -      where: -        fragment( -          "? ->> 'state' = 'pending'", -          a.data -        ), -      where: -        fragment( -          "coalesce((?)->'object'->>'id', (?)->>'object') = ?", -          a.data, -          a.data, -          ^ap_id -        ) -    ) -  end - -  @spec query_by_actor(actor()) :: Ecto.Query.t() -  def query_by_actor(actor) do -    from(a in Activity, where: a.actor == ^actor) +    ap_id +    |> Queries.by_object_id() +    |> Queries.by_type("Follow") +    |> where([a], fragment("? ->> 'state' = 'pending'", a.data))    end    def restrict_deactivated_users(query) do diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex index aa5b29566..13fa33831 100644 --- a/lib/pleroma/activity/queries.ex +++ b/lib/pleroma/activity/queries.ex @@ -13,6 +13,14 @@ defmodule Pleroma.Activity.Queries do    alias Pleroma.Activity +  @spec by_ap_id(query, String.t()) :: query +  def by_ap_id(query \\ Activity, ap_id) do +    from( +      activity in query, +      where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id)) +    ) +  end +    @spec by_actor(query, String.t()) :: query    def by_actor(query \\ Activity, actor) do      from( @@ -21,8 +29,23 @@ defmodule Pleroma.Activity.Queries do      )    end -  @spec by_object_id(query, String.t()) :: query -  def by_object_id(query \\ Activity, object_id) do +  @spec by_object_id(query, String.t() | [String.t()]) :: query +  def by_object_id(query \\ Activity, object_id) + +  def by_object_id(query, object_ids) when is_list(object_ids) do +    from( +      activity in query, +      where: +        fragment( +          "coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)", +          activity.data, +          activity.data, +          ^object_ids +        ) +    ) +  end + +  def by_object_id(query, object_id) when is_binary(object_id) do      from(activity in query,        where:          fragment( @@ -41,9 +64,4 @@ defmodule Pleroma.Activity.Queries do        where: fragment("(?)->>'type' = ?", activity.data, ^activity_type)      )    end - -  @spec limit(query, pos_integer()) :: query -  def limit(query \\ Activity, limit) do -    from(activity in query, limit: ^limit) -  end  end diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 483ac1f39..1d46925f8 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -116,7 +116,8 @@ defmodule Pleroma.Application do        build_cachex("object", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),        build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),        build_cachex("scrubber", limit: 2500), -      build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500) +      build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500), +      build_cachex("web_resp", limit: 2500)      ]    end diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex new file mode 100644 index 000000000..aa578eee2 --- /dev/null +++ b/lib/pleroma/docs/generator.ex @@ -0,0 +1,73 @@ +defmodule Pleroma.Docs.Generator do +  @callback process(keyword()) :: {:ok, String.t()} + +  @spec process(module(), keyword()) :: {:ok, String.t()} +  def process(implementation, descriptions) do +    implementation.process(descriptions) +  end + +  @spec uploaders_list() :: [module()] +  def uploaders_list do +    {:ok, modules} = :application.get_key(:pleroma, :modules) + +    Enum.filter(modules, fn module -> +      name_as_list = Module.split(module) + +      List.starts_with?(name_as_list, ["Pleroma", "Uploaders"]) and +        List.last(name_as_list) != "Uploader" +    end) +  end + +  @spec filters_list() :: [module()] +  def filters_list do +    {:ok, modules} = :application.get_key(:pleroma, :modules) + +    Enum.filter(modules, fn module -> +      name_as_list = Module.split(module) + +      List.starts_with?(name_as_list, ["Pleroma", "Upload", "Filter"]) +    end) +  end + +  @spec mrf_list() :: [module()] +  def mrf_list do +    {:ok, modules} = :application.get_key(:pleroma, :modules) + +    Enum.filter(modules, fn module -> +      name_as_list = Module.split(module) + +      List.starts_with?(name_as_list, ["Pleroma", "Web", "ActivityPub", "MRF"]) and +        length(name_as_list) > 4 +    end) +  end + +  @spec richmedia_parsers() :: [module()] +  def richmedia_parsers do +    {:ok, modules} = :application.get_key(:pleroma, :modules) + +    Enum.filter(modules, fn module -> +      name_as_list = Module.split(module) + +      List.starts_with?(name_as_list, ["Pleroma", "Web", "RichMedia", "Parsers"]) and +        length(name_as_list) == 5 +    end) +  end +end + +defimpl Jason.Encoder, for: Tuple do +  def encode(tuple, opts) do +    Jason.Encode.list(Tuple.to_list(tuple), opts) +  end +end + +defimpl Jason.Encoder, for: [Regex, Function] do +  def encode(term, opts) do +    Jason.Encode.string(inspect(term), opts) +  end +end + +defimpl String.Chars, for: Regex do +  def to_string(term) do +    inspect(term) +  end +end diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex new file mode 100644 index 000000000..18ba01d58 --- /dev/null +++ b/lib/pleroma/docs/json.ex @@ -0,0 +1,20 @@ +defmodule Pleroma.Docs.JSON do +  @behaviour Pleroma.Docs.Generator + +  @spec process(keyword()) :: {:ok, String.t()} +  def process(descriptions) do +    config_path = "docs/generate_config.json" + +    with {:ok, file} <- File.open(config_path, [:write]), +         json <- generate_json(descriptions), +         :ok <- IO.write(file, json), +         :ok <- File.close(file) do +      {:ok, config_path} +    end +  end + +  @spec generate_json([keyword()]) :: String.t() +  def generate_json(descriptions) do +    Jason.encode!(descriptions) +  end +end diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex new file mode 100644 index 000000000..8386dc2fb --- /dev/null +++ b/lib/pleroma/docs/markdown.ex @@ -0,0 +1,78 @@ +defmodule Pleroma.Docs.Markdown do +  @behaviour Pleroma.Docs.Generator + +  @spec process(keyword()) :: {:ok, String.t()} +  def process(descriptions) do +    config_path = "docs/generated_config.md" +    {:ok, file} = File.open(config_path, [:utf8, :write]) +    IO.write(file, "# Generated configuration\n") +    IO.write(file, "Date of generation: #{Date.utc_today()}\n\n") + +    IO.write( +      file, +      "This file describe the configuration, it is recommended to edit the relevant `*.secret.exs` file instead of the others founds in the ``config`` directory.\n\n" <> +        "If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherwise it is ``dev.secret.exs``.\n\n" +    ) + +    for group <- descriptions do +      if is_nil(group[:key]) do +        IO.write(file, "## #{inspect(group[:group])}\n") +      else +        IO.write(file, "## #{inspect(group[:key])}\n") +      end + +      IO.write(file, "#{group[:description]}\n") + +      for child <- group[:children] do +        print_child_header(file, child) + +        print_suggestions(file, child[:suggestions]) + +        if child[:children] do +          for subchild <- child[:children] do +            print_child_header(file, subchild) + +            print_suggestions(file, subchild[:suggestions]) +          end +        end +      end + +      IO.write(file, "\n") +    end + +    :ok = File.close(file) +    {:ok, config_path} +  end + +  defp print_suggestion(file, suggestion) when is_list(suggestion) do +    IO.write(file, "  `#{inspect(suggestion)}`\n") +  end + +  defp print_suggestion(file, suggestion) when is_function(suggestion) do +    IO.write(file, "  `#{inspect(suggestion.())}`\n") +  end + +  defp print_suggestion(file, suggestion, as_list \\ false) do +    list_mark = if as_list, do: "- ", else: "" +    IO.write(file, "  #{list_mark}`#{inspect(suggestion)}`\n") +  end + +  defp print_suggestions(_file, nil), do: nil + +  defp print_suggestions(file, suggestions) do +    IO.write(file, "Suggestions:\n") + +    if length(suggestions) > 1 do +      for suggestion <- suggestions do +        print_suggestion(file, suggestion, true) +      end +    else +      print_suggestion(file, List.first(suggestions)) +    end +  end + +  defp print_child_header(file, child) do +    IO.write(file, "- `#{inspect(child[:key])}` -`#{inspect(child[:type])}`  \n") +    IO.write(file, "#{child[:description]}  \n") +  end +end diff --git a/lib/healthcheck.ex b/lib/pleroma/healthcheck.ex index f97d14432..977b78c26 100644 --- a/lib/healthcheck.ex +++ b/lib/pleroma/healthcheck.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Healthcheck do    alias Pleroma.Healthcheck    alias Pleroma.Repo +  @derive Jason.Encoder    defstruct pool_size: 0,              active: 0,              idle: 0, diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index d58eb7f7d..5033798ae 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -130,14 +130,16 @@ defmodule Pleroma.Object do    def delete(%Object{data: %{"id" => id}} = object) do      with {:ok, _obj} = swap_object_with_tombstone(object),           deleted_activity = Activity.delete_by_ap_id(id), -         {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do +         {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"), +         {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do        {:ok, object, deleted_activity}      end    end    def prune(%Object{data: %{"id" => id}} = object) do      with {:ok, object} <- Repo.delete(object), -         {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do +         {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"), +         {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do        {:ok, object}      end    end diff --git a/lib/pleroma/plugs/cache.ex b/lib/pleroma/plugs/cache.ex new file mode 100644 index 000000000..a81a861d0 --- /dev/null +++ b/lib/pleroma/plugs/cache.ex @@ -0,0 +1,122 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.Cache do +  @moduledoc """ +  Caches successful GET responses. + +  To enable the cache add the plug to a router pipeline or controller: + +      plug(Pleroma.Plugs.Cache) + +  ## Configuration + +  To configure the plug you need to pass settings as the second argument to the `plug/2` macro: + +      plug(Pleroma.Plugs.Cache, [ttl: nil, query_params: true]) + +  Available options: + +  - `ttl`:  An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`. +  - `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`. + +  Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct: + +      def index(conn, _params) do +        ttl = 60_000 # one minute + +        conn +        |> assign(:cache_ttl, ttl) +        |> render("index.html") +      end + +  """ + +  import Phoenix.Controller, only: [current_path: 1, json: 2] +  import Plug.Conn + +  @behaviour Plug + +  @defaults %{ttl: nil, query_params: true} + +  @impl true +  def init([]), do: @defaults + +  def init(opts) do +    opts = Map.new(opts) +    Map.merge(@defaults, opts) +  end + +  @impl true +  def call(%{method: "GET"} = conn, opts) do +    key = cache_key(conn, opts) + +    case Cachex.get(:web_resp_cache, key) do +      {:ok, nil} -> +        cache_resp(conn, opts) + +      {:ok, record} -> +        send_cached(conn, record) + +      {atom, message} when atom in [:ignore, :error] -> +        render_error(conn, message) +    end +  end + +  def call(conn, _), do: conn + +  # full path including query params +  defp cache_key(conn, %{query_params: true}), do: current_path(conn) + +  # request path without query params +  defp cache_key(conn, %{query_params: false}), do: conn.request_path + +  # request path with specific query params +  defp cache_key(conn, %{query_params: query_params}) when is_list(query_params) do +    query_string = +      conn.params +      |> Map.take(query_params) +      |> URI.encode_query() + +    conn.request_path <> "?" <> query_string +  end + +  defp cache_resp(conn, opts) do +    register_before_send(conn, fn +      %{status: 200, resp_body: body} = conn -> +        ttl = Map.get(conn.assigns, :cache_ttl, opts.ttl) +        key = cache_key(conn, opts) +        content_type = content_type(conn) +        record = {content_type, body} + +        Cachex.put(:web_resp_cache, key, record, ttl: ttl) + +        put_resp_header(conn, "x-cache", "MISS from Pleroma") + +      conn -> +        conn +    end) +  end + +  defp content_type(conn) do +    conn +    |> Plug.Conn.get_resp_header("content-type") +    |> hd() +  end + +  defp send_cached(conn, {content_type, body}) do +    conn +    |> put_resp_content_type(content_type, nil) +    |> put_resp_header("x-cache", "HIT from Pleroma") +    |> send_resp(:ok, body) +    |> halt() +  end + +  defp render_error(conn, message) do +    conn +    |> put_status(:internal_server_error) +    |> json(%{error: message}) +    |> halt() +  end +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 3aa245f2a..64b69e686 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -174,11 +174,25 @@ defmodule Pleroma.User do      |> Repo.aggregate(:count, :id)    end +  defp truncate_if_exists(params, key, max_length) do +    if Map.has_key?(params, key) and is_binary(params[key]) do +      {value, _chopped} = String.split_at(params[key], max_length) +      Map.put(params, key, value) +    else +      params +    end +  end +    def remote_user_creation(params) do      bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)      name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) -    params = Map.put(params, :info, params[:info] || %{}) +    params = +      params +      |> Map.put(:info, params[:info] || %{}) +      |> truncate_if_exists(:name, name_limit) +      |> truncate_if_exists(:bio, bio_limit) +      info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])      changes = @@ -1219,7 +1233,7 @@ defmodule Pleroma.User do    def delete_user_activities(%User{ap_id: ap_id} = user) do      ap_id -    |> Activity.query_by_actor() +    |> Activity.Queries.by_actor()      |> RepoStreamer.chunk_stream(50)      |> Stream.each(fn activities ->        Enum.each(activities, &delete_activity(&1)) @@ -1624,4 +1638,13 @@ defmodule Pleroma.User do    def is_internal_user?(%User{nickname: nil}), do: true    def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true    def is_internal_user?(_), do: false + +  def change_email(user, email) do +    user +    |> cast(%{email: email}, [:email]) +    |> validate_required([:email]) +    |> unique_constraint(:email) +    |> validate_format(:email, @email_regex) +    |> update_and_set_cache() +  end  end diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 779bfbc18..151e025de 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -242,6 +242,13 @@ defmodule Pleroma.User.Info do    end    def remote_user_creation(info, params) do +    params = +      if Map.has_key?(params, :fields) do +        Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1)) +      else +        params +      end +      info      |> cast(params, [        :ap_enabled, @@ -326,6 +333,16 @@ defmodule Pleroma.User.Info do    defp valid_field?(_), do: false +  defp truncate_field(%{"name" => name, "value" => value}) do +    {name, _chopped} = +      String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255)) + +    {value, _chopped} = +      String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255)) + +    %{"name" => name, "value" => value} +  end +    @spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t()    def confirmation_changeset(info, opts) do      need_confirmation? = Keyword.get(opts, :need_confirmation) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 7047b8254..4dc01a85b 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -23,6 +23,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    action_fallback(:errors) +  plug(Pleroma.Plugs.Cache, [query_params: false] when action in [:activity, :object]) +    plug(      Pleroma.Plugs.OAuthScopesPlug,      %{scopes: ["read:accounts"]} when action in [:followers, :following] @@ -58,8 +60,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do           %Object{} = object <- Object.get_cached_by_ap_id(ap_id),           {_, true} <- {:public?, Visibility.is_public?(object)} do        conn +      |> set_cache_ttl_for(object)        |> put_resp_content_type("application/activity+json") -      |> json(ObjectView.render("object.json", %{object: object})) +      |> put_view(ObjectView) +      |> render("object.json", object: object)      else        {:public?, false} ->          {:error, :not_found} @@ -101,14 +105,36 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do           %Activity{} = activity <- Activity.normalize(ap_id),           {_, true} <- {:public?, Visibility.is_public?(activity)} do        conn +      |> set_cache_ttl_for(activity)        |> put_resp_content_type("application/activity+json") -      |> json(ObjectView.render("object.json", %{object: activity})) +      |> put_view(ObjectView) +      |> render("object.json", object: activity)      else -      {:public?, false} -> -        {:error, :not_found} +      {:public?, false} -> {:error, :not_found} +      nil -> {:error, :not_found}      end    end +  defp set_cache_ttl_for(conn, %Activity{object: object}) do +    set_cache_ttl_for(conn, object) +  end + +  defp set_cache_ttl_for(conn, entity) do +    ttl = +      case entity do +        %Object{data: %{"type" => "Question"}} -> +          Pleroma.Config.get([:web_cache_ttl, :activity_pub_question]) + +        %Object{} -> +          Pleroma.Config.get([:web_cache_ttl, :activity_pub]) + +        _ -> +          nil +      end + +    assign(conn, :cache_ttl, ttl) +  end +    # GET /relay/following    def following(%{assigns: %{relay: true}} = conn, _params) do      conn @@ -256,22 +282,36 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    def whoami(_conn, _params), do: {:error, :not_found} -  def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do -    if nickname == user.nickname do -      conn -      |> put_resp_content_type("application/activity+json") -      |> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]})) -    else -      err = -        dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}", -          nickname: nickname, -          as_nickname: user.nickname -        ) +  def read_inbox( +        %{assigns: %{user: %{nickname: nickname} = user}} = conn, +        %{"nickname" => nickname} = params +      ) do +    conn +    |> put_resp_content_type("application/activity+json") +    |> put_view(UserView) +    |> render("inbox.json", user: user, max_id: params["max_id"]) +  end -      conn -      |> put_status(:forbidden) -      |> json(err) -    end +  def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do +    err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname) + +    conn +    |> put_status(:forbidden) +    |> json(err) +  end + +  def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{ +        "nickname" => nickname +      }) do +    err = +      dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}", +        nickname: nickname, +        as_nickname: as_nickname +      ) + +    conn +    |> put_status(:forbidden) +    |> json(err)    end    def handle_user_activity(user, %{"type" => "Create"} = params) do diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 468961bd0..350b83abb 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -185,12 +185,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do              |> Map.put("context", replied_object.data["context"] || object["conversation"])            else              e -> -              Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") +              Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")                object            end          e -> -          Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") +          Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")            object        end      else diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index c9c0c3763..47917f5d3 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -85,15 +85,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do    defp extract_list(_), do: []    def maybe_splice_recipient(ap_id, params) do -    need_splice = +    need_splice? =        !recipient_in_collection(ap_id, params["to"]) &&          !recipient_in_collection(ap_id, params["cc"]) -    cc_list = extract_list(params["cc"]) - -    if need_splice do -      params -      |> Map.put("cc", [ap_id | cc_list]) +    if need_splice? do +      cc_list = extract_list(params["cc"]) +      Map.put(params, "cc", [ap_id | cc_list])      else        params      end @@ -139,7 +137,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do        "object" => object      } -    Notification.get_notified_from_activity(%Activity{data: fake_create_activity}, false) +    get_notified_from_object(fake_create_activity)    end    def get_notified_from_object(object) do @@ -188,9 +186,9 @@ defmodule Pleroma.Web.ActivityPub.Utils do    Adds an id and a published data if they aren't there,    also adds it to an included object    """ -  def lazy_put_activity_defaults(map, fake \\ false) do +  def lazy_put_activity_defaults(map, fake? \\ false) do      map = -      unless fake do +      if not fake? do          %{data: %{"id" => context}, id: context_id} = create_context(map["context"])          map @@ -207,7 +205,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do        end      if is_map(map["object"]) do -      object = lazy_put_object_defaults(map["object"], map, fake) +      object = lazy_put_object_defaults(map["object"], map, fake?)        %{map | "object" => object}      else        map @@ -217,9 +215,9 @@ defmodule Pleroma.Web.ActivityPub.Utils do    @doc """    Adds an id and published date if they aren't there.    """ -  def lazy_put_object_defaults(map, activity \\ %{}, fake) +  def lazy_put_object_defaults(map, activity \\ %{}, fake?) -  def lazy_put_object_defaults(map, activity, true = _fake) do +  def lazy_put_object_defaults(map, activity, true = _fake?) do      map      |> Map.put_new_lazy("published", &make_date/0)      |> Map.put_new("id", "pleroma:fake_object_id") @@ -228,7 +226,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do      |> Map.put_new("context_id", activity["context_id"])    end -  def lazy_put_object_defaults(map, activity, _fake) do +  def lazy_put_object_defaults(map, activity, _fake?) do      map      |> Map.put_new_lazy("id", &generate_object_id/0)      |> Map.put_new_lazy("published", &make_date/0) @@ -242,9 +240,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do    def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)        when is_map(object_data) and type in @supported_object_types do      with {:ok, object} <- Object.create(object_data) do -      map = -        map -        |> Map.put("object", object.data["id"]) +      map = Map.put(map, "object", object.data["id"])        {:ok, map, object}      end @@ -263,7 +259,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do      |> Activity.Queries.by_actor()      |> Activity.Queries.by_object_id(id)      |> Activity.Queries.by_type("Like") -    |> Activity.Queries.limit(1) +    |> limit(1)      |> Repo.one()    end @@ -380,12 +376,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do          %Activity{data: %{"actor" => actor, "object" => object}} = activity,          state        ) do -    with new_data <- -           activity.data -           |> Map.put("state", state), -         changeset <- Changeset.change(activity, data: new_data), -         {:ok, activity} <- Repo.update(changeset), -         _ <- User.set_follow_state_cache(actor, object, state) do +    new_data = Map.put(activity.data, "state", state) +    changeset = Changeset.change(activity, data: new_data) + +    with {:ok, activity} <- Repo.update(changeset) do +      User.set_follow_state_cache(actor, object, state)        {:ok, activity}      end    end @@ -410,28 +405,14 @@ defmodule Pleroma.Web.ActivityPub.Utils do    end    def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do -    query = -      from( -        activity in Activity, -        where: -          fragment( -            "? ->> 'type' = 'Follow'", -            activity.data -          ), -        where: activity.actor == ^follower_id, -        # this is to use the index -        where: -          fragment( -            "coalesce((?)->'object'->>'id', (?)->>'object') = ?", -            activity.data, -            activity.data, -            ^followed_id -          ), -        order_by: [fragment("? desc nulls last", activity.id)], -        limit: 1 -      ) - -    Repo.one(query) +    "Follow" +    |> Activity.Queries.by_type() +    |> where(actor: ^follower_id) +    # this is to use the index +    |> Activity.Queries.by_object_id(followed_id) +    |> order_by([activity], fragment("? desc nulls last", activity.id)) +    |> limit(1) +    |> Repo.one()    end    #### Announce-related helpers @@ -439,23 +420,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do    @doc """    Retruns an existing announce activity if the notice has already been announced    """ -  def get_existing_announce(actor, %{data: %{"id" => id}}) do -    query = -      from( -        activity in Activity, -        where: activity.actor == ^actor, -        # this is to use the index -        where: -          fragment( -            "coalesce((?)->'object'->>'id', (?)->>'object') = ?", -            activity.data, -            activity.data, -            ^id -          ), -        where: fragment("(?)->>'type' = 'Announce'", activity.data) -      ) - -    Repo.one(query) +  def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do +    "Announce" +    |> Activity.Queries.by_type() +    |> where(actor: ^actor) +    # this is to use the index +    |> Activity.Queries.by_object_id(ap_id) +    |> Repo.one()    end    @doc """ @@ -538,11 +509,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do          object        ) do      announcements = -      if is_list(object.data["announcements"]), do: object.data["announcements"], else: [] +      if is_list(object.data["announcements"]) do +        Enum.uniq([actor | object.data["announcements"]]) +      else +        [actor] +      end -    with announcements <- [actor | announcements] |> Enum.uniq() do -      update_element_in_object("announcement", announcements, object) -    end +    update_element_in_object("announcement", announcements, object)    end    def add_announce_to_object(_, object), do: {:ok, object} @@ -570,28 +543,14 @@ defmodule Pleroma.Web.ActivityPub.Utils do    #### Block-related helpers    def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do -    query = -      from( -        activity in Activity, -        where: -          fragment( -            "? ->> 'type' = 'Block'", -            activity.data -          ), -        where: activity.actor == ^blocker_id, -        # this is to use the index -        where: -          fragment( -            "coalesce((?)->'object'->>'id', (?)->>'object') = ?", -            activity.data, -            activity.data, -            ^blocked_id -          ), -        order_by: [fragment("? desc nulls last", activity.id)], -        limit: 1 -      ) - -    Repo.one(query) +    "Block" +    |> Activity.Queries.by_type() +    |> where(actor: ^blocker_id) +    # this is to use the index +    |> Activity.Queries.by_object_id(blocked_id) +    |> order_by([activity], fragment("? desc nulls last", activity.id)) +    |> limit(1) +    |> Repo.one()    end    def make_block_data(blocker, blocked, activity_id) do @@ -695,11 +654,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do    #### Report-related helpers    def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do -    with new_data <- Map.put(activity.data, "state", state), -         changeset <- Changeset.change(activity, data: new_data), -         {:ok, activity} <- Repo.update(changeset) do -      {:ok, activity} -    end +    new_data = Map.put(activity.data, "state", state) + +    activity +    |> Changeset.change(data: new_data) +    |> Repo.update()    end    def update_report_state(_, _), do: {:error, "Unsupported state"} @@ -766,21 +725,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do    end    def get_existing_votes(actor, %{data: %{"id" => id}}) do -    query = -      from( -        [activity, object: object] in Activity.with_preloaded_object(Activity), -        where: fragment("(?)->>'type' = 'Create'", activity.data), -        where: fragment("(?)->>'actor' = ?", activity.data, ^actor), -        where: -          fragment( -            "(?)->>'inReplyTo' = ?", -            object.data, -            ^to_string(id) -          ), -        where: fragment("(?)->>'type' = 'Answer'", object.data) -      ) - -    Repo.all(query) +    actor +    |> Activity.Queries.by_actor() +    |> Activity.Queries.by_type("Create") +    |> Activity.with_preloaded_object() +    |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id))) +    |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data)) +    |> Repo.all()    end    defp maybe_put(map, _key, nil), do: map diff --git a/lib/pleroma/web/admin_api/config.ex b/lib/pleroma/web/admin_api/config.ex index a10cc779b..1917a5580 100644 --- a/lib/pleroma/web/admin_api/config.ex +++ b/lib/pleroma/web/admin_api/config.ex @@ -90,6 +90,8 @@ defmodule Pleroma.Web.AdminAPI.Config do      for v <- entity, into: [], do: do_convert(v)    end +  defp do_convert(%Regex{} = entity), do: inspect(entity) +    defp do_convert(entity) when is_map(entity) do      for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)}    end @@ -122,7 +124,7 @@ defmodule Pleroma.Web.AdminAPI.Config do    def transform(entity), do: :erlang.term_to_binary(entity) -  defp do_transform(%Regex{} = entity) when is_map(entity), do: entity +  defp do_transform(%Regex{} = entity), do: entity    defp do_transform(%{"tuple" => [":dispatch", [entity]]}) do      {dispatch_settings, []} = do_eval(entity) @@ -154,8 +156,15 @@ defmodule Pleroma.Web.AdminAPI.Config do    defp do_transform(entity), do: entity    defp do_transform_string("~r/" <> pattern) do -    pattern = String.trim_trailing(pattern, "/") -    ~r/#{pattern}/ +    modificator = String.split(pattern, "/") |> List.last() +    pattern = String.trim_trailing(pattern, "/" <> modificator) + +    case modificator do +      "" -> ~r/#{pattern}/ +      "i" -> ~r/#{pattern}/i +      "u" -> ~r/#{pattern}/u +      "s" -> ~r/#{pattern}/s +    end    end    defp do_transform_string(":" <> atom), do: String.to_atom(atom) diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index eeac9f503..b53a01955 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -34,79 +34,38 @@ defmodule Pleroma.Web.ControllerHelper do    defp param_to_integer(_, default), do: default -  def add_link_headers( -        conn, -        method, -        activities, -        param \\ nil, -        params \\ %{}, -        func3 \\ nil, -        func4 \\ nil -      ) do -    params = -      conn.params -      |> Map.drop(["since_id", "max_id", "min_id"]) -      |> Map.merge(params) +  def add_link_headers(conn, activities, extra_params \\ %{}) do +    case List.last(activities) do +      %{id: max_id} -> +        params = +          conn.params +          |> Map.drop(Map.keys(conn.path_params)) +          |> Map.drop(["since_id", "max_id", "min_id"]) +          |> Map.merge(extra_params) -    last = List.last(activities) +        limit = +          params +          |> Map.get("limit", "20") +          |> String.to_integer() -    func3 = func3 || (&mastodon_api_url/3) -    func4 = func4 || (&mastodon_api_url/4) +        min_id = +          if length(activities) <= limit do +            activities +            |> List.first() +            |> Map.get(:id) +          else +            activities +            |> Enum.at(limit * -1) +            |> Map.get(:id) +          end -    if last do -      max_id = last.id +        next_url = current_url(conn, Map.merge(params, %{max_id: max_id})) +        prev_url = current_url(conn, Map.merge(params, %{min_id: min_id})) -      limit = -        params -        |> Map.get("limit", "20") -        |> String.to_integer() +        put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") -      min_id = -        if length(activities) <= limit do -          activities -          |> List.first() -          |> Map.get(:id) -        else -          activities -          |> Enum.at(limit * -1) -          |> Map.get(:id) -        end - -      {next_url, prev_url} = -        if param do -          { -            func4.( -              Pleroma.Web.Endpoint, -              method, -              param, -              Map.merge(params, %{max_id: max_id}) -            ), -            func4.( -              Pleroma.Web.Endpoint, -              method, -              param, -              Map.merge(params, %{min_id: min_id}) -            ) -          } -        else -          { -            func3.( -              Pleroma.Web.Endpoint, -              method, -              Map.merge(params, %{max_id: max_id}) -            ), -            func3.( -              Pleroma.Web.Endpoint, -              method, -              Map.merge(params, %{min_id: min_id}) -            ) -          } -        end - -      conn -      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") -    else -      conn +      _ -> +        conn      end    end  end diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex index 704664f5f..c5632bb5e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    use Pleroma.Web, :controller    import Pleroma.Web.ControllerHelper, -    only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3] +    only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]    alias Ecto.Changeset    alias Pleroma.Activity @@ -101,7 +101,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    plug(      OAuthScopesPlug,      %{@unauthenticated_access | scopes: ["read:statuses"]} -    when action in [:user_statuses, :get_status, :get_context, :status_card, :get_poll] +    when action in [ +           :user_statuses, +           :get_statuses, +           :get_status, +           :get_context, +           :status_card, +           :get_poll +         ]    )    plug( @@ -526,7 +533,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> Enum.reverse()      conn -    |> add_link_headers(:home_timeline, activities) +    |> add_link_headers(activities)      |> put_view(StatusView)      |> render("index.json", %{activities: activities, for: user, as: :activity})    end @@ -545,7 +552,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> Enum.reverse()      conn -    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only}) +    |> add_link_headers(activities, %{"local" => local_only})      |> put_view(StatusView)      |> render("index.json", %{activities: activities, for: user, as: :activity})    end @@ -559,7 +566,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        activities = ActivityPub.fetch_user_activities(user, reading_user, params)        conn -      |> add_link_headers(:user_statuses, activities, params["id"]) +      |> add_link_headers(activities)        |> put_view(StatusView)        |> render("index.json", %{          activities: activities, @@ -583,11 +590,25 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> Pagination.fetch_paginated(params)      conn -    |> add_link_headers(:dm_timeline, activities) +    |> add_link_headers(activities)      |> put_view(StatusView)      |> render("index.json", %{activities: activities, for: user, as: :activity})    end +  def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do +    limit = 100 + +    activities = +      ids +      |> Enum.take(limit) +      |> Activity.all_by_ids_with_object() +      |> Enum.filter(&Visibility.visible_for_user?(&1, user)) + +    conn +    |> put_view(StatusView) +    |> render("index.json", activities: activities, for: user, as: :activity) +  end +    def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do      with %Activity{} = activity <- Activity.get_by_id_with_object(id),           true <- Visibility.visible_for_user?(activity, user) do @@ -684,7 +705,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do      with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do        conn -      |> add_link_headers(:scheduled_statuses, scheduled_activities) +      |> add_link_headers(scheduled_activities)        |> put_view(ScheduledActivityView)        |> render("index.json", %{scheduled_activities: scheduled_activities})      end @@ -867,7 +888,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      notifications = MastodonAPI.get_notifications(user, params)      conn -    |> add_link_headers(:notifications, notifications) +    |> add_link_headers(notifications)      |> put_view(NotificationView)      |> render("index.json", %{notifications: notifications, for: user})    end @@ -989,6 +1010,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do      with %Activity{} = activity <- Activity.get_by_id_with_object(id), +         {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},           %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do        q = from(u in User, where: u.ap_id in ^likes) @@ -1000,12 +1022,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> put_view(AccountView)        |> render("accounts.json", %{for: user, users: users, as: :user})      else +      {:visible, false} -> {:error, :not_found}        _ -> json(conn, [])      end    end    def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do      with %Activity{} = activity <- Activity.get_by_id_with_object(id), +         {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},           %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do        q = from(u in User, where: u.ap_id in ^announces) @@ -1017,6 +1041,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> put_view(AccountView)        |> render("accounts.json", %{for: user, users: users, as: :user})      else +      {:visible, false} -> {:error, :not_found}        _ -> json(conn, [])      end    end @@ -1055,7 +1080,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> Enum.reverse()      conn -    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only}) +    |> add_link_headers(activities, %{"local" => local_only})      |> put_view(StatusView)      |> render("index.json", %{activities: activities, for: user, as: :activity})    end @@ -1071,7 +1096,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          end        conn -      |> add_link_headers(:followers, followers, user) +      |> add_link_headers(followers)        |> put_view(AccountView)        |> render("accounts.json", %{for: for_user, users: followers, as: :user})      end @@ -1088,7 +1113,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          end        conn -      |> add_link_headers(:following, followers, user) +      |> add_link_headers(followers)        |> put_view(AccountView)        |> render("accounts.json", %{for: for_user, users: followers, as: :user})      end @@ -1313,7 +1338,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> Enum.reverse()      conn -    |> add_link_headers(:favourites, activities) +    |> add_link_headers(activities)      |> put_view(StatusView)      |> render("index.json", %{activities: activities, for: user, as: :activity})    end @@ -1340,7 +1365,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          |> Enum.reverse()        conn -      |> add_link_headers(:favourites, activities) +      |> add_link_headers(activities)        |> put_view(StatusView)        |> render("index.json", %{activities: activities, for: for_user, as: :activity})      else @@ -1361,7 +1386,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)      conn -    |> add_link_headers(:bookmarks, bookmarks) +    |> add_link_headers(bookmarks)      |> put_view(StatusView)      |> render("index.json", %{activities: activities, for: user, as: :activity})    end @@ -1803,7 +1828,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        end)      conn -    |> add_link_headers(:conversations, participations) +    |> add_link_headers(participations)      |> json(conversations)    end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index e71083b91..ef796cddd 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -73,14 +73,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    def render("index.json", opts) do      replied_to_activities = get_replied_to_activities(opts.activities) -    parallel = unless is_nil(opts[:parallel]), do: opts[:parallel], else: true      opts.activities      |> safe_render_many(        StatusView,        "status.json", -      Map.put(opts, :replied_to_activities, replied_to_activities), -      parallel +      Map.put(opts, :replied_to_activities, replied_to_activities)      )    end @@ -499,7 +497,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      object_tags = for tag when is_binary(tag) <- object_tags, do: tag      Enum.reduce(object_tags, [], fn tag, tags -> -      tags ++ [%{name: tag, url: "/tag/#{tag}"}] +      tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]      end)    end diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex index 17c568a9d..f3dc4616c 100644 --- a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex @@ -5,7 +5,7 @@  defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do    use Pleroma.Web, :controller -  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 7] +  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]    alias Pleroma.Conversation.Participation    alias Pleroma.Notification @@ -40,31 +40,22 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do          %{assigns: %{user: user}} = conn,          %{"id" => participation_id} = params        ) do -    params = -      params -      |> Map.put("blocking_user", user) -      |> Map.put("muting_user", user) -      |> Map.put("user", user) - -    participation = -      participation_id -      |> Participation.get(preload: [:conversation]) +    participation = Participation.get(participation_id, preload: [:conversation])      if user.id == participation.user_id do +      params = +        params +        |> Map.put("blocking_user", user) +        |> Map.put("muting_user", user) +        |> Map.put("user", user) +        activities =          participation.conversation.ap_id          |> ActivityPub.fetch_activities_for_context(params)          |> Enum.reverse()        conn -      |> add_link_headers( -        :conversation_statuses, -        activities, -        participation_id, -        params, -        nil, -        &pleroma_api_url/4 -      ) +      |> add_link_headers(activities)        |> put_view(StatusView)        |> render("index.json", %{activities: activities, for: user, as: :activity})      end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 593da01fd..0b41eb2e0 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -192,6 +192,7 @@ defmodule Pleroma.Web.Router do    scope "/api/pleroma", Pleroma.Web.TwitterAPI do      pipe_through(:authenticated_api) +    post("/change_email", UtilController, :change_email)      post("/change_password", UtilController, :change_password)      post("/delete_account", UtilController, :delete_account)      put("/notification_settings", UtilController, :update_notificaton_settings) @@ -360,48 +361,46 @@ defmodule Pleroma.Web.Router do    scope "/api/v1", Pleroma.Web.MastodonAPI do      pipe_through(:api) -    post("/accounts", MastodonAPIController, :account_register) -      get("/instance", MastodonAPIController, :masto_instance)      get("/instance/peers", MastodonAPIController, :peers) +      post("/apps", MastodonAPIController, :create_app)      get("/apps/verify_credentials", MastodonAPIController, :verify_app_credentials) -    get("/custom_emojis", MastodonAPIController, :custom_emojis) - -    get("/statuses/:id/card", MastodonAPIController, :status_card) -    get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by) -    get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by) +    get("/custom_emojis", MastodonAPIController, :custom_emojis)      get("/trends", MastodonAPIController, :empty_array)      get("/accounts/search", SearchController, :account_search) -    post( -      "/pleroma/accounts/confirmation_resend", -      MastodonAPIController, -      :account_confirmation_resend -    ) -      get("/timelines/public", MastodonAPIController, :public_timeline)      get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline) - -    get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites) - -    get("/search", SearchController, :search) +    get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)      get("/polls/:id", MastodonAPIController, :get_poll) +    post("/accounts", MastodonAPIController, :account_register) +    get("/accounts/:id", MastodonAPIController, :user)      get("/accounts/:id/followers", MastodonAPIController, :followers)      get("/accounts/:id/following", MastodonAPIController, :following) +    get("/accounts/:id/statuses", MastodonAPIController, :user_statuses) -    get("/timelines/list/:list_id", MastodonAPIController, :list_timeline) - -    get("/accounts/:id", MastodonAPIController, :user) +    get("/search", SearchController, :search) -    get("/accounts/:id/statuses", MastodonAPIController, :user_statuses) +    get("/statuses", MastodonAPIController, :get_statuses)      get("/statuses/:id", MastodonAPIController, :get_status)      get("/statuses/:id/context", MastodonAPIController, :get_context) +    get("/statuses/:id/card", MastodonAPIController, :status_card) +    get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by) +    get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by) + +    get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites) + +    post( +      "/pleroma/accounts/confirmation_resend", +      MastodonAPIController, +      :account_confirmation_resend +    )    end    scope "/api/v2", Pleroma.Web.MastodonAPI do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 82ed0c287..644a0ae6b 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -31,6 +31,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do      OAuthScopesPlug,      %{scopes: ["write:accounts"]}      when action in [ +           :change_email,             :change_password,             :delete_account,             :update_notificaton_settings, @@ -334,6 +335,25 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do      end    end +  def change_email(%{assigns: %{user: user}} = conn, params) do +    case CommonAPI.Utils.confirm_current_password(user, params["password"]) do +      {:ok, user} -> +        with {:ok, _user} <- User.change_email(user, params["email"]) do +          json(conn, %{status: "success"}) +        else +          {:error, changeset} -> +            {_, {error, _}} = Enum.at(changeset.errors, 0) +            json(conn, %{error: "Email #{error}."}) + +          _ -> +            json(conn, %{error: "Unable to change email."}) +        end + +      {:error, msg} -> +        json(conn, %{error: msg}) +    end +  end +    def delete_account(%{assigns: %{user: user}} = conn, params) do      case CommonAPI.Utils.confirm_current_password(user, params["password"]) do        {:ok, user} -> diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index bfb6c7287..687346554 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -66,23 +66,9 @@ defmodule Pleroma.Web do        end        @doc """ -      Same as `render_many/4` but wrapped in rescue block and parallelized (unless disabled by passing false as a fifth argument). +      Same as `render_many/4` but wrapped in rescue block.        """ -      def safe_render_many(collection, view, template, assigns \\ %{}, parallel \\ true) - -      def safe_render_many(collection, view, template, assigns, true) do -        Enum.map(collection, fn resource -> -          Task.async(fn -> -            as = Map.get(assigns, :as) || view.__resource__ -            assigns = Map.put(assigns, as, resource) -            safe_render(view, template, assigns) -          end) -        end) -        |> Enum.map(&Task.await(&1, :infinity)) -        |> Enum.filter(& &1) -      end - -      def safe_render_many(collection, view, template, assigns, false) do +      def safe_render_many(collection, view, template, assigns \\ %{}) do          Enum.map(collection, fn resource ->            as = Map.get(assigns, :as) || view.__resource__            assigns = Map.put(assigns, as, resource) | 
