diff options
Diffstat (limited to 'lib')
47 files changed, 614 insertions, 555 deletions
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index 3ad6edbfb..9f0bf6ecb 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -3,15 +3,48 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Mix.Pleroma do +  @apps [ +    :restarter, +    :ecto, +    :ecto_sql, +    :postgrex, +    :db_connection, +    :cachex, +    :flake_id, +    :swoosh, +    :timex +  ] +  @cachex_children ["object", "user"]    @doc "Common functions to be reused in mix tasks"    def start_pleroma do +    Pleroma.Config.Holder.save_default()      Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)      if Pleroma.Config.get(:env) != :test do        Application.put_env(:logger, :console, level: :debug)      end -    {:ok, _} = Application.ensure_all_started(:pleroma) +    apps = +      if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do +        [:gun | @apps] +      else +        [:hackney | @apps] +      end + +    Enum.each(apps, &Application.ensure_all_started/1) + +    children = [ +      Pleroma.Repo, +      {Pleroma.Config.TransferTask, false}, +      Pleroma.Web.Endpoint +    ] + +    cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, [])) + +    Supervisor.start_link(children ++ cachex_children, +      strategy: :one_for_one, +      name: Pleroma.Supervisor +    )      if Pleroma.Config.get(:env) not in [:test, :benchmark] do        pleroma_rebooted?() diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 86409738a..91440b453 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -145,7 +145,7 @@ defmodule Mix.Tasks.Pleroma.Instance do            options,            :uploads_dir,            "What directory should media uploads go in (when using the local uploader)?", -          Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads]) +          Config.get([Pleroma.Uploaders.Local, :uploads])          )          |> Path.expand() @@ -154,7 +154,7 @@ defmodule Mix.Tasks.Pleroma.Instance do            options,            :static_dir,            "What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?", -          Pleroma.Config.get([:instance, :static_dir]) +          Config.get([:instance, :static_dir])          )          |> Path.expand() diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index bca7e87bf..01824aa18 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -232,7 +232,7 @@ defmodule Mix.Tasks.Pleroma.User do      with %User{} = user <- User.get_cached_by_nickname(nickname) do        user = user |> User.tag(tags) -      shell_info("Tags of #{user.nickname}: #{inspect(tags)}") +      shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")      else        _ ->          shell_error("Could not change user tags for #{nickname}") @@ -245,7 +245,7 @@ defmodule Mix.Tasks.Pleroma.User do      with %User{} = user <- User.get_cached_by_nickname(nickname) do        user = user |> User.untag(tags) -      shell_info("Tags of #{user.nickname}: #{inspect(tags)}") +      shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")      else        _ ->          shell_error("Could not change user tags for #{nickname}") diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9615af122..84f3aa82d 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -35,7 +35,7 @@ defmodule Pleroma.Application do    # See http://elixir-lang.org/docs/stable/elixir/Application.html    # for more information on OTP Applications    def start(_type, _args) do -    Pleroma.Config.Holder.save_default() +    Config.Holder.save_default()      Pleroma.HTML.compile_scrubbers()      Config.DeprecationWarnings.warn()      Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() @@ -162,7 +162,8 @@ defmodule Pleroma.Application do    defp seconds_valid_interval,      do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid])) -  defp build_cachex(type, opts), +  @spec build_cachex(String.t(), keyword()) :: map() +  def build_cachex(type, opts),      do: %{        id: String.to_atom("cachex_" <> type),        start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]}, diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index eb86b8ff4..a0d7b7d71 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -31,8 +31,8 @@ defmodule Pleroma.Config.TransferTask do      {:pleroma, :gopher, [:enabled]}    ] -  def start_link(_) do -    load_and_update_env() +  def start_link(restart_pleroma? \\ true) do +    load_and_update_env([], restart_pleroma?)      if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)      :ignore    end diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex index 55f61024e..aa0b2a66b 100644 --- a/lib/pleroma/emails/admin_email.ex +++ b/lib/pleroma/emails/admin_email.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Emails.AdminEmail do    alias Pleroma.Config    alias Pleroma.Web.Router.Helpers -  defp instance_config, do: Pleroma.Config.get(:instance) +  defp instance_config, do: Config.get(:instance)    defp instance_name, do: instance_config()[:name]    defp instance_notify_email do @@ -72,6 +72,8 @@ defmodule Pleroma.Emails.AdminEmail do      <p>Reported Account: <a href="#{user_url(account)}">#{account.nickname}</a></p>      #{comment_html}      #{statuses_html} +    <p> +    <a href="#{Pleroma.Web.base_url()}/pleroma/admin/#/reports/index">View Reports in AdminFE</a>      """      new() diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex index 3de2dc762..03a6bca0b 100644 --- a/lib/pleroma/emoji/loader.ex +++ b/lib/pleroma/emoji/loader.ex @@ -108,7 +108,7 @@ defmodule Pleroma.Emoji.Loader do        if File.exists?(emoji_txt) do          load_from_file(emoji_txt, emoji_groups)        else -        extensions = Pleroma.Config.get([:emoji, :pack_extensions]) +        extensions = Config.get([:emoji, :pack_extensions])          Logger.info(            "No emoji.txt found for pack \"#{pack_name}\", assuming all #{ diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index 4d61b3650..5d6df9530 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -34,10 +34,18 @@ defmodule Pleroma.Filter do      Repo.one(query)    end -  def get_filters(%User{id: user_id} = _user) do +  def get_active(query) do +    from(f in query, where: is_nil(f.expires_at) or f.expires_at > ^NaiveDateTime.utc_now()) +  end + +  def get_irreversible(query) do +    from(f in query, where: f.hide) +  end + +  def get_filters(query \\ __MODULE__, %User{id: user_id}) do      query =        from( -        f in Pleroma.Filter, +        f in query,          where: f.user_id == ^user_id,          order_by: [desc: :id]        ) @@ -95,4 +103,34 @@ defmodule Pleroma.Filter do      |> validate_required([:phrase, :context])      |> Repo.update()    end + +  def compose_regex(user_or_filters, format \\ :postgres) + +  def compose_regex(%User{} = user, format) do +    __MODULE__ +    |> get_active() +    |> get_irreversible() +    |> get_filters(user) +    |> compose_regex(format) +  end + +  def compose_regex([_ | _] = filters, format) do +    phrases = +      filters +      |> Enum.map(& &1.phrase) +      |> Enum.join("|") + +    case format do +      :postgres -> +        "\\y(#{phrases})\\y" + +      :re -> +        ~r/\b#{phrases}\b/i + +      _ -> +        nil +    end +  end + +  def compose_regex(_, _), do: nil  end diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 74458c09a..a1f935232 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Instances.Instance do    schema "instances" do      field(:host, :string)      field(:unreachable_since, :naive_datetime_usec) +    field(:favicon, :string) +    field(:favicon_updated_at, :naive_datetime)      timestamps()    end @@ -25,7 +27,7 @@ defmodule Pleroma.Instances.Instance do    def changeset(struct, params \\ %{}) do      struct -    |> cast(params, [:host, :unreachable_since]) +    |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at])      |> validate_required([:host])      |> unique_constraint(:host)    end @@ -120,4 +122,48 @@ defmodule Pleroma.Instances.Instance do    end    defp parse_datetime(datetime), do: datetime + +  def get_or_update_favicon(%URI{host: host} = instance_uri) do +    existing_record = Repo.get_by(Instance, %{host: host}) +    now = NaiveDateTime.utc_now() + +    if existing_record && existing_record.favicon_updated_at && +         NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do +      existing_record.favicon +    else +      favicon = scrape_favicon(instance_uri) + +      if existing_record do +        existing_record +        |> changeset(%{favicon: favicon, favicon_updated_at: now}) +        |> Repo.update() +      else +        %Instance{} +        |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now}) +        |> Repo.insert() +      end + +      favicon +    end +  end + +  defp scrape_favicon(%URI{} = instance_uri) do +    try do +      with {:ok, %Tesla.Env{body: html}} <- +             Pleroma.HTTP.get(to_string(instance_uri), [{:Accept, "text/html"}]), +           favicon_rel <- +             html +             |> Floki.parse_document!() +             |> Floki.attribute("link[rel=icon]", "href") +             |> List.first(), +           favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(), +           true <- is_binary(favicon) do +        favicon +      else +        _ -> nil +      end +    rescue +      _ -> nil +    end +  end  end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 2ef1a80c5..32bcfcaba 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -130,6 +130,7 @@ defmodule Pleroma.Notification do      |> preload([n, a, o], activity: {a, object: o})      |> exclude_notification_muted(user, exclude_notification_muted_opts)      |> exclude_blocked(user, exclude_blocked_opts) +    |> exclude_filtered(user)      |> exclude_visibility(opts)    end @@ -158,6 +159,20 @@ defmodule Pleroma.Notification do      |> where([n, a, o, tm], is_nil(tm.user_id))    end +  defp exclude_filtered(query, user) do +    case Pleroma.Filter.compose_regex(user) do +      nil -> +        query + +      regex -> +        from([_n, a, o] in query, +          where: +            fragment("not(?->>'content' ~* ?)", o.data, ^regex) or +              fragment("?->>'actor' = ?", o.data, ^user.ap_id) +        ) +    end +  end +    @valid_visibilities ~w[direct unlisted public private]    defp exclude_visibility(query, %{exclude_visibilities: visibility}) @@ -337,6 +352,7 @@ defmodule Pleroma.Notification do      end    end +  @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}    def create_notifications(activity, options \\ [])    def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do @@ -481,6 +497,10 @@ defmodule Pleroma.Notification do      end    end +  def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do +    [object_id] +  end +    def get_potential_receiver_ap_ids(activity) do      []      |> Utils.maybe_notify_to_recipients(activity) @@ -555,7 +575,8 @@ defmodule Pleroma.Notification do        :follows,        :non_followers,        :non_follows, -      :recently_followed +      :recently_followed, +      :filtered      ]      |> Enum.find(&skip?(&1, activity, user))    end @@ -624,6 +645,26 @@ defmodule Pleroma.Notification do      end)    end +  def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false + +  def skip?(:filtered, activity, user) do +    object = Object.normalize(activity) + +    cond do +      is_nil(object) -> +        false + +      object.data["actor"] == user.ap_id -> +        false + +      not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) -> +        Regex.match?(regex, object.data["content"]) + +      true -> +        false +    end +  end +    def skip?(_, _, _), do: false    def for_user_and_activity(user, activity) do diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 1420a9611..7d65cf078 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -69,10 +69,11 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do      img_src = "img-src 'self' data: blob:"      media_src = "media-src 'self'" +    # Strict multimedia CSP enforcement only when MediaProxy is enabled      {img_src, media_src} =        if Config.get([:media_proxy, :enabled]) &&             !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do -        sources = get_proxy_and_attachment_sources() +        sources = build_csp_multimedia_source_list()          {[img_src, sources], [media_src, sources]}        else          {[img_src, " https:"], [media_src, " https:"]} @@ -81,14 +82,14 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do      connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]      connect_src = -      if Pleroma.Config.get(:env) == :dev do +      if Config.get(:env) == :dev do          [connect_src, " http://localhost:3035/"]        else          connect_src        end      script_src = -      if Pleroma.Config.get(:env) == :dev do +      if Config.get(:env) == :dev do          "script-src 'self' 'unsafe-eval'"        else          "script-src 'self'" @@ -107,29 +108,28 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do      |> :erlang.iolist_to_binary()    end -  defp get_proxy_and_attachment_sources do +  defp build_csp_multimedia_source_list do      media_proxy_whitelist =        Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->          add_source(acc, host)        end) -    media_proxy_base_url = -      if Config.get([:media_proxy, :base_url]), -        do: URI.parse(Config.get([:media_proxy, :base_url])).host +    media_proxy_base_url = build_csp_param(Config.get([:media_proxy, :base_url])) -    upload_base_url = -      if Config.get([Pleroma.Upload, :base_url]), -        do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host +    upload_base_url = build_csp_param(Config.get([Pleroma.Upload, :base_url])) -    s3_endpoint = -      if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3, -        do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host +    s3_endpoint = build_csp_param(Config.get([Pleroma.Uploaders.S3, :public_endpoint])) + +    captcha_method = Config.get([Pleroma.Captcha, :method]) + +    captcha_endpoint = build_csp_param(Config.get([captcha_method, :endpoint]))      []      |> add_source(media_proxy_base_url)      |> add_source(upload_base_url)      |> add_source(s3_endpoint)      |> add_source(media_proxy_whitelist) +    |> add_source(captcha_endpoint)    end    defp add_source(iodata, nil), do: iodata @@ -139,6 +139,16 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do    defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] +  defp build_csp_param(nil), do: nil + +  defp build_csp_param(url) when is_binary(url) do +    %{host: host, scheme: scheme} = URI.parse(url) + +    if scheme do +      [scheme, "://", host] +    end +  end +    def warn_if_disabled do      unless Config.get([:http_security, :enabled]) do        Logger.warn(" diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex index 156e6788e..143665c71 100644 --- a/lib/pleroma/plugs/static_fe_plug.ex +++ b/lib/pleroma/plugs/static_fe_plug.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do    def init(options), do: options    def call(conn, _) do -    if enabled?() and accepts_html?(conn) do +    if enabled?() and requires_html?(conn) do        conn        |> StaticFEController.call(:show)        |> halt() @@ -20,10 +20,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do    defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) -  defp accepts_html?(conn) do -    case get_req_header(conn, "accept") do -      [accept | _] -> String.contains?(accept, "text/html") -      _ -> false -    end +  defp requires_html?(conn) do +    Phoenix.Controller.get_format(conn) == "html"    end  end diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 797555bff..0fa6b89dc 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -63,6 +63,10 @@ defmodule Pleroma.Upload do      with {:ok, upload} <- prepare_upload(upload, opts),           upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},           {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload), +         description = Map.get(opts, :description) || upload.name, +         {_, true} <- +           {:description_limit, +            String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},           {:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do        {:ok,         %{ @@ -75,9 +79,12 @@ defmodule Pleroma.Upload do               "href" => url_from_spec(upload, opts.base_url, url_spec)             }           ], -         "name" => Map.get(opts, :description) || upload.name +         "name" => description         }}      else +      {:description_limit, _} -> +        {:error, :description_too_long} +        {:error, error} ->          Logger.error(            "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}" diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 712bc3047..b9989f901 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -89,7 +89,7 @@ defmodule Pleroma.User do      field(:keys, :string)      field(:public_key, :string)      field(:ap_id, :string) -    field(:avatar, :map) +    field(:avatar, :map, default: %{})      field(:local, :boolean, default: true)      field(:follower_address, :string)      field(:following_address, :string) @@ -389,8 +389,8 @@ defmodule Pleroma.User do    defp fix_follower_address(params), do: params    def remote_user_changeset(struct \\ %User{local: false}, params) do -    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) -    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) +    bio_limit = Config.get([:instance, :user_bio_length], 5000) +    name_limit = Config.get([:instance, :user_name_length], 100)      name =        case params[:name] do @@ -450,8 +450,8 @@ defmodule Pleroma.User do    end    def update_changeset(struct, params \\ %{}) do -    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) -    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) +    bio_limit = Config.get([:instance, :user_bio_length], 5000) +    name_limit = Config.get([:instance, :user_name_length], 100)      struct      |> cast( @@ -542,14 +542,11 @@ defmodule Pleroma.User do    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 -        put_change(changeset, map_field, new_value) -      else -        _ -> changeset -      end +    with {:ok, value} <- fetch_change(changeset, map_field), +         {:ok, new_value} <- value_function.(value) do +      put_change(changeset, map_field, new_value)      else -      changeset +      _ -> changeset      end    end @@ -624,13 +621,13 @@ defmodule Pleroma.User do    def force_password_reset(user), do: update_password_reset_pending(user, true)    def register_changeset(struct, params \\ %{}, opts \\ []) do -    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) -    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) +    bio_limit = Config.get([:instance, :user_bio_length], 5000) +    name_limit = Config.get([:instance, :user_name_length], 100)      params = Map.put_new(params, :accepts_chat_messages, true)      need_confirmation? =        if is_nil(opts[:need_confirmation]) do -        Pleroma.Config.get([:instance, :account_activation_required]) +        Config.get([:instance, :account_activation_required])        else          opts[:need_confirmation]        end @@ -652,7 +649,7 @@ defmodule Pleroma.User do      |> validate_confirmation(:password)      |> unique_constraint(:email)      |> unique_constraint(:nickname) -    |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames])) +    |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))      |> validate_format(:nickname, local_nickname_regex())      |> validate_format(:email, @email_regex)      |> validate_length(:bio, max: bio_limit) @@ -667,7 +664,7 @@ defmodule Pleroma.User do    def maybe_validate_required_email(changeset, true), do: changeset    def maybe_validate_required_email(changeset, _) do -    if Pleroma.Config.get([:instance, :account_activation_required]) do +    if Config.get([:instance, :account_activation_required]) do        validate_required(changeset, [:email])      else        changeset @@ -687,7 +684,7 @@ defmodule Pleroma.User do    end    defp autofollow_users(user) do -    candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames]) +    candidates = Config.get([:instance, :autofollowed_nicknames])      autofollowed_users =        User.Query.build(%{nickname: candidates, local: true, deactivated: false}) @@ -714,7 +711,7 @@ defmodule Pleroma.User do    def try_send_confirmation_email(%User{} = user) do      if user.confirmation_pending && -         Pleroma.Config.get([:instance, :account_activation_required]) do +         Config.get([:instance, :account_activation_required]) do        user        |> Pleroma.Emails.UserEmail.account_confirmation_email()        |> Pleroma.Emails.Mailer.deliver_async() @@ -771,7 +768,7 @@ defmodule Pleroma.User do    defdelegate following(user), to: FollowingRelationship    def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do -    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) +    deny_follow_blocked = Config.get([:user, :deny_follow_blocked])      cond do        followed.deactivated -> @@ -972,7 +969,7 @@ defmodule Pleroma.User do    end    def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do -    restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content]) +    restrict_to_local = Config.get([:instance, :limit_to_local_content])      cond do        is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) -> @@ -1168,7 +1165,7 @@ defmodule Pleroma.User do    @spec update_follower_count(User.t()) :: {:ok, User.t()}    def update_follower_count(%User{} = user) do -    if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do +    if user.local or !Config.get([:instance, :external_user_synchronization]) do        follower_count = FollowingRelationship.follower_count(user)        user @@ -1181,7 +1178,7 @@ defmodule Pleroma.User do    @spec update_following_count(User.t()) :: {:ok, User.t()}    def update_following_count(%User{local: false} = user) do -    if Pleroma.Config.get([:instance, :external_user_synchronization]) do +    if Config.get([:instance, :external_user_synchronization]) do        {:ok, maybe_fetch_follow_information(user)}      else        {:ok, user} @@ -1268,7 +1265,7 @@ defmodule Pleroma.User do    end    def subscribe(%User{} = subscriber, %User{} = target) do -    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) +    deny_follow_blocked = Config.get([:user, :deny_follow_blocked])      if blocks?(target, subscriber) and deny_follow_blocked do        {:error, "Could not subscribe: #{target.nickname} is blocking you"} @@ -1551,7 +1548,7 @@ defmodule Pleroma.User do        fn followed_identifier ->          with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),               {:ok, follower} <- maybe_direct_follow(follower, followed), -             {:ok, _} <- ActivityPub.follow(follower, followed) do +             {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do            followed          else            err -> @@ -1659,7 +1656,7 @@ defmodule Pleroma.User do      Pleroma.HTML.Scrubber.TwitterText    end -  def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy]) +  def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])    def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id) @@ -1841,7 +1838,7 @@ defmodule Pleroma.User do    end    defp local_nickname_regex do -    if Pleroma.Config.get([:instance, :extended_nickname_format]) do +    if Config.get([:instance, :extended_nickname_format]) do        @extended_local_nickname_regex      else        @strict_local_nickname_regex @@ -1969,8 +1966,8 @@ defmodule Pleroma.User do    def get_mascot(%{mascot: mascot}) when is_nil(mascot) do      # use instance-default -    config = Pleroma.Config.get([:assets, :mascots]) -    default_mascot = Pleroma.Config.get([:assets, :default_mascot]) +    config = Config.get([:assets, :mascots]) +    default_mascot = Config.get([:assets, :default_mascot])      mascot = Keyword.get(config, default_mascot)      %{ @@ -2065,7 +2062,7 @@ defmodule Pleroma.User do    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) +    limit = Config.get([:instance, limit_name], 0)      changeset      |> validate_length(:fields, max: limit) @@ -2079,8 +2076,8 @@ defmodule Pleroma.User do    end    defp valid_field?(%{"name" => name, "value" => value}) do -    name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255) -    value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255) +    name_limit = Config.get([:instance, :account_field_name_length], 255) +    value_limit = Config.get([:instance, :account_field_value_length], 255)      is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&        String.length(value) <= value_limit @@ -2090,10 +2087,10 @@ defmodule Pleroma.User do    defp truncate_field(%{"name" => name, "value" => value}) do      {name, _chopped} = -      String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255)) +      String.split_at(name, Config.get([:instance, :account_field_name_length], 255))      {value, _chopped} = -      String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255)) +      String.split_at(value, Config.get([:instance, :account_field_value_length], 255))      %{"name" => name, "value" => value}    end @@ -2148,7 +2145,7 @@ defmodule Pleroma.User do    def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do      if id not in user.pinned_activities do -      max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0) +      max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)        params = %{pinned_activities: user.pinned_activities ++ [id]}        user diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 42ff1de78..d4fd31069 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -69,11 +69,15 @@ defmodule Pleroma.User.Search do        u in query,        where:          fragment( +          # The fragment must _exactly_ match `users_fts_index`, otherwise the index won't work            """ -          (to_tsvector('simple', ?) || to_tsvector('simple', ?)) @@ to_tsquery('simple', ?) +          ( +            setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') || +            setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B') +          ) @@ to_tsquery('simple', ?)            """, -          u.name,            u.nickname, +          u.name,            ^query_string          )      ) @@ -88,15 +92,23 @@ defmodule Pleroma.User.Search do      |> Enum.join(" | ")    end +  # Considers nickname match, localized nickname match, name match; preferences nickname match    defp trigram_rank(query, query_string) do      from(        u in query,        select_merge: %{          search_rank:            fragment( -            "similarity(?, trim(? || ' ' || coalesce(?, '')))", +            """ +            similarity(?, ?) + +            similarity(?, regexp_replace(?, '@.+', '')) + +            similarity(?, trim(coalesce(?, ''))) +            """,              ^query_string,              u.nickname, +            ^query_string, +            u.nickname, +            ^query_string,              u.name            )        } diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 17c9d8f21..8da5cf938 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    alias Pleroma.Constants    alias Pleroma.Conversation    alias Pleroma.Conversation.Participation +  alias Pleroma.Filter    alias Pleroma.Maps    alias Pleroma.Notification    alias Pleroma.Object @@ -321,28 +322,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  @spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) :: -          {:ok, Activity.t()} | {:error, any()} -  def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do -    with {:ok, result} <- -           Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do -      result -    end -  end - -  defp do_follow(follower, followed, activity_id, local, opts) do -    skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false) -    data = make_follow_data(follower, followed, activity_id) - -    with {:ok, activity} <- insert(data, local), -         _ <- skip_notify_and_stream || notify_and_stream(activity), -         :ok <- maybe_federate(activity) do -      {:ok, activity} -    else -      {:error, error} -> Repo.rollback(error) -    end -  end -    @spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::            {:ok, Activity.t()} | nil | {:error, any()}    def unfollow(follower, followed, activity_id \\ nil, local \\ true) do @@ -446,6 +425,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> maybe_set_thread_muted_field(opts)      |> restrict_blocked(opts)      |> restrict_recipients(recipients, opts[:user]) +    |> restrict_filtered(opts)      |> where(        [activity],        fragment( @@ -961,6 +941,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_instance(query, _), do: query +  defp restrict_filtered(query, %{user: %User{} = user}) do +    case Filter.compose_regex(user) do +      nil -> +        query + +      regex -> +        from([activity, object] in query, +          where: +            fragment("not(?->>'content' ~* ?)", object.data, ^regex) or +              activity.actor == ^user.ap_id +        ) +    end +  end + +  defp restrict_filtered(query, %{blocking_user: %User{} = user}) do +    restrict_filtered(query, %{user: user}) +  end + +  defp restrict_filtered(query, _), do: query +    defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query    defp exclude_poll_votes(query, _) do @@ -1091,6 +1091,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> restrict_favorited_by(opts)      |> restrict_blocked(restrict_blocked_opts)      |> restrict_muted(restrict_muted_opts) +    |> restrict_filtered(opts)      |> restrict_media(opts)      |> restrict_visibility(opts)      |> restrict_thread_visibility(opts, config) @@ -1099,6 +1100,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> restrict_muted_reblogs(restrict_muted_reblogs_opts)      |> restrict_instance(opts)      |> restrict_announce_object_actor(opts) +    |> restrict_filtered(opts)      |> Activity.restrict_deactivated_users()      |> exclude_poll_votes(opts)      |> exclude_chat_messages(opts) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index cabc28de9..d5f3610ed 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -14,6 +14,19 @@ defmodule Pleroma.Web.ActivityPub.Builder do    require Pleroma.Constants +  @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()} +  def follow(follower, followed) do +    data = %{ +      "id" => Utils.generate_activity_id(), +      "actor" => follower.ap_id, +      "type" => "Follow", +      "object" => followed.ap_id, +      "to" => [followed.ap_id] +    } + +    {:ok, data, []} +  end +    @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}    def emoji_react(actor, object, emoji) do      with {:ok, data, meta} <- object_action(actor, object) do diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex index b0ccb63c8..a62914135 100644 --- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex @@ -98,7 +98,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do    @impl true    def describe do      mrf_object_age = -      Pleroma.Config.get(:mrf_object_age) +      Config.get(:mrf_object_age)        |> Enum.into(%{})      {:ok, %{mrf_object_age: mrf_object_age}} diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index 3092f3272..4fd63106d 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -47,5 +47,5 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do    @impl true    def describe, -    do: {:ok, %{mrf_rejectnonpublic: Pleroma.Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}} +    do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}  end diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 9cea6bcf9..70a2ca053 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -155,7 +155,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do      %{host: actor_host} = URI.parse(actor)      reject_deletes = -      Pleroma.Config.get([:mrf_simple, :reject_deletes]) +      Config.get([:mrf_simple, :reject_deletes])        |> MRF.subdomains_regex()      if MRF.subdomain_match?(reject_deletes, actor_host) do diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index bb6324460..df926829c 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -18,6 +18,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator +  alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator @@ -25,6 +26,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}    def validate(object, meta) +  def validate(%{"type" => "Follow"} = object, meta) do +    with {:ok, object} <- +           object +           |> FollowValidator.cast_and_validate() +           |> Ecto.Changeset.apply_action(:insert) do +      object = stringify_keys(object) +      {:ok, object, meta} +    end +  end +    def validate(%{"type" => "Block"} = block_activity, meta) do      with {:ok, block_activity} <-             block_activity diff --git a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex new file mode 100644 index 000000000..ca2724616 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/follow_validator.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.ActivityPub.ObjectValidators.FollowValidator do +  use Ecto.Schema + +  alias Pleroma.EctoType.ActivityPub.ObjectValidators + +  import Ecto.Changeset +  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + +  @primary_key false + +  embedded_schema do +    field(:id, ObjectValidators.ObjectID, primary_key: true) +    field(:type, :string) +    field(:actor, ObjectValidators.ObjectID) +    field(:to, ObjectValidators.Recipients, default: []) +    field(:cc, ObjectValidators.Recipients, default: []) +    field(:object, ObjectValidators.ObjectID) +    field(:state, :string, default: "pending") +  end + +  def cast_data(data) do +    %__MODULE__{} +    |> cast(data, __schema__(:fields)) +  end + +  def validate_data(cng) do +    cng +    |> validate_required([:id, :type, :actor, :to, :cc, :object]) +    |> validate_inclusion(:type, ["Follow"]) +    |> validate_inclusion(:state, ~w{pending reject accept}) +    |> validate_actor_presence() +    |> validate_actor_presence(field_name: :object) +  end + +  def cast_and_validate(data) do +    data +    |> cast_data +    |> validate_data +  end +end diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index 484178edd..b09764d2b 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do    def follow(target_instance) do      with %User{} = local_user <- get_actor(),           {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), -         {:ok, activity} <- ActivityPub.follow(local_user, target_user) do +         {:ok, _, _, activity} <- CommonAPI.follow(local_user, target_user) do        Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")        {:ok, activity}      else diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 61feeae4d..1d2c296a5 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    alias Pleroma.Activity.Ir.Topics    alias Pleroma.Chat    alias Pleroma.Chat.MessageReference +  alias Pleroma.FollowingRelationship    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Repo @@ -21,6 +22,69 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    def handle(object, meta \\ []) +  # Tasks this handle +  # - Follows if possible +  # - Sends a notification +  # - Generates accept or reject if appropriate +  def handle( +        %{ +          data: %{ +            "id" => follow_id, +            "type" => "Follow", +            "object" => followed_user, +            "actor" => following_user +          } +        } = object, +        meta +      ) do +    with %User{} = follower <- User.get_cached_by_ap_id(following_user), +         %User{} = followed <- User.get_cached_by_ap_id(followed_user), +         {_, {:ok, _}, _, _} <- +           {:following, User.follow(follower, followed, :follow_pending), follower, followed} do +      if followed.local && !followed.locked do +        Utils.update_follow_state_for_all(object, "accept") +        FollowingRelationship.update(follower, followed, :follow_accept) +        User.update_follower_count(followed) +        User.update_following_count(follower) + +        %{ +          to: [following_user], +          actor: followed, +          object: follow_id, +          local: true +        } +        |> ActivityPub.accept() +      end +    else +      {:following, {:error, _}, follower, followed} -> +        Utils.update_follow_state_for_all(object, "reject") +        FollowingRelationship.update(follower, followed, :follow_reject) + +        if followed.local do +          %{ +            to: [follower.ap_id], +            actor: followed, +            object: follow_id, +            local: true +          } +          |> ActivityPub.reject() +        end + +      _ -> +        nil +    end + +    {:ok, notifications} = Notification.create_notifications(object, do_send: false) + +    meta = +      meta +      |> add_notifications(notifications) + +    updated_object = Activity.get_by_ap_id(follow_id) + +    {:ok, updated_object, meta} +  end +    # Tasks this handles:    # - Unfollow and block    def handle( @@ -209,14 +273,20 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do      {:ok, object}    end -  def handle_undoing(%{data: %{"type" => "Like"}} = object) do -    with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]), -         {:ok, _} <- Utils.remove_like_from_object(object, liked_object), -         {:ok, _} <- Repo.delete(object) do -      :ok +  defp undo_like(nil, object), do: delete_object(object) + +  defp undo_like(%Object{} = liked_object, object) do +    with {:ok, _} <- Utils.remove_like_from_object(object, liked_object) do +      delete_object(object)      end    end +  def handle_undoing(%{data: %{"type" => "Like"}} = object) do +    object.data["object"] +    |> Object.get_by_ap_id() +    |> undo_like(object) +  end +    def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do      with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),           {:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object), @@ -246,6 +316,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    def handle_undoing(object), do: {:error, ["don't know how to handle", object]} +  @spec delete_object(Object.t()) :: :ok | {:error, Ecto.Changeset.t()} +  defp delete_object(object) do +    with {:ok, _} <- Repo.delete(object), do: :ok +  end +    defp send_notifications(meta) do      Keyword.get(meta, :notifications, [])      |> Enum.each(fn notification -> diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index bc6fc4bd8..884646ceb 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -233,18 +233,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do              is_map(url) && is_binary(url["href"]) -> url["href"]              is_binary(data["url"]) -> data["url"]              is_binary(data["href"]) -> data["href"] +            true -> nil            end -        attachment_url = -          %{"href" => href} -          |> Maps.put_if_present("mediaType", media_type) -          |> Maps.put_if_present("type", Map.get(url || %{}, "type")) +        if href do +          attachment_url = +            %{"href" => href} +            |> Maps.put_if_present("mediaType", media_type) +            |> Maps.put_if_present("type", Map.get(url || %{}, "type")) -        %{"url" => [attachment_url]} -        |> Maps.put_if_present("mediaType", media_type) -        |> Maps.put_if_present("type", data["type"]) -        |> Maps.put_if_present("name", data["name"]) +          %{"url" => [attachment_url]} +          |> Maps.put_if_present("mediaType", media_type) +          |> Maps.put_if_present("type", data["type"]) +          |> Maps.put_if_present("name", data["name"]) +        else +          nil +        end        end) +      |> Enum.filter(& &1)      Map.put(object, "attachment", attachments)    end @@ -263,12 +269,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def fix_url(%{"type" => object_type, "url" => url} = object)        when object_type in ["Video", "Audio"] and is_list(url) do -    first_element = Enum.at(url, 0) +    attachment = +      Enum.find(url, fn x -> +        media_type = x["mediaType"] || x["mimeType"] || "" -    link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end) +        is_map(x) and String.starts_with?(media_type, ["audio/", "video/"]) +      end) + +    link_element = +      Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)      object -    |> Map.put("attachment", [first_element]) +    |> Map.put("attachment", [attachment])      |> Map.put("url", link_element["href"])    end @@ -518,66 +530,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming( -        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data, -        _options -      ) do -    with %User{local: true} = followed <- -           User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})), -         {:ok, %User{} = follower} <- -           User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})), -         {:ok, activity} <- -           ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do -      with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), -           {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, -           {_, false} <- {:user_locked, User.locked?(followed)}, -           {_, {: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, :follow_accept) do -        ActivityPub.accept(%{ -          to: [follower.ap_id], -          actor: followed, -          object: data, -          local: true -        }) -      else -        {:user_blocked, true} -> -          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") -          {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject) - -          ActivityPub.reject(%{ -            to: [follower.ap_id], -            actor: followed, -            object: data, -            local: true -          }) - -        {:follow, {:error, _}} -> -          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") -          {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject) - -          ActivityPub.reject(%{ -            to: [follower.ap_id], -            actor: followed, -            object: data, -            local: true -          }) - -        {:user_locked, true} -> -          {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending) -          :noop -      end - -      ActivityPub.notify_and_stream(activity) -      {:ok, activity} -    else -      _e -> -        :error -    end -  end - -  def handle_incoming(          %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,          _options        ) do @@ -684,7 +636,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          %{"type" => type} = data,          _options        ) -      when type in ~w{Update Block} do +      when type in ~w{Update Block Follow} do      with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),           {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do        {:ok, activity} diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index f9545d895..e5f14269a 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -206,8 +206,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      end    end -  def user_show(conn, %{"nickname" => nickname}) do -    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do +  def user_show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do +    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do        conn        |> put_view(AccountView)        |> render("show.json", %{user: user}) @@ -233,11 +233,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      |> render("index.json", %{activities: activities, as: :activity})    end -  def list_user_statuses(conn, %{"nickname" => nickname} = params) do +  def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do      with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true      godmode = params["godmode"] == "true" || params["godmode"] == true -    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do +    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do        {_, page_size} = page_params(params)        activities = @@ -526,7 +526,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    @doc "Show a given user's credentials"    def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do -    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do +    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do        conn        |> put_view(AccountView)        |> render("credentials.json", %{user: user, for: admin}) diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 3c05fa55f..952d9347b 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -203,14 +203,23 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do        security: [%{"oAuth" => ["follow", "write:follows"]}],        description: "Follow the given account",        parameters: [ -        %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, -        Operation.parameter( -          :reblogs, -          :query, -          BooleanLike, -          "Receive this account's reblogs in home timeline? Defaults to true." -        ) +        %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}        ], +      requestBody: +        request_body( +          "Parameters", +          %Schema{ +            type: :object, +            properties: %{ +              reblogs: %Schema{ +                type: :boolean, +                description: "Receive this account's reblogs in home timeline? Defaults to true.", +                default: true +              } +            } +          }, +          required: false +        ),        responses: %{          200 => Operation.response("Relationship", "application/json", AccountRelationship),          400 => Operation.response("Error", "application/json", ApiError), @@ -438,6 +447,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do      }    end +  # TODO: This is actually a token respone, but there's no oauth operation file yet.    defp create_response do      %Schema{        title: "AccountCreateResponse", @@ -446,14 +456,20 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do        properties: %{          token_type: %Schema{type: :string},          access_token: %Schema{type: :string}, -        scope: %Schema{type: :array, items: %Schema{type: :string}}, -        created_at: %Schema{type: :integer, format: :"date-time"} +        refresh_token: %Schema{type: :string}, +        scope: %Schema{type: :string}, +        created_at: %Schema{type: :integer, format: :"date-time"}, +        me: %Schema{type: :string}, +        expires_in: %Schema{type: :integer}        },        example: %{ +        "token_type" => "Bearer",          "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk", +        "refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz",          "created_at" => 1_585_918_714, -        "scope" => ["read", "write", "follow", "push"], -        "token_type" => "Bearer" +        "expires_in" => 600, +        "scope" => "read write follow push", +        "me" => "https://gensokyo.2hu/users/raymoo"        }      }    end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex index 90922c064..97836b2eb 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex @@ -4,7 +4,6 @@  defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do    alias OpenApiSpex.Operation -  alias OpenApiSpex.Schema    alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship    alias Pleroma.Web.ApiSpec.Schemas.ApiError    alias Pleroma.Web.ApiSpec.Schemas.FlakeID @@ -40,48 +39,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do      }    end -  def update_avatar_operation do -    %Operation{ -      tags: ["Accounts"], -      summary: "Set/clear user avatar image", -      operationId: "PleromaAPI.AccountController.update_avatar", -      requestBody: -        request_body("Parameters", update_avatar_or_background_request(), required: true), -      security: [%{"oAuth" => ["write:accounts"]}], -      responses: %{ -        200 => update_response(), -        403 => Operation.response("Forbidden", "application/json", ApiError) -      } -    } -  end - -  def update_banner_operation do -    %Operation{ -      tags: ["Accounts"], -      summary: "Set/clear user banner image", -      operationId: "PleromaAPI.AccountController.update_banner", -      requestBody: request_body("Parameters", update_banner_request(), required: true), -      security: [%{"oAuth" => ["write:accounts"]}], -      responses: %{ -        200 => update_response() -      } -    } -  end - -  def update_background_operation do -    %Operation{ -      tags: ["Accounts"], -      summary: "Set/clear user background image", -      operationId: "PleromaAPI.AccountController.update_background", -      security: [%{"oAuth" => ["write:accounts"]}], -      requestBody: -        request_body("Parameters", update_avatar_or_background_request(), required: true), -      responses: %{ -        200 => update_response() -      } -    } -  end -    def favourites_operation do      %Operation{        tags: ["Accounts"], @@ -136,52 +93,4 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do        required: true      )    end - -  defp update_avatar_or_background_request do -    %Schema{ -      title: "PleromaAccountUpdateAvatarOrBackgroundRequest", -      type: :object, -      properties: %{ -        img: %Schema{ -          nullable: true, -          type: :string, -          format: :binary, -          description: "Image encoded using `multipart/form-data` or an empty string to clear" -        } -      } -    } -  end - -  defp update_banner_request do -    %Schema{ -      title: "PleromaAccountUpdateBannerRequest", -      type: :object, -      properties: %{ -        banner: %Schema{ -          type: :string, -          nullable: true, -          format: :binary, -          description: "Image encoded using `multipart/form-data` or an empty string to clear" -        } -      } -    } -  end - -  defp update_response do -    Operation.response("PleromaAccountUpdateResponse", "application/json", %Schema{ -      type: :object, -      properties: %{ -        url: %Schema{ -          type: :string, -          format: :uri, -          nullable: true, -          description: "Image URL" -        } -      }, -      example: %{ -        "url" => -          "https://cofe.party/media/9d0add56-bcb6-4c0f-8225-cbbd0b6dd773/13eadb6972c9ccd3f4ffa3b8196f0e0d38b4d2f27594457c52e52946c054cd9a.gif" -      } -    }) -  end  end diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index 3a84a1593..cf148bc9d 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -103,7 +103,13 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do              description:                "A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`"            }, -          accepts_chat_messages: %Schema{type: :boolean, nullable: true} +          accepts_chat_messages: %Schema{type: :boolean, nullable: true}, +          favicon: %Schema{ +            type: :string, +            format: :uri, +            nullable: true, +            description: "Favicon image of the user's instance" +          }          }        },        source: %Schema{ diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index fd7149079..4d5b0decf 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -101,10 +101,14 @@ defmodule Pleroma.Web.CommonAPI do    def follow(follower, followed) do      timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) -    with {:ok, follower} <- User.maybe_direct_follow(follower, followed), -         {:ok, activity} <- ActivityPub.follow(follower, followed), +    with {:ok, follow_data, _} <- Builder.follow(follower, followed), +         {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),           {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do -      {:ok, follower, followed, activity} +      if activity.data["state"] == "reject" do +        {:error, :rejected} +      else +        {:ok, follower, followed, activity} +      end      end    end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 15594125f..9c38b73eb 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -143,7 +143,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data)        when is_list(options) do -    limits = Pleroma.Config.get([:instance, :poll_limits]) +    limits = Config.get([:instance, :poll_limits])      with :ok <- validate_poll_expiration(expires_in, limits),           :ok <- validate_poll_options_amount(options, limits), @@ -502,7 +502,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def make_report_content_html(nil), do: {:ok, {nil, [], []}}    def make_report_content_html(comment) do -    max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000) +    max_size = Config.get([:instance, :max_report_comment_size], 1000)      if String.length(comment) <= max_size do        {:ok, format_input(comment, "text/plain")} @@ -564,7 +564,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    end    def validate_character_limit(full_payload, _attachments) do -    limit = Pleroma.Config.get([:instance, :limit]) +    limit = Config.get([:instance, :limit])      length = String.length(full_payload)      if length <= limit do diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 7ff767db6..fe5d022f5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -27,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    alias Pleroma.Web.MastodonAPI.MastodonAPI    alias Pleroma.Web.MastodonAPI.MastodonAPIController    alias Pleroma.Web.MastodonAPI.StatusView +  alias Pleroma.Web.OAuth.OAuthView    alias Pleroma.Web.OAuth.Token    alias Pleroma.Web.TwitterAPI.TwitterAPI @@ -101,12 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do           :ok <- TwitterAPI.validate_captcha(app, params),           {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),           {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do -      json(conn, %{ -        token_type: "Bearer", -        access_token: token.token, -        scope: app.scopes, -        created_at: Token.Utils.format_created_at(token) -      }) +      json(conn, OAuthView.render("token.json", %{user: user, token: token}))      else        {:error, error} -> json_response(conn, :bad_request, %{error: error})      end @@ -148,6 +144,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do        |> Enum.filter(fn {_, value} -> not is_nil(value) end)        |> Enum.into(%{}) +    # We use an empty string as a special value to reset +    # avatars, banners, backgrounds +    user_image_value = fn +      "" -> {:ok, nil} +      value -> {:ok, value} +    end +      user_params =        [          :no_rich_text, @@ -169,9 +172,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do        |> Maps.put_if_present(:name, params[:display_name])        |> Maps.put_if_present(:bio, params[:note])        |> Maps.put_if_present(:raw_bio, params[:note]) -      |> Maps.put_if_present(:avatar, params[:avatar]) -      |> Maps.put_if_present(:banner, params[:header]) -      |> Maps.put_if_present(:background, params[:pleroma_background_image]) +      |> Maps.put_if_present(:avatar, params[:avatar], user_image_value) +      |> Maps.put_if_present(:banner, params[:header], user_image_value) +      |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)        |> Maps.put_if_present(          :raw_fields,          params[:fields_attributes], @@ -347,7 +350,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do      {:error, "Can not follow yourself"}    end -  def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do +  def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do      with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do        render(conn, "relationship.json", user: follower, target: followed)      else diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 3f4c53437..12be530c9 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -201,15 +201,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do    @doc "DELETE /api/v1/statuses/:id"    def delete(%{assigns: %{user: user}} = conn, %{id: id}) do      with %Activity{} = activity <- Activity.get_by_id_with_object(id), -         render <- -           try_render(conn, "show.json", -             activity: activity, -             for: user, -             with_direct_conversation_id: true, -             with_source: true -           ),           {:ok, %Activity{}} <- CommonAPI.delete(id, user) do -      render +      try_render(conn, "show.json", +        activity: activity, +        for: user, +        with_direct_conversation_id: true, +        with_source: true +      )      else        _e -> {:error, :not_found}      end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 4bdd46d7e..ab7b1d6aa 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -88,21 +88,20 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      )    end +  defp restrict_unauthenticated?(true = _local_only) do +    Pleroma.Config.get([:restrict_unauthenticated, :timelines, :local]) +  end + +  defp restrict_unauthenticated?(_) do +    Pleroma.Config.get([:restrict_unauthenticated, :timelines, :federated]) +  end +    # GET /api/v1/timelines/public    def public(%{assigns: %{user: user}} = conn, params) do      local_only = params[:local] -    cfg_key = -      if local_only do -        :local -      else -        :federated -      end - -    restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key]) - -    if restrict? and is_nil(user) do -      render_error(conn, :unauthorized, "authorization required for timeline view") +    if is_nil(user) and restrict_unauthenticated?(local_only) do +      fail_on_bad_auth(conn)      else        activities =          params @@ -123,6 +122,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      end    end +  defp fail_on_bad_auth(conn) do +    render_error(conn, :unauthorized, "authorization required for timeline view") +  end +    defp hashtag_fetching(params, user, local_only) do      tags =        [params[:tag], params[:any]] @@ -157,15 +160,20 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do    # GET /api/v1/timelines/tag/:tag    def hashtag(%{assigns: %{user: user}} = conn, params) do      local_only = params[:local] -    activities = hashtag_fetching(params, user, local_only) -    conn -    |> add_link_headers(activities, %{"local" => local_only}) -    |> render("index.json", -      activities: activities, -      for: user, -      as: :activity -    ) +    if is_nil(user) and restrict_unauthenticated?(local_only) do +      fail_on_bad_auth(conn) +    else +      activities = hashtag_fetching(params, user, local_only) + +      conn +      |> add_link_headers(activities, %{"local" => local_only}) +      |> render("index.json", +        activities: activities, +        for: user, +        as: :activity +      ) +    end    end    # GET /api/v1/timelines/list/:list_id diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 6a643bfcc..bc9745044 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -204,6 +204,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do          %{}        end +    favicon = +      if Pleroma.Config.get([:instances_favicons, :enabled]) do +        user +        |> Map.get(:ap_id, "") +        |> URI.parse() +        |> URI.merge("/") +        |> Pleroma.Instances.Instance.get_or_update_favicon() +        |> MediaProxy.url() +      else +        nil +      end +      %{        id: to_string(user.id),        username: username_from_nickname(user.nickname), @@ -246,7 +258,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do          relationship: relationship,          skip_thread_containment: user.skip_thread_containment,          background_image: image_url(user.background) |> MediaProxy.url(), -        accepts_chat_messages: user.accepts_chat_messages +        accepts_chat_messages: user.accepts_chat_messages, +        favicon: favicon        }      }      |> maybe_put_role(user, opts[:for]) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 89e48fba5..5deb0d7ed 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -34,6 +34,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do        background_upload_limit: Keyword.get(instance, :background_upload_limit),        banner_upload_limit: Keyword.get(instance, :banner_upload_limit),        background_image: Keyword.get(instance, :background_image), +      chat_limit: Keyword.get(instance, :chat_limit), +      description_limit: Keyword.get(instance, :description_limit),        pleroma: %{          metadata: %{            account_activation_required: Keyword.get(instance, :account_activation_required), diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 077fabe47..6f35826da 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -106,7 +106,7 @@ defmodule Pleroma.Web.MediaProxy do    def build_url(sig_base64, url_base64, filename \\ nil) do      [ -      Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()), +      Config.get([:media_proxy, :base_url], Web.base_url()),        "proxy",        sig_base64,        url_base64, diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex index 53e19f82e..f102c93e7 100644 --- a/lib/pleroma/web/oauth/mfa_controller.ex +++ b/lib/pleroma/web/oauth/mfa_controller.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.OAuth.MFAController do    alias Pleroma.Web.Auth.TOTPAuthenticator    alias Pleroma.Web.OAuth.MFAView, as: View    alias Pleroma.Web.OAuth.OAuthController +  alias Pleroma.Web.OAuth.OAuthView    alias Pleroma.Web.OAuth.Token    plug(:fetch_session when action in [:show, :verify]) @@ -74,7 +75,7 @@ defmodule Pleroma.Web.OAuth.MFAController do           {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),           {:ok, _} <- validates_challenge(user, params),           {:ok, token} <- Token.exchange_token(app, auth) do -      json(conn, Token.Response.build(user, token)) +      json(conn, OAuthView.render("token.json", %{user: user, token: token}))      else        _error ->          conn diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex index 41d5578dc..5d87db268 100644 --- a/lib/pleroma/web/oauth/mfa_view.ex +++ b/lib/pleroma/web/oauth/mfa_view.ex @@ -5,4 +5,13 @@  defmodule Pleroma.Web.OAuth.MFAView do    use Pleroma.Web, :view    import Phoenix.HTML.Form +  alias Pleroma.MFA + +  def render("mfa_response.json", %{token: token, user: user}) do +    %{ +      error: "mfa_required", +      mfa_token: token.token, +      supported_challenge_types: MFA.supported_methods(user) +    } +  end  end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index c557778ca..7683589cf 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do    alias Pleroma.Web.OAuth.App    alias Pleroma.Web.OAuth.Authorization    alias Pleroma.Web.OAuth.MFAController +  alias Pleroma.Web.OAuth.MFAView +  alias Pleroma.Web.OAuth.OAuthView    alias Pleroma.Web.OAuth.Scopes    alias Pleroma.Web.OAuth.Token    alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken @@ -233,9 +235,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      with {:ok, app} <- Token.Utils.fetch_app(conn),           {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),           {:ok, token} <- RefreshToken.grant(token) do -      response_attrs = %{created_at: Token.Utils.format_created_at(token)} - -      json(conn, Token.Response.build(user, token, response_attrs)) +      json(conn, OAuthView.render("token.json", %{user: user, token: token}))      else        _error -> render_invalid_credentials_error(conn)      end @@ -247,9 +247,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do           {:ok, auth} <- Authorization.get_by_token(app, fixed_token),           %User{} = user <- User.get_cached_by_id(auth.user_id),           {:ok, token} <- Token.exchange_token(app, auth) do -      response_attrs = %{created_at: Token.Utils.format_created_at(token)} - -      json(conn, Token.Response.build(user, token, response_attrs)) +      json(conn, OAuthView.render("token.json", %{user: user, token: token}))      else        error ->          handle_token_exchange_error(conn, error) @@ -267,7 +265,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do           {:ok, auth} <- Authorization.create_authorization(app, user, scopes),           {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},           {:ok, token} <- Token.exchange_token(app, auth) do -      json(conn, Token.Response.build(user, token)) +      json(conn, OAuthView.render("token.json", %{user: user, token: token}))      else        error ->          handle_token_exchange_error(conn, error) @@ -290,7 +288,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      with {:ok, app} <- Token.Utils.fetch_app(conn),           {:ok, auth} <- Authorization.create_authorization(app, %User{}),           {:ok, token} <- Token.exchange_token(app, auth) do -      json(conn, Token.Response.build_for_client_credentials(token)) +      json(conn, OAuthView.render("token.json", %{token: token}))      else        _error ->          handle_token_exchange_error(conn, :invalid_credentails) @@ -548,7 +546,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do    defp build_and_response_mfa_token(user, auth) do      with {:ok, token} <- MFA.Token.create_token(user, auth) do -      Token.Response.build_for_mfa_token(user, token) +      MFAView.render("mfa_response.json", %{token: token, user: user})      end    end diff --git a/lib/pleroma/web/oauth/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex index 94ddaf913..f55247ebd 100644 --- a/lib/pleroma/web/oauth/oauth_view.ex +++ b/lib/pleroma/web/oauth/oauth_view.ex @@ -5,4 +5,26 @@  defmodule Pleroma.Web.OAuth.OAuthView do    use Pleroma.Web, :view    import Phoenix.HTML.Form + +  alias Pleroma.Web.OAuth.Token.Utils + +  def render("token.json", %{token: token} = opts) do +    response = %{ +      token_type: "Bearer", +      access_token: token.token, +      refresh_token: token.refresh_token, +      expires_in: expires_in(), +      scope: Enum.join(token.scopes, " "), +      created_at: Utils.format_created_at(token) +    } + +    if user = opts[:user] do +      response +      |> Map.put(:me, user.ap_id) +    else +      response +    end +  end + +  defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)  end diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex deleted file mode 100644 index 0e72c31e9..000000000 --- a/lib/pleroma/web/oauth/token/response.ex +++ /dev/null @@ -1,45 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Response do -  @moduledoc false - -  alias Pleroma.MFA -  alias Pleroma.User -  alias Pleroma.Web.OAuth.Token.Utils - -  @doc false -  def build(%User{} = user, token, opts \\ %{}) do -    %{ -      token_type: "Bearer", -      access_token: token.token, -      refresh_token: token.refresh_token, -      expires_in: expires_in(), -      scope: Enum.join(token.scopes, " "), -      me: user.ap_id -    } -    |> Map.merge(opts) -  end - -  def build_for_client_credentials(token) do -    %{ -      token_type: "Bearer", -      access_token: token.token, -      refresh_token: token.refresh_token, -      created_at: Utils.format_created_at(token), -      expires_in: expires_in(), -      scope: Enum.join(token.scopes, " ") -    } -  end - -  def build_for_mfa_token(user, mfa_token) do -    %{ -      error: "mfa_required", -      mfa_token: mfa_token.token, -      supported_challenge_types: MFA.supported_methods(user) -    } -  end - -  defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) -end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index f3554d919..563edded7 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do    import Pleroma.Web.ControllerHelper,      only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] -  alias Ecto.Changeset    alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.Plugs.RateLimiter @@ -37,17 +36,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do    plug(      OAuthScopesPlug, -    %{scopes: ["write:accounts"]} -    # Note: the following actions are not permission-secured in Mastodon: -    when action in [ -           :update_avatar, -           :update_banner, -           :update_background -         ] -  ) - -  plug( -    OAuthScopesPlug,      %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites    ) @@ -68,56 +56,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do      end    end -  @doc "PATCH /api/v1/pleroma/accounts/update_avatar" -  def update_avatar(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do -    {:ok, _user} = -      user -      |> Changeset.change(%{avatar: nil}) -      |> User.update_and_set_cache() - -    json(conn, %{url: nil}) -  end - -  def update_avatar(%{assigns: %{user: user}, body_params: params} = conn, _params) do -    {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar) -    {:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache() -    %{"url" => [%{"href" => href} | _]} = data - -    json(conn, %{url: href}) -  end - -  @doc "PATCH /api/v1/pleroma/accounts/update_banner" -  def update_banner(%{assigns: %{user: user}, body_params: %{banner: ""}} = conn, _) do -    with {:ok, _user} <- User.update_banner(user, %{}) do -      json(conn, %{url: nil}) -    end -  end - -  def update_banner(%{assigns: %{user: user}, body_params: params} = conn, _) do -    with {:ok, object} <- ActivityPub.upload(%{img: params[:banner]}, type: :banner), -         {:ok, _user} <- User.update_banner(user, object.data) do -      %{"url" => [%{"href" => href} | _]} = object.data - -      json(conn, %{url: href}) -    end -  end - -  @doc "PATCH /api/v1/pleroma/accounts/update_background" -  def update_background(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do -    with {:ok, _user} <- User.update_background(user, %{}) do -      json(conn, %{url: nil}) -    end -  end - -  def update_background(%{assigns: %{user: user}, body_params: params} = conn, _) do -    with {:ok, object} <- ActivityPub.upload(params, type: :background), -         {:ok, _user} <- User.update_background(user, object.data) do -      %{"url" => [%{"href" => href} | _]} = object.data - -      json(conn, %{url: href}) -    end -  end -    @doc "GET /api/v1/pleroma/accounts/:id/favourites"    def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do      render_error(conn, :forbidden, "Can't get favorites") diff --git a/lib/pleroma/web/preload/status_net.ex b/lib/pleroma/web/preload/status_net.ex deleted file mode 100644 index 9b62f87a2..000000000 --- a/lib/pleroma/web/preload/status_net.ex +++ /dev/null @@ -1,25 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.StatusNet do -  alias Pleroma.Web.Preload.Providers.Provider -  alias Pleroma.Web.TwitterAPI.UtilController - -  @behaviour Provider -  @config_url "/api/statusnet/config.json" - -  @impl Provider -  def generate_terms(_params) do -    %{} -    |> build_config_tag() -  end - -  defp build_config_tag(acc) do -    resp = -      Plug.Test.conn(:get, @config_url |> to_string()) -      |> UtilController.config(nil) - -    Map.put(acc, @config_url, resp.resp_body) -  end -end diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index ef5ead2da..c8a767935 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -86,7 +86,10 @@ defmodule Pleroma.Web.RichMedia.Parser do        end      try do -      {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: opts) +      rich_media_agent = Pleroma.Application.user_agent() <> "; Bot" + +      {:ok, %Tesla.Env{body: html}} = +        Pleroma.HTTP.get(url, [{"user-agent", rich_media_agent}], adapter: opts)        html        |> parse_html() diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 9e457848e..386308362 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -328,10 +328,6 @@ defmodule Pleroma.Web.Router do        delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)        post("/notifications/read", NotificationController, :mark_as_read) -      patch("/accounts/update_avatar", AccountController, :update_avatar) -      patch("/accounts/update_banner", AccountController, :update_banner) -      patch("/accounts/update_background", AccountController, :update_background) -        get("/mascot", MascotController, :show)        put("/mascot", MascotController, :update) @@ -516,10 +512,6 @@ defmodule Pleroma.Web.Router do    scope "/api", Pleroma.Web do      pipe_through(:config) -    get("/help/test", TwitterAPI.UtilController, :help_test) -    post("/help/test", TwitterAPI.UtilController, :help_test) -    get("/statusnet/config", TwitterAPI.UtilController, :config) -    get("/statusnet/version", TwitterAPI.UtilController, :version)      get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)    end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index aaca182ec..f02c4075c 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -13,9 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    alias Pleroma.Notification    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.User -  alias Pleroma.Web    alias Pleroma.Web.CommonAPI -  alias Pleroma.Web.TwitterAPI.UtilView    alias Pleroma.Web.WebFinger    plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe) @@ -42,12 +40,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) -  plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version]) - -  def help_test(conn, _params) do -    json(conn, "ok") -  end -    def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do      with %User{} = user <- User.get_cached_by_nickname(nick),           avatar = User.avatar_url(user) do @@ -89,80 +81,14 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do      end    end -  def config(%{assigns: %{format: "xml"}} = conn, _params) do -    instance = Pleroma.Config.get(:instance) -    response = UtilView.status_net_config(instance) - -    conn -    |> put_resp_content_type("application/xml") -    |> send_resp(200, response) -  end - -  def config(conn, _params) do -    instance = Pleroma.Config.get(:instance) - -    vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - -    uploadlimit = %{ -      uploadlimit: to_string(Keyword.get(instance, :upload_limit)), -      avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)), -      backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)), -      bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit)) -    } - -    data = %{ -      name: Keyword.get(instance, :name), -      description: Keyword.get(instance, :description), -      server: Web.base_url(), -      textlimit: to_string(Keyword.get(instance, :limit)), -      uploadlimit: uploadlimit, -      closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"), -      private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"), -      vapidPublicKey: vapid_public_key, -      accountActivationRequired: -        bool_to_val(Keyword.get(instance, :account_activation_required, false)), -      invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)), -      safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions])) -    } - -    managed_config = Keyword.get(instance, :managed_config) - -    data = -      if managed_config do -        pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) -        Map.put(data, "pleromafe", pleroma_fe) -      else -        data -      end - -    json(conn, %{site: data}) -  end - -  defp bool_to_val(true), do: "1" -  defp bool_to_val(_), do: "0" -  defp bool_to_val(true, val, _), do: val -  defp bool_to_val(_, _, val), do: val -    def frontend_configurations(conn, _params) do      config = -      Pleroma.Config.get(:frontend_configurations, %{}) +      Config.get(:frontend_configurations, %{})        |> Enum.into(%{})      json(conn, config)    end -  def version(%{assigns: %{format: "xml"}} = conn, _params) do -    version = Pleroma.Application.named_version() - -    conn -    |> put_resp_content_type("application/xml") -    |> send_resp(200, "<version>#{version}</version>") -  end - -  def version(conn, _params) do -    json(conn, Pleroma.Application.named_version()) -  end -    def emoji(conn, _params) do      emoji =        Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->  | 
