diff options
| author | Ivan Tashkinov <ivantashkinov@gmail.com> | 2020-04-15 19:20:34 +0300 | 
|---|---|---|
| committer | Ivan Tashkinov <ivantashkinov@gmail.com> | 2020-04-15 19:20:34 +0300 | 
| commit | bedf92e064ec96f0b9bb95c2263616a2fe49017d (patch) | |
| tree | f07832cb01dcc5574c6be537c78e5e51fbb3fda6 /lib | |
| parent | fc81e5a49c34224e07e85f490a30f92db0835d45 (diff) | |
| parent | b1c1d2e5e1e1c43a068a7223f9ba69c689c878a3 (diff) | |
| download | pleroma-bedf92e064ec96f0b9bb95c2263616a2fe49017d.tar.gz pleroma-bedf92e064ec96f0b9bb95c2263616a2fe49017d.zip  | |
Merge remote-tracking branch 'remotes/origin/develop' into authenticated-api-oauth-check-enforcement
Diffstat (limited to 'lib')
58 files changed, 1400 insertions, 258 deletions
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index 4dfcc32e7..3ad6edbfb 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -5,7 +5,6 @@  defmodule Mix.Pleroma do    @doc "Common functions to be reused in mix tasks"    def start_pleroma do -    Mix.Task.run("app.start")      Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)      if Pleroma.Config.get(:env) != :test do diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex index dd2b9c8f2..6ab7fe8ef 100644 --- a/lib/mix/tasks/pleroma/benchmark.ex +++ b/lib/mix/tasks/pleroma/benchmark.ex @@ -67,7 +67,8 @@ defmodule Mix.Tasks.Pleroma.Benchmark do            Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{              activities: activities,              for: user, -            as: :activity +            as: :activity, +            skip_relationships: true            })          end        }, diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 429d763c7..cdffa88b2 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -14,8 +14,8 @@ defmodule Mix.Tasks.Pleroma.Emoji do      {options, [], []} = parse_global_opts(args) -    manifest = -      fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest()) +    url_or_path = options[:manifest] || default_manifest() +    manifest = fetch_manifest(url_or_path)      Enum.each(manifest, fn {name, info} ->        to_print = [ @@ -40,9 +40,9 @@ defmodule Mix.Tasks.Pleroma.Emoji do      {options, pack_names, []} = parse_global_opts(args) -    manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest() +    url_or_path = options[:manifest] || default_manifest() -    manifest = fetch_manifest(manifest_url) +    manifest = fetch_manifest(url_or_path)      for pack_name <- pack_names do        if Map.has_key?(manifest, pack_name) do @@ -75,7 +75,10 @@ defmodule Mix.Tasks.Pleroma.Emoji do          end          # The url specified in files should be in the same directory -        files_url = Path.join(Path.dirname(manifest_url), pack["files"]) +        files_url = +          url_or_path +          |> Path.dirname() +          |> Path.join(pack["files"])          IO.puts(            IO.ANSI.format([ @@ -133,38 +136,51 @@ defmodule Mix.Tasks.Pleroma.Emoji do      end    end -  def run(["gen-pack", src]) do +  def run(["gen-pack" | args]) do      start_pleroma() -    proposed_name = Path.basename(src) |> Path.rootname() -    name = String.trim(IO.gets("Pack name [#{proposed_name}]: ")) -    # If there's no name, use the default one -    name = if String.length(name) > 0, do: name, else: proposed_name +    {opts, [src], []} = +      OptionParser.parse( +        args, +        strict: [ +          name: :string, +          license: :string, +          homepage: :string, +          description: :string, +          files: :string, +          extensions: :string +        ] +      ) -    license = String.trim(IO.gets("License: ")) -    homepage = String.trim(IO.gets("Homepage: ")) -    description = String.trim(IO.gets("Description: ")) +    proposed_name = Path.basename(src) |> Path.rootname() +    name = get_option(opts, :name, "Pack name:", proposed_name) +    license = get_option(opts, :license, "License:") +    homepage = get_option(opts, :homepage, "Homepage:") +    description = get_option(opts, :description, "Description:") -    proposed_files_name = "#{name}.json" -    files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: ")) -    files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name +    proposed_files_name = "#{name}_files.json" +    files_name = get_option(opts, :files, "Save file list to:", proposed_files_name)      default_exts = [".png", ".gif"] -    default_exts_str = Enum.join(default_exts, " ") -    exts = -      String.trim( -        IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ") +    custom_exts = +      get_option( +        opts, +        :extensions, +        "Emoji file extensions (separated with spaces):", +        Enum.join(default_exts, " ")        ) +      |> String.split(" ", trim: true)      exts = -      if String.length(exts) > 0 do -        String.split(exts, " ") -        |> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end) -      else +      if MapSet.equal?(MapSet.new(default_exts), MapSet.new(custom_exts)) do          default_exts +      else +        custom_exts        end +    IO.puts("Using #{Enum.join(exts, " ")} extensions") +      IO.puts("Downloading the pack and generating SHA256")      binary_archive = Tesla.get!(client(), src).body @@ -194,14 +210,16 @@ defmodule Mix.Tasks.Pleroma.Emoji do      IO.puts("""      #{files_name} has been created and contains the list of all found emojis in the pack. -    Please review the files in the remove those not needed. +    Please review the files in the pack and remove those not needed.      """) -    if File.exists?("index.json") do -      existing_data = File.read!("index.json") |> Jason.decode!() +    pack_file = "#{name}.json" + +    if File.exists?(pack_file) do +      existing_data = File.read!(pack_file) |> Jason.decode!()        File.write!( -        "index.json", +        pack_file,          Jason.encode!(            Map.merge(              existing_data, @@ -211,11 +229,11 @@ defmodule Mix.Tasks.Pleroma.Emoji do          )        ) -      IO.puts("index.json file has been update with the #{name} pack") +      IO.puts("#{pack_file} has been updated with the #{name} pack")      else -      File.write!("index.json", Jason.encode!(pack_json, pretty: true)) +      File.write!(pack_file, Jason.encode!(pack_json, pretty: true)) -      IO.puts("index.json has been created with the #{name} pack") +      IO.puts("#{pack_file} has been created with the #{name} pack")      end    end diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index 936bc9ab1..3871e1cbb 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -54,10 +54,19 @@ defmodule Pleroma.Config.TransferTask do            [:pleroma, nil, :prometheus]          end +      {logger, other} = +        (Repo.all(ConfigDB) ++ deleted_settings) +        |> Enum.map(&transform_and_merge/1) +        |> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end) + +      logger +      |> Enum.sort() +      |> Enum.each(&configure/1) +        started_applications = Application.started_applications() -      (Repo.all(ConfigDB) ++ deleted_settings) -      |> Enum.map(&merge_and_update/1) +      other +      |> Enum.map(&update/1)        |> Enum.uniq()        |> Enum.reject(&(&1 in reject_restart))        |> maybe_set_pleroma_last() @@ -81,51 +90,66 @@ defmodule Pleroma.Config.TransferTask do      end    end -  defp group_for_restart(:logger, key, _, merged_value) do -    # change logger configuration in runtime, without restart -    if Keyword.keyword?(merged_value) and -         key not in [:compile_time_application, :backends, :compile_time_purge_matching] do -      Logger.configure_backend(key, merged_value) -    else -      Logger.configure([{key, merged_value}]) -    end +  defp transform_and_merge(%{group: group, key: key, value: value} = setting) do +    group = ConfigDB.from_string(group) +    key = ConfigDB.from_string(key) +    value = ConfigDB.from_binary(value) -    nil -  end +    default = Config.Holder.default_config(group, key) -  defp group_for_restart(group, _, _, _) when group != :pleroma, do: group +    merged = +      cond do +        Ecto.get_meta(setting, :state) == :deleted -> default +        can_be_merged?(default, value) -> ConfigDB.merge_group(group, key, default, value) +        true -> value +      end -  defp group_for_restart(group, key, value, _) do -    if pleroma_need_restart?(group, key, value), do: group +    {group, key, value, merged}    end -  defp merge_and_update(setting) do -    try do -      key = ConfigDB.from_string(setting.key) -      group = ConfigDB.from_string(setting.group) +  # change logger configuration in runtime, without restart +  defp configure({:quack, key, _, merged}) do +    Logger.configure_backend(Quack.Logger, [{key, merged}]) +    :ok = update_env(:quack, key, merged) +  end -      default = Config.Holder.default_config(group, key) -      value = ConfigDB.from_binary(setting.value) +  defp configure({_, :backends, _, merged}) do +    # removing current backends +    Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1) -      merged_value = -        cond do -          Ecto.get_meta(setting, :state) == :deleted -> default -          can_be_merged?(default, value) -> ConfigDB.merge_group(group, key, default, value) -          true -> value -        end +    Enum.each(merged, &Logger.add_backend/1) -      :ok = update_env(group, key, merged_value) +    :ok = update_env(:logger, :backends, merged) +  end -      group_for_restart(group, key, value, merged_value) +  defp configure({group, key, _, merged}) do +    merged = +      if key == :console do +        put_in(merged[:format], merged[:format] <> "\n") +      else +        merged +      end + +    backend = +      if key == :ex_syslogger, +        do: {ExSyslogger, :ex_syslogger}, +        else: key + +    Logger.configure_backend(backend, merged) +    :ok = update_env(:logger, group, merged) +  end + +  defp update({group, key, value, merged}) do +    try do +      :ok = update_env(group, key, merged) + +      if group != :pleroma or pleroma_need_restart?(group, key, value), do: group      rescue        error ->          error_msg = -          "updating env causes error, group: " <> -            inspect(setting.group) <> -            " key: " <> -            inspect(setting.key) <> -            " value: " <> -            inspect(ConfigDB.from_binary(setting.value)) <> " error: " <> inspect(error) +          "updating env causes error, group: #{inspect(group)}, key: #{inspect(key)}, value: #{ +            inspect(value) +          } error: #{inspect(error)}"          Logger.warn(error_msg) @@ -133,6 +157,9 @@ defmodule Pleroma.Config.TransferTask do      end    end +  defp update_env(group, key, nil), do: Application.delete_env(group, key) +  defp update_env(group, key, value), do: Application.put_env(group, key, value) +    @spec pleroma_need_restart?(atom(), atom(), any()) :: boolean()    def pleroma_need_restart?(group, key, value) do      group_and_key_need_reboot?(group, key) or group_and_subkey_need_reboot?(group, key, value) @@ -150,9 +177,6 @@ defmodule Pleroma.Config.TransferTask do        end)    end -  defp update_env(group, key, nil), do: Application.delete_env(group, key) -  defp update_env(group, key, value), do: Application.put_env(group, key, value) -    defp restart(_, :pleroma, env), do: Restarter.Pleroma.restart_after_boot(env)    defp restart(started_applications, app, _) do diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex index d9b601223..6fc47620c 100644 --- a/lib/pleroma/ecto_enums.ex +++ b/lib/pleroma/ecto_enums.ex @@ -4,10 +4,16 @@  import EctoEnum -defenum(UserRelationshipTypeEnum, +defenum(Pleroma.UserRelationship.Type,    block: 1,    mute: 2,    reblog_mute: 3,    notification_mute: 4,    inverse_subscription: 5  ) + +defenum(Pleroma.FollowingRelationship.State, +  follow_pending: 1, +  follow_accept: 2, +  follow_reject: 3 +) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index a9538ea4e..9ccf40495 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -8,12 +8,13 @@ defmodule Pleroma.FollowingRelationship do    import Ecto.Changeset    import Ecto.Query +  alias Ecto.Changeset    alias FlakeId.Ecto.CompatType    alias Pleroma.Repo    alias Pleroma.User    schema "following_relationships" do -    field(:state, :string, default: "accept") +    field(:state, Pleroma.FollowingRelationship.State, default: :follow_pending)      belongs_to(:follower, User, type: CompatType)      belongs_to(:following, User, type: CompatType) @@ -27,6 +28,18 @@ defmodule Pleroma.FollowingRelationship do      |> put_assoc(:follower, attrs.follower)      |> put_assoc(:following, attrs.following)      |> validate_required([:state, :follower, :following]) +    |> unique_constraint(:follower_id, +      name: :following_relationships_follower_id_following_id_index +    ) +    |> validate_not_self_relationship() +  end + +  def state_to_enum(state) when state in ["pending", "accept", "reject"] do +    String.to_existing_atom("follow_#{state}") +  end + +  def state_to_enum(state) do +    raise "State is not convertible to Pleroma.FollowingRelationship.State: #{state}"    end    def get(%User{} = follower, %User{} = following) do @@ -35,7 +48,7 @@ defmodule Pleroma.FollowingRelationship do      |> Repo.one()    end -  def update(follower, following, "reject"), do: unfollow(follower, following) +  def update(follower, following, :follow_reject), do: unfollow(follower, following)    def update(%User{} = follower, %User{} = following, state) do      case get(follower, following) do @@ -50,7 +63,7 @@ defmodule Pleroma.FollowingRelationship do      end    end -  def follow(%User{} = follower, %User{} = following, state \\ "accept") do +  def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do      %__MODULE__{}      |> changeset(%{follower: follower, following: following, state: state})      |> Repo.insert(on_conflict: :nothing) @@ -80,7 +93,7 @@ defmodule Pleroma.FollowingRelationship do    def get_follow_requests(%User{id: id}) do      __MODULE__      |> join(:inner, [r], f in assoc(r, :follower)) -    |> where([r], r.state == "pending") +    |> where([r], r.state == ^:follow_pending)      |> where([r], r.following_id == ^id)      |> select([r, f], f)      |> Repo.all() @@ -88,7 +101,7 @@ defmodule Pleroma.FollowingRelationship do    def following?(%User{id: follower_id}, %User{id: followed_id}) do      __MODULE__ -    |> where(follower_id: ^follower_id, following_id: ^followed_id, state: "accept") +    |> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)      |> Repo.exists?()    end @@ -97,7 +110,7 @@ defmodule Pleroma.FollowingRelationship do        __MODULE__        |> join(:inner, [r], u in User, on: r.following_id == u.id)        |> where([r], r.follower_id == ^user.id) -      |> where([r], r.state == "accept") +      |> where([r], r.state == ^:follow_accept)        |> select([r, u], u.follower_address)        |> Repo.all() @@ -157,4 +170,30 @@ defmodule Pleroma.FollowingRelationship do        fr -> fr.follower_id == follower.id and fr.following_id == following.id      end)    end + +  defp validate_not_self_relationship(%Changeset{} = changeset) do +    changeset +    |> validate_follower_id_following_id_inequality() +    |> validate_following_id_follower_id_inequality() +  end + +  defp validate_follower_id_following_id_inequality(%Changeset{} = changeset) do +    validate_change(changeset, :follower_id, fn _, follower_id -> +      if follower_id == get_field(changeset, :following_id) do +        [source_id: "can't be equal to following_id"] +      else +        [] +      end +    end) +  end + +  defp validate_following_id_follower_id_inequality(%Changeset{} = changeset) do +    validate_change(changeset, :following_id, fn _, following_id -> +      if following_id == get_field(changeset, :follower_id) do +        [target_id: "can't be equal to follower_id"] +      else +        [] +      end +    end) +  end  end diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index e2a658cb3..c44e7fc8b 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -35,9 +35,19 @@ defmodule Pleroma.Formatter do          nickname_text = get_nickname_text(nickname, opts)          link = -          ~s(<span class="h-card"><a data-user="#{id}" class="u-url mention" href="#{ap_id}" rel="ugc">@<span>#{ -            nickname_text -          }</span></a></span>) +          Phoenix.HTML.Tag.content_tag( +            :span, +            Phoenix.HTML.Tag.content_tag( +              :a, +              ["@", Phoenix.HTML.Tag.content_tag(:span, nickname_text)], +              "data-user": id, +              class: "u-url mention", +              href: ap_id, +              rel: "ugc" +            ), +            class: "h-card" +          ) +          |> Phoenix.HTML.safe_to_string()          {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}} @@ -49,7 +59,15 @@ defmodule Pleroma.Formatter do    def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do      tag = String.downcase(tag)      url = "#{Pleroma.Web.base_url()}/tag/#{tag}" -    link = ~s(<a class="hashtag" data-tag="#{tag}" href="#{url}" rel="tag ugc">#{tag_text}</a>) + +    link = +      Phoenix.HTML.Tag.content_tag(:a, tag_text, +        class: "hashtag", +        "data-tag": tag, +        href: url, +        rel: "tag ugc" +      ) +      |> Phoenix.HTML.safe_to_string()      {link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}    end diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index 20823a765..cd25a2e74 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -49,8 +49,10 @@ defmodule Pleroma.Gun.Conn do      key = "#{uri.scheme}:#{uri.host}:#{uri.port}" +    max_connections = pool_opts[:max_connections] || 250 +      conn_pid = -      if Connections.count(name) < opts[:max_connection] do +      if Connections.count(name) < max_connections do          do_open(uri, opts)        else          close_least_used_and_do_open(name, uri, opts) diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index 9ae6a5600..99608b8a5 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -32,6 +32,18 @@ defmodule Pleroma.Object.Containment do      get_actor(%{"actor" => actor})    end +  def get_object(%{"object" => id}) when is_binary(id) do +    id +  end + +  def get_object(%{"object" => %{"id" => id}}) when is_binary(id) do +    id +  end + +  def get_object(_) do +    nil +  end +    # TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus    # objects being present in the test suite environment.  Once these objects are    # removed, please also remove this. diff --git a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex index 4f124ed4d..84b7c5d83 100644 --- a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex +++ b/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex @@ -42,13 +42,13 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do      else        {:user_match, false} ->          Logger.debug("Failed to map identity from signature (payload actor mismatch)") -        Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}") +        Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")          assign(conn, :valid_signature, false)        # remove me once testsuite uses mapped capabilities instead of what we do now        {:user, nil} ->          Logger.debug("Failed to map identity from signature (lookup failure)") -        Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}") +        Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")          conn      end    end @@ -60,7 +60,7 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do      else        _ ->          Logger.debug("Failed to map identity from signature (no payload actor mismatch)") -        Logger.debug("key_id=#{key_id_from_conn(conn)}") +        Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")          assign(conn, :valid_signature, false)      end    end diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex index 1529da717..c51e2c634 100644 --- a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex +++ b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex @@ -110,20 +110,9 @@ defmodule Pleroma.Plugs.RateLimiter do    end    def disabled?(conn) do -    localhost_or_socket = -      case Config.get([Pleroma.Web.Endpoint, :http, :ip]) do -        {127, 0, 0, 1} -> true -        {0, 0, 0, 0, 0, 0, 0, 1} -> true -        {:local, _} -> true -        _ -> false -      end - -    remote_ip_not_found = -      if Map.has_key?(conn.assigns, :remote_ip_found), -        do: !conn.assigns.remote_ip_found, -        else: false - -    localhost_or_socket and remote_ip_not_found +    if Map.has_key?(conn.assigns, :remote_ip_found), +      do: !conn.assigns.remote_ip_found, +      else: false    end    @inspect_bucket_not_found {:error, :not_found} diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex index 0ac9050d0..2eca4f8f6 100644 --- a/lib/pleroma/plugs/remote_ip.ex +++ b/lib/pleroma/plugs/remote_ip.ex @@ -7,8 +7,6 @@ defmodule Pleroma.Plugs.RemoteIp do    This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.    """ -  import Plug.Conn -    @behaviour Plug    @headers ~w[ @@ -28,12 +26,11 @@ defmodule Pleroma.Plugs.RemoteIp do    def init(_), do: nil -  def call(%{remote_ip: original_remote_ip} = conn, _) do +  def call(conn, _) do      config = Pleroma.Config.get(__MODULE__, [])      if Keyword.get(config, :enabled, false) do -      %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts(config)) -      assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip) +      RemoteIp.call(conn, remote_ip_opts(config))      else        conn      end diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex index 36ff024a7..94147e0c4 100644 --- a/lib/pleroma/plugs/uploaded_media.ex +++ b/lib/pleroma/plugs/uploaded_media.ex @@ -41,6 +41,7 @@ defmodule Pleroma.Plugs.UploadedMedia do          conn ->            conn        end +      |> merge_resp_headers([{"content-security-policy", "sandbox"}])      config = Pleroma.Config.get(Pleroma.Upload) diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index 4d4ba913c..acafe1bea 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -243,7 +243,7 @@ defmodule Pleroma.Pool.Connections do    @impl true    def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do -    Logger.debug("received DOWM message for #{inspect(conn_pid)} reason -> #{inspect(reason)}") +    Logger.debug("received DOWN message for #{inspect(conn_pid)} reason -> #{inspect(reason)}")      state =        with {key, conn} <- find_conn(state.conns, conn_pid) do diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index ff828aa17..670ce397b 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -16,6 +16,7 @@ defmodule Pleroma.User do    alias Pleroma.Conversation.Participation    alias Pleroma.Delivery    alias Pleroma.FollowingRelationship +  alias Pleroma.Formatter    alias Pleroma.HTML    alias Pleroma.Keys    alias Pleroma.Notification @@ -452,7 +453,7 @@ defmodule Pleroma.User do        fields =          raw_fields -        |> Enum.map(fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) +        |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)        changeset        |> put_change(:raw_fields, raw_fields) @@ -462,6 +463,12 @@ defmodule Pleroma.User do      end    end +  defp parse_fields(value) do +    value +    |> Formatter.linkify(mentions_format: :full) +    |> elem(0) +  end +    defp put_change_if_present(changeset, map_field, value_function) do      if value = get_change(changeset, map_field) do        with {:ok, new_value} <- value_function.(value) do @@ -693,7 +700,7 @@ defmodule Pleroma.User do    @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}    def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do -    follow(follower, followed, "pending") +    follow(follower, followed, :follow_pending)    end    def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do @@ -713,14 +720,14 @@ defmodule Pleroma.User do    def follow_all(follower, followeds) do      followeds      |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end) -    |> Enum.each(&follow(follower, &1, "accept")) +    |> Enum.each(&follow(follower, &1, :follow_accept))      set_cache(follower)    end    defdelegate following(user), to: FollowingRelationship -  def follow(%User{} = follower, %User{} = followed, state \\ "accept") do +  def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do      deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])      cond do @@ -747,7 +754,7 @@ defmodule Pleroma.User do    def unfollow(%User{} = follower, %User{} = followed) do      case get_follow_state(follower, followed) do -      state when state in ["accept", "pending"] -> +      state when state in [:follow_pending, :follow_accept] ->          FollowingRelationship.unfollow(follower, followed)          {:ok, followed} = update_follower_count(followed) @@ -765,6 +772,7 @@ defmodule Pleroma.User do    defdelegate following?(follower, followed), to: FollowingRelationship +  @doc "Returns follow state as Pleroma.FollowingRelationship.State value"    def get_follow_state(%User{} = follower, %User{} = following) do      following_relationship = FollowingRelationship.get(follower, following)      get_follow_state(follower, following, following_relationship) @@ -778,8 +786,11 @@ defmodule Pleroma.User do      case {following_relationship, following.local} do        {nil, false} ->          case Utils.fetch_latest_follow(follower, following) do -          %{data: %{"state" => state}} when state in ["pending", "accept"] -> state -          _ -> nil +          %Activity{data: %{"state" => state}} when state in ["pending", "accept"] -> +            FollowingRelationship.state_to_enum(state) + +          _ -> +            nil          end        {%{state: state}, _} -> @@ -1278,7 +1289,7 @@ defmodule Pleroma.User do    def blocks?(%User{} = user, %User{} = target) do      blocks_user?(user, target) || -      (!User.following?(user, target) && blocks_domain?(user, target)) +      (blocks_domain?(user, target) and not User.following?(user, target))    end    def blocks_user?(%User{} = user, %User{} = target) do @@ -1979,17 +1990,6 @@ defmodule Pleroma.User do    def fields(%{fields: fields}), do: fields -  def sanitized_fields(%User{} = user) do -    user -    |> User.fields() -    |> Enum.map(fn %{"name" => name, "value" => value} -> -      %{ -        "name" => name, -        "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly) -      } -    end) -  end -    def validate_fields(changeset, remote? \\ false) do      limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields      limit = Pleroma.Config.get([:instance, limit_name], 0) diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index 884e33039..ec88088cf 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -148,7 +148,7 @@ defmodule Pleroma.User.Query do        as: :relationships,        on: r.following_id == ^id and r.follower_id == u.id      ) -    |> where([relationships: r], r.state == "accept") +    |> where([relationships: r], r.state == ^:follow_accept)    end    defp compose_query({:friends, %User{id: id}}, query) do @@ -158,7 +158,7 @@ defmodule Pleroma.User.Query do        as: :relationships,        on: r.following_id == u.id and r.follower_id == ^id      ) -    |> where([relationships: r], r.state == "accept") +    |> where([relationships: r], r.state == ^:follow_accept)    end    defp compose_query({:recipients_from_activity, to}, query) do @@ -173,7 +173,7 @@ defmodule Pleroma.User.Query do      )      |> where(        [u, following: f, relationships: r], -      u.ap_id in ^to or (f.follower_address in ^to and r.state == "accept") +      u.ap_id in ^to or (f.follower_address in ^to and r.state == ^:follow_accept)      )      |> distinct(true)    end diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 18a5eec72..235ad427c 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -8,6 +8,7 @@ defmodule Pleroma.UserRelationship do    import Ecto.Changeset    import Ecto.Query +  alias Ecto.Changeset    alias Pleroma.FollowingRelationship    alias Pleroma.Repo    alias Pleroma.User @@ -16,12 +17,12 @@ defmodule Pleroma.UserRelationship do    schema "user_relationships" do      belongs_to(:source, User, type: FlakeId.Ecto.CompatType)      belongs_to(:target, User, type: FlakeId.Ecto.CompatType) -    field(:relationship_type, UserRelationshipTypeEnum) +    field(:relationship_type, Pleroma.UserRelationship.Type)      timestamps(updated_at: false)    end -  for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do +  for relationship_type <- Keyword.keys(Pleroma.UserRelationship.Type.__enum_map__()) do      # `def create_block/2`, `def create_mute/2`, `def create_reblog_mute/2`,      #   `def create_notification_mute/2`, `def create_inverse_subscription/2`      def unquote(:"create_#{relationship_type}")(source, target), @@ -40,7 +41,7 @@ defmodule Pleroma.UserRelationship do    def user_relationship_types, do: Keyword.keys(user_relationship_mappings()) -  def user_relationship_mappings, do: UserRelationshipTypeEnum.__enum_map__() +  def user_relationship_mappings, do: Pleroma.UserRelationship.Type.__enum_map__()    def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do      user_relationship @@ -129,17 +130,27 @@ defmodule Pleroma.UserRelationship do    end    @doc ":relationships option for StatusView / AccountView / NotificationView" -  def view_relationships_option(nil = _reading_user, _actors) do +  def view_relationships_option(reading_user, actors, opts \\ []) + +  def view_relationships_option(nil = _reading_user, _actors, _opts) do      %{user_relationships: [], following_relationships: []}    end -  def view_relationships_option(%User{} = reading_user, actors) do +  def view_relationships_option(%User{} = reading_user, actors, opts) do +    {source_to_target_rel_types, target_to_source_rel_types} = +      if opts[:source_mutes_only] do +        # This option is used for rendering statuses (FE needs `muted` flag for each one anyways) +        {[:mute], []} +      else +        {[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]} +      end +      user_relationships =        UserRelationship.dictionary(          [reading_user],          actors, -        [:block, :mute, :notification_mute, :reblog_mute], -        [:block, :inverse_subscription] +        source_to_target_rel_types, +        target_to_source_rel_types        )      following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors) @@ -147,18 +158,26 @@ defmodule Pleroma.UserRelationship do      %{user_relationships: user_relationships, following_relationships: following_relationships}    end -  defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do +  defp validate_not_self_relationship(%Changeset{} = changeset) do      changeset -    |> validate_change(:target_id, fn _, target_id -> -      if target_id == get_field(changeset, :source_id) do -        [target_id: "can't be equal to source_id"] +    |> validate_source_id_target_id_inequality() +    |> validate_target_id_source_id_inequality() +  end + +  defp validate_source_id_target_id_inequality(%Changeset{} = changeset) do +    validate_change(changeset, :source_id, fn _, source_id -> +      if source_id == get_field(changeset, :target_id) do +        [source_id: "can't be equal to target_id"]        else          []        end      end) -    |> validate_change(:source_id, fn _, source_id -> -      if source_id == get_field(changeset, :target_id) do -        [source_id: "can't be equal to target_id"] +  end + +  defp validate_target_id_source_id_inequality(%Changeset{} = changeset) do +    validate_change(changeset, :target_id, fn _, target_id -> +      if target_id == get_field(changeset, :source_id) do +        [target_id: "can't be equal to source_id"]        else          []        end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 53b6ad654..86b105b7f 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -125,6 +125,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    def increase_poll_votes_if_vote(_create_data), do: :noop +  @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} +  def persist(object, meta) do +    with local <- Keyword.fetch!(meta, :local), +         {recipients, _, _} <- get_recipients(object), +         {:ok, activity} <- +           Repo.insert(%Activity{ +             data: object, +             local: local, +             recipients: recipients, +             actor: object["actor"] +           }) do +      {:ok, activity, meta} +    end +  end +    @spec insert(map(), boolean(), boolean(), boolean()) :: {:ok, Activity.t()} | {:error, any()}    def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do      with nil <- Activity.normalize(map), @@ -706,7 +721,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  defp fetch_activities_for_context_query(context, opts) do +  def fetch_activities_for_context_query(context, opts) do      public = [Constants.as_public()]      recipients = diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex new file mode 100644 index 000000000..429a510b8 --- /dev/null +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -0,0 +1,43 @@ +defmodule Pleroma.Web.ActivityPub.Builder do +  @moduledoc """ +  This module builds the objects. Meant to be used for creating local objects. + +  This module encodes our addressing policies and general shape of our objects. +  """ + +  alias Pleroma.Object +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.Utils +  alias Pleroma.Web.ActivityPub.Visibility + +  @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()} +  def like(actor, object) do +    object_actor = User.get_cached_by_ap_id(object.data["actor"]) + +    # Address the actor of the object, and our actor's follower collection if the post is public. +    to = +      if Visibility.is_public?(object) do +        [actor.follower_address, object.data["actor"]] +      else +        [object.data["actor"]] +      end + +    # CC everyone who's been addressed in the object, except ourself and the object actor's +    # follower collection +    cc = +      (object.data["to"] ++ (object.data["cc"] || [])) +      |> List.delete(actor.ap_id) +      |> List.delete(object_actor.follower_address) + +    {:ok, +     %{ +       "id" => Utils.generate_activity_id(), +       "actor" => actor.ap_id, +       "type" => "Like", +       "object" => object.data["id"], +       "to" => to, +       "cc" => cc, +       "context" => object.data["context"] +     }, []} +  end +end diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex new file mode 100644 index 000000000..dc4bce059 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidator do +  @moduledoc """ +  This module is responsible for validating an object (which can be an activity) +  and checking if it is both well formed and also compatible with our view of +  the system. +  """ + +  alias Pleroma.Object +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator + +  @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} +  def validate(object, meta) + +  def validate(%{"type" => "Like"} = object, meta) do +    with {:ok, object} <- +           object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do +      object = stringify_keys(object |> Map.from_struct()) +      {:ok, object, meta} +    end +  end + +  def stringify_keys(object) do +    object +    |> Map.new(fn {key, val} -> {to_string(key), val} end) +  end + +  def fetch_actor_and_object(object) do +    User.get_or_fetch_by_ap_id(object["actor"]) +    Object.normalize(object["object"]) +    :ok +  end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex new file mode 100644 index 000000000..b479c3918 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do +  import Ecto.Changeset + +  alias Pleroma.Object +  alias Pleroma.User + +  def validate_actor_presence(cng, field_name \\ :actor) do +    cng +    |> validate_change(field_name, fn field_name, actor -> +      if User.get_cached_by_ap_id(actor) do +        [] +      else +        [{field_name, "can't find user"}] +      end +    end) +  end + +  def validate_object_presence(cng, field_name \\ :object) do +    cng +    |> validate_change(field_name, fn field_name, object -> +      if Object.get_cached_by_ap_id(object) do +        [] +      else +        [{field_name, "can't find object"}] +      end +    end) +  end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/create_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_validator.ex new file mode 100644 index 000000000..926804ce7 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/create_validator.ex @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateNoteValidator do +  use Ecto.Schema + +  alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator +  alias Pleroma.Web.ActivityPub.ObjectValidators.Types + +  import Ecto.Changeset + +  @primary_key false + +  embedded_schema do +    field(:id, Types.ObjectID, primary_key: true) +    field(:actor, Types.ObjectID) +    field(:type, :string) +    field(:to, {:array, :string}) +    field(:cc, {:array, :string}) +    field(:bto, {:array, :string}, default: []) +    field(:bcc, {:array, :string}, default: []) + +    embeds_one(:object, NoteValidator) +  end + +  def cast_data(data) do +    cast(%__MODULE__{}, data, __schema__(:fields)) +  end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex new file mode 100644 index 000000000..49546ceaa --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex @@ -0,0 +1,57 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do +  use Ecto.Schema + +  alias Pleroma.Web.ActivityPub.ObjectValidators.Types +  alias Pleroma.Web.ActivityPub.Utils + +  import Ecto.Changeset +  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + +  @primary_key false + +  embedded_schema do +    field(:id, Types.ObjectID, primary_key: true) +    field(:type, :string) +    field(:object, Types.ObjectID) +    field(:actor, Types.ObjectID) +    field(:context, :string) +    field(:to, {:array, :string}) +    field(:cc, {:array, :string}) +  end + +  def cast_and_validate(data) do +    data +    |> cast_data() +    |> validate_data() +  end + +  def cast_data(data) do +    %__MODULE__{} +    |> cast(data, [:id, :type, :object, :actor, :context, :to, :cc]) +  end + +  def validate_data(data_cng) do +    data_cng +    |> validate_inclusion(:type, ["Like"]) +    |> validate_required([:id, :type, :object, :actor, :context, :to, :cc]) +    |> validate_actor_presence() +    |> validate_object_presence() +    |> validate_existing_like() +  end + +  def validate_existing_like(%{changes: %{actor: actor, object: object}} = cng) do +    if Utils.get_existing_like(actor, %{data: %{"id" => object}}) do +      cng +      |> add_error(:actor, "already liked this object") +      |> add_error(:object, "already liked by this actor") +    else +      cng +    end +  end + +  def validate_existing_like(cng), do: cng +end diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex new file mode 100644 index 000000000..c95b622e4 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do +  use Ecto.Schema + +  alias Pleroma.Web.ActivityPub.ObjectValidators.Types + +  import Ecto.Changeset + +  @primary_key false + +  embedded_schema do +    field(:id, Types.ObjectID, primary_key: true) +    field(:to, {:array, :string}, default: []) +    field(:cc, {:array, :string}, default: []) +    field(:bto, {:array, :string}, default: []) +    field(:bcc, {:array, :string}, default: []) +    # TODO: Write type +    field(:tag, {:array, :map}, default: []) +    field(:type, :string) +    field(:content, :string) +    field(:context, :string) +    field(:actor, Types.ObjectID) +    field(:attributedTo, Types.ObjectID) +    field(:summary, :string) +    field(:published, Types.DateTime) +    # TODO: Write type +    field(:emoji, :map, default: %{}) +    field(:sensitive, :boolean, default: false) +    # TODO: Write type +    field(:attachment, {:array, :map}, default: []) +    field(:replies_count, :integer, default: 0) +    field(:like_count, :integer, default: 0) +    field(:announcement_count, :integer, default: 0) +    field(:inRepyTo, :string) + +    field(:likes, {:array, :string}, default: []) +    field(:announcements, {:array, :string}, default: []) + +    # see if needed +    field(:conversation, :string) +    field(:context_id, :string) +  end + +  def cast_and_validate(data) do +    data +    |> cast_data() +    |> validate_data() +  end + +  def cast_data(data) do +    %__MODULE__{} +    |> cast(data, __schema__(:fields)) +  end + +  def validate_data(data_cng) do +    data_cng +    |> validate_inclusion(:type, ["Note"]) +    |> validate_required([:id, :actor, :to, :cc, :type, :content, :context]) +  end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex b/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex new file mode 100644 index 000000000..4f412fcde --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex @@ -0,0 +1,34 @@ +defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTime do +  @moduledoc """ +  The AP standard defines the date fields in AP as xsd:DateTime. Elixir's +  DateTime can't parse this, but it can parse the related iso8601. This +  module punches the date until it looks like iso8601 and normalizes to +  it. + +  DateTimes without a timezone offset are treated as UTC. + +  Reference: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-published +  """ +  use Ecto.Type + +  def type, do: :string + +  def cast(datetime) when is_binary(datetime) do +    with {:ok, datetime, _} <- DateTime.from_iso8601(datetime) do +      {:ok, DateTime.to_iso8601(datetime)} +    else +      {:error, :missing_offset} -> cast("#{datetime}Z") +      _e -> :error +    end +  end + +  def cast(_), do: :error + +  def dump(data) do +    {:ok, data} +  end + +  def load(data) do +    {:ok, data} +  end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex b/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex new file mode 100644 index 000000000..f6e749b33 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex @@ -0,0 +1,29 @@ +defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do +  use Ecto.Type + +  def type, do: :string + +  def cast(object) when is_binary(object) do +    # Host has to be present and scheme has to be an http scheme (for now) +    case URI.parse(object) do +      %URI{host: nil} -> :error +      %URI{host: ""} -> :error +      %URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, object} +      _ -> :error +    end +  end + +  def cast(%{"id" => object}), do: cast(object) + +  def cast(_) do +    :error +  end + +  def dump(data) do +    {:ok, data} +  end + +  def load(data) do +    {:ok, data} +  end +end diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex new file mode 100644 index 000000000..7ccee54c9 --- /dev/null +++ b/lib/pleroma/web/activity_pub/pipeline.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Pipeline do +  alias Pleroma.Activity +  alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.ActivityPub.MRF +  alias Pleroma.Web.ActivityPub.ObjectValidator +  alias Pleroma.Web.ActivityPub.SideEffects +  alias Pleroma.Web.Federator + +  @spec common_pipeline(map(), keyword()) :: {:ok, Activity.t(), keyword()} | {:error, any()} +  def common_pipeline(object, meta) do +    with {_, {:ok, validated_object, meta}} <- +           {:validate_object, ObjectValidator.validate(object, meta)}, +         {_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)}, +         {_, {:ok, %Activity{} = activity, meta}} <- +           {:persist_object, ActivityPub.persist(mrfd_object, meta)}, +         {_, {:ok, %Activity{} = activity, meta}} <- +           {:execute_side_effects, SideEffects.handle(activity, meta)}, +         {_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do +      {:ok, activity, meta} +    else +      {:mrf_object, {:reject, _}} -> {:ok, nil, meta} +      e -> {:error, e} +    end +  end + +  defp maybe_federate(activity, meta) do +    with {:ok, local} <- Keyword.fetch(meta, :local) do +      if local do +        Federator.publish(activity) +        {:ok, :federated} +      else +        {:ok, :not_federated} +      end +    else +      _e -> {:error, :badarg} +    end +  end +end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex new file mode 100644 index 000000000..666a4e310 --- /dev/null +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -0,0 +1,28 @@ +defmodule Pleroma.Web.ActivityPub.SideEffects do +  @moduledoc """ +  This module looks at an inserted object and executes the side effects that it +  implies. For example, a `Like` activity will increase the like count on the +  liked object, a `Follow` activity will add the user to the follower +  collection, and so on. +  """ +  alias Pleroma.Notification +  alias Pleroma.Object +  alias Pleroma.Web.ActivityPub.Utils + +  def handle(object, meta \\ []) + +  # Tasks this handles: +  # - Add like to object +  # - Set up notification +  def handle(%{data: %{"type" => "Like"}} = object, meta) do +    liked_object = Object.get_by_ap_id(object.data["object"]) +    Utils.add_like_to_object(object, liked_object) +    Notification.create_notifications(object) +    {:ok, object, meta} +  end + +  # Nothing to do +  def handle(object, meta) do +    {:ok, object, meta} +  end +end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 09bd9a442..39feae285 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -13,6 +13,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.ActivityPub.ObjectValidator +  alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator +  alias Pleroma.Web.ActivityPub.Pipeline    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.ActivityPub.Visibility    alias Pleroma.Web.Federator @@ -202,16 +205,46 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> Map.put("conversation", context)    end +  defp add_if_present(map, _key, nil), do: map + +  defp add_if_present(map, key, value) do +    Map.put(map, key, value) +  end +    def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do      attachments =        Enum.map(attachment, fn data -> -        media_type = data["mediaType"] || data["mimeType"] -        href = data["url"] || data["href"] -        url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}] +        url = +          cond do +            is_list(data["url"]) -> List.first(data["url"]) +            is_map(data["url"]) -> data["url"] +            true -> nil +          end -        data -        |> Map.put("mediaType", media_type) -        |> Map.put("url", url) +        media_type = +          cond do +            is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"] +            is_binary(data["mediaType"]) -> data["mediaType"] +            is_binary(data["mimeType"]) -> data["mimeType"] +            true -> nil +          end + +        href = +          cond do +            is_map(url) && is_binary(url["href"]) -> url["href"] +            is_binary(data["url"]) -> data["url"] +            is_binary(data["href"]) -> data["href"] +          end + +        attachment_url = +          %{"href" => href} +          |> add_if_present("mediaType", media_type) +          |> add_if_present("type", Map.get(url || %{}, "type")) + +        %{"url" => [attachment_url]} +        |> add_if_present("mediaType", media_type) +        |> add_if_present("type", data["type"]) +        |> add_if_present("name", data["name"])        end)      Map.put(object, "attachment", attachments) @@ -491,7 +524,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do             {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},             {_, {:ok, _}} <-               {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")}, -           {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do +           {:ok, _relationship} <- +             FollowingRelationship.update(follower, followed, :follow_accept) do          ActivityPub.accept(%{            to: [follower.ap_id],            actor: followed, @@ -501,7 +535,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do        else          {:user_blocked, true} ->            {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") -          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject") +          {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)            ActivityPub.reject(%{              to: [follower.ap_id], @@ -512,7 +546,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          {:follow, {:error, _}} ->            {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") -          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject") +          {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)            ActivityPub.reject(%{              to: [follower.ap_id], @@ -522,7 +556,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do            })          {:user_locked, true} -> -          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending") +          {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)            :noop        end @@ -542,7 +576,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do           {:ok, follow_activity} <- get_follow_activity(follow_object, followed),           {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),           %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), -         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do +         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do        ActivityPub.accept(%{          to: follow_activity.data["to"],          type: "Accept", @@ -565,7 +599,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do           {:ok, follow_activity} <- get_follow_activity(follow_object, followed),           {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),           %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), -         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"), +         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),           {:ok, activity} <-             ActivityPub.reject(%{               to: follow_activity.data["to"], @@ -609,17 +643,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> handle_incoming(options)    end -  def handle_incoming( -        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data, -        _options -      ) do -    with actor <- Containment.get_actor(data), -         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), -         {:ok, object} <- get_obj_helper(object_id), -         {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do +  def handle_incoming(%{"type" => "Like"} = data, _options) do +    with {_, {:ok, cast_data_sym}} <- +           {:casting_data, +            data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)}, +         cast_data = ObjectValidator.stringify_keys(Map.from_struct(cast_data_sym)), +         :ok <- ObjectValidator.fetch_actor_and_object(cast_data), +         {_, {:ok, cast_data}} <- {:ensure_context_presence, ensure_context_presence(cast_data)}, +         {_, {:ok, cast_data}} <- +           {:ensure_recipients_presence, ensure_recipients_presence(cast_data)}, +         {_, {:ok, activity, _meta}} <- +           {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do        {:ok, activity}      else -      _e -> :error +      e -> {:error, e}      end    end @@ -1243,4 +1280,45 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def maybe_fix_user_url(data), do: data    def maybe_fix_user_object(data), do: maybe_fix_user_url(data) + +  defp ensure_context_presence(%{"context" => context} = data) when is_binary(context), +    do: {:ok, data} + +  defp ensure_context_presence(%{"object" => object} = data) when is_binary(object) do +    with %{data: %{"context" => context}} when is_binary(context) <- Object.normalize(object) do +      {:ok, Map.put(data, "context", context)} +    else +      _ -> +        {:error, :no_context} +    end +  end + +  defp ensure_context_presence(_) do +    {:error, :no_context} +  end + +  defp ensure_recipients_presence(%{"to" => [_ | _], "cc" => [_ | _]} = data), +    do: {:ok, data} + +  defp ensure_recipients_presence(%{"object" => object} = data) do +    case Object.normalize(object) do +      %{data: %{"actor" => actor}} -> +        data = +          data +          |> Map.put("to", [actor]) +          |> Map.put("cc", data["cc"] || []) + +        {:ok, data} + +      nil -> +        {:error, :no_object} + +      _ -> +        {:error, :no_actor} +    end +  end + +  defp ensure_recipients_presence(_) do +    {:error, :no_object} +  end  end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index ca5439920..831c3bd02 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -258,7 +258,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      conn      |> put_view(Pleroma.Web.AdminAPI.StatusView) -    |> render("index.json", %{activities: activities, as: :activity}) +    |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})    end    def list_user_statuses(conn, %{"nickname" => nickname} = params) do @@ -277,7 +277,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do        conn        |> put_view(StatusView) -      |> render("index.json", %{activities: activities, as: :activity}) +      |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})      else        _ -> {:error, :not_found}      end @@ -576,9 +576,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    @doc "Sends registration invite via email"    def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do -    with true <- -           Config.get([:instance, :invites_enabled]) && -             !Config.get([:instance, :registrations_open]), +    with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, +         {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},           {:ok, invite_token} <- UserInviteToken.create_invite(),           email <-             Pleroma.Emails.UserEmail.user_invitation_email( @@ -589,6 +588,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do             ),           {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do        json_response(conn, :no_content, "") +    else +      {:registrations_open, _} -> +        errors( +          conn, +          {:error, "To send invites you need to set the `registrations_open` option to false."} +        ) + +      {:invites_enabled, _} -> +        errors( +          conn, +          {:error, "To send invites you need to set the `invites_enabled` option to true."} +        )      end    end @@ -801,7 +812,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      conn      |> put_view(Pleroma.Web.AdminAPI.StatusView) -    |> render("index.json", %{activities: activities, as: :activity}) +    |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})    end    def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index ca0bcebc7..d50969b2a 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -38,7 +38,12 @@ defmodule Pleroma.Web.AdminAPI.ReportView do        actor: merge_account_views(user),        content: content,        created_at: created_at, -      statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}), +      statuses: +        StatusView.render("index.json", %{ +          activities: statuses, +          as: :activity, +          skip_relationships: false +        }),        state: report.data["state"],        notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})      } diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex new file mode 100644 index 000000000..3890489e3 --- /dev/null +++ b/lib/pleroma/web/api_spec.ex @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec do +  alias OpenApiSpex.OpenApi +  alias Pleroma.Web.Endpoint +  alias Pleroma.Web.Router + +  @behaviour OpenApi + +  @impl OpenApi +  def spec do +    %OpenApi{ +      servers: [ +        # Populate the Server info from a phoenix endpoint +        OpenApiSpex.Server.from_endpoint(Endpoint) +      ], +      info: %OpenApiSpex.Info{ +        title: "Pleroma", +        description: Application.spec(:pleroma, :description) |> to_string(), +        version: Application.spec(:pleroma, :vsn) |> to_string() +      }, +      # populate the paths from a phoenix router +      paths: OpenApiSpex.Paths.from_router(Router), +      components: %OpenApiSpex.Components{ +        securitySchemes: %{ +          "oAuth" => %OpenApiSpex.SecurityScheme{ +            type: "oauth2", +            flows: %OpenApiSpex.OAuthFlows{ +              password: %OpenApiSpex.OAuthFlow{ +                authorizationUrl: "/oauth/authorize", +                tokenUrl: "/oauth/token", +                scopes: %{"read" => "read", "write" => "write", "follow" => "follow"} +              } +            } +          } +        } +      } +    } +    # discover request/response schemas from path specs +    |> OpenApiSpex.resolve_schema_modules() +  end +end diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex new file mode 100644 index 000000000..7348dcbee --- /dev/null +++ b/lib/pleroma/web/api_spec/helpers.ex @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Helpers do +  def request_body(description, schema_ref, opts \\ []) do +    media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"] + +    content = +      media_types +      |> Enum.map(fn type -> +        {type, +         %OpenApiSpex.MediaType{ +           schema: schema_ref, +           example: opts[:example], +           examples: opts[:examples] +         }} +      end) +      |> Enum.into(%{}) + +    %OpenApiSpex.RequestBody{ +      description: description, +      content: content, +      required: opts[:required] || false +    } +  end +end diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex new file mode 100644 index 000000000..26d8dbd42 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/app_operation.ex @@ -0,0 +1,96 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.AppOperation do +  alias OpenApiSpex.Operation +  alias OpenApiSpex.Schema +  alias Pleroma.Web.ApiSpec.Helpers +  alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest +  alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse + +  @spec open_api_operation(atom) :: Operation.t() +  def open_api_operation(action) do +    operation = String.to_existing_atom("#{action}_operation") +    apply(__MODULE__, operation, []) +  end + +  @spec create_operation() :: Operation.t() +  def create_operation do +    %Operation{ +      tags: ["apps"], +      summary: "Create an application", +      description: "Create a new application to obtain OAuth2 credentials", +      operationId: "AppController.create", +      requestBody: Helpers.request_body("Parameters", AppCreateRequest, required: true), +      responses: %{ +        200 => Operation.response("App", "application/json", AppCreateResponse), +        422 => +          Operation.response( +            "Unprocessable Entity", +            "application/json", +            %Schema{ +              type: :object, +              description: +                "If a required parameter is missing or improperly formatted, the request will fail.", +              properties: %{ +                error: %Schema{type: :string} +              }, +              example: %{ +                "error" => "Validation failed: Redirect URI must be an absolute URI." +              } +            } +          ) +      } +    } +  end + +  def verify_credentials_operation do +    %Operation{ +      tags: ["apps"], +      summary: "Verify your app works", +      description: "Confirm that the app's OAuth2 credentials work.", +      operationId: "AppController.verify_credentials", +      security: [ +        %{ +          "oAuth" => ["read"] +        } +      ], +      responses: %{ +        200 => +          Operation.response("App", "application/json", %Schema{ +            type: :object, +            description: +              "If the Authorization header was provided with a valid token, you should see your app returned as an Application entity.", +            properties: %{ +              name: %Schema{type: :string}, +              vapid_key: %Schema{type: :string}, +              website: %Schema{type: :string, nullable: true} +            }, +            example: %{ +              "name" => "My App", +              "vapid_key" => +                "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=", +              "website" => "https://myapp.com/" +            } +          }), +        422 => +          Operation.response( +            "Unauthorized", +            "application/json", +            %Schema{ +              type: :object, +              description: +                "If the Authorization header contains an invalid token, is malformed, or is not present, an error will be returned indicating an authorization failure.", +              properties: %{ +                error: %Schema{type: :string} +              }, +              example: %{ +                "error" => "The access token is invalid." +              } +            } +          ) +      } +    } +  end +end diff --git a/lib/pleroma/web/api_spec/operations/domain_block_operation.ex b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex new file mode 100644 index 000000000..dd14837c3 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex @@ -0,0 +1,64 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do +  alias OpenApiSpex.Operation +  alias OpenApiSpex.Schema +  alias Pleroma.Web.ApiSpec.Helpers +  alias Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest +  alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse + +  def open_api_operation(action) do +    operation = String.to_existing_atom("#{action}_operation") +    apply(__MODULE__, operation, []) +  end + +  def index_operation do +    %Operation{ +      tags: ["domain_blocks"], +      summary: "Fetch domain blocks", +      description: "View domains the user has blocked.", +      security: [%{"oAuth" => ["follow", "read:blocks"]}], +      operationId: "DomainBlockController.index", +      responses: %{ +        200 => Operation.response("Domain blocks", "application/json", DomainBlocksResponse) +      } +    } +  end + +  def create_operation do +    %Operation{ +      tags: ["domain_blocks"], +      summary: "Block a domain", +      description: """ +      Block a domain to: + +      - hide all public posts from it +      - hide all notifications from it +      - remove all followers from it +      - prevent following new users from it (but does not remove existing follows) +      """, +      operationId: "DomainBlockController.create", +      requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true), +      security: [%{"oAuth" => ["follow", "write:blocks"]}], +      responses: %{ +        200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) +      } +    } +  end + +  def delete_operation do +    %Operation{ +      tags: ["domain_blocks"], +      summary: "Unblock a domain", +      description: "Remove a domain block, if it exists in the user's array of blocked domains.", +      operationId: "DomainBlockController.delete", +      requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true), +      security: [%{"oAuth" => ["follow", "write:blocks"]}], +      responses: %{ +        200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) +      } +    } +  end +end diff --git a/lib/pleroma/web/api_spec/schemas/app_create_request.ex b/lib/pleroma/web/api_spec/schemas/app_create_request.ex new file mode 100644 index 000000000..8a83abef3 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/app_create_request.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateRequest do +  alias OpenApiSpex.Schema +  require OpenApiSpex + +  OpenApiSpex.schema(%{ +    title: "AppCreateRequest", +    description: "POST body for creating an app", +    type: :object, +    properties: %{ +      client_name: %Schema{type: :string, description: "A name for your application."}, +      redirect_uris: %Schema{ +        type: :string, +        description: +          "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." +      }, +      scopes: %Schema{ +        type: :string, +        description: "Space separated list of scopes. If none is provided, defaults to `read`." +      }, +      website: %Schema{type: :string, description: "A URL to the homepage of your app"} +    }, +    required: [:client_name, :redirect_uris], +    example: %{ +      "client_name" => "My App", +      "redirect_uris" => "https://myapp.com/auth/callback", +      "website" => "https://myapp.com/" +    } +  }) +end diff --git a/lib/pleroma/web/api_spec/schemas/app_create_response.ex b/lib/pleroma/web/api_spec/schemas/app_create_response.ex new file mode 100644 index 000000000..f290fb031 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/app_create_response.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateResponse do +  alias OpenApiSpex.Schema + +  require OpenApiSpex + +  OpenApiSpex.schema(%{ +    title: "AppCreateResponse", +    description: "Response schema for an app", +    type: :object, +    properties: %{ +      id: %Schema{type: :string}, +      name: %Schema{type: :string}, +      client_id: %Schema{type: :string}, +      client_secret: %Schema{type: :string}, +      redirect_uri: %Schema{type: :string}, +      vapid_key: %Schema{type: :string}, +      website: %Schema{type: :string, nullable: true} +    }, +    example: %{ +      "id" => "123", +      "name" => "My App", +      "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM", +      "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw", +      "vapid_key" => +        "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=", +      "website" => "https://myapp.com/" +    } +  }) +end diff --git a/lib/pleroma/web/api_spec/schemas/domain_block_request.ex b/lib/pleroma/web/api_spec/schemas/domain_block_request.ex new file mode 100644 index 000000000..ee9238361 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/domain_block_request.ex @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest do +  alias OpenApiSpex.Schema +  require OpenApiSpex + +  OpenApiSpex.schema(%{ +    title: "DomainBlockRequest", +    type: :object, +    properties: %{ +      domain: %Schema{type: :string} +    }, +    required: [:domain], +    example: %{ +      "domain" => "facebook.com" +    } +  }) +end diff --git a/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex b/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex new file mode 100644 index 000000000..d895aca4e --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex @@ -0,0 +1,16 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse do +  require OpenApiSpex +  alias OpenApiSpex.Schema + +  OpenApiSpex.schema(%{ +    title: "DomainBlocksResponse", +    description: "Response schema for domain blocks", +    type: :array, +    items: %Schema{type: :string}, +    example: ["google.com", "facebook.com"] +  }) +end diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index c4356f93b..c1cd15bb2 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -187,7 +187,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do    end    defp preview?(draft) do -    preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"]) || false +    preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"])      %__MODULE__{draft | preview?: preview?}    end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 2646b9f7b..c56756a3d 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -12,6 +12,8 @@ defmodule Pleroma.Web.CommonAPI do    alias Pleroma.User    alias Pleroma.UserRelationship    alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.ActivityPub.Builder +  alias Pleroma.Web.ActivityPub.Pipeline    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.ActivityPub.Visibility @@ -19,6 +21,7 @@ defmodule Pleroma.Web.CommonAPI do    import Pleroma.Web.CommonAPI.Utils    require Pleroma.Constants +  require Logger    def follow(follower, followed) do      timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) @@ -42,7 +45,7 @@ defmodule Pleroma.Web.CommonAPI do      with {:ok, follower} <- User.follow(follower, followed),           %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),           {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), -         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"), +         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),           {:ok, _activity} <-             ActivityPub.accept(%{               to: [follower.ap_id], @@ -57,7 +60,7 @@ defmodule Pleroma.Web.CommonAPI do    def reject_follow_request(follower, followed) do      with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),           {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), -         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"), +         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),           {:ok, _activity} <-             ActivityPub.reject(%{               to: [follower.ap_id], @@ -109,18 +112,51 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  def favorite(id_or_ap_id, user) do -    with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)}, -         object <- Object.normalize(activity), -         like_activity <- Utils.get_existing_like(user.ap_id, object) do -      if like_activity do -        {:ok, like_activity, object} -      else -        ActivityPub.like(user, object) -      end +  @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()} +  def favorite(%User{} = user, id) do +    case favorite_helper(user, id) do +      {:ok, _} = res -> +        res + +      {:error, :not_found} = res -> +        res + +      {:error, e} -> +        Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}") +        {:error, dgettext("errors", "Could not favorite")} +    end +  end + +  def favorite_helper(user, id) do +    with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)}, +         {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)}, +         {_, {:ok, %Activity{} = activity, _meta}} <- +           {:common_pipeline, +            Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do +      {:ok, activity}      else -      {:find_activity, _} -> {:error, :not_found} -      _ -> {:error, dgettext("errors", "Could not favorite")} +      {:find_object, _} -> +        {:error, :not_found} + +      {:common_pipeline, +       { +         :error, +         { +           :validate_object, +           { +             :error, +             changeset +           } +         } +       }} = e -> +        if {:object, {"already liked by this actor", []}} in changeset.errors do +          {:ok, :already_liked} +        else +          {:error, e} +        end + +      e -> +        {:error, e}      end    end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index b49523ec3..4780081b2 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -5,10 +5,18 @@  defmodule Pleroma.Web.ControllerHelper do    use Pleroma.Web, :controller -  # As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html +  alias Pleroma.Config + +  # As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html    @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"] -  def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil -  def truthy_param?(value), do: value not in @falsy_param_values + +  def explicitly_falsy_param?(value), do: value in @falsy_param_values + +  # Note: `nil` and `""` are considered falsy values in Pleroma +  def falsy_param?(value), +    do: explicitly_falsy_param?(value) or value in [nil, ""] + +  def truthy_param?(value), do: not falsy_param?(value)    def json_response(conn, status, json) do      conn @@ -96,4 +104,14 @@ defmodule Pleroma.Web.ControllerHelper do    def put_if_exist(map, _key, nil), do: map    def put_if_exist(map, key, value), do: Map.put(map, key, value) + +  @doc "Whether to skip rendering `[:account][:pleroma][:relationship]`for statuses/notifications" +  def skip_relationships?(params) do +    if Config.get([:extensions, :output_relationships_in_statuses_by_default]) do +      false +    else +      # BREAKING: older PleromaFE versions do not send this param but _do_ expect relationships. +      not truthy_param?(params["with_relationships"]) +    end +  end  end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index bd6853d12..fe425c2f4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -6,7 +6,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    use Pleroma.Web, :controller    import Pleroma.Web.ControllerHelper, -    only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3] +    only: [ +      add_link_headers: 2, +      truthy_param?: 1, +      assign_account_by_id: 2, +      json_response: 3, +      skip_relationships?: 1 +    ]    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.Plugs.RateLimiter @@ -240,7 +246,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do        conn        |> add_link_headers(activities)        |> put_view(StatusView) -      |> render("index.json", activities: activities, for: reading_user, as: :activity) +      |> render("index.json", +        activities: activities, +        for: reading_user, +        as: :activity, +        skip_relationships: skip_relationships?(params) +      )      else        _e -> render_error(conn, :not_found, "Can't find user")      end diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index 5e2871f18..005c60444 100644 --- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -14,17 +14,20 @@ defmodule Pleroma.Web.MastodonAPI.AppController do    action_fallback(Pleroma.Web.MastodonAPI.FallbackController)    plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials) +  plug(OpenApiSpex.Plug.CastAndValidate)    @local_mastodon_name "Mastodon-Local" +  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AppOperation +    @doc "POST /api/v1/apps" -  def create(conn, params) do +  def create(%{body_params: params} = conn, _params) do      scopes = Scopes.fetch_scopes(params, ["read"])      app_attrs =        params -      |> Map.drop(["scope", "scopes"]) -      |> Map.put("scopes", scopes) +      |> Map.take([:client_name, :redirect_uris, :website]) +      |> Map.put(:scopes, scopes)      with cs <- App.register_changeset(%App{}, app_attrs),           false <- cs.changes[:client_name] == @local_mastodon_name, diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex index e4156cbe6..84de79413 100644 --- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex @@ -8,6 +8,9 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.User +  plug(OpenApiSpex.Plug.CastAndValidate) +  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation +    plug(      OAuthScopesPlug,      %{scopes: ["follow", "read:blocks"]} when action == :index @@ -26,13 +29,13 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do    end    @doc "POST /api/v1/domain_blocks" -  def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do +  def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do      User.block_domain(blocker, domain)      json(conn, %{})    end    @doc "DELETE /api/v1/domain_blocks" -  def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do +  def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do      User.unblock_domain(blocker, domain)      json(conn, %{})    end diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 0c9218454..7fb536b09 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -5,7 +5,7 @@  defmodule Pleroma.Web.MastodonAPI.NotificationController do    use Pleroma.Web, :controller -  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] +  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]    alias Pleroma.Notification    alias Pleroma.Plugs.OAuthScopesPlug @@ -45,7 +45,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do      conn      |> add_link_headers(notifications) -    |> render("index.json", notifications: notifications, for: user) +    |> render("index.json", +      notifications: notifications, +      for: user, +      skip_relationships: skip_relationships?(params) +    )    end    # GET /api/v1/notifications/:id @@ -66,7 +70,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do      json(conn, %{})    end -  # POST /api/v1/notifications/dismiss +  # POST /api/v1/notifications/:id/dismiss +  # POST /api/v1/notifications/dismiss (deprecated)    def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do      with {:ok, _notif} <- Notification.dismiss(user, id) do        json(conn, %{}) diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index fcab4ef63..c258742dd 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -5,13 +5,14 @@  defmodule Pleroma.Web.MastodonAPI.SearchController do    use Pleroma.Web, :controller +  import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 2, skip_relationships?: 1] +    alias Pleroma.Activity    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.Plugs.RateLimiter    alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web -  alias Pleroma.Web.ControllerHelper    alias Pleroma.Web.MastodonAPI.AccountView    alias Pleroma.Web.MastodonAPI.StatusView @@ -66,10 +67,11 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    defp search_options(params, user) do      [ +      skip_relationships: skip_relationships?(params),        resolve: params["resolve"] == "true",        following: params["following"] == "true", -      limit: ControllerHelper.fetch_integer_param(params, "limit"), -      offset: ControllerHelper.fetch_integer_param(params, "offset"), +      limit: fetch_integer_param(params, "limit"), +      offset: fetch_integer_param(params, "offset"),        type: params["type"],        author: get_author(params),        for_user: user @@ -79,12 +81,24 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    defp resource_search(_, "accounts", query, options) do      accounts = with_fallback(fn -> User.search(query, options) end) -    AccountView.render("index.json", users: accounts, for: options[:for_user], as: :user) + +    AccountView.render("index.json", +      users: accounts, +      for: options[:for_user], +      as: :user, +      skip_relationships: false +    )    end    defp resource_search(_, "statuses", query, options) do      statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end) -    StatusView.render("index.json", activities: statuses, for: options[:for_user], as: :activity) + +    StatusView.render("index.json", +      activities: statuses, +      for: options[:for_user], +      as: :activity, +      skip_relationships: options[:skip_relationships] +    )    end    defp resource_search(:v2, "hashtags", query, _options) do diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 37afe6949..397dd10e3 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -5,7 +5,8 @@  defmodule Pleroma.Web.MastodonAPI.StatusController do    use Pleroma.Web, :controller -  import Pleroma.Web.ControllerHelper, only: [try_render: 3, add_link_headers: 2] +  import Pleroma.Web.ControllerHelper, +    only: [try_render: 3, add_link_headers: 2, skip_relationships?: 1]    require Ecto.Query @@ -101,7 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do    `ids` query param is required    """ -  def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do +  def index(%{assigns: %{user: user}} = conn, %{"ids" => ids} = params) do      limit = 100      activities = @@ -110,7 +111,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do        |> Activity.all_by_ids_with_object()        |> Enum.filter(&Visibility.visible_for_user?(&1, user)) -    render(conn, "index.json", activities: activities, for: user, as: :activity) +    render(conn, "index.json", +      activities: activities, +      for: user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end    @doc """ @@ -207,9 +213,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do    end    @doc "POST /api/v1/statuses/:id/favourite" -  def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do -    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user), -         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do +  def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do +    with {:ok, _fav} <- CommonAPI.favorite(user, activity_id), +         %Activity{} = activity <- Activity.get_by_id(activity_id) do        try_render(conn, "show.json", activity: activity, for: user, as: :activity)      end    end @@ -360,7 +366,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do      conn      |> add_link_headers(activities) -    |> render("index.json", activities: activities, for: user, as: :activity) +    |> render("index.json", +      activities: activities, +      for: user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end    @doc "GET /api/v1/bookmarks" @@ -378,6 +389,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do      conn      |> add_link_headers(bookmarks) -    |> render("index.json", %{activities: activities, for: user, as: :activity}) +    |> render("index.json", +      activities: activities, +      for: user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end  end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 91f41416d..b3c58005e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do    use Pleroma.Web, :controller    import Pleroma.Web.ControllerHelper, -    only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1] +    only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1]    alias Pleroma.Pagination    alias Pleroma.Plugs.OAuthScopesPlug @@ -14,9 +14,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub -  # TODO: Replace with a macro when there is a Phoenix release with +  # TODO: Replace with a macro when there is a Phoenix release with the following commit in it:    # https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e -  # in it    plug(RateLimiter, [name: :timeline, bucket_name: :direct_timeline] when action == :direct)    plug(RateLimiter, [name: :timeline, bucket_name: :public_timeline] when action == :public) @@ -49,7 +48,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      conn      |> add_link_headers(activities) -    |> render("index.json", activities: activities, for: user, as: :activity) +    |> render("index.json", +      activities: activities, +      for: user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end    # GET /api/v1/timelines/direct @@ -68,7 +72,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      conn      |> add_link_headers(activities) -    |> render("index.json", activities: activities, for: user, as: :activity) +    |> render("index.json", +      activities: activities, +      for: user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end    # GET /api/v1/timelines/public @@ -95,7 +104,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do        conn        |> add_link_headers(activities, %{"local" => local_only}) -      |> render("index.json", activities: activities, for: user, as: :activity) +      |> render("index.json", +        activities: activities, +        for: user, +        as: :activity, +        skip_relationships: skip_relationships?(params) +      )      else        render_error(conn, :unauthorized, "authorization required for timeline view")      end @@ -140,7 +154,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      conn      |> add_link_headers(activities, %{"local" => local_only}) -    |> render("index.json", activities: activities, for: user, as: :activity) +    |> render("index.json", +      activities: activities, +      for: user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end    # GET /api/v1/timelines/list/:list_id @@ -164,7 +183,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do          |> ActivityPub.fetch_activities_bounded(following, params)          |> Enum.reverse() -      render(conn, "index.json", activities: activities, for: user, as: :activity) +      render(conn, "index.json", +        activities: activities, +        for: user, +        as: :activity, +        skip_relationships: skip_relationships?(params) +      )      else        _e -> render_error(conn, :forbidden, "Error.")      end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 99e62f580..8fb96a22a 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    def render("index.json", %{users: users} = opts) do      reading_user = opts[:for] +    # Note: :skip_relationships option is currently intentionally not supported for accounts      relationships_opt =        cond do          Map.has_key?(opts, :relationships) -> @@ -73,7 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do      followed_by =        if following_relationships do          case FollowingRelationship.find(following_relationships, target, reading_user) do -          %{state: "accept"} -> true +          %{state: :follow_accept} -> true            _ -> false          end        else @@ -83,7 +84,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do      # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags      %{        id: to_string(target.id), -      following: follow_state == "accept", +      following: follow_state == :follow_accept,        followed_by: followed_by,        blocking:          UserRelationship.exists?( @@ -125,7 +126,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do            reading_user,            &User.subscribed_to?(&2, &1)          ), -      requested: follow_state == "pending", +      requested: follow_state == :follow_pending,        domain_blocking: User.blocks_domain?(reading_user, target),        showing_reblogs:          not UserRelationship.exists?( @@ -192,11 +193,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do        end)      relationship = -      render("relationship.json", %{ -        user: opts[:for], -        target: user, -        relationships: opts[:relationships] -      }) +      if opts[:skip_relationships] do +        %{} +      else +        render("relationship.json", %{ +          user: opts[:for], +          target: user, +          relationships: opts[:relationships] +        }) +      end      %{        id: to_string(user.id), diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index ae87d4701..734ffbf39 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -51,14 +51,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do              |> Enum.filter(& &1)              |> Kernel.++(move_activities_targets) -          UserRelationship.view_relationships_option(reading_user, actors) +          UserRelationship.view_relationships_option(reading_user, actors, +            source_mutes_only: opts[:skip_relationships] +          )        end -    opts = %{ -      for: reading_user, -      parent_activities: parent_activities, -      relationships: relationships_opt -    } +    opts = +      opts +      |> Map.put(:parent_activities, parent_activities) +      |> Map.put(:relationships, relationships_opt)      safe_render_many(notifications, NotificationView, "show.json", opts)    end @@ -82,12 +83,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do      mastodon_type = Activity.mastodon_notification_type(activity) +    render_opts = %{ +      relationships: opts[:relationships], +      skip_relationships: opts[:skip_relationships] +    } +      with %{id: _} = account <- -           AccountView.render("show.json", %{ -             user: actor, -             for: reading_user, -             relationships: opts[:relationships] -           }) do +           AccountView.render( +             "show.json", +             Map.merge(render_opts, %{user: actor, for: reading_user}) +           ) do        response = %{          id: to_string(notification.id),          type: mastodon_type, @@ -98,8 +103,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do          }        } -      render_opts = %{relationships: opts[:relationships]} -        case mastodon_type do          "mention" ->            put_status(response, activity, reading_user, render_opts) @@ -111,6 +114,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do            put_status(response, parent_activity_fn.(), reading_user, render_opts)          "move" -> +          # Note: :skip_relationships option being applied to _account_ rendering (here)            put_target(response, activity, reading_user, render_opts)          "follow" -> diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index cea76e735..b5850e1ae 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -99,7 +99,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          true ->            actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"])) -          UserRelationship.view_relationships_option(reading_user, actors) +          UserRelationship.view_relationships_option(reading_user, actors, +            source_mutes_only: opts[:skip_relationships] +          )        end      opts = @@ -153,7 +155,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          AccountView.render("show.json", %{            user: user,            for: opts[:for], -          relationships: opts[:relationships] +          relationships: opts[:relationships], +          skip_relationships: opts[:skip_relationships]          }),        in_reply_to_id: nil,        in_reply_to_account_id: nil, @@ -301,6 +304,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          _ -> []        end +    # Status muted state (would do 1 request per status unless user mutes are preloaded)      muted =        thread_muted? ||          UserRelationship.exists?( @@ -319,7 +323,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          AccountView.render("show.json", %{            user: user,            for: opts[:for], -          relationships: opts[:relationships] +          relationships: opts[:relationships], +          skip_relationships: opts[:skip_relationships]          }),        in_reply_to_id: reply_to && to_string(reply_to.id),        in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 30838b1eb..f9a5ddcc0 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -75,7 +75,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do          end,          if Config.get([:instance, :safe_dm_mentions]) do            "safe_dm_mentions" -        end +        end, +        "pleroma_emoji_reactions"        ]        |> Enum.filter(& &1) diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index 8ecf901f3..1023f16d4 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -15,7 +15,12 @@ defmodule Pleroma.Web.OAuth.Scopes do    Note: `scopes` is used by Mastodon — supporting it but sticking to    OAuth's standard `scope` wherever we control it    """ -  @spec fetch_scopes(map(), list()) :: list() +  @spec fetch_scopes(map() | struct(), list()) :: list() + +  def fetch_scopes(%Pleroma.Web.ApiSpec.Schemas.AppCreateRequest{scopes: scopes}, default) do +    parse_scopes(scopes, default) +  end +    def fetch_scopes(params, default) do      parse_scopes(params["scope"] || params["scopes"], default)    end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index dcba67d03..9d0b3b1e4 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do    use Pleroma.Web, :controller    import Pleroma.Web.ControllerHelper, -    only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] +    only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1]    alias Ecto.Changeset    alias Pleroma.Plugs.OAuthScopesPlug @@ -139,7 +139,12 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do      conn      |> add_link_headers(activities)      |> put_view(StatusView) -    |> render("index.json", activities: activities, for: for_user, as: :activity) +    |> render("index.json", +      activities: activities, +      for: for_user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end    @doc "POST /api/v1/pleroma/accounts/:id/subscribe" diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index 75f61b675..fe1b97a20 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/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: 2] +  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]    alias Pleroma.Activity    alias Pleroma.Conversation.Participation @@ -110,12 +110,11 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do    end    def conversation_statuses( -        %{assigns: %{user: user}} = conn, +        %{assigns: %{user: %{id: user_id} = user}} = conn,          %{"id" => participation_id} = params        ) do -    with %Participation{} = participation <- -           Participation.get(participation_id, preload: [:conversation]), -         true <- user.id == participation.user_id do +    with %Participation{user_id: ^user_id} = participation <- +           Participation.get(participation_id, preload: [:conversation]) do        params =          params          |> Map.put("blocking_user", user) @@ -124,13 +123,19 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do        activities =          participation.conversation.ap_id -        |> ActivityPub.fetch_activities_for_context(params) +        |> ActivityPub.fetch_activities_for_context_query(params) +        |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false))          |> Enum.reverse()        conn        |> add_link_headers(activities)        |> put_view(StatusView) -      |> render("index.json", %{activities: activities, for: user, as: :activity}) +      |> render("index.json", +        activities: activities, +        for: user, +        as: :activity, +        skip_relationships: skip_relationships?(params) +      )      else        _error ->          conn @@ -184,13 +189,17 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do      end    end -  def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do +  def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do      with notifications <- Notification.set_read_up_to(user, max_id) do        notifications = Enum.take(notifications, 80)        conn        |> put_view(NotificationView) -      |> render("index.json", %{notifications: notifications, for: user}) +      |> render("index.json", +        notifications: notifications, +        for: user, +        skip_relationships: skip_relationships?(params) +      )      end    end  end diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 0314535d2..9d3d7f978 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -64,5 +64,8 @@ defmodule Pleroma.Web.RichMedia.Helpers do    def fetch_data_for_activity(_), do: %{} -  def perform(:fetch, %Activity{} = activity), do: fetch_data_for_activity(activity) +  def perform(:fetch, %Activity{} = activity) do +    fetch_data_for_activity(activity) +    :ok +  end  end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 3d57073d0..8d13cd6c9 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -29,6 +29,7 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.SetUserSessionIdPlug)      plug(Pleroma.Plugs.EnsureUserKeyPlug)      plug(Pleroma.Plugs.IdempotencyPlug) +    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)    end    pipeline :authenticated_api do @@ -45,6 +46,7 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.SetUserSessionIdPlug)      plug(Pleroma.Plugs.EnsureAuthenticatedPlug)      plug(Pleroma.Plugs.IdempotencyPlug) +    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)    end    pipeline :admin_api do @@ -62,6 +64,7 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.EnsureAuthenticatedPlug)      plug(Pleroma.Plugs.UserIsAdminPlug)      plug(Pleroma.Plugs.IdempotencyPlug) +    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)    end    pipeline :mastodon_html do @@ -95,10 +98,12 @@ defmodule Pleroma.Web.Router do    pipeline :config do      plug(:accepts, ["json", "xml"]) +    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)    end    pipeline :pleroma_api do      plug(:accepts, ["html", "json"]) +    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)    end    pipeline :mailbox_preview do @@ -348,9 +353,11 @@ defmodule Pleroma.Web.Router do      get("/notifications", NotificationController, :index)      get("/notifications/:id", NotificationController, :show) +    post("/notifications/:id/dismiss", NotificationController, :dismiss)      post("/notifications/clear", NotificationController, :clear) -    post("/notifications/dismiss", NotificationController, :dismiss)      delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple) +    # Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead +    post("/notifications/dismiss", NotificationController, :dismiss)      get("/scheduled_statuses", ScheduledActivityController, :index)      get("/scheduled_statuses/:id", ScheduledActivityController, :show) @@ -501,6 +508,12 @@ defmodule Pleroma.Web.Router do      )    end +  scope "/api" do +    pipe_through(:api) + +    get("/openapi", OpenApiSpex.Plug.RenderSpec, []) +  end +    scope "/api", Pleroma.Web, as: :authenticated_twitter_api do      pipe_through(:authenticated_api)  | 
