diff options
Diffstat (limited to 'lib')
108 files changed, 3782 insertions, 784 deletions
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 0a2c891c0..1ba452275 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -105,6 +105,7 @@ defmodule Mix.Tasks.Pleroma.Instance do          )        secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) +      signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)        {web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)        result_config = @@ -120,6 +121,7 @@ defmodule Mix.Tasks.Pleroma.Instance do            dbpass: dbpass,            version: Pleroma.Mixfile.project() |> Keyword.get(:version),            secret: secret, +          signing_salt: signing_salt,            web_push_public_key: Base.url_encode64(web_push_public_key, padding: false),            web_push_private_key: Base.url_encode64(web_push_private_key, padding: false)          ) diff --git a/lib/mix/tasks/pleroma/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex index 740b9f8d1..1c935c0d8 100644 --- a/lib/mix/tasks/pleroma/sample_config.eex +++ b/lib/mix/tasks/pleroma/sample_config.eex @@ -7,7 +7,8 @@ use Mix.Config  config :pleroma, Pleroma.Web.Endpoint,     url: [host: "<%= domain %>", scheme: "https", port: <%= port %>], -   secret_key_base: "<%= secret %>" +   secret_key_base: "<%= secret %>", +   signing_salt: "<%= signing_salt %>"  config :pleroma, :instance,    name: "<%= name %>", diff --git a/lib/mix/tasks/pleroma/uploads.ex b/lib/mix/tasks/pleroma/uploads.ex index f0eb13e1a..a01e61627 100644 --- a/lib/mix/tasks/pleroma/uploads.ex +++ b/lib/mix/tasks/pleroma/uploads.ex @@ -4,7 +4,8 @@  defmodule Mix.Tasks.Pleroma.Uploads do    use Mix.Task -  alias Pleroma.{Upload, Uploaders.Local} +  alias Pleroma.Upload +  alias Pleroma.Uploaders.Local    alias Mix.Tasks.Pleroma.Common    require Logger @@ -20,7 +21,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do     - `--delete` - delete local uploads after migrating them to the target uploader -   A list of avalible uploaders can be seen in config.exs +   A list of available uploaders can be seen in config.exs    """    def run(["migrate_local", target_uploader | args]) do      delete? = Enum.member?(args, "--delete") @@ -96,6 +97,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do        timeout: 150_000      )      |> Stream.chunk_every(@log_every) +    # credo:disable-for-next-line Credo.Check.Warning.UnusedEnumOperation      |> Enum.reduce(0, fn done, count ->        count = count + length(done)        Mix.shell().info("Uploaded #{count}/#{total_count} files") diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 217a52fdd..037e44716 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -5,7 +5,8 @@  defmodule Mix.Tasks.Pleroma.User do    use Mix.Task    import Ecto.Changeset -  alias Pleroma.{Repo, User} +  alias Pleroma.Repo +  alias Pleroma.User    alias Mix.Tasks.Pleroma.Common    @shortdoc "Manages Pleroma users" @@ -22,6 +23,7 @@ defmodule Mix.Tasks.Pleroma.User do    - `--password PASSWORD` - the user's password    - `--moderator`/`--no-moderator` - whether the user is a moderator    - `--admin`/`--no-admin` - whether the user is an admin +  - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions     ## Generate an invite link. @@ -51,6 +53,14 @@ defmodule Mix.Tasks.Pleroma.User do    - `--locked`/`--no-locked` - whether the user's account is locked    - `--moderator`/`--no-moderator` - whether the user is a moderator    - `--admin`/`--no-admin` - whether the user is an admin + +  ## Add tags to a user. + +      mix pleroma.user tag NICKNAME TAGS + +  ## Delete tags from a user. + +      mix pleroma.user untag NICKNAME TAGS    """    def run(["new", nickname, email | rest]) do      {options, [], []} = @@ -61,7 +71,11 @@ defmodule Mix.Tasks.Pleroma.User do            bio: :string,            password: :string,            moderator: :boolean, -          admin: :boolean +          admin: :boolean, +          assume_yes: :boolean +        ], +        aliases: [ +          y: :assume_yes          ]        ) @@ -79,6 +93,7 @@ defmodule Mix.Tasks.Pleroma.User do      moderator? = Keyword.get(options, :moderator, false)      admin? = Keyword.get(options, :admin, false) +    assume_yes? = Keyword.get(options, :assume_yes, false)      Mix.shell().info("""      A user will be created with the following information: @@ -93,7 +108,7 @@ defmodule Mix.Tasks.Pleroma.User do        - admin: #{if(admin?, do: "true", else: "false")}      """) -    proceed? = Mix.shell().yes?("Continue?") +    proceed? = assume_yes? or Mix.shell().yes?("Continue?")      unless not proceed? do        Common.start_pleroma() @@ -197,7 +212,7 @@ defmodule Mix.Tasks.Pleroma.User do        user = Repo.get(User, user.id) -      if length(user.following) == 0 do +      if Enum.empty?(user.following) do          Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}")        end      else @@ -243,6 +258,32 @@ defmodule Mix.Tasks.Pleroma.User do      end    end +  def run(["tag", nickname | tags]) do +    Common.start_pleroma() + +    with %User{} = user <- User.get_by_nickname(nickname) do +      user = user |> User.tag(tags) + +      Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") +    else +      _ -> +        Mix.shell().error("Could not change user tags for #{nickname}") +    end +  end + +  def run(["untag", nickname | tags]) do +    Common.start_pleroma() + +    with %User{} = user <- User.get_by_nickname(nickname) do +      user = user |> User.untag(tags) + +      Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") +    else +      _ -> +        Mix.shell().error("Could not change user tags for #{nickname}") +    end +  end +    def run(["invite"]) do      Common.start_pleroma() diff --git a/lib/pleroma/PasswordResetToken.ex b/lib/pleroma/PasswordResetToken.ex index 1dccdadae..750ddd3c0 100644 --- a/lib/pleroma/PasswordResetToken.ex +++ b/lib/pleroma/PasswordResetToken.ex @@ -7,10 +7,12 @@ defmodule Pleroma.PasswordResetToken do    import Ecto.Changeset -  alias Pleroma.{User, PasswordResetToken, Repo} +  alias Pleroma.User +  alias Pleroma.Repo +  alias Pleroma.PasswordResetToken    schema "password_reset_tokens" do -    belongs_to(:user, User) +    belongs_to(:user, User, type: Pleroma.FlakeId)      field(:token, :string)      field(:used, :boolean, default: false) diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 353f9f6cd..cdfe7ea9e 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -4,10 +4,15 @@  defmodule Pleroma.Activity do    use Ecto.Schema -  alias Pleroma.{Repo, Activity, Notification} + +  alias Pleroma.Repo +  alias Pleroma.Activity +  alias Pleroma.Notification +    import Ecto.Query    @type t :: %__MODULE__{} +  @primary_key {:id, Pleroma.FlakeId, autogenerate: true}    # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19    @mastodon_notification_types %{ @@ -36,10 +41,11 @@ defmodule Pleroma.Activity do      )    end -  # TODO: -  # Go through these and fix them everywhere. -  # Wrong name, only returns create activities -  def all_by_object_ap_id_q(ap_id) do +  def get_by_id(id) do +    Repo.get(Activity, id) +  end + +  def by_object_ap_id(ap_id) do      from(        activity in Activity,        where: @@ -48,57 +54,55 @@ defmodule Pleroma.Activity do            activity.data,            activity.data,            ^to_string(ap_id) -        ), -      where: fragment("(?)->>'type' = 'Create'", activity.data) +        )      )    end -  # Wrong name, returns all. -  def all_non_create_by_object_ap_id_q(ap_id) do +  def create_by_object_ap_id(ap_ids) when is_list(ap_ids) do      from(        activity in Activity,        where:          fragment( -          "coalesce((?)->'object'->>'id', (?)->>'object') = ?", +          "coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)",            activity.data,            activity.data, -          ^to_string(ap_id) -        ) +          ^ap_ids +        ), +      where: fragment("(?)->>'type' = 'Create'", activity.data)      )    end -  # Wrong name plz fix thx -  def all_by_object_ap_id(ap_id) do -    Repo.all(all_by_object_ap_id_q(ap_id)) -  end - -  def create_activity_by_object_id_query(ap_ids) do +  def create_by_object_ap_id(ap_id) do      from(        activity in Activity,        where:          fragment( -          "coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)", +          "coalesce((?)->'object'->>'id', (?)->>'object') = ?",            activity.data,            activity.data, -          ^ap_ids +          ^to_string(ap_id)          ),        where: fragment("(?)->>'type' = 'Create'", activity.data)      )    end -  def get_create_activity_by_object_ap_id(ap_id) when is_binary(ap_id) do -    create_activity_by_object_id_query([ap_id]) +  def get_all_create_by_object_ap_id(ap_id) do +    Repo.all(create_by_object_ap_id(ap_id)) +  end + +  def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do +    create_by_object_ap_id(ap_id)      |> Repo.one()    end -  def get_create_activity_by_object_ap_id(_), do: nil +  def get_create_by_object_ap_id(_), do: nil    def normalize(obj) when is_map(obj), do: Activity.get_by_ap_id(obj["id"])    def normalize(ap_id) when is_binary(ap_id), do: Activity.get_by_ap_id(ap_id)    def normalize(_), do: nil    def get_in_reply_to_activity(%Activity{data: %{"object" => %{"inReplyTo" => ap_id}}}) do -    get_create_activity_by_object_ap_id(ap_id) +    get_create_by_object_ap_id(ap_id)    end    def get_in_reply_to_activity(_), do: nil diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index cb3e6b69b..d67e2cdc8 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -6,11 +6,13 @@ defmodule Pleroma.Application do    use Application    import Supervisor.Spec -  @name "Pleroma" +  @name Mix.Project.config()[:name]    @version Mix.Project.config()[:version] +  @repository Mix.Project.config()[:source_url]    def name, do: @name    def version, do: @version    def named_version(), do: @name <> " " <> @version +  def repository, do: @repository    def user_agent() do      info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>" @@ -22,6 +24,8 @@ defmodule Pleroma.Application do    def start(_type, _args) do      import Cachex.Spec +    Pleroma.Config.DeprecationWarnings.warn() +      # Define workers and child supervisors to be supervised      children =        [ @@ -66,6 +70,17 @@ defmodule Pleroma.Application do          worker(            Cachex,            [ +            :rich_media_cache, +            [ +              default_ttl: :timer.minutes(120), +              limit: 5000 +            ] +          ], +          id: :cachex_rich_media +        ), +        worker( +          Cachex, +          [              :scrubber_cache,              [                limit: 2500 @@ -88,11 +103,15 @@ defmodule Pleroma.Application do            ],            id: :cachex_idem          ), -        worker(Pleroma.Web.Federator.RetryQueue, []), -        worker(Pleroma.Web.Federator, []), -        worker(Pleroma.Stats, []), -        worker(Pleroma.Web.Push, []) +        worker(Pleroma.FlakeId, [])        ] ++ +        hackney_pool_children() ++ +        [ +          worker(Pleroma.Web.Federator.RetryQueue, []), +          worker(Pleroma.Web.Federator, []), +          worker(Pleroma.Stats, []), +          worker(Pleroma.Web.Push, []) +        ] ++          streamer_child() ++          chat_child() ++          [ @@ -107,6 +126,20 @@ defmodule Pleroma.Application do      Supervisor.start_link(children, opts)    end +  def enabled_hackney_pools() do +    [:media] ++ +      if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do +        [:federation] +      else +        [] +      end ++ +      if Pleroma.Config.get([Pleroma.Uploader, :proxy_remote]) do +        [:upload] +      else +        [] +      end +  end +    if Mix.env() == :test do      defp streamer_child(), do: []      defp chat_child(), do: [] @@ -123,4 +156,11 @@ defmodule Pleroma.Application do        end      end    end + +  defp hackney_pool_children() do +    for pool <- enabled_hackney_pools() do +      options = Pleroma.Config.get([:hackney_pools, pool]) +      :hackney_pool.child_spec(pool, options) +    end +  end  end diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha/captcha.ex index 0207bcbea..aa41acd1a 100644 --- a/lib/pleroma/captcha/captcha.ex +++ b/lib/pleroma/captcha/captcha.ex @@ -3,9 +3,9 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Captcha do +  alias Calendar.DateTime    alias Plug.Crypto.KeyGenerator    alias Plug.Crypto.MessageEncryptor -  alias Calendar.DateTime    use GenServer diff --git a/lib/pleroma/clippy.ex b/lib/pleroma/clippy.ex new file mode 100644 index 000000000..4e9bdbe19 --- /dev/null +++ b/lib/pleroma/clippy.ex @@ -0,0 +1,155 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Clippy do +  @moduledoc false +  # No software is complete until they have a Clippy implementation. +  # A ballmer peak _may_ be required to change this module. + +  def tip() do +    tips() +    |> Enum.random() +    |> puts() +  end + +  def tips() do +    host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) + +    [ +      "“πλήρωμα” is “pleroma” in greek", +      "For an extended Pleroma Clippy Experience, use the “Redmond” themes in Pleroma FE settings", +      "Staff accounts and MRF policies of Pleroma instances are disclosed on the NodeInfo endpoints for easy transparency!\n +- https://catgirl.science/misc/nodeinfo.lua?#{host} +- https://fediverse.network/#{host}/federation", +      "Pleroma can federate to the Dark Web!\n +- Tor: https://git.pleroma.social/pleroma/pleroma/wikis/Easy%20Onion%20Federation%20(Tor) +- i2p: https://git.pleroma.social/pleroma/pleroma/wikis/I2p%20federation", +      "Lists of Pleroma instances:\n\n- http://distsn.org/pleroma-instances.html\n- https://fediverse.network/pleroma\n- https://the-federation.info/pleroma", +      "Pleroma uses the LitePub protocol - https://litepub.social", +      "To receive more federated posts, subscribe to relays!\n +- How-to: https://git.pleroma.social/pleroma/pleroma/wikis/Admin%20tasks#relay-managment +- Relays: https://fediverse.network/activityrelay" +    ] +  end + +  @spec puts(String.t() | [[IO.ANSI.ansicode() | String.t(), ...], ...]) :: nil +  def puts(text_or_lines) do +    import IO.ANSI + +    lines = +      if is_binary(text_or_lines) do +        String.split(text_or_lines, ~r/\n/) +      else +        text_or_lines +      end + +    longest_line_size = +      lines +      |> Enum.map(&charlist_count_text/1) +      |> Enum.sort(&>=/2) +      |> List.first() + +    pad_text = longest_line_size + +    pad = +      for(_ <- 1..pad_text, do: "_") +      |> Enum.join("") + +    pad_spaces = +      for(_ <- 1..pad_text, do: " ") +      |> Enum.join("") + +    spaces = "      " + +    pre_lines = [ +      "  /  \\#{spaces}  _#{pad}___", +      "  |  |#{spaces} / #{pad_spaces}   \\" +    ] + +    for l <- pre_lines do +      IO.puts(l) +    end + +    clippy_lines = [ +      "  #{bright()}@  @#{reset()}#{spaces} ", +      "  || ||#{spaces}", +      "  || ||   <--", +      "  |\\_/|      ", +      "  \\___/      " +    ] + +    noclippy_line = "             " + +    env = %{ +      max_size: pad_text, +      pad: pad, +      pad_spaces: pad_spaces, +      spaces: spaces, +      pre_lines: pre_lines, +      noclippy_line: noclippy_line +    } + +    # surrond one/five line clippy with blank lines around to not fuck up the layout +    # +    # yes this fix sucks but it's good enough, have you ever seen a release of windows wihtout some butched +    # features anyway? +    lines = +      if length(lines) == 1 or length(lines) == 5 do +        [""] ++ lines ++ [""] +      else +        lines +      end + +    clippy_line(lines, clippy_lines, env) +  rescue +    e -> +      IO.puts("(Clippy crashed, sorry: #{inspect(e)})") +      IO.puts(text_or_lines) +  end + +  defp clippy_line([line | lines], [prefix | clippy_lines], env) do +    IO.puts([prefix <> "| ", rpad_line(line, env.max_size)]) +    clippy_line(lines, clippy_lines, env) +  end + +  # more text lines but clippy's complete +  defp clippy_line([line | lines], [], env) do +    IO.puts([env.noclippy_line, "| ", rpad_line(line, env.max_size)]) + +    if lines == [] do +      IO.puts(env.noclippy_line <> "\\_#{env.pad}___/") +    end + +    clippy_line(lines, [], env) +  end + +  # no more text lines but clippy's not complete +  defp clippy_line([], [clippy | clippy_lines], env) do +    if env.pad do +      IO.puts(clippy <> "\\_#{env.pad}___/") +      clippy_line([], clippy_lines, %{env | pad: nil}) +    else +      IO.puts(clippy) +      clippy_line([], clippy_lines, env) +    end +  end + +  defp clippy_line(_, _, _) do +  end + +  defp rpad_line(line, max) do +    pad = max - (charlist_count_text(line) - 2) +    pads = Enum.join(for(_ <- 1..pad, do: " ")) +    [IO.ANSI.format(line), pads <> " |"] +  end + +  defp charlist_count_text(line) do +    if is_list(line) do +      text = Enum.join(Enum.filter(line, &is_binary/1)) +      String.length(text) +    else +      String.length(line) +    end +  end +end diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex new file mode 100644 index 000000000..7451fd0a7 --- /dev/null +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Config.DeprecationWarnings do +  require Logger + +  def check_frontend_config_mechanism() do +    if Pleroma.Config.get(:fe) do +      Logger.warn(""" +      !!!DEPRECATION WARNING!!! +      You are using the old configuration mechanism for the frontend. Please check config.md. +      """) +    end +  end + +  def check_hellthread_threshold do +    if Pleroma.Config.get([:mrf_hellthread, :threshold]) do +      Logger.warn(""" +      !!!DEPRECATION WARNING!!! +      You are using the old configuration mechanism for the hellthread filter. Please check config.md. +      """) +    end +  end + +  def warn do +    check_frontend_config_mechanism() +    check_hellthread_threshold() +  end +end diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index c42c53c99..a3a09e96c 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -7,7 +7,8 @@ defmodule Pleroma.UserEmail do    import Swoosh.Email -  alias Pleroma.Web.{Endpoint, Router} +  alias Pleroma.Web.Endpoint +  alias Pleroma.Web.Router    defp instance_config, do: Pleroma.Config.get(:instance) diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index df5374a5c..bdc34698c 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -4,11 +4,15 @@  defmodule Pleroma.Filter do    use Ecto.Schema -  import Ecto.{Changeset, Query} -  alias Pleroma.{User, Repo} + +  import Ecto.Changeset +  import Ecto.Query + +  alias Pleroma.User +  alias Pleroma.Repo    schema "filters" do -    belongs_to(:user, User) +    belongs_to(:user, User, type: Pleroma.FlakeId)      field(:filter_id, :integer)      field(:hide, :boolean, default: false)      field(:whole_word, :boolean, default: true) diff --git a/lib/pleroma/flake_id.ex b/lib/pleroma/flake_id.ex new file mode 100644 index 000000000..9f098ce33 --- /dev/null +++ b/lib/pleroma/flake_id.ex @@ -0,0 +1,172 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.FlakeId do +  @moduledoc """ +  Flake is a decentralized, k-ordered id generation service. + +  Adapted from: + +  * [flaky](https://github.com/nirvana/flaky), released under the terms of the Truly Free License, +  * [Flake](https://github.com/boundary/flake), Copyright 2012, Boundary, Apache License, Version 2.0 +  """ + +  @type t :: binary + +  @behaviour Ecto.Type +  use GenServer +  require Logger +  alias __MODULE__ +  import Kernel, except: [to_string: 1] + +  defstruct node: nil, time: 0, sq: 0 + +  @doc "Converts a binary Flake to a String" +  def to_string(<<0::integer-size(64), id::integer-size(64)>>) do +    Kernel.to_string(id) +  end + +  def to_string(<<_::integer-size(64), _::integer-size(48), _::integer-size(16)>> = flake) do +    encode_base62(flake) +  end + +  def to_string(s), do: s + +  def from_string(int) when is_integer(int) do +    from_string(Kernel.to_string(int)) +  end + +  for i <- [-1, 0] do +    def from_string(unquote(i)), do: <<0::integer-size(128)>> +    def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>> +  end + +  def from_string(<<_::integer-size(128)>> = flake), do: flake + +  def from_string(string) when is_binary(string) and byte_size(string) < 18 do +    case Integer.parse(string) do +      {id, _} -> <<0::integer-size(64), id::integer-size(64)>> +      _ -> nil +    end +  end + +  def from_string(string) do +    string |> decode_base62 |> from_integer +  end + +  def to_integer(<<integer::integer-size(128)>>), do: integer + +  def from_integer(integer) do +    <<_time::integer-size(64), _node::integer-size(48), _seq::integer-size(16)>> = +      <<integer::integer-size(128)>> +  end + +  @doc "Generates a Flake" +  @spec get :: binary +  def get, do: to_string(:gen_server.call(:flake, :get)) + +  # -- Ecto.Type API +  @impl Ecto.Type +  def type, do: :uuid + +  @impl Ecto.Type +  def cast(value) do +    {:ok, FlakeId.to_string(value)} +  end + +  @impl Ecto.Type +  def load(value) do +    {:ok, FlakeId.to_string(value)} +  end + +  @impl Ecto.Type +  def dump(value) do +    {:ok, FlakeId.from_string(value)} +  end + +  def autogenerate(), do: get() + +  # -- GenServer API +  def start_link do +    :gen_server.start_link({:local, :flake}, __MODULE__, [], []) +  end + +  @impl GenServer +  def init([]) do +    {:ok, %FlakeId{node: worker_id(), time: time()}} +  end + +  @impl GenServer +  def handle_call(:get, _from, state) do +    {flake, new_state} = get(time(), state) +    {:reply, flake, new_state} +  end + +  # Matches when the calling time is the same as the state time. Incr. sq +  defp get(time, %FlakeId{time: time, node: node, sq: seq}) do +    new_state = %FlakeId{time: time, node: node, sq: seq + 1} +    {gen_flake(new_state), new_state} +  end + +  # Matches when the times are different, reset sq +  defp get(newtime, %FlakeId{time: time, node: node}) when newtime > time do +    new_state = %FlakeId{time: newtime, node: node, sq: 0} +    {gen_flake(new_state), new_state} +  end + +  # Error when clock is running backwards +  defp get(newtime, %FlakeId{time: time}) when newtime < time do +    {:error, :clock_running_backwards} +  end + +  defp gen_flake(%FlakeId{time: time, node: node, sq: seq}) do +    <<time::integer-size(64), node::integer-size(48), seq::integer-size(16)>> +  end + +  defp nthchar_base62(n) when n <= 9, do: ?0 + n +  defp nthchar_base62(n) when n <= 35, do: ?A + n - 10 +  defp nthchar_base62(n), do: ?a + n - 36 + +  defp encode_base62(<<integer::integer-size(128)>>) do +    integer +    |> encode_base62([]) +    |> List.to_string() +  end + +  defp encode_base62(int, acc) when int < 0, do: encode_base62(-int, acc) +  defp encode_base62(int, []) when int == 0, do: '0' +  defp encode_base62(int, acc) when int == 0, do: acc + +  defp encode_base62(int, acc) do +    r = rem(int, 62) +    id = div(int, 62) +    acc = [nthchar_base62(r) | acc] +    encode_base62(id, acc) +  end + +  defp decode_base62(s) do +    decode_base62(String.to_charlist(s), 0) +  end + +  defp decode_base62([c | cs], acc) when c >= ?0 and c <= ?9, +    do: decode_base62(cs, 62 * acc + (c - ?0)) + +  defp decode_base62([c | cs], acc) when c >= ?A and c <= ?Z, +    do: decode_base62(cs, 62 * acc + (c - ?A + 10)) + +  defp decode_base62([c | cs], acc) when c >= ?a and c <= ?z, +    do: decode_base62(cs, 62 * acc + (c - ?a + 36)) + +  defp decode_base62([], acc), do: acc + +  defp time do +    {mega_seconds, seconds, micro_seconds} = :erlang.timestamp() +    1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000) +  end + +  defp worker_id() do +    <<worker::integer-size(48)>> = :crypto.strong_rand_bytes(6) +    worker +  end +end diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index d80ae6576..f31aafa0d 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -3,10 +3,10 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Formatter do +  alias Pleroma.Emoji +  alias Pleroma.HTML    alias Pleroma.User    alias Pleroma.Web.MediaProxy -  alias Pleroma.HTML -  alias Pleroma.Emoji    @tag_regex ~r/((?<=[^&])|\A)(\#)(\w+)/u    @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/ @@ -43,7 +43,7 @@ defmodule Pleroma.Formatter do    def emojify(text, nil), do: text -  def emojify(text, emoji) do +  def emojify(text, emoji, strip \\ false) do      Enum.reduce(emoji, text, fn {emoji, file}, text ->        emoji = HTML.strip_tags(emoji)        file = HTML.strip_tags(file) @@ -51,14 +51,24 @@ defmodule Pleroma.Formatter do        String.replace(          text,          ":#{emoji}:", -        "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{ -          MediaProxy.url(file) -        }' />" +        if not strip do +          "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{ +            MediaProxy.url(file) +          }' />" +        else +          "" +        end        )        |> HTML.filter_tags()      end)    end +  def demojify(text) do +    emojify(text, Emoji.get_all(), true) +  end + +  def demojify(text, nil), do: text +    def get_emoji(text) when is_binary(text) do      Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)    end @@ -120,7 +130,7 @@ defmodule Pleroma.Formatter do    end    @doc "Adds the links to mentioned users" -  def add_user_links({subs, text}, mentions) do +  def add_user_links({subs, text}, mentions, options \\ []) do      mentions =        mentions        |> Enum.sort_by(fn {name, _} -> -String.length(name) end) @@ -142,10 +152,16 @@ defmodule Pleroma.Formatter do                ap_id              end -          short_match = String.split(match, "@") |> tl() |> hd() +          nickname = +            if options[:format] == :full do +              User.full_nickname(match) +            else +              User.local_nickname(match) +            end            {uuid, -           "<span><a data-user='#{id}' class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"} +           "<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>" <> +             "@<span>#{nickname}</span></a></span>"}          end)      {subs, uuid_text} @@ -168,7 +184,7 @@ defmodule Pleroma.Formatter do        subs ++          Enum.map(tags, fn {tag_text, tag, uuid} ->            url = -            "<a data-tag='#{tag}' href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{ +            "<a class='hashtag' data-tag='#{tag}' href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{                tag_text              }</a>" @@ -183,4 +199,16 @@ defmodule Pleroma.Formatter do        String.replace(result_text, uuid, replacement)      end)    end + +  def truncate(text, max_length \\ 200, omission \\ "...") do +    # Remove trailing whitespace +    text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}") + +    if String.length(text) < max_length do +      text +    else +      length_with_omission = max_length - String.length(omission) +      String.slice(text, 0, length_with_omission) <> omission +    end +  end  end diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex index 336142e9b..32cb817d2 100644 --- a/lib/pleroma/gopher/server.ex +++ b/lib/pleroma/gopher/server.ex @@ -37,17 +37,17 @@ end  defmodule Pleroma.Gopher.Server.ProtocolHandler do    alias Pleroma.Web.ActivityPub.ActivityPub -  alias Pleroma.User    alias Pleroma.Activity -  alias Pleroma.Repo    alias Pleroma.HTML +  alias Pleroma.User +  alias Pleroma.Repo    def start_link(ref, socket, transport, opts) do      pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts])      {:ok, pid}    end -  def init(ref, socket, transport, _Opts = []) do +  def init(ref, socket, transport, [] = _Opts) do      :ok = :ranch.accept_ack(ref)      loop(socket, transport)    end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 71db516e6..4dc6998b1 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -28,13 +28,18 @@ defmodule Pleroma.HTML do    def filter_tags(html), do: filter_tags(html, nil)    def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags) -  def get_cached_scrubbed_html_for_object(content, scrubbers, object) do -    key = "#{generate_scrubber_signature(scrubbers)}|#{object.id}" +  def get_cached_scrubbed_html_for_object(content, scrubbers, object, module) do +    key = "#{module}#{generate_scrubber_signature(scrubbers)}|#{object.id}"      Cachex.fetch!(:scrubber_cache, key, fn _key -> ensure_scrubbed_html(content, scrubbers) end)    end -  def get_cached_stripped_html_for_object(content, object) do -    get_cached_scrubbed_html_for_object(content, HtmlSanitizeEx.Scrubber.StripTags, object) +  def get_cached_stripped_html_for_object(content, object, module) do +    get_cached_scrubbed_html_for_object( +      content, +      HtmlSanitizeEx.Scrubber.StripTags, +      object, +      module +    )    end    def ensure_scrubbed_html( @@ -50,15 +55,23 @@ defmodule Pleroma.HTML do    defp generate_scrubber_signature(scrubbers) do      Enum.reduce(scrubbers, "", fn scrubber, signature -> -      # If a scrubber does not have a version(e.g HtmlSanitizeEx.Scrubber.StripTags) it is assumed it is always 0) -      version = -        if Kernel.function_exported?(scrubber, :version, 0) do -          scrubber.version -        else -          0 -        end - -      "#{signature}#{to_string(scrubber)}#{version}" +      "#{signature}#{to_string(scrubber)}" +    end) +  end + +  def extract_first_external_url(_, nil), do: {:error, "No content"} + +  def extract_first_external_url(object, content) do +    key = "URL|#{object.id}" + +    Cachex.fetch!(:scrubber_cache, key, fn _key -> +      result = +        content +        |> Floki.filter_out("a.mention") +        |> Floki.attribute("a", "href") +        |> Enum.at(0) + +      {:commit, {:ok, result}}      end)    end  end @@ -70,29 +83,24 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do    """    @markup Application.get_env(:pleroma, :markup) -  @uri_schemes Application.get_env(:pleroma, :uri_schemes, []) -  @valid_schemes Keyword.get(@uri_schemes, :valid_schemes, []) +  @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])    require HtmlSanitizeEx.Scrubber.Meta    alias HtmlSanitizeEx.Scrubber.Meta -  def version do -    0 -  end -    Meta.remove_cdata_sections_before_scrub()    Meta.strip_comments()    # links    Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) -  Meta.allow_tag_with_these_attributes("a", ["name", "title"]) +  Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])    # paragraphs and linebreaks    Meta.allow_tag_with_these_attributes("br", [])    Meta.allow_tag_with_these_attributes("p", [])    # microformats -  Meta.allow_tag_with_these_attributes("span", []) +  Meta.allow_tag_with_these_attributes("span", ["class"])    # allow inline images for custom emoji    @allow_inline_images Keyword.get(@markup, :allow_inline_images) @@ -117,20 +125,17 @@ defmodule Pleroma.HTML.Scrubber.Default do    require HtmlSanitizeEx.Scrubber.Meta    alias HtmlSanitizeEx.Scrubber.Meta - -  def version do -    0 -  end +  # credo:disable-for-previous-line +  # No idea how to fix this one…    @markup Application.get_env(:pleroma, :markup) -  @uri_schemes Application.get_env(:pleroma, :uri_schemes, []) -  @valid_schemes Keyword.get(@uri_schemes, :valid_schemes, []) +  @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])    Meta.remove_cdata_sections_before_scrub()    Meta.strip_comments()    Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) -  Meta.allow_tag_with_these_attributes("a", ["name", "title"]) +  Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])    Meta.allow_tag_with_these_attributes("abbr", ["title"]) @@ -145,7 +150,7 @@ defmodule Pleroma.HTML.Scrubber.Default do    Meta.allow_tag_with_these_attributes("ol", [])    Meta.allow_tag_with_these_attributes("p", [])    Meta.allow_tag_with_these_attributes("pre", []) -  Meta.allow_tag_with_these_attributes("span", []) +  Meta.allow_tag_with_these_attributes("span", ["class"])    Meta.allow_tag_with_these_attributes("strong", [])    Meta.allow_tag_with_these_attributes("u", [])    Meta.allow_tag_with_these_attributes("ul", []) @@ -199,10 +204,6 @@ defmodule Pleroma.HTML.Transform.MediaProxy do    alias Pleroma.Web.MediaProxy -  def version do -    0 -  end -    def before_scrub(html), do: html    def scrub_attribute("img", {"src", "http" <> target}) do diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index 699d80cd7..b798eaa5a 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -10,7 +10,8 @@ defmodule Pleroma.HTTP.Connection do    @hackney_options [      timeout: 10000,      recv_timeout: 20000, -    follow_redirect: true +    follow_redirect: true, +    pool: :federation    ]    @adapter Application.get_env(:tesla, :adapter) diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index b8103cef6..75c58e6c9 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -31,12 +31,15 @@ defmodule Pleroma.HTTP do        process_request_options(options)        |> process_sni_options(url) +    params = Keyword.get(options, :params, []) +      %{}      |> Builder.method(method)      |> Builder.headers(headers)      |> Builder.opts(options)      |> Builder.url(url)      |> Builder.add_param(:body, :body, body) +    |> Builder.add_param(:query, :query, params)      |> Enum.into([])      |> (&Tesla.request(Connection.new(), &1)).()    end diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex index bffc7c6fe..5f2cff2c0 100644 --- a/lib/pleroma/http/request_builder.ex +++ b/lib/pleroma/http/request_builder.ex @@ -100,6 +100,8 @@ defmodule Pleroma.HTTP.RequestBuilder do    Map    """    @spec add_param(map(), atom, atom, any()) :: map() +  def add_param(request, :query, :query, values), do: Map.put(request, :query, values) +    def add_param(request, :body, :body, value), do: Map.put(request, :body, value)    def add_param(request, :body, key, value) do @@ -107,7 +109,10 @@ defmodule Pleroma.HTTP.RequestBuilder do      |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)      |> Map.update!(        :body, -      &Tesla.Multipart.add_field(&1, key, Poison.encode!(value), +      &Tesla.Multipart.add_field( +        &1, +        key, +        Jason.encode!(value),          headers: [{:"Content-Type", "application/json"}]        )      ) diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex new file mode 100644 index 000000000..5e107f4c9 --- /dev/null +++ b/lib/pleroma/instances.ex @@ -0,0 +1,36 @@ +defmodule Pleroma.Instances do +  @moduledoc "Instances context." + +  @adapter Pleroma.Instances.Instance + +  defdelegate filter_reachable(urls_or_hosts), to: @adapter +  defdelegate reachable?(url_or_host), to: @adapter +  defdelegate set_reachable(url_or_host), to: @adapter +  defdelegate set_unreachable(url_or_host, unreachable_since \\ nil), to: @adapter + +  def set_consistently_unreachable(url_or_host), +    do: set_unreachable(url_or_host, reachability_datetime_threshold()) + +  def reachability_datetime_threshold do +    federation_reachability_timeout_days = +      Pleroma.Config.get(:instance)[:federation_reachability_timeout_days] || 0 + +    if federation_reachability_timeout_days > 0 do +      NaiveDateTime.add( +        NaiveDateTime.utc_now(), +        -federation_reachability_timeout_days * 24 * 3600, +        :second +      ) +    else +      ~N[0000-01-01 00:00:00] +    end +  end + +  def host(url_or_host) when is_binary(url_or_host) do +    if url_or_host =~ ~r/^http/i do +      URI.parse(url_or_host).host +    else +      url_or_host +    end +  end +end diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex new file mode 100644 index 000000000..48bc939dd --- /dev/null +++ b/lib/pleroma/instances/instance.ex @@ -0,0 +1,113 @@ +defmodule Pleroma.Instances.Instance do +  @moduledoc "Instance." + +  alias Pleroma.Instances +  alias Pleroma.Repo +  alias Pleroma.Instances.Instance + +  use Ecto.Schema + +  import Ecto.Query +  import Ecto.Changeset + +  schema "instances" do +    field(:host, :string) +    field(:unreachable_since, :naive_datetime) + +    timestamps() +  end + +  defdelegate host(url_or_host), to: Instances + +  def changeset(struct, params \\ %{}) do +    struct +    |> cast(params, [:host, :unreachable_since]) +    |> validate_required([:host]) +    |> unique_constraint(:host) +  end + +  def filter_reachable([]), do: %{} + +  def filter_reachable(urls_or_hosts) when is_list(urls_or_hosts) do +    hosts = +      urls_or_hosts +      |> Enum.map(&(&1 && host(&1))) +      |> Enum.filter(&(to_string(&1) != "")) + +    unreachable_since_by_host = +      Repo.all( +        from(i in Instance, +          where: i.host in ^hosts, +          select: {i.host, i.unreachable_since} +        ) +      ) +      |> Map.new(& &1) + +    reachability_datetime_threshold = Instances.reachability_datetime_threshold() + +    for entry <- Enum.filter(urls_or_hosts, &is_binary/1) do +      host = host(entry) +      unreachable_since = unreachable_since_by_host[host] + +      if !unreachable_since || +           NaiveDateTime.compare(unreachable_since, reachability_datetime_threshold) == :gt do +        {entry, unreachable_since} +      end +    end +    |> Enum.filter(& &1) +    |> Map.new(& &1) +  end + +  def reachable?(url_or_host) when is_binary(url_or_host) do +    !Repo.one( +      from(i in Instance, +        where: +          i.host == ^host(url_or_host) and +            i.unreachable_since <= ^Instances.reachability_datetime_threshold(), +        select: true +      ) +    ) +  end + +  def reachable?(_), do: true + +  def set_reachable(url_or_host) when is_binary(url_or_host) do +    with host <- host(url_or_host), +         %Instance{} = existing_record <- Repo.get_by(Instance, %{host: host}) do +      {:ok, _instance} = +        existing_record +        |> changeset(%{unreachable_since: nil}) +        |> Repo.update() +    end +  end + +  def set_reachable(_), do: {:error, nil} + +  def set_unreachable(url_or_host, unreachable_since \\ nil) + +  def set_unreachable(url_or_host, unreachable_since) when is_binary(url_or_host) do +    unreachable_since = unreachable_since || DateTime.utc_now() +    host = host(url_or_host) +    existing_record = Repo.get_by(Instance, %{host: host}) + +    changes = %{unreachable_since: unreachable_since} + +    cond do +      is_nil(existing_record) -> +        %Instance{} +        |> changeset(Map.put(changes, :host, host)) +        |> Repo.insert() + +      existing_record.unreachable_since && +          NaiveDateTime.compare(existing_record.unreachable_since, unreachable_since) != :gt -> +        {:ok, existing_record} + +      true -> +        existing_record +        |> changeset(changes) +        |> Repo.update() +    end +  end + +  def set_unreachable(_, _), do: {:error, nil} +end diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index a75dc006e..55c4cf6df 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -4,11 +4,16 @@  defmodule Pleroma.List do    use Ecto.Schema -  import Ecto.{Changeset, Query} -  alias Pleroma.{User, Repo, Activity} + +  import Ecto.Query +  import Ecto.Changeset + +  alias Pleroma.Activity +  alias Pleroma.Repo +  alias Pleroma.User    schema "lists" do -    belongs_to(:user, Pleroma.User) +    belongs_to(:user, User, type: Pleroma.FlakeId)      field(:title, :string)      field(:following, {:array, :string}, default: []) diff --git a/lib/pleroma/mime.ex b/lib/pleroma/mime.ex index 84fb536e0..36771533f 100644 --- a/lib/pleroma/mime.ex +++ b/lib/pleroma/mime.ex @@ -102,10 +102,18 @@ defmodule Pleroma.MIME do      "audio/ogg"    end -  defp check_mime_type(<<0x52, 0x49, 0x46, 0x46, _::binary>>) do +  defp check_mime_type(<<"RIFF", _::binary-size(4), "WAVE", _::binary>>) do      "audio/wav"    end +  defp check_mime_type(<<"RIFF", _::binary-size(4), "WEBP", _::binary>>) do +    "image/webp" +  end + +  defp check_mime_type(<<"RIFF", _::binary-size(4), "AVI.", _::binary>>) do +    "video/avi" +  end +    defp check_mime_type(_) do      @default    end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 51d59870c..c88512567 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -4,13 +4,20 @@  defmodule Pleroma.Notification do    use Ecto.Schema -  alias Pleroma.{User, Activity, Notification, Repo, Object} + +  alias Pleroma.User +  alias Pleroma.Activity +  alias Pleroma.Notification +  alias Pleroma.Repo +  alias Pleroma.Web.CommonAPI.Utils +  alias Pleroma.Web.CommonAPI +    import Ecto.Query    schema "notifications" do      field(:seen, :boolean, default: false) -    belongs_to(:user, Pleroma.User) -    belongs_to(:activity, Pleroma.Activity) +    belongs_to(:user, User, type: Pleroma.FlakeId) +    belongs_to(:activity, Activity, type: Pleroma.FlakeId)      timestamps()    end @@ -34,7 +41,8 @@ defmodule Pleroma.Notification do          n in Notification,          where: n.user_id == ^user.id,          order_by: [desc: n.id], -        preload: [:activity], +        join: activity in assoc(n, :activity), +        preload: [activity: activity],          limit: 20        ) @@ -65,7 +73,8 @@ defmodule Pleroma.Notification do        from(          n in Notification,          where: n.id == ^id, -        preload: [:activity] +        join: activity in assoc(n, :activity), +        preload: [activity: activity]        )      notification = Repo.one(query) @@ -96,7 +105,7 @@ defmodule Pleroma.Notification do      end    end -  def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity) +  def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)        when type in ["Create", "Like", "Announce", "Follow"] do      users = get_notified_from_activity(activity) @@ -109,7 +118,12 @@ defmodule Pleroma.Notification do    # TODO move to sql, too.    def create_notification(%Activity{} = activity, %User{} = user) do      unless User.blocks?(user, %{ap_id: activity.data["actor"]}) or -             user.ap_id == activity.data["actor"] do +             CommonAPI.thread_muted?(user, activity) or user.ap_id == activity.data["actor"] or +             (activity.data["type"] == "Follow" and +                Enum.any?(Notification.for_user(user), fn notif -> +                  notif.activity.data["type"] == "Follow" and +                    notif.activity.data["actor"] == activity.data["actor"] +                end)) do        notification = %Notification{user_id: user.id, activity: activity}        {:ok, notification} = Repo.insert(notification)        Pleroma.Web.Streamer.stream("user", notification) @@ -127,54 +141,12 @@ defmodule Pleroma.Notification do        when type in ["Create", "Like", "Announce", "Follow"] do      recipients =        [] -      |> maybe_notify_to_recipients(activity) -      |> maybe_notify_mentioned_recipients(activity) +      |> Utils.maybe_notify_to_recipients(activity) +      |> Utils.maybe_notify_mentioned_recipients(activity)        |> Enum.uniq()      User.get_users_from_set(recipients, local_only)    end    def get_notified_from_activity(_, _local_only), do: [] - -  defp maybe_notify_to_recipients( -         recipients, -         %Activity{data: %{"to" => to, "type" => _type}} = _activity -       ) do -    recipients ++ to -  end - -  defp maybe_notify_mentioned_recipients( -         recipients, -         %Activity{data: %{"to" => _to, "type" => type} = data} = _activity -       ) -       when type == "Create" do -    object = Object.normalize(data["object"]) - -    object_data = -      cond do -        !is_nil(object) -> -          object.data - -        is_map(data["object"]) -> -          data["object"] - -        true -> -          %{} -      end - -    tagged_mentions = maybe_extract_mentions(object_data) - -    recipients ++ tagged_mentions -  end - -  defp maybe_notify_mentioned_recipients(recipients, _), do: recipients - -  defp maybe_extract_mentions(%{"tag" => tag}) do -    tag -    |> Enum.filter(fn x -> is_map(x) end) -    |> Enum.filter(fn x -> x["type"] == "Mention" end) -    |> Enum.map(fn x -> x["href"] end) -  end - -  defp maybe_extract_mentions(_), do: []  end diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index ff5eb9b27..5f1fc801b 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -4,8 +4,15 @@  defmodule Pleroma.Object do    use Ecto.Schema -  alias Pleroma.{Repo, Object, User, Activity, ObjectTombstone} -  import Ecto.{Query, Changeset} + +  alias Pleroma.Repo +  alias Pleroma.Object +  alias Pleroma.User +  alias Pleroma.Activity +  alias Pleroma.ObjectTombstone + +  import Ecto.Query +  import Ecto.Changeset    schema "objects" do      field(:data, :map) @@ -31,8 +38,8 @@ defmodule Pleroma.Object do      Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))    end -  def normalize(obj) when is_map(obj), do: Object.get_by_ap_id(obj["id"]) -  def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id) +  def normalize(%{"id" => ap_id}), do: normalize(ap_id) +  def normalize(ap_id) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)    def normalize(_), do: nil    # Owned objects can only be mutated by their owner @@ -42,24 +49,18 @@ defmodule Pleroma.Object do    # Legacy objects can be mutated by anybody    def authorize_mutation(%Object{}, %User{}), do: true -  if Mix.env() == :test do -    def get_cached_by_ap_id(ap_id) do -      get_by_ap_id(ap_id) -    end -  else -    def get_cached_by_ap_id(ap_id) do -      key = "object:#{ap_id}" - -      Cachex.fetch!(:object_cache, key, fn _ -> -        object = get_by_ap_id(ap_id) - -        if object do -          {:commit, object} -        else -          {:ignore, object} -        end -      end) -    end +  def get_cached_by_ap_id(ap_id) do +    key = "object:#{ap_id}" + +    Cachex.fetch!(:object_cache, key, fn _ -> +      object = get_by_ap_id(ap_id) + +      if object do +        {:commit, object} +      else +        {:ignore, object} +      end +    end)    end    def context_mapping(context) do @@ -85,9 +86,22 @@ defmodule Pleroma.Object do    def delete(%Object{data: %{"id" => id}} = object) do      with {:ok, _obj} = swap_object_with_tombstone(object), -         Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)), +         Repo.delete_all(Activity.by_object_ap_id(id)),           {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do        {:ok, object}      end    end + +  def set_cache(%Object{data: %{"id" => ap_id}} = object) do +    Cachex.put(:object_cache, "object:#{ap_id}", object) +    {:ok, object} +  end + +  def update_and_set_cache(changeset) do +    with {:ok, object} <- Repo.update(changeset) do +      set_cache(object) +    else +      e -> e +    end +  end  end diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 2a266c407..057553e24 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -33,7 +33,22 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do    end    defp csp_string do -    protocol = Config.get([Pleroma.Web.Endpoint, :protocol]) +    scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] +    websocket_url = String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws") + +    connect_src = +      if Mix.env() == :dev do +        "connect-src 'self' http://localhost:3035/ " <> websocket_url +      else +        "connect-src 'self' " <> websocket_url +      end + +    script_src = +      if Mix.env() == :dev do +        "script-src 'self' 'unsafe-eval'" +      else +        "script-src 'self'" +      end      [        "default-src 'none'", @@ -43,10 +58,10 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do        "media-src 'self' https:",        "style-src 'self' 'unsafe-inline'",        "font-src 'self'", -      "script-src 'self'", -      "connect-src 'self' " <> String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),        "manifest-src 'self'", -      if protocol == "https" do +      connect_src, +      script_src, +      if scheme == "https" do          "upgrade-insecure-requests"        end      ] diff --git a/lib/pleroma/plugs/instance_static.ex b/lib/pleroma/plugs/instance_static.ex index af2f6f331..41125921a 100644 --- a/lib/pleroma/plugs/instance_static.ex +++ b/lib/pleroma/plugs/instance_static.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Plugs.InstanceStatic do      end    end -  @only ~w(index.html static emoji packs sounds images instance favicon.png) +  @only ~w(index.html static emoji packs sounds images instance favicon.png sw.js sw-pleroma.js)    def init(opts) do      opts @@ -33,7 +33,7 @@ defmodule Pleroma.Plugs.InstanceStatic do    for only <- @only do      at = Plug.Router.Utils.split("/") -    def call(conn = %{request_path: "/" <> unquote(only) <> _}, opts) do +    def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do        call_static(          conn,          opts, diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex index 437aa95b3..22f0406f4 100644 --- a/lib/pleroma/plugs/oauth_plug.ex +++ b/lib/pleroma/plugs/oauth_plug.ex @@ -6,11 +6,9 @@ defmodule Pleroma.Plugs.OAuthPlug do    import Plug.Conn    import Ecto.Query -  alias Pleroma.{ -    User, -    Repo, -    Web.OAuth.Token -  } +  alias Pleroma.User +  alias Pleroma.Repo +  alias Pleroma.Web.OAuth.Token    @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") @@ -33,7 +31,12 @@ defmodule Pleroma.Plugs.OAuthPlug do    #    @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil    defp fetch_user_and_token(token) do -    query = from(q in Token, where: q.token == ^token, preload: [:user]) +    query = +      from(t in Token, +        where: t.token == ^token, +        join: user in assoc(t, :user), +        preload: [user: user] +      )      with %Token{user: %{info: %{deactivated: false} = _} = user} = token_record <- Repo.one(query) do        {:ok, user, token_record} diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex index be53ac00c..13aa8641a 100644 --- a/lib/pleroma/plugs/uploaded_media.ex +++ b/lib/pleroma/plugs/uploaded_media.ex @@ -23,7 +23,7 @@ defmodule Pleroma.Plugs.UploadedMedia do      %{static_plug_opts: static_plug_opts}    end -  def call(conn = %{request_path: <<"/", @path, "/", file::binary>>}, opts) do +  def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do      config = Pleroma.Config.get([Pleroma.Upload])      with uploader <- Keyword.fetch!(config, :uploader), diff --git a/lib/pleroma/plugs/user_fetcher_plug.ex b/lib/pleroma/plugs/user_fetcher_plug.ex index f874e2f95..7ed4602bb 100644 --- a/lib/pleroma/plugs/user_fetcher_plug.ex +++ b/lib/pleroma/plugs/user_fetcher_plug.ex @@ -3,9 +3,10 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Plugs.UserFetcherPlug do -  import Plug.Conn -  alias Pleroma.Repo    alias Pleroma.User +  alias Pleroma.Repo + +  import Plug.Conn    def init(options) do      options diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index a3846c3bb..a25b5ea4e 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy.ex @@ -275,11 +275,24 @@ defmodule Pleroma.ReverseProxy do    defp build_resp_cache_headers(headers, _opts) do      has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end) - -    if has_cache? do -      headers -    else -      List.keystore(headers, "cache-control", 0, {"cache-control", @default_cache_control_header}) +    has_cache_control? = List.keymember?(headers, "cache-control", 0) + +    cond do +      has_cache? && has_cache_control? -> +        headers + +      has_cache? -> +        # There's caching header present but no cache-control -- we need to explicitely override it to public +        # as Plug defaults to "max-age=0, private, must-revalidate" +        List.keystore(headers, "cache-control", 0, {"cache-control", "public"}) + +      true -> +        List.keystore( +          headers, +          "cache-control", +          0, +          {"cache-control", @default_cache_control_header} +        )      end    end diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index 8a030ecd0..fe0ce9051 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -4,7 +4,8 @@  defmodule Pleroma.Stats do    import Ecto.Query -  alias Pleroma.{User, Repo} +  alias Pleroma.User +  alias Pleroma.Repo    def start_link do      agent = Agent.start_link(fn -> {[], %{}} end, name: __MODULE__) @@ -23,7 +24,7 @@ defmodule Pleroma.Stats do    def schedule_update do      spawn(fn ->        # 1 hour -      Process.sleep(1000 * 60 * 60 * 1) +      Process.sleep(1000 * 60 * 60)        schedule_update()      end) @@ -34,10 +35,11 @@ defmodule Pleroma.Stats do      peers =        from(          u in Pleroma.User, -        select: fragment("distinct ?->'host'", u.info), +        select: fragment("distinct split_part(?, '@', 2)", u.nickname),          where: u.local != ^true        )        |> Repo.all() +      |> Enum.filter(& &1)      domain_count = Enum.count(peers) @@ -45,7 +47,7 @@ defmodule Pleroma.Stats do        from(u in User.local_user_query(), select: fragment("sum((?->>'note_count')::int)", u.info))      status_count = Repo.one(status_query) -    user_count = Repo.aggregate(User.local_user_query(), :count, :id) +    user_count = Repo.aggregate(User.active_local_user_query(), :count, :id)      Agent.update(__MODULE__, fn _ ->        {peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}} diff --git a/lib/pleroma/thread_mute.ex b/lib/pleroma/thread_mute.ex new file mode 100644 index 000000000..0b577113d --- /dev/null +++ b/lib/pleroma/thread_mute.ex @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ThreadMute do +  use Ecto.Schema +  alias Pleroma.{Repo, User, ThreadMute} +  require Ecto.Query + +  schema "thread_mutes" do +    belongs_to(:user, User, type: Pleroma.FlakeId) +    field(:context, :string) +  end + +  def changeset(mute, params \\ %{}) do +    mute +    |> Ecto.Changeset.cast(params, [:user_id, :context]) +    |> Ecto.Changeset.foreign_key_constraint(:user_id) +    |> Ecto.Changeset.unique_constraint(:user_id, name: :unique_index) +  end + +  def query(user_id, context) do +    user_id = Pleroma.FlakeId.from_string(user_id) + +    ThreadMute +    |> Ecto.Query.where(user_id: ^user_id) +    |> Ecto.Query.where(context: ^context) +  end + +  def add_mute(user_id, context) do +    %ThreadMute{} +    |> changeset(%{user_id: user_id, context: context}) +    |> Repo.insert() +  end + +  def remove_mute(user_id, context) do +    query(user_id, context) +    |> Repo.delete_all() +  end + +  def check_muted(user_id, context) do +    query(user_id, context) +    |> Repo.all() +  end +end diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 0b1bdeec4..91a5db8c5 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -34,8 +34,9 @@ defmodule Pleroma.Upload do    require Logger    @type source :: -          Plug.Upload.t() | data_uri_string :: -          String.t() | {:from_local, name :: String.t(), id :: String.t(), path :: String.t()} +          Plug.Upload.t() +          | (data_uri_string :: String.t()) +          | {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}    @type option ::            {:type, :avatar | :banner | :background} @@ -123,10 +124,10 @@ defmodule Pleroma.Upload do            :pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Mogrify]] -          :pleroma, Pleroma.Upload.Filter.Mogrify, args: "strip" +          :pleroma, Pleroma.Upload.Filter.Mogrify, args: ["strip", "auto-orient"]          """) -        Pleroma.Config.put([Pleroma.Upload.Filter.Mogrify], args: "strip") +        Pleroma.Config.put([Pleroma.Upload.Filter.Mogrify], args: ["strip", "auto-orient"])          Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Mogrify])        else          opts @@ -179,7 +180,7 @@ defmodule Pleroma.Upload do    end    # For Mix.Tasks.MigrateLocalUploads -  defp prepare_upload(upload = %__MODULE__{tempfile: path}, _opts) do +  defp prepare_upload(%__MODULE__{tempfile: path} = upload, _opts) do      with {:ok, content_type} <- Pleroma.MIME.file_mime_type(path) do        {:ok, %__MODULE__{upload | content_type: content_type}}      end @@ -215,6 +216,12 @@ defmodule Pleroma.Upload do    end    defp url_from_spec(base_url, {:file, path}) do +    path = +      path +      |> URI.encode() +      |> String.replace("?", "%3F") +      |> String.replace(":", "%3A") +      [base_url, "media", path]      |> Path.join()    end diff --git a/lib/pleroma/upload/filter/dedupe.ex b/lib/pleroma/upload/filter/dedupe.ex index 8fcce320f..e4c225833 100644 --- a/lib/pleroma/upload/filter/dedupe.ex +++ b/lib/pleroma/upload/filter/dedupe.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Upload.Filter.Dedupe do    @behaviour Pleroma.Upload.Filter    alias Pleroma.Upload -  def filter(upload = %Upload{name: name}) do +  def filter(%Upload{name: name} = upload) do      extension = String.split(name, ".") |> List.last()      shasum = :crypto.hash(:sha256, File.read!(upload.tempfile)) |> Base.encode16(case: :lower)      filename = shasum <> "." <> extension diff --git a/lib/pleroma/uploaders/mdii.ex b/lib/pleroma/uploaders/mdii.ex index 530b34362..190ed9f3a 100644 --- a/lib/pleroma/uploaders/mdii.ex +++ b/lib/pleroma/uploaders/mdii.ex @@ -24,7 +24,8 @@ defmodule Pleroma.Uploaders.MDII do      extension = String.split(upload.name, ".") |> List.last()      query = "#{cgi}?#{extension}" -    with {:ok, %{status: 200, body: body}} <- @httpoison.post(query, file_data) do +    with {:ok, %{status: 200, body: body}} <- +           @httpoison.post(query, file_data, [], adapter: [pool: :default]) do        remote_file_name = String.split(body) |> List.first()        public_url = "#{files}/#{remote_file_name}.#{extension}"        {:ok, {:url, public_url}} diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex index 108cf06b5..0038ba01f 100644 --- a/lib/pleroma/uploaders/s3.ex +++ b/lib/pleroma/uploaders/s3.ex @@ -9,17 +9,25 @@ defmodule Pleroma.Uploaders.S3 do    # The file name is re-encoded with S3's constraints here to comply with previous links with less strict filenames    def get_file(file) do      config = Pleroma.Config.get([__MODULE__]) +    bucket = Keyword.fetch!(config, :bucket) + +    bucket_with_namespace = +      if namespace = Keyword.get(config, :bucket_namespace) do +        namespace <> ":" <> bucket +      else +        bucket +      end      {:ok,       {:url,        Path.join([          Keyword.fetch!(config, :public_endpoint), -        Keyword.fetch!(config, :bucket), +        bucket_with_namespace,          strict_encode(URI.decode(file))        ])}}    end -  def put_file(upload = %Pleroma.Upload{}) do +  def put_file(%Pleroma.Upload{} = upload) do      config = Pleroma.Config.get([__MODULE__])      bucket = Keyword.get(config, :bucket) diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex index 0959d7a3e..ce83cbbbc 100644 --- a/lib/pleroma/uploaders/uploader.ex +++ b/lib/pleroma/uploaders/uploader.ex @@ -27,18 +27,47 @@ defmodule Pleroma.Uploaders.Uploader do      This allows to correctly proxy or redirect requests to the backend, while allowing to migrate backends without breaking any URL.    * `{url, url :: String.t}` to bypass `get_file/2` and use the `url` directly in the activity.    * `{:error, String.t}` error information if the file failed to be saved to the backend. +  * `:wait_callback` will wait for an http post request at `/api/pleroma/upload_callback/:upload_path` and call the uploader's `http_callback/3` method.    """ +  @type file_spec :: {:file | :url, String.t()}    @callback put_file(Pleroma.Upload.t()) :: -              :ok | {:ok, {:file | :url, String.t()}} | {:error, String.t()} +              :ok | {:ok, file_spec()} | {:error, String.t()} | :wait_callback + +  @callback http_callback(Plug.Conn.t(), Map.t()) :: +              {:ok, Plug.Conn.t()} +              | {:ok, Plug.Conn.t(), file_spec()} +              | {:error, Plug.Conn.t(), String.t()} +  @optional_callbacks http_callback: 2 + +  @spec put_file(module(), Pleroma.Upload.t()) :: {:ok, file_spec()} | {:error, String.t()} -  @spec put_file(module(), Pleroma.Upload.t()) :: -          {:ok, {:file | :url, String.t()}} | {:error, String.t()}    def put_file(uploader, upload) do      case uploader.put_file(upload) do        :ok -> {:ok, {:file, upload.path}} -      other -> other +      :wait_callback -> handle_callback(uploader, upload) +      {:ok, _} = ok -> ok +      {:error, _} = error -> error +    end +  end + +  defp handle_callback(uploader, upload) do +    :global.register_name({__MODULE__, upload.path}, self()) + +    receive do +      {__MODULE__, pid, conn, params} -> +        case uploader.http_callback(conn, params) do +          {:ok, conn, ok} -> +            send(pid, {__MODULE__, conn}) +            {:ok, ok} + +          {:error, conn, error} -> +            send(pid, {__MODULE__, conn}) +            {:error, error} +        end +    after +      30_000 -> {:error, "Uploader callback timeout"}      end    end  end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 1edded415..3232cb842 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -5,18 +5,30 @@  defmodule Pleroma.User do    use Ecto.Schema -  import Ecto.{Changeset, Query} -  alias Pleroma.{Repo, User, Object, Web, Activity, Notification} +  import Ecto.Changeset +  import Ecto.Query + +  alias Pleroma.Repo +  alias Pleroma.User +  alias Pleroma.Object +  alias Pleroma.Web +  alias Pleroma.Activity +  alias Pleroma.Notification    alias Comeonin.Pbkdf2    alias Pleroma.Formatter    alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils -  alias Pleroma.Web.{OStatus, Websub, OAuth} -  alias Pleroma.Web.ActivityPub.{Utils, ActivityPub} +  alias Pleroma.Web.OStatus +  alias Pleroma.Web.Websub +  alias Pleroma.Web.OAuth +  alias Pleroma.Web.ActivityPub.Utils +  alias Pleroma.Web.ActivityPub.ActivityPub    require Logger    @type t :: %__MODULE__{} +  @primary_key {:id, Pleroma.FlakeId, autogenerate: true} +    @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/    @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/ @@ -35,8 +47,9 @@ defmodule Pleroma.User do      field(:avatar, :map)      field(:local, :boolean, default: true)      field(:follower_address, :string) -    field(:search_distance, :float, virtual: true) +    field(:search_rank, :float, virtual: true)      field(:tags, {:array, :string}, default: []) +    field(:bookmarks, {:array, :string}, default: [])      field(:last_refreshed_at, :naive_datetime)      has_many(:notifications, Notification)      embeds_one(:info, Pleroma.User.Info) @@ -44,20 +57,28 @@ defmodule Pleroma.User do      timestamps()    end -  def auth_active?(%User{} = user) do -    (user.info && !user.info.confirmation_pending) || -      !Pleroma.Config.get([:instance, :account_activation_required]) -  end +  def auth_active?(%User{local: false}), do: true + +  def auth_active?(%User{info: %User.Info{confirmation_pending: false}}), do: true + +  def auth_active?(%User{info: %User.Info{confirmation_pending: true}}), +    do: !Pleroma.Config.get([:instance, :account_activation_required]) -  def remote_or_auth_active?(%User{} = user), do: !user.local || auth_active?(user) +  def auth_active?(_), do: false -  def visible_for?(%User{} = user, for_user \\ nil) do -    User.remote_or_auth_active?(user) || (for_user && for_user.id == user.id) || -      User.superuser?(for_user) +  def visible_for?(user, for_user \\ nil) + +  def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true + +  def visible_for?(%User{} = user, for_user) do +    auth_active?(user) || superuser?(for_user)    end -  def superuser?(nil), do: false -  def superuser?(%User{} = user), do: user.info && User.Info.superuser?(user.info) +  def visible_for?(_, _), do: false + +  def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true +  def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true +  def superuser?(_), do: false    def avatar_url(user) do      case user.avatar do @@ -85,12 +106,6 @@ defmodule Pleroma.User do      "#{ap_id(user)}/followers"    end -  def follow_changeset(struct, params \\ %{}) do -    struct -    |> cast(params, [:following]) -    |> validate_required([:following]) -  end -    def user_info(%User{} = user) do      oneself = if user.local, do: 1, else: 0 @@ -229,10 +244,24 @@ defmodule Pleroma.User do      end    end +  defp autofollow_users(user) do +    candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames]) + +    autofollowed_users = +      from(u in User, +        where: u.local == true, +        where: u.nickname in ^candidates +      ) +      |> Repo.all() + +    follow_all(user, autofollowed_users) +  end +    @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"    def register(%Ecto.Changeset{} = changeset) do      with {:ok, user} <- Repo.insert(changeset), -         {:ok, _} = try_send_confirmation_email(user) do +         {:ok, user} <- autofollow_users(user), +         {:ok, _} <- try_send_confirmation_email(user) do        {:ok, user}      end    end @@ -282,6 +311,38 @@ defmodule Pleroma.User do      end    end +  @doc "A mass follow for local users. Respects blocks in both directions but does not create activities." +  @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()} +  def follow_all(follower, followeds) do +    followed_addresses = +      followeds +      |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end) +      |> Enum.map(fn %{follower_address: fa} -> fa end) + +    q = +      from(u in User, +        where: u.id == ^follower.id, +        update: [ +          set: [ +            following: +              fragment( +                "array(select distinct unnest (array_cat(?, ?)))", +                u.following, +                ^followed_addresses +              ) +          ] +        ] +      ) + +    {1, [follower]} = Repo.update_all(q, [], returning: true) + +    Enum.each(followeds, fn followed -> +      update_follower_count(followed) +    end) + +    set_cache(follower) +  end +    def follow(%User{} = follower, %User{info: info} = followed) do      user_config = Application.get_env(:pleroma, :user)      deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked) @@ -300,18 +361,17 @@ defmodule Pleroma.User do            Websub.subscribe(follower, followed)          end -        following = -          [ap_followers | follower.following] -          |> Enum.uniq() +        q = +          from(u in User, +            where: u.id == ^follower.id, +            update: [push: [following: ^ap_followers]] +          ) -        follower = -          follower -          |> follow_changeset(%{following: following}) -          |> update_and_set_cache +        {1, [follower]} = Repo.update_all(q, [], returning: true)          {:ok, _} = update_follower_count(followed) -        follower +        set_cache(follower)      end    end @@ -319,17 +379,18 @@ defmodule Pleroma.User do      ap_followers = followed.follower_address      if following?(follower, followed) and follower.ap_id != followed.ap_id do -      following = -        follower.following -        |> List.delete(ap_followers) +      q = +        from(u in User, +          where: u.id == ^follower.id, +          update: [pull: [following: ^ap_followers]] +        ) -      {:ok, follower} = -        follower -        |> follow_changeset(%{following: following}) -        |> update_and_set_cache +      {1, [follower]} = Repo.update_all(q, [], returning: true)        {:ok, followed} = update_follower_count(followed) +      set_cache(follower) +        {:ok, follower, Utils.fetch_latest_follow(follower, followed)}      else        {:error, "Not subscribed!"} @@ -363,16 +424,33 @@ defmodule Pleroma.User do      user.info.locked || false    end +  def get_by_id(id) do +    Repo.get_by(User, id: id) +  end +    def get_by_ap_id(ap_id) do      Repo.get_by(User, ap_id: ap_id)    end +  # This is mostly an SPC migration fix. This guesses the user nickname (by taking the last part of the ap_id and the domain) and tries to get that user +  def get_by_guessed_nickname(ap_id) do +    domain = URI.parse(ap_id).host +    name = List.last(String.split(ap_id, "/")) +    nickname = "#{name}@#{domain}" + +    get_by_nickname(nickname) +  end + +  def set_cache(user) do +    Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user) +    Cachex.put(:user_cache, "nickname:#{user.nickname}", user) +    Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user)) +    {:ok, user} +  end +    def update_and_set_cache(changeset) do      with {:ok, user} <- Repo.update(changeset) do -      Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user) -      Cachex.put(:user_cache, "nickname:#{user.nickname}", user) -      Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user)) -      {:ok, user} +      set_cache(user)      else        e -> e      end @@ -389,16 +467,37 @@ defmodule Pleroma.User do      Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)    end +  def get_cached_by_id(id) do +    key = "id:#{id}" + +    ap_id = +      Cachex.fetch!(:user_cache, key, fn _ -> +        user = get_by_id(id) + +        if user do +          Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user) +          {:commit, user.ap_id} +        else +          {:ignore, ""} +        end +      end) + +    get_cached_by_ap_id(ap_id) +  end +    def get_cached_by_nickname(nickname) do      key = "nickname:#{nickname}"      Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)    end +  def get_cached_by_nickname_or_id(nickname_or_id) do +    get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id) +  end +    def get_by_nickname(nickname) do      Repo.get_by(User, nickname: nickname) ||        if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do -        [local_nickname, _] = String.split(nickname, "@") -        Repo.get_by(User, nickname: local_nickname) +        Repo.get_by(User, nickname: local_nickname(nickname))        end    end @@ -437,7 +536,7 @@ defmodule Pleroma.User do      end    end -  def get_followers_query(%User{id: id, follower_address: follower_address}) do +  def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do      from(        u in User,        where: fragment("? <@ ?", ^[follower_address], u.following), @@ -445,13 +544,29 @@ defmodule Pleroma.User do      )    end -  def get_followers(user) do -    q = get_followers_query(user) +  def get_followers_query(user, page) do +    from( +      u in get_followers_query(user, nil), +      limit: 20, +      offset: ^((page - 1) * 20) +    ) +  end + +  def get_followers_query(user), do: get_followers_query(user, nil) + +  def get_followers(user, page \\ nil) do +    q = get_followers_query(user, page)      {:ok, Repo.all(q)}    end -  def get_friends_query(%User{id: id, following: following}) do +  def get_followers_ids(user, page \\ nil) do +    q = get_followers_query(user, page) + +    Repo.all(from(u in q, select: u.id)) +  end + +  def get_friends_query(%User{id: id, following: following}, nil) do      from(        u in User,        where: u.follower_address in ^following, @@ -459,12 +574,28 @@ defmodule Pleroma.User do      )    end -  def get_friends(user) do -    q = get_friends_query(user) +  def get_friends_query(user, page) do +    from( +      u in get_friends_query(user, nil), +      limit: 20, +      offset: ^((page - 1) * 20) +    ) +  end + +  def get_friends_query(user), do: get_friends_query(user, nil) + +  def get_friends(user, page \\ nil) do +    q = get_friends_query(user, page)      {:ok, Repo.all(q)}    end +  def get_friends_ids(user, page \\ nil) do +    q = get_friends_query(user, page) + +    Repo.all(from(u in q, select: u.id)) +  end +    def get_follow_requests_query(%User{} = user) do      from(        a in Activity, @@ -596,37 +727,120 @@ defmodule Pleroma.User do      Repo.all(query)    end -  def search(query, resolve \\ false) do -    # strip the beginning @ off if there is a query +  def search(query, resolve \\ false, for_user \\ nil) do +    # Strip the beginning @ off if there is a query      query = String.trim_leading(query, "@") -    if resolve do -      User.get_or_fetch_by_nickname(query) -    end +    if resolve, do: User.get_or_fetch_by_nickname(query) -    inner = -      from( -        u in User, -        select_merge: %{ -          search_distance: -            fragment( -              "? <-> (? || ?)", -              ^query, -              u.nickname, -              u.name -            ) -        }, -        where: not is_nil(u.nickname) -      ) +    fts_results = do_search(fts_search_subquery(query), for_user) + +    {:ok, trigram_results} = +      Repo.transaction(fn -> +        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", []) +        do_search(trigram_search_subquery(query), for_user) +      end) + +    Enum.uniq_by(fts_results ++ trigram_results, & &1.id) +  end +  defp do_search(subquery, for_user, options \\ []) do      q =        from( -        s in subquery(inner), -        order_by: s.search_distance, -        limit: 20 +        s in subquery(subquery), +        order_by: [desc: s.search_rank], +        limit: ^(options[:limit] || 20)        ) -    Repo.all(q) +    results = +      q +      |> Repo.all() +      |> Enum.filter(&(&1.search_rank > 0)) + +    boost_search_results(results, for_user) +  end + +  defp fts_search_subquery(query) do +    processed_query = +      query +      |> String.replace(~r/\W+/, " ") +      |> String.trim() +      |> String.split() +      |> Enum.map(&(&1 <> ":*")) +      |> Enum.join(" | ") + +    from( +      u in User, +      select_merge: %{ +        search_rank: +          fragment( +            """ +            ts_rank_cd( +              setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') || +              setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'), +              to_tsquery('simple', ?), +              32 +            ) +            """, +            u.nickname, +            u.name, +            ^processed_query +          ) +      }, +      where: +        fragment( +          """ +            (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') || +            setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?) +          """, +          u.nickname, +          u.name, +          ^processed_query +        ) +    ) +  end + +  defp trigram_search_subquery(query) do +    from( +      u in User, +      select_merge: %{ +        search_rank: +          fragment( +            "similarity(?, trim(? || ' ' || coalesce(?, '')))", +            ^query, +            u.nickname, +            u.name +          ) +      }, +      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^query) +    ) +  end + +  defp boost_search_results(results, nil), do: results + +  defp boost_search_results(results, for_user) do +    friends_ids = get_friends_ids(for_user) +    followers_ids = get_followers_ids(for_user) + +    Enum.map( +      results, +      fn u -> +        search_rank_coef = +          cond do +            u.id in friends_ids -> +              1.2 + +            u.id in followers_ids -> +              1.1 + +            true -> +              1 +          end + +        Map.put(u, :search_rank, u.search_rank * search_rank_coef) +      end +    ) +    |> Enum.sort_by(&(-&1.search_rank))    end    def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do @@ -726,7 +940,7 @@ defmodule Pleroma.User do      update_and_set_cache(cng)    end -  def local_user_query() do +  def local_user_query do      from(        u in User,        where: u.local == true, @@ -734,7 +948,14 @@ defmodule Pleroma.User do      )    end -  def moderator_user_query() do +  def active_local_user_query do +    from( +      u in local_user_query(), +      where: fragment("not (?->'deactivated' @> 'true')", u.info) +    ) +  end + +  def moderator_user_query do      from(        u in User,        where: u.local == true, @@ -920,7 +1141,7 @@ defmodule Pleroma.User do        end)      bio -    |> CommonUtils.format_input(mentions, tags, "text/plain") +    |> CommonUtils.format_input(mentions, tags, "text/plain", user_links: [format: :full])      |> Formatter.emojify(emoji)    end @@ -957,6 +1178,22 @@ defmodule Pleroma.User do      updated_user    end +  def bookmark(%User{} = user, status_id) do +    bookmarks = Enum.uniq(user.bookmarks ++ [status_id]) +    update_bookmarks(user, bookmarks) +  end + +  def unbookmark(%User{} = user, status_id) do +    bookmarks = Enum.uniq(user.bookmarks -- [status_id]) +    update_bookmarks(user, bookmarks) +  end + +  def update_bookmarks(%User{} = user, bookmarks) do +    user +    |> change(%{bookmarks: bookmarks}) +    |> update_and_set_cache +  end +    defp normalize_tags(tags) do      [tags]      |> List.flatten() @@ -970,4 +1207,24 @@ defmodule Pleroma.User do        @strict_local_nickname_regex      end    end + +  def local_nickname(nickname_or_mention) do +    nickname_or_mention +    |> full_nickname() +    |> String.split("@") +    |> hd() +  end + +  def full_nickname(nickname_or_mention), +    do: String.trim_leading(nickname_or_mention, "@") + +  def error_user(ap_id) do +    %User{ +      name: ap_id, +      ap_id: ap_id, +      info: %User.Info{}, +      nickname: "erroruser@example.com", +      inserted_at: NaiveDateTime.utc_now() +    } +  end  end diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 2f419a5a2..9d8779fab 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -23,6 +23,7 @@ defmodule Pleroma.User.Info do      field(:ap_enabled, :boolean, default: false)      field(:is_moderator, :boolean, default: false)      field(:is_admin, :boolean, default: false) +    field(:show_role, :boolean, default: true)      field(:keys, :string, default: nil)      field(:settings, :map, default: nil)      field(:magic_key, :string, default: nil) @@ -30,7 +31,9 @@ defmodule Pleroma.User.Info do      field(:topic, :string, default: nil)      field(:hub, :string, default: nil)      field(:salmon, :string, default: nil) -    field(:hide_network, :boolean, default: false) +    field(:hide_followers, :boolean, default: false) +    field(:hide_follows, :boolean, default: false) +    field(:pinned_activities, {:array, :string}, default: [])      # Found in the wild      # ap_id -> Where is this used? @@ -41,8 +44,6 @@ defmodule Pleroma.User.Info do      # subject _> Where is this used?    end -  def superuser?(info), do: info.is_admin || info.is_moderator -    def set_activation_status(info, deactivated) do      params = %{deactivated: deactivated} @@ -144,8 +145,10 @@ defmodule Pleroma.User.Info do        :no_rich_text,        :default_scope,        :banner, -      :hide_network, -      :background +      :hide_follows, +      :hide_followers, +      :background, +      :show_role      ])    end @@ -195,7 +198,30 @@ defmodule Pleroma.User.Info do      info      |> cast(params, [        :is_moderator, -      :is_admin +      :is_admin, +      :show_role      ])    end + +  def add_pinnned_activity(info, %Pleroma.Activity{id: id}) do +    if id not in info.pinned_activities do +      max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0) +      params = %{pinned_activities: info.pinned_activities ++ [id]} + +      info +      |> cast(params, [:pinned_activities]) +      |> validate_length(:pinned_activities, +        max: max_pinned_statuses, +        message: "You have already pinned the maximum number of statuses" +      ) +    else +      change(info) +    end +  end + +  def remove_pinnned_activity(info, %Pleroma.Activity{id: id}) do +    params = %{pinned_activities: List.delete(info.pinned_activities, id)} + +    cast(info, params, [:pinned_activities]) +  end  end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 4d754de13..c46d8233e 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -3,13 +3,22 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.ActivityPub.ActivityPub do -  alias Pleroma.{Activity, Repo, Object, Upload, User, Notification} -  alias Pleroma.Web.ActivityPub.{Transmogrifier, MRF} +  alias Pleroma.Activity +  alias Pleroma.Repo +  alias Pleroma.Object +  alias Pleroma.Upload +  alias Pleroma.User +  alias Pleroma.Notification +  alias Pleroma.Instances +  alias Pleroma.Web.ActivityPub.Transmogrifier +  alias Pleroma.Web.ActivityPub.MRF    alias Pleroma.Web.WebFinger    alias Pleroma.Web.Federator    alias Pleroma.Web.OStatus +    import Ecto.Query    import Pleroma.Web.ActivityPub.Utils +    require Logger    @httpoison Application.get_env(:pleroma, :httpoison) @@ -19,20 +28,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp get_recipients(%{"type" => "Announce"} = data) do      to = data["to"] || []      cc = data["cc"] || [] -    recipients = to ++ cc      actor = User.get_cached_by_ap_id(data["actor"]) -    recipients -    |> Enum.filter(fn recipient -> -      case User.get_cached_by_ap_id(recipient) do -        nil -> -          true +    recipients = +      (to ++ cc) +      |> Enum.filter(fn recipient -> +        case User.get_cached_by_ap_id(recipient) do +          nil -> +            true + +          user -> +            User.following?(user, actor) +        end +      end) -        user -> -          User.following?(user, actor) -      end -    end) +    {recipients, to, cc} +  end +  defp get_recipients(%{"type" => "Create"} = data) do +    to = data["to"] || [] +    cc = data["cc"] || [] +    actor = data["actor"] || [] +    recipients = (to ++ cc ++ [actor]) |> Enum.uniq()      {recipients, to, cc}    end @@ -56,7 +73,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  defp check_remote_limit(%{"object" => %{"content" => content}}) do +  defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do      limit = Pleroma.Config.get([:instance, :remote_limit])      String.length(content) <= limit    end @@ -80,6 +97,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do            recipients: recipients          }) +      Task.start(fn -> +        Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) +      end) +        Notification.create_notifications(activity)        stream_out(activity)        {:ok, activity} @@ -92,7 +113,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    def stream_out(activity) do      public = "https://www.w3.org/ns/activitystreams#Public" -    if activity.data["type"] in ["Create", "Announce"] do +    if activity.data["type"] in ["Create", "Announce", "Delete"] do        Pleroma.Web.Streamer.stream("user", activity)        Pleroma.Web.Streamer.stream("list", activity) @@ -103,16 +124,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do            Pleroma.Web.Streamer.stream("public:local", activity)          end -        activity.data["object"] -        |> Map.get("tag", []) -        |> Enum.filter(fn tag -> is_bitstring(tag) end) -        |> Enum.map(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) +        if activity.data["type"] in ["Create"] do +          activity.data["object"] +          |> Map.get("tag", []) +          |> Enum.filter(fn tag -> is_bitstring(tag) end) +          |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) -        if activity.data["object"]["attachment"] != [] do -          Pleroma.Web.Streamer.stream("public:media", activity) +          if activity.data["object"]["attachment"] != [] do +            Pleroma.Web.Streamer.stream("public:media", activity) -          if activity.local do -            Pleroma.Web.Streamer.stream("public:local:media", activity) +            if activity.local do +              Pleroma.Web.Streamer.stream("public:local:media", activity) +            end            end          end        else @@ -138,8 +161,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do               additional             ),           {:ok, activity} <- insert(create_data, local), -         :ok <- maybe_federate(activity), -         {:ok, _actor} <- User.increase_note_count(actor) do +         # Changing note count prior to enqueuing federation task in order to avoid race conditions on updating user.info +         {:ok, _actor} <- User.increase_note_count(actor), +         :ok <- maybe_federate(activity) do        {:ok, activity}      end    end @@ -224,10 +248,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do          %User{ap_id: _} = user,          %Object{data: %{"id" => _}} = object,          activity_id \\ nil, -        local \\ true +        local \\ true, +        public \\ true        ) do      with true <- is_public?(object), -         announce_data <- make_announce_data(user, object, activity_id), +         announce_data <- make_announce_data(user, object, activity_id, public),           {:ok, activity} <- insert(announce_data, local),           {:ok, object} <- add_announce_to_object(activity, object),           :ok <- maybe_federate(activity) do @@ -285,8 +310,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      with {:ok, _} <- Object.delete(object),           {:ok, activity} <- insert(data, local), -         :ok <- maybe_federate(activity), -         {:ok, _actor} <- User.decrease_note_count(user) do +         # Changing note count prior to enqueuing federation task in order to avoid race conditions on updating user.info +         {:ok, _actor} <- User.decrease_note_count(user), +         :ok <- maybe_federate(activity) do        {:ok, activity}      end    end @@ -364,21 +390,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    @valid_visibilities ~w[direct unlisted public private] -  defp restrict_visibility(query, %{visibility: "direct"}) do -    public = "https://www.w3.org/ns/activitystreams#Public" +  defp restrict_visibility(query, %{visibility: visibility}) +       when visibility in @valid_visibilities do +    query = +      from( +        a in query, +        where: +          fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility) +      ) -    from( -      activity in query, -      join: sender in User, -      on: sender.ap_id == activity.actor, -      # Are non-direct statuses with no to/cc possible? -      where: -        fragment( -          "not (? && ?)", -          [^public, sender.follower_address], -          activity.recipients -        ) -    ) +    Ecto.Adapters.SQL.to_sql(:all, Repo, query) + +    query    end    defp restrict_visibility(_query, %{visibility: visibility}) @@ -394,6 +417,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        |> Map.put("type", ["Create", "Announce"])        |> Map.put("actor_id", user.ap_id)        |> Map.put("whole_db", true) +      |> Map.put("pinned_activity_ids", user.info.pinned_activities)      recipients =        if reading_user do @@ -407,13 +431,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> Enum.reverse()    end +  defp restrict_since(query, %{"since_id" => ""}), do: query +    defp restrict_since(query, %{"since_id" => since_id}) do      from(activity in query, where: activity.id > ^since_id)    end    defp restrict_since(query, _), do: query -  defp restrict_tag(query, %{"tag" => tag}) do +  defp restrict_tag_reject(query, %{"tag_reject" => tag_reject}) +       when is_list(tag_reject) and tag_reject != [] do +    from( +      activity in query, +      where: fragment("(not (? #> '{\"object\",\"tag\"}') \\?| ?)", activity.data, ^tag_reject) +    ) +  end + +  defp restrict_tag_reject(query, _), do: query + +  defp restrict_tag_all(query, %{"tag_all" => tag_all}) +       when is_list(tag_all) and tag_all != [] do +    from( +      activity in query, +      where: fragment("(? #> '{\"object\",\"tag\"}') \\?& ?", activity.data, ^tag_all) +    ) +  end + +  defp restrict_tag_all(query, _), do: query + +  defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do +    from( +      activity in query, +      where: fragment("(? #> '{\"object\",\"tag\"}') \\?| ?", activity.data, ^tag) +    ) +  end + +  defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do      from(        activity in query,        where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data) @@ -462,6 +515,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_local(query, _), do: query +  defp restrict_max(query, %{"max_id" => ""}), do: query +    defp restrict_max(query, %{"max_id" => max_id}) do      from(activity in query, where: activity.id < ^max_id)    end @@ -475,7 +530,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_actor(query, _), do: query    defp restrict_type(query, %{"type" => type}) when is_binary(type) do -    restrict_type(query, %{"type" => [type]}) +    from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))    end    defp restrict_type(query, %{"type" => type}) do @@ -517,15 +572,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_reblogs(query, _), do: query -  # Only search through last 100_000 activities by default -  defp restrict_recent(query, %{"whole_db" => true}), do: query - -  defp restrict_recent(query, _) do -    since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_000 - -    from(activity in query, where: activity.id > ^since) -  end -    defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do      blocks = info.blocks || []      domain_blocks = info.domain_blocks || [] @@ -552,6 +598,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      )    end +  defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do +    from(activity in query, where: activity.id in ^ids) +  end + +  defp restrict_pinned(query, _), do: query +    def fetch_activities_query(recipients, opts \\ %{}) do      base_query =        from( @@ -563,6 +615,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      base_query      |> restrict_recipients(recipients, opts["user"])      |> restrict_tag(opts) +    |> restrict_tag_reject(opts) +    |> restrict_tag_all(opts)      |> restrict_since(opts)      |> restrict_local(opts)      |> restrict_limit(opts) @@ -570,12 +624,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> restrict_actor(opts)      |> restrict_type(opts)      |> restrict_favorited_by(opts) -    |> restrict_recent(opts)      |> restrict_blocked(opts)      |> restrict_media(opts)      |> restrict_visibility(opts)      |> restrict_replies(opts)      |> restrict_reblogs(opts) +    |> restrict_pinned(opts)    end    def fetch_activities(recipients, opts \\ %{}) do @@ -689,7 +743,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    end    def publish(actor, activity) do -    followers = +    remote_followers =        if actor.follower_address in activity.recipients do          {:ok, followers} = User.get_followers(actor)          followers |> Enum.filter(&(!&1.local)) @@ -699,29 +753,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      public = is_public?(activity) -    remote_inboxes = -      (Pleroma.Web.Salmon.remote_users(activity) ++ followers) +    reachable_inboxes_metadata = +      (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)        |> Enum.filter(fn user -> User.ap_enabled?(user) end)        |> Enum.map(fn %{info: %{source_data: data}} ->          (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]        end)        |> Enum.uniq()        |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) +      |> Instances.filter_reachable()      {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)      json = Jason.encode!(data) -    Enum.each(remote_inboxes, fn inbox -> +    Enum.each(reachable_inboxes_metadata, fn {inbox, unreachable_since} ->        Federator.enqueue(:publish_single_ap, %{          inbox: inbox,          json: json,          actor: actor, -        id: activity.data["id"] +        id: activity.data["id"], +        unreachable_since: unreachable_since        })      end)    end -  def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do +  def publish_one(%{inbox: inbox, json: json, actor: actor, id: id} = params) do      Logger.info("Federating #{id} to #{inbox}")      host = URI.parse(inbox).host @@ -734,15 +790,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do          digest: digest        }) -    @httpoison.post( -      inbox, -      json, -      [ -        {"Content-Type", "application/activity+json"}, -        {"signature", signature}, -        {"digest", digest} -      ] -    ) +    with {:ok, %{status: code}} when code in 200..299 <- +           result = +             @httpoison.post( +               inbox, +               json, +               [ +                 {"Content-Type", "application/activity+json"}, +                 {"signature", signature}, +                 {"digest", digest} +               ] +             ) do +      if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], +        do: Instances.set_reachable(inbox) + +      result +    else +      {_post_result, response} -> +        unless params[:unreachable_since], do: Instances.set_unreachable(inbox) +        {:error, response} +    end    end    # TODO: @@ -801,9 +868,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  def is_public?(activity) do -    "https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++ -                                                         (activity.data["cc"] || [])) +  def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false +  def is_public?(%Object{data: data}), do: is_public?(data) +  def is_public?(%Activity{data: data}), do: is_public?(data) +  def is_public?(%{"directMessage" => true}), do: false + +  def is_public?(data) do +    "https://www.w3.org/ns/activitystreams#Public" in (data["to"] ++ (data["cc"] || [])) +  end + +  def is_private?(activity) do +    !is_public?(activity) && Enum.any?(activity.data["to"], &String.contains?(&1, "/followers")) +  end + +  def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true +  def is_direct?(%Object{data: %{"directMessage" => true}}), do: true + +  def is_direct?(activity) do +    !is_public?(activity) && !is_private?(activity)    end    def visible_for_user?(activity, nil) do diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index fc7972eaf..69879476e 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -4,12 +4,16 @@  defmodule Pleroma.Web.ActivityPub.ActivityPubController do    use Pleroma.Web, :controller -  alias Pleroma.{Activity, User, Object} -  alias Pleroma.Web.ActivityPub.{ObjectView, UserView} + +  alias Pleroma.Activity +  alias Pleroma.User +  alias Pleroma.Object +  alias Pleroma.Web.ActivityPub.ObjectView +  alias Pleroma.Web.ActivityPub.UserView    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Relay -  alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.ActivityPub.Transmogrifier +  alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.Federator    require Logger @@ -17,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    action_fallback(:errors)    plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay]) +  plug(:set_requester_reachable when action in [:inbox])    plug(:relay_active? when action in [:relay])    def relay_active?(conn, _) do @@ -54,6 +59,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do      end    end +  def object_likes(conn, %{"uuid" => uuid, "page" => page}) do +    with ap_id <- o_status_url(conn, :object, uuid), +         %Object{} = object <- Object.get_cached_by_ap_id(ap_id), +         {_, true} <- {:public?, ActivityPub.is_public?(object)}, +         likes <- Utils.get_object_likes(object) do +      {page, _} = Integer.parse(page) + +      conn +      |> put_resp_header("content-type", "application/activity+json") +      |> json(ObjectView.render("likes.json", ap_id, likes, page)) +    else +      {:public?, false} -> +        {:error, :not_found} +    end +  end + +  def object_likes(conn, %{"uuid" => uuid}) do +    with ap_id <- o_status_url(conn, :object, uuid), +         %Object{} = object <- Object.get_cached_by_ap_id(ap_id), +         {_, true} <- {:public?, ActivityPub.is_public?(object)}, +         likes <- Utils.get_object_likes(object) do +      conn +      |> put_resp_header("content-type", "application/activity+json") +      |> json(ObjectView.render("likes.json", ap_id, likes)) +    else +      {:public?, false} -> +        {:error, :not_found} +    end +  end + +  def activity(conn, %{"uuid" => uuid}) do +    with ap_id <- o_status_url(conn, :activity, uuid), +         %Activity{} = activity <- Activity.normalize(ap_id), +         {_, true} <- {:public?, ActivityPub.is_public?(activity)} do +      conn +      |> put_resp_header("content-type", "application/activity+json") +      |> json(ObjectView.render("object.json", %{object: activity})) +    else +      {:public?, false} -> +        {:error, :not_found} +    end +  end +    def following(conn, %{"nickname" => nickname, "page" => page}) do      with %User{} = user <- User.get_cached_by_nickname(nickname),           {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do @@ -153,6 +201,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do      end    end +  def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do +    conn +    |> put_resp_header("content-type", "application/activity+json") +    |> json(UserView.render("user.json", %{user: user})) +  end + +  def whoami(_conn, _params), do: {:error, :not_found} +    def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do      if nickname == user.nickname do        conn @@ -165,9 +221,48 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do      end    end +  def handle_user_activity(user, %{"type" => "Create"} = params) do +    object = +      params["object"] +      |> Map.merge(Map.take(params, ["to", "cc"])) +      |> Map.put("attributedTo", user.ap_id()) +      |> Transmogrifier.fix_object() + +    ActivityPub.create(%{ +      to: params["to"], +      actor: user, +      context: object["context"], +      object: object, +      additional: Map.take(params, ["cc"]) +    }) +  end + +  def handle_user_activity(user, %{"type" => "Delete"} = params) do +    with %Object{} = object <- Object.normalize(params["object"]), +         true <- user.info.is_moderator || user.ap_id == object.data["actor"], +         {:ok, delete} <- ActivityPub.delete(object) do +      {:ok, delete} +    else +      _ -> {:error, "Can't delete object"} +    end +  end + +  def handle_user_activity(user, %{"type" => "Like"} = params) do +    with %Object{} = object <- Object.normalize(params["object"]), +         {:ok, activity, _object} <- ActivityPub.like(user, object) do +      {:ok, activity} +    else +      _ -> {:error, "Can't like object"} +    end +  end + +  def handle_user_activity(_, _) do +    {:error, "Unhandled activity type"} +  end +    def update_outbox(          %{assigns: %{user: user}} = conn, -        %{"nickname" => nickname, "type" => "Create"} = params +        %{"nickname" => nickname} = params        ) do      if nickname == user.nickname do        actor = user.ap_id() @@ -178,24 +273,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do          |> Map.put("actor", actor)          |> Transmogrifier.fix_addressing() -      object = -        params["object"] -        |> Map.merge(Map.take(params, ["to", "cc"])) -        |> Map.put("attributedTo", actor) -        |> Transmogrifier.fix_object() - -      with {:ok, %Activity{} = activity} <- -             ActivityPub.create(%{ -               to: params["to"], -               actor: user, -               context: object["context"], -               object: object, -               additional: Map.take(params, ["cc"]) -             }) do +      with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do          conn          |> put_status(:created)          |> put_resp_header("location", activity.data["id"])          |> json(activity.data) +      else +        {:error, message} -> +          conn +          |> put_status(:bad_request) +          |> json(message)        end      else        conn @@ -215,4 +302,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do      |> put_status(500)      |> json("error")    end + +  defp set_requester_reachable(%Plug.Conn{} = conn, _) do +    with actor <- conn.params["actor"], +         true <- is_binary(actor) do +      Pleroma.Instances.set_reachable(actor) +    end + +    conn +  end  end diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex new file mode 100644 index 000000000..7c6ad582a --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -0,0 +1,57 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do +  alias Pleroma.User + +  @behaviour Pleroma.Web.ActivityPub.MRF + +  # XXX: this should become User.normalize_by_ap_id() or similar, really. +  defp normalize_by_ap_id(%{"id" => id}), do: User.get_cached_by_ap_id(id) +  defp normalize_by_ap_id(uri) when is_binary(uri), do: User.get_cached_by_ap_id(uri) +  defp normalize_by_ap_id(_), do: nil + +  defp score_nickname("followbot@" <> _), do: 1.0 +  defp score_nickname("federationbot@" <> _), do: 1.0 +  defp score_nickname("federation_bot@" <> _), do: 1.0 +  defp score_nickname(_), do: 0.0 + +  defp score_displayname("federation bot"), do: 1.0 +  defp score_displayname("federationbot"), do: 1.0 +  defp score_displayname("fedibot"), do: 1.0 +  defp score_displayname(_), do: 0.0 + +  defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do +    nick_score = +      nickname +      |> String.downcase() +      |> score_nickname() + +    name_score = +      displayname +      |> String.downcase() +      |> score_displayname() + +    nick_score + name_score +  end + +  defp determine_if_followbot(_), do: 0.0 + +  @impl true +  def filter(%{"type" => "Follow", "actor" => actor_id} = message) do +    %User{} = actor = normalize_by_ap_id(actor_id) + +    score = determine_if_followbot(actor) + +    # TODO: scan biography data for keywords and score it somehow. +    if score < 0.8 do +      {:ok, message} +    else +      {:reject, nil} +    end +  end + +  @impl true +  def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex index a3f516ae7..4c6e612b2 100644 --- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex @@ -3,20 +3,46 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do +  alias Pleroma.User    @behaviour Pleroma.Web.ActivityPub.MRF +  defp delist_message(message) do +    follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address + +    message +    |> Map.put("to", [follower_collection]) +    |> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"]) +  end +    @impl true -  def filter(%{"type" => "Create"} = object) do -    threshold = Pleroma.Config.get([:mrf_hellthread, :threshold]) -    recipients = (object["to"] || []) ++ (object["cc"] || []) - -    if length(recipients) > threshold do -      {:reject, nil} -    else -      {:ok, object} +  def filter(%{"type" => "Create"} = message) do +    delist_threshold = Pleroma.Config.get([:mrf_hellthread, :delist_threshold]) + +    reject_threshold = +      Pleroma.Config.get( +        [:mrf_hellthread, :reject_threshold], +        Pleroma.Config.get([:mrf_hellthread, :threshold]) +      ) + +    recipients = (message["to"] || []) ++ (message["cc"] || []) + +    cond do +      length(recipients) > reject_threshold and reject_threshold > 0 -> +        {:reject, nil} + +      length(recipients) > delist_threshold and delist_threshold > 0 -> +        if Enum.member?(message["to"], "https://www.w3.org/ns/activitystreams#Public") or +             Enum.member?(message["cc"], "https://www.w3.org/ns/activitystreams#Public") do +          {:ok, delist_message(message)} +        else +          {:ok, message} +        end + +      true -> +        {:ok, message}      end    end    @impl true -  def filter(object), do: {:ok, object} +  def filter(message), do: {:ok, message}  end diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex new file mode 100644 index 000000000..5fdc03414 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -0,0 +1,81 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do +  @behaviour Pleroma.Web.ActivityPub.MRF +  defp string_matches?(string, pattern) when is_binary(pattern) do +    String.contains?(string, pattern) +  end + +  defp string_matches?(string, pattern) do +    String.match?(string, pattern) +  end + +  defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = message) do +    if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern -> +         string_matches?(content, pattern) or string_matches?(summary, pattern) +       end) do +      {:reject, nil} +    else +      {:ok, message} +    end +  end + +  defp check_ftl_removal( +         %{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message +       ) do +    if "https://www.w3.org/ns/activitystreams#Public" in to and +         Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern -> +           string_matches?(content, pattern) or string_matches?(summary, pattern) +         end) do +      to = List.delete(to, "https://www.w3.org/ns/activitystreams#Public") +      cc = ["https://www.w3.org/ns/activitystreams#Public" | message["cc"] || []] + +      message = +        message +        |> Map.put("to", to) +        |> Map.put("cc", cc) + +      {:ok, message} +    else +      {:ok, message} +    end +  end + +  defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do +    {content, summary} = +      Enum.reduce(Pleroma.Config.get([:mrf_keyword, :replace]), {content, summary}, fn {pattern, +                                                                                        replacement}, +                                                                                       {content_acc, +                                                                                        summary_acc} -> +        {String.replace(content_acc, pattern, replacement), +         String.replace(summary_acc, pattern, replacement)} +      end) + +    {:ok, +     message +     |> put_in(["object", "content"], content) +     |> put_in(["object", "summary"], summary)} +  end + +  @impl true +  def filter(%{"object" => %{"content" => nil}} = message) do +    {:ok, message} +  end + +  @impl true +  def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do +    with {:ok, message} <- check_reject(message), +         {:ok, message} <- check_ftl_removal(message), +         {:ok, message} <- check_replace(message) do +      {:ok, message} +    else +      _e -> +        {:reject, nil} +    end +  end + +  @impl true +  def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex new file mode 100644 index 000000000..081456046 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do +  @behaviour Pleroma.Web.ActivityPub.MRF + +  @impl true +  def filter( +        %{ +          "type" => "Create", +          "object" => %{"content" => content, "attachment" => _attachment} = child_object +        } = object +      ) +      when content in [".", "<p>.</p>"] do +    child_object = +      child_object +      |> Map.put("content", "") + +    object = +      object +      |> Map.put("object", child_object) + +    {:ok, object} +  end + +  @impl true +  def filter(object), do: {:ok, object} +end diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex new file mode 100644 index 000000000..b242e44e6 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -0,0 +1,139 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do +  alias Pleroma.User +  @behaviour Pleroma.Web.ActivityPub.MRF + +  defp get_tags(%User{tags: tags}) when is_list(tags), do: tags +  defp get_tags(_), do: [] + +  defp process_tag( +         "mrf_tag:media-force-nsfw", +         %{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message +       ) +       when length(child_attachment) > 0 do +    tags = (object["tag"] || []) ++ ["nsfw"] + +    object = +      object +      |> Map.put("tags", tags) +      |> Map.put("sensitive", true) + +    message = Map.put(message, "object", object) + +    {:ok, message} +  end + +  defp process_tag( +         "mrf_tag:media-strip", +         %{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message +       ) +       when length(child_attachment) > 0 do +    object = Map.delete(object, "attachment") +    message = Map.put(message, "object", object) + +    {:ok, message} +  end + +  defp process_tag( +         "mrf_tag:force-unlisted", +         %{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message +       ) do +    user = User.get_cached_by_ap_id(actor) + +    if Enum.member?(to, "https://www.w3.org/ns/activitystreams#Public") do +      to = +        List.delete(to, "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address] + +      cc = +        List.delete(cc, user.follower_address) ++ ["https://www.w3.org/ns/activitystreams#Public"] + +      object = +        message["object"] +        |> Map.put("to", to) +        |> Map.put("cc", cc) + +      message = +        message +        |> Map.put("to", to) +        |> Map.put("cc", cc) +        |> Map.put("object", object) + +      {:ok, message} +    else +      {:ok, message} +    end +  end + +  defp process_tag( +         "mrf_tag:sandbox", +         %{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message +       ) do +    user = User.get_cached_by_ap_id(actor) + +    if Enum.member?(to, "https://www.w3.org/ns/activitystreams#Public") or +         Enum.member?(cc, "https://www.w3.org/ns/activitystreams#Public") do +      to = +        List.delete(to, "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address] + +      cc = List.delete(cc, "https://www.w3.org/ns/activitystreams#Public") + +      object = +        message["object"] +        |> Map.put("to", to) +        |> Map.put("cc", cc) + +      message = +        message +        |> Map.put("to", to) +        |> Map.put("cc", cc) +        |> Map.put("object", object) + +      {:ok, message} +    else +      {:ok, message} +    end +  end + +  defp process_tag( +         "mrf_tag:disable-remote-subscription", +         %{"type" => "Follow", "actor" => actor} = message +       ) do +    user = User.get_cached_by_ap_id(actor) + +    if user.local == true do +      {:ok, message} +    else +      {:reject, nil} +    end +  end + +  defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), do: {:reject, nil} + +  defp process_tag(_, message), do: {:ok, message} + +  def filter_message(actor, message) do +    User.get_cached_by_ap_id(actor) +    |> get_tags() +    |> Enum.reduce({:ok, message}, fn +      tag, {:ok, message} -> +        process_tag(tag, message) + +      _, error -> +        error +    end) +  end + +  @impl true +  def filter(%{"object" => target_actor, "type" => "Follow"} = message), +    do: filter_message(target_actor, message) + +  @impl true +  def filter(%{"actor" => actor, "type" => "Create"} = message), +    do: filter_message(actor, message) + +  @impl true +  def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index abddbc790..c496063ea 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -3,7 +3,9 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.ActivityPub.Relay do -  alias Pleroma.{User, Object, Activity} +  alias Pleroma.User +  alias Pleroma.Object +  alias Pleroma.Activity    alias Pleroma.Web.ActivityPub.ActivityPub    require Logger @@ -40,7 +42,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do    def publish(%Activity{data: %{"type" => "Create"}} = activity) do      with %User{} = user <- get_actor(),           %Object{} = object <- Object.normalize(activity.data["object"]["id"]) do -      ActivityPub.announce(user, object) +      ActivityPub.announce(user, object, nil, true, false)      else        e -> Logger.error("error: #{inspect(e)}")      end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 87b7fc07f..98a2af819 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -6,9 +6,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    @moduledoc """    A module to handle coding from internal to wire ActivityPub and back.    """ +  alias Pleroma.Activity    alias Pleroma.User    alias Pleroma.Object -  alias Pleroma.Activity    alias Pleroma.Repo    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Utils @@ -93,12 +93,47 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      end    end -  def fix_addressing(map) do -    map +  def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do +    explicit_to = +      to +      |> Enum.filter(fn x -> x in explicit_mentions end) + +    explicit_cc = +      to +      |> Enum.filter(fn x -> x not in explicit_mentions end) + +    final_cc = +      (cc ++ explicit_cc) +      |> Enum.uniq() + +    object +    |> Map.put("to", explicit_to) +    |> Map.put("cc", final_cc) +  end + +  def fix_explicit_addressing(object, _explicit_mentions), do: object + +  # if directMessage flag is set to true, leave the addressing alone +  def fix_explicit_addressing(%{"directMessage" => true} = object), do: object + +  def fix_explicit_addressing(object) do +    explicit_mentions = +      object +      |> Utils.determine_explicit_mentions() + +    explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"] + +    object +    |> fix_explicit_addressing(explicit_mentions) +  end + +  def fix_addressing(object) do +    object      |> fix_addressing_list("to")      |> fix_addressing_list("cc")      |> fix_addressing_list("bto")      |> fix_addressing_list("bcc") +    |> fix_explicit_addressing    end    def fix_actor(%{"attributedTo" => actor} = object) do @@ -106,11 +141,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> Map.put("actor", get_actor(%{"actor" => actor}))    end -  def fix_likes(%{"likes" => likes} = object) -      when is_bitstring(likes) do -    # Check for standardisation -    # This is what Peertube does -    # curl -H 'Accept: application/activity+json' $likes | jq .totalItems +  # Check for standardisation +  # This is what Peertube does +  # curl -H 'Accept: application/activity+json' $likes | jq .totalItems +  # Prismo returns only an integer (count) as "likes" +  def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do      object      |> Map.put("likes", [])      |> Map.put("like_count", 0) @@ -141,7 +176,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      case fetch_obj_helper(in_reply_to_id) do        {:ok, replied_object} ->          with %Activity{} = activity <- -               Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) do +               Activity.get_create_by_object_ap_id(replied_object.data["id"]) do            object            |> Map.put("inReplyTo", replied_object.data["id"])            |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) @@ -278,6 +313,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> Map.put("tag", combined)    end +  def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag]) +    def fix_tag(object), do: object    # content map usually only has one language so this will do for now. @@ -334,7 +371,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do        Map.put(data, "actor", actor)        |> fix_addressing -    with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]), +    with nil <- Activity.get_create_by_object_ap_id(object["id"]),           %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do        object = fix_object(data["object"]) @@ -348,6 +385,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          additional:            Map.take(data, [              "cc", +            "directMessage",              "id"            ])        } @@ -417,9 +455,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do           {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),           %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),           {:ok, activity} <- -           ActivityPub.accept(%{ +           ActivityPub.reject(%{               to: follow_activity.data["to"], -             type: "Accept", +             type: "Reject",               actor: followed.ap_id,               object: follow_activity.data["id"],               local: false @@ -451,7 +489,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      with actor <- get_actor(data),           %User{} = actor <- User.get_or_fetch_by_ap_id(actor),           {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id), -         {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do +         public <- ActivityPub.is_public?(data), +         {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do        {:ok, activity}      else        _e -> :error @@ -629,6 +668,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> add_mention_tags      |> add_emoji_tags      |> add_attributed_to +    |> add_likes      |> prepare_attachments      |> set_conversation      |> set_reply_to_uri @@ -641,7 +681,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    #  internal -> Mastodon    #  """ -  def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do +  def prepare_outgoing(%{"type" => "Create", "object" => object} = data) do      object =        object        |> prepare_object @@ -788,6 +828,22 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> Map.put("attributedTo", attributedTo)    end +  def add_likes(%{"id" => id, "like_count" => likes} = object) do +    likes = %{ +      "id" => "#{id}/likes", +      "first" => "#{id}/likes?page=1", +      "type" => "OrderedCollection", +      "totalItems" => likes +    } + +    object +    |> Map.put("likes", likes) +  end + +  def add_likes(object) do +    object +  end +    def prepare_attachments(object) do      attachments =        (object["attachment"] || []) @@ -803,7 +859,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    defp strip_internal_fields(object) do      object      |> Map.drop([ -      "likes",        "like_count",        "announcements",        "announcement_count", @@ -847,15 +902,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      maybe_retire_websub(user.ap_id) -    # Only do this for recent activties, don't go through the whole db. -    # Only look at the last 1000 activities. -    since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000 -      q =        from(          a in Activity,          where: ^old_follower_address in a.recipients, -        where: a.id > ^since,          update: [            set: [              recipients: diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index b313996db..964e11c9d 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -3,11 +3,19 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.ActivityPub.Utils do -  alias Pleroma.{Repo, Web, Object, Activity, User, Notification} +  alias Pleroma.Repo +  alias Pleroma.Web +  alias Pleroma.Object +  alias Pleroma.Activity +  alias Pleroma.User +  alias Pleroma.Notification    alias Pleroma.Web.Router.Helpers    alias Pleroma.Web.Endpoint -  alias Ecto.{Changeset, UUID} +  alias Ecto.Changeset +  alias Ecto.UUID +    import Ecto.Query +    require Logger    @supported_object_types ["Article", "Note", "Video", "Page"] @@ -25,6 +33,20 @@ defmodule Pleroma.Web.ActivityPub.Utils do      Map.put(params, "actor", get_ap_id(params["actor"]))    end +  def determine_explicit_mentions(%{"tag" => tag} = _object) when is_list(tag) do +    tag +    |> Enum.filter(fn x -> is_map(x) end) +    |> Enum.filter(fn x -> x["type"] == "Mention" end) +    |> Enum.map(fn x -> x["href"] end) +  end + +  def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do +    Map.put(object, "tag", [tag]) +    |> determine_explicit_mentions() +  end + +  def determine_explicit_mentions(_), do: [] +    defp recipient_in_collection(ap_id, coll) when is_binary(coll), do: ap_id == coll    defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll    defp recipient_in_collection(_, _), do: false @@ -198,7 +220,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do      # Update activities that already had this. Could be done in a seperate process.      # Alternatively, just don't do this and fetch the current object each time. Most      # could probably be taken from cache. -    relevant_activities = Activity.all_by_object_ap_id(id) +    relevant_activities = Activity.get_all_create_by_object_ap_id(id)      Enum.map(relevant_activities, fn activity ->        new_activity_data = activity.data |> Map.put("object", object.data) @@ -231,6 +253,27 @@ defmodule Pleroma.Web.ActivityPub.Utils do      Repo.one(query)    end +  @doc """ +  Returns like activities targeting an object +  """ +  def get_object_likes(%{data: %{"id" => id}}) do +    query = +      from( +        activity in Activity, +        # this is to use the index +        where: +          fragment( +            "coalesce((?)->'object'->>'id', (?)->>'object') = ?", +            activity.data, +            activity.data, +            ^id +          ), +        where: fragment("(?)->>'type' = 'Like'", activity.data) +      ) + +    Repo.all(query) +  end +    def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object, activity_id) do      data = %{        "type" => "Like", @@ -250,7 +293,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do             |> Map.put("#{property}_count", length(element))             |> Map.put("#{property}s", element),           changeset <- Changeset.change(object, data: new_data), -         {:ok, object} <- Repo.update(changeset), +         {:ok, object} <- Object.update_and_set_cache(changeset),           _ <- update_object_in_activities(object) do        {:ok, object}      end @@ -281,6 +324,25 @@ defmodule Pleroma.Web.ActivityPub.Utils do    @doc """    Updates a follow activity's state (for locked accounts).    """ +  def update_follow_state( +        %Activity{data: %{"actor" => actor, "object" => object, "state" => "pending"}} = activity, +        state +      ) do +    try do +      Ecto.Adapters.SQL.query!( +        Repo, +        "UPDATE activities SET data = jsonb_set(data, '{state}', $1) WHERE data->>'type' = 'Follow' AND data->>'actor' = $2 AND data->>'object' = $3 AND data->>'state' = 'pending'", +        [state, actor, object] +      ) + +      activity = Repo.get(Activity, activity.id) +      {:ok, activity} +    rescue +      e -> +        {:error, e} +    end +  end +    def update_follow_state(%Activity{} = activity, state) do      with new_data <-             activity.data @@ -365,9 +427,10 @@ defmodule Pleroma.Web.ActivityPub.Utils do    """    # for relayed messages, we only want to send to subscribers    def make_announce_data( -        %User{ap_id: ap_id, nickname: nil} = user, +        %User{ap_id: ap_id} = user,          %Object{data: %{"id" => id}} = object, -        activity_id +        activity_id, +        false        ) do      data = %{        "type" => "Announce", @@ -384,7 +447,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do    def make_announce_data(          %User{ap_id: ap_id} = user,          %Object{data: %{"id" => id}} = object, -        activity_id +        activity_id, +        true        ) do      data = %{        "type" => "Announce", diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex index b5c9bf8d0..84fa94e32 100644 --- a/lib/pleroma/web/activity_pub/views/object_view.ex +++ b/lib/pleroma/web/activity_pub/views/object_view.ex @@ -4,7 +4,8 @@  defmodule Pleroma.Web.ActivityPub.ObjectView do    use Pleroma.Web, :view -  alias Pleroma.{Object, Activity} +  alias Pleroma.Activity +  alias Pleroma.Object    alias Pleroma.Web.ActivityPub.Transmogrifier    def render("object.json", %{object: %Object{} = object}) do @@ -35,4 +36,38 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do      Map.merge(base, additional)    end + +  def render("likes.json", ap_id, likes, page) do +    collection(likes, "#{ap_id}/likes", page) +    |> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header()) +  end + +  def render("likes.json", ap_id, likes) do +    %{ +      "id" => "#{ap_id}/likes", +      "type" => "OrderedCollection", +      "totalItems" => length(likes), +      "first" => collection(likes, "#{ap_id}/likes", 1) +    } +    |> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header()) +  end + +  def collection(collection, iri, page) do +    offset = (page - 1) * 10 +    items = Enum.slice(collection, offset, 10) +    items = Enum.map(items, fn object -> Transmogrifier.prepare_object(object.data) end) +    total = length(collection) + +    map = %{ +      "id" => "#{iri}?page=#{page}", +      "type" => "OrderedCollectionPage", +      "partOf" => iri, +      "totalItems" => total, +      "orderedItems" => items +    } + +    if offset < total do +      Map.put(map, "next", "#{iri}?page=#{page + 1}") +    end +  end  end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index fe8248107..c8e154989 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -4,15 +4,34 @@  defmodule Pleroma.Web.ActivityPub.UserView do    use Pleroma.Web, :view -  alias Pleroma.Web.Salmon +    alias Pleroma.Web.WebFinger +  alias Pleroma.Web.Salmon    alias Pleroma.User    alias Pleroma.Repo    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Transmogrifier    alias Pleroma.Web.ActivityPub.Utils +  alias Pleroma.Web.Router.Helpers +  alias Pleroma.Web.Endpoint +    import Ecto.Query +  def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do +    %{"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)} +  end + +  def render("endpoints.json", %{user: %User{local: true} = _user}) do +    %{ +      "oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize), +      "oauthRegistrationEndpoint" => Helpers.mastodon_api_url(Endpoint, :create_app), +      "oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange), +      "sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox) +    } +  end + +  def render("endpoints.json", _), do: %{} +    # the instance itself is not a Person, but instead an Application    def render("user.json", %{user: %{nickname: nil} = user}) do      {:ok, user} = WebFinger.ensure_keys_present(user) @@ -20,6 +39,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do      public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)      public_key = :public_key.pem_encode([public_key]) +    endpoints = render("endpoints.json", %{user: user}) +      %{        "id" => user.ap_id,        "type" => "Application", @@ -35,9 +56,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do          "owner" => user.ap_id,          "publicKeyPem" => public_key        }, -      "endpoints" => %{ -        "sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox" -      } +      "endpoints" => endpoints      }      |> Map.merge(Utils.make_json_ld_header())    end @@ -48,6 +67,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do      public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)      public_key = :public_key.pem_encode([public_key]) +    endpoints = render("endpoints.json", %{user: user}) +      %{        "id" => user.ap_id,        "type" => "Person", @@ -65,9 +86,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do          "owner" => user.ap_id,          "publicKeyPem" => public_key        }, -      "endpoints" => %{ -        "sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox" -      }, +      "endpoints" => endpoints,        "icon" => %{          "type" => "Image",          "url" => User.avatar_url(user) @@ -86,7 +105,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do      query = from(user in query, select: [:ap_id])      following = Repo.all(query) -    collection(following, "#{user.ap_id}/following", page, !user.info.hide_network) +    total = +      if !user.info.hide_follows do +        length(following) +      else +        0 +      end + +    collection(following, "#{user.ap_id}/following", page, !user.info.hide_follows, total)      |> Map.merge(Utils.make_json_ld_header())    end @@ -95,11 +121,18 @@ defmodule Pleroma.Web.ActivityPub.UserView do      query = from(user in query, select: [:ap_id])      following = Repo.all(query) +    total = +      if !user.info.hide_follows do +        length(following) +      else +        0 +      end +      %{        "id" => "#{user.ap_id}/following",        "type" => "OrderedCollection", -      "totalItems" => length(following), -      "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_network) +      "totalItems" => total, +      "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows)      }      |> Map.merge(Utils.make_json_ld_header())    end @@ -109,7 +142,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do      query = from(user in query, select: [:ap_id])      followers = Repo.all(query) -    collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_network) +    total = +      if !user.info.hide_followers do +        length(followers) +      else +        0 +      end + +    collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_followers, total)      |> Map.merge(Utils.make_json_ld_header())    end @@ -118,19 +158,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do      query = from(user in query, select: [:ap_id])      followers = Repo.all(query) +    total = +      if !user.info.hide_followers do +        length(followers) +      else +        0 +      end +      %{        "id" => "#{user.ap_id}/followers",        "type" => "OrderedCollection", -      "totalItems" => length(followers), -      "first" => collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_network) +      "totalItems" => total, +      "first" => +        collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_followers, total)      }      |> Map.merge(Utils.make_json_ld_header())    end    def render("outbox.json", %{user: user, max_id: max_qid}) do -    # XXX: technically note_count is wrong for this, but it's better than nothing -    info = User.user_info(user) -      params = %{        "limit" => "10"      } @@ -158,16 +203,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do        "id" => "#{iri}?max_id=#{max_id}",        "type" => "OrderedCollectionPage",        "partOf" => iri, -      "totalItems" => info.note_count,        "orderedItems" => collection, -      "next" => "#{iri}?max_id=#{min_id - 1}" +      "next" => "#{iri}?max_id=#{min_id}"      }      if max_qid == nil do        %{          "id" => iri,          "type" => "OrderedCollection", -        "totalItems" => info.note_count,          "first" => page        }        |> Map.merge(Utils.make_json_ld_header()) @@ -205,16 +248,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do        "id" => "#{iri}?max_id=#{max_id}",        "type" => "OrderedCollectionPage",        "partOf" => iri, -      "totalItems" => -1,        "orderedItems" => collection, -      "next" => "#{iri}?max_id=#{min_id - 1}" +      "next" => "#{iri}?max_id=#{min_id}"      }      if max_qid == nil do        %{          "id" => iri,          "type" => "OrderedCollection", -        "totalItems" => -1,          "first" => page        }        |> Map.merge(Utils.make_json_ld_header()) @@ -239,6 +280,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do      if offset < total do        Map.put(map, "next", "#{iri}?page=#{page + 1}") +    else +      map      end    end  end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index ef79b9c5d..90b208e54 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -3,7 +3,11 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.CommonAPI do -  alias Pleroma.{User, Repo, Activity, Object} +  alias Pleroma.User +  alias Pleroma.Repo +  alias Pleroma.Activity +  alias Pleroma.Object +  alias Pleroma.ThreadMute    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Formatter @@ -14,6 +18,7 @@ defmodule Pleroma.Web.CommonAPI do      with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id),           %Object{} = object <- Object.normalize(object_id),           true <- user.info.is_moderator || user.ap_id == object.data["actor"], +         {:ok, _} <- unpin(activity_id, user),           {:ok, delete} <- ActivityPub.delete(object) do        {:ok, delete}      end @@ -102,7 +107,14 @@ defmodule Pleroma.Web.CommonAPI do               attachments,               tags,               get_content_type(data["content_type"]), -             Enum.member?([true, "true"], data["no_attachment_links"]) +             Enum.member?( +               [true, "true"], +               Map.get( +                 data, +                 "no_attachment_links", +                 Pleroma.Config.get([:instance, :no_attachment_links], false) +               ) +             )             ),           context <- make_context(inReplyTo),           cw <- data["spoiler_text"], @@ -124,7 +136,7 @@ defmodule Pleroma.Web.CommonAPI do             Map.put(               object,               "emoji", -             Formatter.get_emoji(status) +             (Formatter.get_emoji(status) ++ Formatter.get_emoji(data["spoiler_text"]))               |> Enum.reduce(%{}, fn {name, file}, acc ->                 Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")               end) @@ -135,7 +147,7 @@ defmodule Pleroma.Web.CommonAPI do            actor: user,            context: context,            object: object, -          additional: %{"cc" => cc} +          additional: %{"cc" => cc, "directMessage" => visibility == "direct"}          })        res @@ -164,4 +176,71 @@ defmodule Pleroma.Web.CommonAPI do        object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})      })    end + +  def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do +    with %Activity{ +           actor: ^user_ap_id, +           data: %{ +             "type" => "Create", +             "object" => %{ +               "to" => object_to, +               "type" => "Note" +             } +           } +         } = activity <- get_by_id_or_ap_id(id_or_ap_id), +         true <- Enum.member?(object_to, "https://www.w3.org/ns/activitystreams#Public"), +         %{valid?: true} = info_changeset <- +           Pleroma.User.Info.add_pinnned_activity(user.info, activity), +         changeset <- +           Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset), +         {:ok, _user} <- User.update_and_set_cache(changeset) do +      {:ok, activity} +    else +      %{errors: [pinned_activities: {err, _}]} -> +        {:error, err} + +      _ -> +        {:error, "Could not pin"} +    end +  end + +  def unpin(id_or_ap_id, user) do +    with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), +         %{valid?: true} = info_changeset <- +           Pleroma.User.Info.remove_pinnned_activity(user.info, activity), +         changeset <- +           Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset), +         {:ok, _user} <- User.update_and_set_cache(changeset) do +      {:ok, activity} +    else +      %{errors: [pinned_activities: {err, _}]} -> +        {:error, err} + +      _ -> +        {:error, "Could not unpin"} +    end +  end + +  def add_mute(user, activity) do +    with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do +      {:ok, activity} +    else +      {:error, _} -> {:error, "conversation is already muted"} +    end +  end + +  def remove_mute(user, activity) do +    ThreadMute.remove_mute(user.id, activity.data["context"]) +    {:ok, activity} +  end + +  def thread_muted?(%{id: nil} = _user, _activity), do: false + +  def thread_muted?(user, activity) do +    with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do +      false +    else +      _ -> true +    end +  end  end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 59df48ed6..abdeee947 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -5,22 +5,25 @@  defmodule Pleroma.Web.CommonAPI.Utils do    alias Calendar.Strftime    alias Comeonin.Pbkdf2 -  alias Pleroma.{Activity, Formatter, Object, Repo} +  alias Pleroma.Activity +  alias Pleroma.Formatter +  alias Pleroma.Object +  alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web -  alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.Endpoint    alias Pleroma.Web.MediaProxy +  alias Pleroma.Web.ActivityPub.Utils    # This is a hack for twidere.    def get_by_id_or_ap_id(id) do -    activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id) +    activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)      activity &&        if activity.data["type"] == "Create" do          activity        else -        Activity.get_create_activity_by_object_ap_id(activity.data["object"]) +        Activity.get_create_by_object_ap_id(activity.data["object"])        end    end @@ -111,7 +114,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def make_context(%Activity{data: %{"context" => context}}), do: context    def make_context(_), do: Utils.generate_context_id() -  def maybe_add_attachments(text, _attachments, _no_links = true), do: text +  def maybe_add_attachments(text, _attachments, true = _no_links), do: text    def maybe_add_attachments(text, attachments, _no_links) do      add_attachments(text, attachments) @@ -132,16 +135,18 @@ defmodule Pleroma.Web.CommonAPI.Utils do      Enum.join([text | attachment_text], "<br>")    end +  def format_input(text, mentions, tags, format, options \\ []) +    @doc """    Formatting text to plain text.    """ -  def format_input(text, mentions, tags, "text/plain") do +  def format_input(text, mentions, tags, "text/plain", options) do      text      |> Formatter.html_escape("text/plain")      |> String.replace(~r/\r?\n/, "<br>")      |> (&{[], &1}).()      |> Formatter.add_links() -    |> Formatter.add_user_links(mentions) +    |> Formatter.add_user_links(mentions, options[:user_links] || [])      |> Formatter.add_hashtag_links(tags)      |> Formatter.finalize()    end @@ -149,26 +154,24 @@ defmodule Pleroma.Web.CommonAPI.Utils do    @doc """    Formatting text to html.    """ -  def format_input(text, mentions, _tags, "text/html") do +  def format_input(text, mentions, _tags, "text/html", options) do      text      |> Formatter.html_escape("text/html") -    |> String.replace(~r/\r?\n/, "<br>")      |> (&{[], &1}).() -    |> Formatter.add_user_links(mentions) +    |> Formatter.add_user_links(mentions, options[:user_links] || [])      |> Formatter.finalize()    end    @doc """    Formatting text to markdown.    """ -  def format_input(text, mentions, tags, "text/markdown") do +  def format_input(text, mentions, tags, "text/markdown", options) do      text      |> Formatter.mentions_escape(mentions)      |> Earmark.as_html!()      |> Formatter.html_escape("text/html") -    |> String.replace(~r/\r?\n/, "")      |> (&{[], &1}).() -    |> Formatter.add_user_links(mentions) +    |> Formatter.add_user_links(mentions, options[:user_links] || [])      |> Formatter.add_hashtag_links(tags)      |> Formatter.finalize()    end @@ -277,4 +280,46 @@ defmodule Pleroma.Web.CommonAPI.Utils do        }      end)    end + +  def maybe_notify_to_recipients( +        recipients, +        %Activity{data: %{"to" => to, "type" => _type}} = _activity +      ) do +    recipients ++ to +  end + +  def maybe_notify_mentioned_recipients( +        recipients, +        %Activity{data: %{"to" => _to, "type" => type} = data} = _activity +      ) +      when type == "Create" do +    object = Object.normalize(data["object"]) + +    object_data = +      cond do +        !is_nil(object) -> +          object.data + +        is_map(data["object"]) -> +          data["object"] + +        true -> +          %{} +      end + +    tagged_mentions = maybe_extract_mentions(object_data) + +    recipients ++ tagged_mentions +  end + +  def maybe_notify_mentioned_recipients(recipients, _), do: recipients + +  def maybe_extract_mentions(%{"tag" => tag}) do +    tag +    |> Enum.filter(fn x -> is_map(x) end) +    |> Enum.filter(fn x -> x["type"] == "Mention" end) +    |> Enum.map(fn x -> x["href"] end) +  end + +  def maybe_extract_mentions(_), do: []  end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 0b4ce9cc4..3eed047ca 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -25,7 +25,7 @@ defmodule Pleroma.Web.Endpoint do      at: "/",      from: :pleroma,      only: -      ~w(index.html static finmoji emoji packs sounds images instance sw.js favicon.png schemas doc) +      ~w(index.html static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)    )    # Code reloading can be explicitly enabled under the @@ -82,4 +82,8 @@ defmodule Pleroma.Web.Endpoint do      port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"      {:ok, Keyword.put(config, :http, [:inet6, port: port])}    end + +  def websocket_url do +    String.replace_leading(url(), "http", "ws") +  end  end diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index f3a0e18b8..468959a65 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -4,15 +4,19 @@  defmodule Pleroma.Web.Federator do    use GenServer -  alias Pleroma.User +    alias Pleroma.Activity -  alias Pleroma.Web.{WebFinger, Websub} -  alias Pleroma.Web.Federator.RetryQueue +  alias Pleroma.User +  alias Pleroma.Web.WebFinger +  alias Pleroma.Web.Websub +  alias Pleroma.Web.Salmon    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Relay    alias Pleroma.Web.ActivityPub.Transmogrifier    alias Pleroma.Web.ActivityPub.Utils +  alias Pleroma.Web.Federator.RetryQueue    alias Pleroma.Web.OStatus +    require Logger    @websub Application.get_env(:pleroma, :websub) @@ -25,7 +29,7 @@ defmodule Pleroma.Web.Federator do    def start_link do      spawn(fn ->        # 1 minute -      Process.sleep(1000 * 60 * 1) +      Process.sleep(1000 * 60)        enqueue(:refresh_subscriptions, nil)      end) @@ -124,6 +128,10 @@ defmodule Pleroma.Web.Federator do      end    end +  def handle(:publish_single_salmon, params) do +    Salmon.send_to_user(params) +  end +    def handle(:publish_single_ap, params) do      case ActivityPub.publish_one(params) do        {:ok, _} -> @@ -192,8 +200,7 @@ defmodule Pleroma.Web.Federator do      {:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}    end -  def handle_cast(m, state) do -    IO.inspect("Unknown: #{inspect(m)}, #{inspect(state)}") +  def handle_cast(_, state) do      {:noreply, state}    end diff --git a/lib/pleroma/web/http_signatures/http_signatures.ex b/lib/pleroma/web/http_signatures/http_signatures.ex index e81f9e27a..8e2e2a44b 100644 --- a/lib/pleroma/web/http_signatures/http_signatures.ex +++ b/lib/pleroma/web/http_signatures/http_signatures.ex @@ -5,8 +5,9 @@  # https://tools.ietf.org/html/draft-cavage-http-signatures-08  defmodule Pleroma.Web.HTTPSignatures do    alias Pleroma.User -  alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.ActivityPub.Utils +    require Logger    def split_signature(sig) do diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 95d0f849c..dcaeccac6 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -4,34 +4,44 @@  defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    use Pleroma.Web, :controller -  alias Pleroma.{Repo, Object, Activity, User, Notification, Stats} +  alias Pleroma.Activity +  alias Pleroma.Config +  alias Pleroma.Filter +  alias Pleroma.Notification +  alias Pleroma.Object +  alias Pleroma.Repo +  alias Pleroma.Stats +  alias Pleroma.User    alias Pleroma.Web - -  alias Pleroma.Web.MastodonAPI.{ -    StatusView, -    AccountView, -    MastodonView, -    ListView, -    FilterView, -    PushSubscriptionView -  } - -  alias Pleroma.Web.ActivityPub.ActivityPub -  alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.CommonAPI -  alias Pleroma.Web.OAuth.{Authorization, Token, App}    alias Pleroma.Web.MediaProxy +  alias Pleroma.Web.Push +  alias Push.Subscription + +  alias Pleroma.Web.MastodonAPI.AccountView +  alias Pleroma.Web.MastodonAPI.FilterView +  alias Pleroma.Web.MastodonAPI.ListView +  alias Pleroma.Web.MastodonAPI.MastodonView +  alias Pleroma.Web.MastodonAPI.PushSubscriptionView +  alias Pleroma.Web.MastodonAPI.StatusView +  alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.ActivityPub.Utils +  alias Pleroma.Web.OAuth.App +  alias Pleroma.Web.OAuth.Authorization +  alias Pleroma.Web.OAuth.Token    import Ecto.Query    require Logger    @httpoison Application.get_env(:pleroma, :httpoison) +  @local_mastodon_name "Mastodon-Local"    action_fallback(:errors)    def create_app(conn, params) do -    with cs <- App.register_changeset(%App{}, params) |> IO.inspect(), -         {:ok, app} <- Repo.insert(cs) |> IO.inspect() do +    with cs <- App.register_changeset(%App{}, params), +         false <- cs.changes[:client_name] == @local_mastodon_name, +         {:ok, app} <- Repo.insert(cs) do        res = %{          id: app.id |> to_string,          name: app.client_name, @@ -129,7 +139,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    @mastodon_api_level "2.5.0"    def masto_instance(conn, _params) do -    instance = Pleroma.Config.get(:instance) +    instance = Config.get(:instance)      response = %{        uri: Web.base_url(), @@ -138,7 +148,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",        email: Keyword.get(instance, :email),        urls: %{ -        streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws") +        streaming_api: Pleroma.Web.Endpoint.websocket_url()        },        stats: Stats.get_stats(),        thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg", @@ -225,7 +235,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> Map.put("user", user)      activities = -      ActivityPub.fetch_activities([user.ap_id | user.following], params) +      [user.ap_id | user.following] +      |> ActivityPub.fetch_activities(params)        |> ActivityPub.contain_timeline(user)        |> Enum.reverse() @@ -238,14 +249,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def public_timeline(%{assigns: %{user: user}} = conn, params) do      local_only = params["local"] in [true, "True", "true", "1"] -    params = +    activities =        params        |> Map.put("type", ["Create", "Announce"])        |> Map.put("local_only", local_only)        |> Map.put("blocking_user", user) - -    activities = -      ActivityPub.fetch_public_activities(params) +      |> ActivityPub.fetch_public_activities()        |> Enum.reverse()      conn @@ -256,13 +265,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do      with %User{} = user <- Repo.get(User, params["id"]) do -      # Since Pleroma has no "pinned" posts feature, we'll just set an empty list here -      activities = -        if params["pinned"] == "true" do -          [] -        else -          ActivityPub.fetch_user_activities(user, reading_user, params) -        end +      activities = ActivityPub.fetch_user_activities(user, reading_user, params)        conn        |> add_link_headers(:user_statuses, activities, params["id"]) @@ -320,6 +323,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do              as: :activity            )            |> Enum.reverse(), +        # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart          descendants:            StatusView.render(              "index.json", @@ -328,6 +332,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do              as: :activity            )            |> Enum.reverse() +        # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart        }        json(conn, result) @@ -347,7 +352,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      params =        params        |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) -      |> Map.put("no_attachment_links", true)      idempotency_key =        case get_req_header(conn, "idempotency-key") do @@ -384,7 +388,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do      with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user), -         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do        conn        |> put_view(StatusView)        |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -393,7 +397,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def fav_status(%{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_activity_by_object_ap_id(id) do +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do        conn        |> put_view(StatusView)        |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -402,7 +406,75 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do      with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user), -         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do +      conn +      |> put_view(StatusView) +      |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +    end +  end + +  def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do +    with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do +      conn +      |> put_view(StatusView) +      |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +    else +      {:error, reason} -> +        conn +        |> put_resp_content_type("application/json") +        |> send_resp(:bad_request, Jason.encode!(%{"error" => reason})) +    end +  end + +  def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do +    with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do +      conn +      |> put_view(StatusView) +      |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +    end +  end + +  def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do +    with %Activity{} = activity <- Repo.get(Activity, id), +         %User{} = user <- User.get_by_nickname(user.nickname), +         true <- ActivityPub.visible_for_user?(activity, user), +         {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do +      conn +      |> put_view(StatusView) +      |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +    end +  end + +  def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do +    with %Activity{} = activity <- Repo.get(Activity, id), +         %User{} = user <- User.get_by_nickname(user.nickname), +         true <- ActivityPub.visible_for_user?(activity, user), +         {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do +      conn +      |> put_view(StatusView) +      |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +    end +  end + +  def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do +    activity = Activity.get_by_id(id) + +    with {:ok, activity} <- CommonAPI.add_mute(user, activity) do +      conn +      |> put_view(StatusView) +      |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +    else +      {:error, reason} -> +        conn +        |> put_resp_content_type("application/json") +        |> send_resp(:bad_request, Jason.encode!(%{"error" => reason})) +    end +  end + +  def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do +    activity = Activity.get_by_id(id) + +    with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do        conn        |> put_view(StatusView)        |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -413,9 +485,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      notifications = Notification.for_user(user, params)      result = -      Enum.map(notifications, fn x -> -        render_notification(user, x) -      end) +      notifications +      |> Enum.map(fn x -> render_notification(user, x) end)        |> Enum.filter(& &1)      conn @@ -485,7 +556,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do      with {:ok, object} <- -           ActivityPub.upload(file, +           ActivityPub.upload( +             file,               actor: User.ap_id(user),               description: Map.get(data, "description")             ) do @@ -526,15 +598,32 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do      local_only = params["local"] in [true, "True", "true", "1"] -    params = +    tags = +      [params["tag"], params["any"]] +      |> List.flatten() +      |> Enum.uniq() +      |> Enum.filter(& &1) +      |> Enum.map(&String.downcase(&1)) + +    tag_all = +      params["all"] || +        [] +        |> Enum.map(&String.downcase(&1)) + +    tag_reject = +      params["none"] || +        [] +        |> Enum.map(&String.downcase(&1)) + +    activities =        params        |> Map.put("type", "Create")        |> Map.put("local_only", local_only)        |> Map.put("blocking_user", user) -      |> Map.put("tag", String.downcase(params["tag"])) - -    activities = -      ActivityPub.fetch_public_activities(params) +      |> Map.put("tag", tags) +      |> Map.put("tag_all", tag_all) +      |> Map.put("tag_reject", tag_reject) +      |> ActivityPub.fetch_public_activities()        |> Enum.reverse()      conn @@ -549,7 +638,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        followers =          cond do            for_user && user.id == for_user.id -> followers -          user.info.hide_network -> [] +          user.info.hide_followers -> []            true -> followers          end @@ -565,7 +654,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        followers =          cond do            for_user && user.id == for_user.id -> followers -          user.info.hide_network -> [] +          user.info.hide_follows -> []            true -> followers          end @@ -634,7 +723,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do           {:ok, _activity} <- ActivityPub.follow(follower, followed),           {:ok, follower, followed} <-             User.wait_and_refresh( -             Pleroma.Config.get([:activitypub, :follow_handshake_timeout]), +             Config.get([:activitypub, :follow_handshake_timeout]),               follower,               followed             ) do @@ -725,11 +814,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      json(conn, %{})    end -  def status_search(query) do +  def status_search(user, query) do      fetched =        if Regex.match?(~r/https?:/, query) do -        with {:ok, object} <- ActivityPub.fetch_object_from_id(query) do -          [Activity.get_create_activity_by_object_ap_id(object.data["id"])] +        with {:ok, object} <- ActivityPub.fetch_object_from_id(query), +             %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), +             true <- ActivityPub.visible_for_user?(activity, user) do +          [activity]          else            _e -> []          end @@ -754,14 +845,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do -    accounts = User.search(query, params["resolve"] == "true") +    accounts = User.search(query, params["resolve"] == "true", user) -    statuses = status_search(query) +    statuses = status_search(user, query)      tags_path = Web.base_url() <> "/tag/"      tags = -      String.split(query) +      query +      |> String.split()        |> Enum.uniq()        |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)        |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) @@ -778,12 +870,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do -    accounts = User.search(query, params["resolve"] == "true") +    accounts = User.search(query, params["resolve"] == "true", user) -    statuses = status_search(query) +    statuses = status_search(user, query)      tags = -      String.split(query) +      query +      |> String.split()        |> Enum.uniq()        |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)        |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) @@ -799,22 +892,34 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do -    accounts = User.search(query, params["resolve"] == "true") +    accounts = User.search(query, params["resolve"] == "true", user)      res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)      json(conn, res)    end -  def favourites(%{assigns: %{user: user}} = conn, _) do -    params = -      %{} +  def favourites(%{assigns: %{user: user}} = conn, params) do +    activities = +      params        |> Map.put("type", "Create")        |> Map.put("favorited_by", user.ap_id)        |> Map.put("blocking_user", user) +      |> ActivityPub.fetch_public_activities() +      |> Enum.reverse() + +    conn +    |> add_link_headers(:favourites, activities) +    |> put_view(StatusView) +    |> render("index.json", %{activities: activities, for: user, as: :activity}) +  end + +  def bookmarks(%{assigns: %{user: user}} = conn, _) do +    user = Repo.get(User, user.id)      activities = -      ActivityPub.fetch_public_activities(params) +      user.bookmarks +      |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)        |> Enum.reverse()      conn @@ -833,7 +938,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        res = ListView.render("list.json", list: list)        json(conn, res)      else -      _e -> json(conn, "error") +      _e -> +        conn +        |> put_status(404) +        |> json(%{error: "Record not found"})      end    end @@ -913,12 +1021,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        # we must filter the following list for the user to avoid leaking statuses the user        # does not actually have permission to see (for more info, peruse security issue #270). -      following_to = +      activities =          following          |> Enum.filter(fn x -> x in user.following end) - -      activities = -        ActivityPub.fetch_activities_bounded(following_to, following, params) +        |> ActivityPub.fetch_activities_bounded(following, params)          |> Enum.reverse()        conn @@ -940,7 +1046,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      if user && token do        mastodon_emoji = mastodonized_emoji() -      limit = Pleroma.Config.get([:instance, :limit]) +      limit = Config.get([:instance, :limit])        accounts =          Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) @@ -964,8 +1070,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do              max_toot_chars: limit            },            rights: %{ -            delete_others_notice: !!user.info.is_moderator, -            admin: !!user.info.is_admin +            delete_others_notice: present?(user.info.is_moderator), +            admin: present?(user.info.is_admin)            },            compose: %{              me: "#{user.id}", @@ -1064,7 +1170,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def login(conn, _) do      with {:ok, app} <- get_or_make_app() do        path = -        o_auth_path(conn, :authorize, +        o_auth_path( +          conn, +          :authorize,            response_type: "code",            client_id: app.client_id,            redirect_uri: ".", @@ -1077,16 +1185,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    defp get_or_make_app() do -    with %App{} = app <- Repo.get_by(App, client_name: "Mastodon-Local") do +    find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."} + +    with %App{} = app <- Repo.get_by(App, find_attrs) do        {:ok, app}      else        _e -> -        cs = -          App.register_changeset(%App{}, %{ -            client_name: "Mastodon-Local", -            redirect_uris: ".", -            scopes: "read,write,follow" -          }) +        cs = App.register_changeset(%App{}, Map.put(find_attrs, :scopes, "read,write,follow"))          Repo.insert(cs)      end @@ -1120,7 +1225,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do      actor = User.get_cached_by_ap_id(activity.data["actor"]) -    parent_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) +    parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])      mastodon_type = Activity.mastodon_notification_type(activity)      response = %{ @@ -1158,7 +1263,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def get_filters(%{assigns: %{user: user}} = conn, _) do -    filters = Pleroma.Filter.get_filters(user) +    filters = Filter.get_filters(user)      res = FilterView.render("filters.json", filters: filters)      json(conn, res)    end @@ -1167,7 +1272,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          %{assigns: %{user: user}} = conn,          %{"phrase" => phrase, "context" => context} = params        ) do -    query = %Pleroma.Filter{ +    query = %Filter{        user_id: user.id,        phrase: phrase,        context: context, @@ -1176,13 +1281,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        # expires_at      } -    {:ok, response} = Pleroma.Filter.create(query) +    {:ok, response} = Filter.create(query)      res = FilterView.render("filter.json", filter: response)      json(conn, res)    end    def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do -    filter = Pleroma.Filter.get(filter_id, user) +    filter = Filter.get(filter_id, user)      res = FilterView.render("filter.json", filter: filter)      json(conn, res)    end @@ -1191,7 +1296,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          %{assigns: %{user: user}} = conn,          %{"phrase" => phrase, "context" => context, "id" => filter_id} = params        ) do -    query = %Pleroma.Filter{ +    query = %Filter{        user_id: user.id,        filter_id: filter_id,        phrase: phrase, @@ -1201,32 +1306,32 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        # expires_at      } -    {:ok, response} = Pleroma.Filter.update(query) +    {:ok, response} = Filter.update(query)      res = FilterView.render("filter.json", filter: response)      json(conn, res)    end    def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do -    query = %Pleroma.Filter{ +    query = %Filter{        user_id: user.id,        filter_id: filter_id      } -    {:ok, _} = Pleroma.Filter.delete(query) +    {:ok, _} = Filter.delete(query)      json(conn, %{})    end    def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do -    true = Pleroma.Web.Push.enabled() -    Pleroma.Web.Push.Subscription.delete_if_exists(user, token) -    {:ok, subscription} = Pleroma.Web.Push.Subscription.create(user, token, params) +    true = Push.enabled() +    Subscription.delete_if_exists(user, token) +    {:ok, subscription} = Subscription.create(user, token, params)      view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)      json(conn, view)    end    def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do -    true = Pleroma.Web.Push.enabled() -    subscription = Pleroma.Web.Push.Subscription.get(user, token) +    true = Push.enabled() +    subscription = Subscription.get(user, token)      view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)      json(conn, view)    end @@ -1235,15 +1340,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          %{assigns: %{user: user, token: token}} = conn,          params        ) do -    true = Pleroma.Web.Push.enabled() -    {:ok, subscription} = Pleroma.Web.Push.Subscription.update(user, token, params) +    true = Push.enabled() +    {:ok, subscription} = Subscription.update(user, token, params)      view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)      json(conn, view)    end    def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do -    true = Pleroma.Web.Push.enabled() -    {:ok, _response} = Pleroma.Web.Push.Subscription.delete(user, token) +    true = Push.enabled() +    {:ok, _response} = Subscription.delete(user, token)      json(conn, %{})    end @@ -1254,17 +1359,21 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def suggestions(%{assigns: %{user: user}} = conn, _) do -    suggestions = Pleroma.Config.get(:suggestions) +    suggestions = Config.get(:suggestions)      if Keyword.get(suggestions, :enabled, false) do        api = Keyword.get(suggestions, :third_party_engine, "")        timeout = Keyword.get(suggestions, :timeout, 5000)        limit = Keyword.get(suggestions, :limit, 23) -      host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) +      host = Config.get([Pleroma.Web.Endpoint, :url, :host])        user = user.nickname -      url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user) + +      url = +        api +        |> String.replace("{{host}}", host) +        |> String.replace("{{user}}", user)        with {:ok, %{status: 200, body: body}} <-               @httpoison.get( @@ -1272,12 +1381,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do                 [],                 adapter: [                   timeout: timeout, -                 recv_timeout: timeout +                 recv_timeout: timeout, +                 pool: :default                 ]               ),             {:ok, data} <- Jason.decode(body) do -        data2 = -          Enum.slice(data, 0, limit) +        data = +          data +          |> Enum.slice(0, limit)            |> Enum.map(fn x ->              Map.put(                x, @@ -1296,7 +1407,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do            end)          conn -        |> json(data2) +        |> json(data)        else          e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")        end @@ -1305,6 +1416,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end +  def status_card(conn, %{"id" => status_id}) do +    with %Activity{} = activity <- Repo.get(Activity, status_id), +         true <- ActivityPub.is_public?(activity) do +      data = +        StatusView.render( +          "card.json", +          Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) +        ) + +      json(conn, data) +    else +      _e -> +        %{} +    end +  end +    def try_render(conn, target, params)        when is_binary(target) do      res = render(conn, target, params) @@ -1323,4 +1450,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      |> put_status(501)      |> json(%{error: "Can't display this activity"})    end + +  defp present?(nil), do: false +  defp present?(false), do: false +  defp present?(_), do: true  end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index bfd6b8b22..9df9f14b2 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -4,11 +4,12 @@  defmodule Pleroma.Web.MastodonAPI.AccountView do    use Pleroma.Web, :view + +  alias Pleroma.HTML    alias Pleroma.User -  alias Pleroma.Web.MastodonAPI.AccountView    alias Pleroma.Web.CommonAPI.Utils +  alias Pleroma.Web.MastodonAPI.AccountView    alias Pleroma.Web.MediaProxy -  alias Pleroma.HTML    def render("accounts.json", %{users: users} = opts) do      users @@ -112,7 +113,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do        # Pleroma extension        pleroma: %{          confirmation_pending: user_info.confirmation_pending, -        tags: user.tags +        tags: user.tags, +        is_moderator: user.info.is_moderator, +        is_admin: user.info.is_admin        }      }    end diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex index 1052a449d..a685bc7b6 100644 --- a/lib/pleroma/web/mastodon_api/views/filter_view.ex +++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex @@ -4,8 +4,8 @@  defmodule Pleroma.Web.MastodonAPI.FilterView do    use Pleroma.Web, :view -  alias Pleroma.Web.MastodonAPI.FilterView    alias Pleroma.Web.CommonAPI.Utils +  alias Pleroma.Web.MastodonAPI.FilterView    def render("filters.json", %{filters: filters} = opts) do      render_many(filters, FilterView, "filter.json", opts) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 477ab3b5f..69f5f992c 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -9,10 +9,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    alias Pleroma.HTML    alias Pleroma.Repo    alias Pleroma.User +  alias Pleroma.Web.CommonAPI    alias Pleroma.Web.CommonAPI.Utils -  alias Pleroma.Web.MediaProxy    alias Pleroma.Web.MastodonAPI.AccountView    alias Pleroma.Web.MastodonAPI.StatusView +  alias Pleroma.Web.MediaProxy    # TODO: Add cached version.    defp get_replied_to_activities(activities) do @@ -25,33 +26,45 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          nil      end)      |> Enum.filter(& &1) -    |> Activity.create_activity_by_object_id_query() +    |> Activity.create_by_object_ap_id()      |> Repo.all()      |> Enum.reduce(%{}, fn activity, acc ->        Map.put(acc, activity.data["object"]["id"], activity)      end)    end +  defp get_user(ap_id) do +    cond do +      user = User.get_cached_by_ap_id(ap_id) -> +        user + +      user = User.get_by_guessed_nickname(ap_id) -> +        user + +      true -> +        User.error_user(ap_id) +    end +  end +    def render("index.json", opts) do      replied_to_activities = get_replied_to_activities(opts.activities)      opts.activities -    |> render_many( +    |> safe_render_many(        StatusView,        "status.json",        Map.put(opts, :replied_to_activities, replied_to_activities)      ) -    |> Enum.filter(fn x -> not is_nil(x) end)    end    def render(          "status.json",          %{activity: %{data: %{"type" => "Announce", "object" => object}} = activity} = opts        ) do -    user = User.get_cached_by_ap_id(activity.data["actor"]) +    user = get_user(activity.data["actor"])      created_at = Utils.to_masto_date(activity.data["published"]) -    reblogged = Activity.get_create_activity_by_object_ap_id(object) +    reblogged = Activity.get_create_by_object_ap_id(object)      reblogged = render("status.json", Map.put(opts, :activity, reblogged))      mentions = @@ -75,7 +88,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        favourites_count: 0,        reblogged: false,        favourited: false, +      bookmarked: false,        muted: false, +      pinned: pinned?(activity, user),        sensitive: false,        spoiler_text: "",        visibility: "public", @@ -92,7 +107,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    end    def render("status.json", %{activity: %{data: %{"object" => object}} = activity} = opts) do -    user = User.get_cached_by_ap_id(activity.data["actor"]) +    user = get_user(activity.data["actor"])      like_count = object["like_count"] || 0      announcement_count = object["announcement_count"] || 0 @@ -108,6 +123,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])      favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || []) +    bookmarked = opts[:for] && object["id"] in opts[:for].bookmarks      attachment_data = object["attachment"] || []      attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment) @@ -115,12 +131,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      created_at = Utils.to_masto_date(object["published"])      reply_to = get_reply_to(activity, opts) -    reply_to_user = reply_to && User.get_cached_by_ap_id(reply_to.data["actor"]) +    reply_to_user = reply_to && get_user(reply_to.data["actor"])      content =        object        |> render_content() -      |> HTML.get_cached_scrubbed_html_for_object(User.html_filter_policy(opts[:for]), activity) +      |> HTML.get_cached_scrubbed_html_for_object( +        User.html_filter_policy(opts[:for]), +        activity, +        __MODULE__ +      ) + +    card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))      %{        id: to_string(activity.id), @@ -130,6 +152,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        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),        reblog: nil, +      card: card,        content: content,        created_at: created_at,        reblogs_count: announcement_count, @@ -137,7 +160,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        favourites_count: like_count,        reblogged: present?(repeated),        favourited: present?(favorited), -      muted: false, +      bookmarked: present?(bookmarked), +      muted: CommonAPI.thread_muted?(user, activity), +      pinned: pinned?(activity, user),        sensitive: sensitive,        spoiler_text: object["summary"] || "",        visibility: get_visibility(object), @@ -157,6 +182,46 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      nil    end +  def render("card.json", %{rich_media: rich_media, page_url: page_url}) do +    page_url_data = URI.parse(page_url) + +    page_url_data = +      if rich_media[:url] != nil do +        URI.merge(page_url_data, URI.parse(rich_media[:url])) +      else +        page_url_data +      end + +    page_url = page_url_data |> to_string + +    image_url = +      if rich_media[:image] != nil do +        URI.merge(page_url_data, URI.parse(rich_media[:image])) +        |> to_string +      else +        nil +      end + +    site_name = rich_media[:site_name] || page_url_data.host + +    %{ +      type: "link", +      provider_name: site_name, +      provider_url: page_url_data.scheme <> "://" <> page_url_data.host, +      url: page_url, +      image: image_url |> MediaProxy.url(), +      title: rich_media[:title], +      description: rich_media[:description], +      pleroma: %{ +        opengraph: rich_media +      } +    } +  end + +  def render("card.json", _) do +    nil +  end +    def render("attachment.json", %{attachment: attachment}) do      [attachment_url | _] = attachment["url"]      media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image" @@ -190,7 +255,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    def get_reply_to(%{data: %{"object" => object}}, _) do      if object["inReplyTo"] && object["inReplyTo"] != "" do -      Activity.get_create_activity_by_object_ap_id(object["inReplyTo"]) +      Activity.get_create_by_object_ap_id(object["inReplyTo"])      else        nil      end @@ -212,6 +277,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        Enum.any?(to, &String.contains?(&1, "/followers")) ->          "private" +      length(cc) > 0 -> +        "private" +        true ->          "direct"      end @@ -291,4 +359,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    defp present?(nil), do: false    defp present?(false), do: false    defp present?(_), do: true + +  defp pinned?(%Activity{id: id}, %User{info: %{pinned_activities: pinned_activities}}), +    do: id in pinned_activities  end diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index c0254c8e6..ea75070c4 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -6,7 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do    require Logger    alias Pleroma.Web.OAuth.Token -  alias Pleroma.{User, Repo} +  alias Pleroma.Repo +  alias Pleroma.User    @behaviour :cowboy_websocket_handler diff --git a/lib/pleroma/web/media_proxy/controller.ex b/lib/pleroma/web/media_proxy/controller.ex index de79cad73..c0552d89f 100644 --- a/lib/pleroma/web/media_proxy/controller.ex +++ b/lib/pleroma/web/media_proxy/controller.ex @@ -4,11 +4,12 @@  defmodule Pleroma.Web.MediaProxy.MediaProxyController do    use Pleroma.Web, :controller -  alias Pleroma.{Web.MediaProxy, ReverseProxy} +  alias Pleroma.ReverseProxy +  alias Pleroma.Web.MediaProxy    @default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]] -  def remote(conn, params = %{"sig" => sig64, "url" => url64}) do +  def remote(conn, %{"sig" => sig64, "url" => url64} = params) do      with config <- Pleroma.Config.get([:media_proxy], []),           true <- Keyword.get(config, :enabled, false),           {:ok, url} <- MediaProxy.decode_url(sig64, url64), diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index e1eb1472d..39a725a69 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.MediaProxy do    def url(""), do: nil -  def url(url = "/" <> _), do: url +  def url("/" <> _ = url), do: url    def url(url) do      config = Application.get_env(:pleroma, :media_proxy, []) @@ -19,11 +19,16 @@ defmodule Pleroma.Web.MediaProxy do      else        secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] +      # Must preserve `%2F` for compatibility with S3 (https://git.pleroma.social/pleroma/pleroma/issues/580) +      replacement = get_replacement(url, ":2F:") +        # The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.        base64 =          url +        |> String.replace("%2F", replacement)          |> URI.decode()          |> URI.encode() +        |> String.replace(replacement, "%2F")          |> Base.url_encode64(@base64_opts)        sig = :crypto.hmac(:sha, secret, base64) @@ -60,4 +65,12 @@ defmodule Pleroma.Web.MediaProxy do      |> Enum.filter(fn value -> value end)      |> Path.join()    end + +  defp get_replacement(url, replacement) do +    if String.contains?(url, replacement) do +      get_replacement(url, replacement <> replacement) +    else +      replacement +    end +  end  end diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex new file mode 100644 index 000000000..8761260f2 --- /dev/null +++ b/lib/pleroma/web/metadata.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata do +  alias Phoenix.HTML + +  def build_tags(params) do +    Enum.reduce(Pleroma.Config.get([__MODULE__, :providers], []), "", fn parser, acc -> +      rendered_html = +        params +        |> parser.build_tags() +        |> Enum.map(&to_tag/1) +        |> Enum.map(&HTML.safe_to_string/1) +        |> Enum.join() + +      acc <> rendered_html +    end) +  end + +  def to_tag(data) do +    with {name, attrs, _content = []} <- data do +      HTML.Tag.tag(name, attrs) +    else +      {name, attrs, content} -> +        HTML.Tag.content_tag(name, content, attrs) + +      _ -> +        raise ArgumentError, message: "make_tag invalid args" +    end +  end + +  def activity_nsfw?(%{data: %{"sensitive" => sensitive}}) do +    Pleroma.Config.get([__MODULE__, :unfurl_nsfw], false) == false and sensitive +  end + +  def activity_nsfw?(_) do +    false +  end +end diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex new file mode 100644 index 000000000..190377767 --- /dev/null +++ b/lib/pleroma/web/metadata/opengraph.ex @@ -0,0 +1,156 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.OpenGraph do +  alias Pleroma.HTML +  alias Pleroma.Formatter +  alias Pleroma.User +  alias Pleroma.Web.Metadata +  alias Pleroma.Web.MediaProxy +  alias Pleroma.Web.Metadata.Providers.Provider + +  @behaviour Provider + +  @impl Provider +  def build_tags(%{ +        object: object, +        url: url, +        user: user +      }) do +    attachments = build_attachments(object) +    scrubbed_content = scrub_html_and_truncate(object) +    # Zero width space +    content = +      if scrubbed_content != "" and scrubbed_content != "\u200B" do +        ": “" <> scrubbed_content <> "”" +      else +        "" +      end + +    # Most previews only show og:title which is inconvenient. Instagram +    # hacks this by putting the description in the title and making the +    # description longer prefixed by how many likes and shares the post +    # has. Here we use the descriptive nickname in the title, and expand +    # the full account & nickname in the description. We also use the cute^Wevil +    # smart quotes around the status text like Instagram, too. +    [ +      {:meta, +       [ +         property: "og:title", +         content: "#{user.name}" <> content +       ], []}, +      {:meta, [property: "og:url", content: url], []}, +      {:meta, +       [ +         property: "og:description", +         content: "#{user_name_string(user)}" <> content +       ], []}, +      {:meta, [property: "og:type", content: "website"], []} +    ] ++ +      if attachments == [] or Metadata.activity_nsfw?(object) do +        [ +          {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []}, +          {:meta, [property: "og:image:width", content: 150], []}, +          {:meta, [property: "og:image:height", content: 150], []} +        ] +      else +        attachments +      end +  end + +  @impl Provider +  def build_tags(%{user: user}) do +    with truncated_bio = scrub_html_and_truncate(user.bio || "") do +      [ +        {:meta, +         [ +           property: "og:title", +           content: user_name_string(user) +         ], []}, +        {:meta, [property: "og:url", content: User.profile_url(user)], []}, +        {:meta, [property: "og:description", content: truncated_bio], []}, +        {:meta, [property: "og:type", content: "website"], []}, +        {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []}, +        {:meta, [property: "og:image:width", content: 150], []}, +        {:meta, [property: "og:image:height", content: 150], []} +      ] +    end +  end + +  defp build_attachments(%{data: %{"attachment" => attachments}}) do +    Enum.reduce(attachments, [], fn attachment, acc -> +      rendered_tags = +        Enum.reduce(attachment["url"], [], fn url, acc -> +          media_type = +            Enum.find(["image", "audio", "video"], fn media_type -> +              String.starts_with?(url["mediaType"], media_type) +            end) + +          # TODO: Add additional properties to objects when we have the data available. +          # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image +          # object when a Video or GIF is attached it will display that in the Whatsapp Rich Preview. +          case media_type do +            "audio" -> +              [ +                {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []} +                | acc +              ] + +            "image" -> +              [ +                {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], +                 []}, +                {:meta, [property: "og:image:width", content: 150], []}, +                {:meta, [property: "og:image:height", content: 150], []} +                | acc +              ] + +            "video" -> +              [ +                {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []} +                | acc +              ] + +            _ -> +              acc +          end +        end) + +      acc ++ rendered_tags +    end) +  end + +  defp scrub_html_and_truncate(%{data: %{"content" => content}} = object) do +    content +    # html content comes from DB already encoded, decode first and scrub after +    |> HtmlEntities.decode() +    |> String.replace(~r/<br\s?\/?>/, " ") +    |> HTML.get_cached_stripped_html_for_object(object, __MODULE__) +    |> Formatter.demojify() +    |> Formatter.truncate() +  end + +  defp scrub_html_and_truncate(content) when is_binary(content) do +    content +    # html content comes from DB already encoded, decode first and scrub after +    |> HtmlEntities.decode() +    |> String.replace(~r/<br\s?\/?>/, " ") +    |> HTML.strip_tags() +    |> Formatter.demojify() +    |> Formatter.truncate() +  end + +  defp attachment_url(url) do +    MediaProxy.url(url) +  end + +  defp user_name_string(user) do +    "#{user.name} " <> +      if user.local do +        "(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})" +      else +        "(@#{user.nickname})" +      end +  end +end diff --git a/lib/pleroma/web/metadata/provider.ex b/lib/pleroma/web/metadata/provider.ex new file mode 100644 index 000000000..197fb2a77 --- /dev/null +++ b/lib/pleroma/web/metadata/provider.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.Provider do +  @callback build_tags(map()) :: list() +end diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex new file mode 100644 index 000000000..32b979357 --- /dev/null +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.TwitterCard do +  alias Pleroma.Web.Metadata.Providers.Provider +  alias Pleroma.Web.Metadata + +  @behaviour Provider + +  @impl Provider +  def build_tags(%{object: object}) do +    if Metadata.activity_nsfw?(object) or object.data["attachment"] == [] do +      build_tags(nil) +    else +      case find_first_acceptable_media_type(object) do +        "image" -> +          [{:meta, [property: "twitter:card", content: "summary_large_image"], []}] + +        "audio" -> +          [{:meta, [property: "twitter:card", content: "player"], []}] + +        "video" -> +          [{:meta, [property: "twitter:card", content: "player"], []}] + +        _ -> +          build_tags(nil) +      end +    end +  end + +  @impl Provider +  def build_tags(_) do +    [{:meta, [property: "twitter:card", content: "summary"], []}] +  end + +  def find_first_acceptable_media_type(%{data: %{"attachment" => attachment}}) do +    Enum.find_value(attachment, fn attachment -> +      Enum.find_value(attachment["url"], fn url -> +        Enum.find(["image", "audio", "video"], fn media_type -> +          String.starts_with?(url["mediaType"], media_type) +        end) +      end) +    end) +  end +end diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 11b97164d..f4867d05b 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -5,10 +5,11 @@  defmodule Pleroma.Web.Nodeinfo.NodeinfoController do    use Pleroma.Web, :controller +  alias Pleroma.Config +  alias Pleroma.Repo    alias Pleroma.Stats +  alias Pleroma.User    alias Pleroma.Web -  alias Pleroma.{User, Repo} -  alias Pleroma.Config    alias Pleroma.Web.ActivityPub.MRF    plug(Pleroma.Web.FederatingPlug) @@ -19,6 +20,10 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do          %{            rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",            href: Web.base_url() <> "/nodeinfo/2.0.json" +        }, +        %{ +          rel: "http://nodeinfo.diaspora.software/ns/schema/2.1", +          href: Web.base_url() <> "/nodeinfo/2.1.json"          }        ]      } @@ -26,8 +31,9 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do      json(conn, response)    end -  # Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json -  def nodeinfo(conn, %{"version" => "2.0"}) do +  # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field +  # under software. +  def raw_nodeinfo do      instance = Application.get_env(:pleroma, :instance)      media_proxy = Application.get_env(:pleroma, :media_proxy)      suggestions = Application.get_env(:pleroma, :suggestions) @@ -39,6 +45,33 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do        Application.get_env(:pleroma, :mrf_simple)        |> Enum.into(%{}) +    # This horror is needed to convert regex sigils to strings +    mrf_keyword = +      Application.get_env(:pleroma, :mrf_keyword, []) +      |> Enum.map(fn {key, value} -> +        {key, +         Enum.map(value, fn +           {pattern, replacement} -> +             %{ +               "pattern" => +                 if not is_binary(pattern) do +                   inspect(pattern) +                 else +                   pattern +                 end, +               "replacement" => replacement +             } + +           pattern -> +             if not is_binary(pattern) do +               inspect(pattern) +             else +               pattern +             end +         end)} +      end) +      |> Enum.into(%{}) +      mrf_policies =        MRF.get_policies()        |> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end) @@ -61,13 +94,12 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do        Config.get([:mrf_user_allowlist], [])        |> Enum.into(%{}, fn {k, v} -> {k, length(v)} end) -    mrf_transparency = Keyword.get(instance, :mrf_transparency) -      federation_response = -      if mrf_transparency do +      if Keyword.get(instance, :mrf_transparency) do          %{            mrf_policies: mrf_policies,            mrf_simple: mrf_simple, +          mrf_keyword: mrf_keyword,            mrf_user_allowlist: mrf_user_allowlist,            quarantined_instances: quarantined          } @@ -98,10 +130,10 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do        ]        |> Enum.filter(& &1) -    response = %{ +    %{        version: "2.0",        software: %{ -        name: Pleroma.Application.name(), +        name: Pleroma.Application.name() |> String.downcase(),          version: Pleroma.Application.version()        },        protocols: ["ostatus", "activitypub"], @@ -142,12 +174,37 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do          restrictedNicknames: Pleroma.Config.get([Pleroma.User, :restricted_nicknames])        }      } +  end +  # Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json +  # and https://github.com/jhass/nodeinfo/blob/master/schemas/2.1/schema.json +  def nodeinfo(conn, %{"version" => "2.0"}) do      conn      |> put_resp_header(        "content-type",        "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"      ) +    |> json(raw_nodeinfo()) +  end + +  def nodeinfo(conn, %{"version" => "2.1"}) do +    raw_response = raw_nodeinfo() + +    updated_software = +      raw_response +      |> Map.get(:software) +      |> Map.put(:repository, Pleroma.Application.repository()) + +    response = +      raw_response +      |> Map.put(:software, updated_software) +      |> Map.put(:version, "2.1") + +    conn +    |> put_resp_header( +      "content-type", +      "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.1#; charset=utf-8" +    )      |> json(response)    end diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex index 967ac04b5..3e8acde31 100644 --- a/lib/pleroma/web/oauth/app.ex +++ b/lib/pleroma/web/oauth/app.ex @@ -4,7 +4,7 @@  defmodule Pleroma.Web.OAuth.App do    use Ecto.Schema -  import Ecto.{Changeset} +  import Ecto.Changeset    schema "apps" do      field(:client_name, :string) diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index cc4b74bc5..75c9ab9aa 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -5,16 +5,19 @@  defmodule Pleroma.Web.OAuth.Authorization do    use Ecto.Schema -  alias Pleroma.{User, Repo} -  alias Pleroma.Web.OAuth.{Authorization, App} +  alias Pleroma.User +  alias Pleroma.Repo +  alias Pleroma.Web.OAuth.Authorization +  alias Pleroma.Web.OAuth.App -  import Ecto.{Changeset, Query} +  import Ecto.Changeset +  import Ecto.Query    schema "oauth_authorizations" do      field(:token, :string)      field(:valid_until, :naive_datetime)      field(:used, :boolean, default: false) -    belongs_to(:user, Pleroma.User) +    belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)      belongs_to(:app, App)      timestamps() diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex index 1eeda3d24..f0fe3b578 100644 --- a/lib/pleroma/web/oauth/fallback_controller.ex +++ b/lib/pleroma/web/oauth/fallback_controller.ex @@ -9,7 +9,8 @@ defmodule Pleroma.Web.OAuth.FallbackController do    # No user/password    def call(conn, _) do      conn +    |> put_status(:unauthorized)      |> put_flash(:error, "Invalid Username/Password") -    |> OAuthController.authorize(conn.params) +    |> OAuthController.authorize(conn.params["authorization"])    end  end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 4d4e85836..e4d0601f8 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -5,8 +5,11 @@  defmodule Pleroma.Web.OAuth.OAuthController do    use Pleroma.Web, :controller -  alias Pleroma.Web.OAuth.{Authorization, Token, App} -  alias Pleroma.{Repo, User} +  alias Pleroma.Web.OAuth.Authorization +  alias Pleroma.Web.OAuth.Token +  alias Pleroma.Web.OAuth.App +  alias Pleroma.Repo +  alias Pleroma.User    alias Comeonin.Pbkdf2    plug(:fetch_session) @@ -37,6 +40,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do           true <- Pbkdf2.checkpw(password, user.password_hash),           {:auth_active, true} <- {:auth_active, User.auth_active?(user)},           %App{} = app <- Repo.get_by(App, client_id: client_id), +         true <- redirect_uri in String.split(app.redirect_uris),           {:ok, auth} <- Authorization.create_authorization(app, user) do        # Special case: Local MastodonFE.        redirect_uri = diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index f0ebc63f6..b0bbeeb69 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -7,14 +7,17 @@ defmodule Pleroma.Web.OAuth.Token do    import Ecto.Query -  alias Pleroma.{User, Repo} -  alias Pleroma.Web.OAuth.{Token, App, Authorization} +  alias Pleroma.User +  alias Pleroma.Repo +  alias Pleroma.Web.OAuth.Token +  alias Pleroma.Web.OAuth.App +  alias Pleroma.Web.OAuth.Authorization    schema "oauth_tokens" do      field(:token, :string)      field(:refresh_token, :string)      field(:valid_until, :naive_datetime) -    belongs_to(:user, Pleroma.User) +    belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)      belongs_to(:app, App)      timestamps() diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex index 94b1a7ad1..9e1f24bc4 100644 --- a/lib/pleroma/web/ostatus/activity_representer.ex +++ b/lib/pleroma/web/ostatus/activity_representer.ex @@ -3,8 +3,11 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.OStatus.ActivityRepresenter do -  alias Pleroma.{Activity, User, Object} +  alias Pleroma.Activity +  alias Pleroma.User +  alias Pleroma.Object    alias Pleroma.Web.OStatus.UserRepresenter +    require Logger    defp get_href(id) do @@ -183,7 +186,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do      _in_reply_to = get_in_reply_to(activity.data)      author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] -    retweeted_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) +    retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"])      retweeted_user = User.get_cached_by_ap_id(retweeted_activity.data["actor"])      retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true) diff --git a/lib/pleroma/web/ostatus/feed_representer.ex b/lib/pleroma/web/ostatus/feed_representer.ex index 934d4042f..025d4731c 100644 --- a/lib/pleroma/web/ostatus/feed_representer.ex +++ b/lib/pleroma/web/ostatus/feed_representer.ex @@ -3,10 +3,11 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.OStatus.FeedRepresenter do -  alias Pleroma.Web.OStatus -  alias Pleroma.Web.OStatus.{UserRepresenter, ActivityRepresenter}    alias Pleroma.User +  alias Pleroma.Web.OStatus    alias Pleroma.Web.MediaProxy +  alias Pleroma.Web.OStatus.ActivityRepresenter +  alias Pleroma.Web.OStatus.UserRepresenter    def to_simple_form(user, activities, _users) do      most_recent_update = diff --git a/lib/pleroma/web/ostatus/handlers/follow_handler.ex b/lib/pleroma/web/ostatus/handlers/follow_handler.ex index becdf2fbf..91ad4bc40 100644 --- a/lib/pleroma/web/ostatus/handlers/follow_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/follow_handler.ex @@ -3,7 +3,8 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.OStatus.FollowHandler do -  alias Pleroma.Web.{XML, OStatus} +  alias Pleroma.Web.XML +  alias Pleroma.Web.OStatus    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.User diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex index 5aeed46f0..c2e585cac 100644 --- a/lib/pleroma/web/ostatus/handlers/note_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/note_handler.ex @@ -4,8 +4,10 @@  defmodule Pleroma.Web.OStatus.NoteHandler do    require Logger -  alias Pleroma.Web.{XML, OStatus} -  alias Pleroma.{Object, Activity} +  alias Pleroma.Web.OStatus +  alias Pleroma.Web.XML +  alias Pleroma.Activity +  alias Pleroma.Object    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.CommonAPI @@ -86,7 +88,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do    end    def fetch_replied_to_activity(entry, inReplyTo) do -    with %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(inReplyTo) do +    with %Activity{} = activity <- Activity.get_create_by_object_ap_id(inReplyTo) do        activity      else        _e -> @@ -103,7 +105,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do    # TODO: Clean this up a bit.    def handle_note(entry, doc \\ nil) do      with id <- XML.string_from_xpath("//id", entry), -         activity when is_nil(activity) <- Activity.get_create_activity_by_object_ap_id(id), +         activity when is_nil(activity) <- Activity.get_create_by_object_ap_id(id),           [author] <- :xmerl_xpath.string('//author[1]', doc),           {:ok, actor} <- OStatus.find_make_or_update_user(author),           content_html <- OStatus.get_content(entry), diff --git a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex b/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex index 1c64f3c3d..c9085894d 100644 --- a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex @@ -3,7 +3,8 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.OStatus.UnfollowHandler do -  alias Pleroma.Web.{XML, OStatus} +  alias Pleroma.Web.XML +  alias Pleroma.Web.OStatus    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.User diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index bb28cd786..b4f5761ac 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -9,11 +9,19 @@ defmodule Pleroma.Web.OStatus do    import Pleroma.Web.XML    require Logger -  alias Pleroma.{Repo, User, Web, Object, Activity} +  alias Pleroma.Repo +  alias Pleroma.User +  alias Pleroma.Web +  alias Pleroma.Object +  alias Pleroma.Activity    alias Pleroma.Web.ActivityPub.ActivityPub -  alias Pleroma.Web.{WebFinger, Websub} -  alias Pleroma.Web.OStatus.{FollowHandler, UnfollowHandler, NoteHandler, DeleteHandler}    alias Pleroma.Web.ActivityPub.Transmogrifier +  alias Pleroma.Web.WebFinger +  alias Pleroma.Web.Websub +  alias Pleroma.Web.OStatus.FollowHandler +  alias Pleroma.Web.OStatus.UnfollowHandler +  alias Pleroma.Web.OStatus.NoteHandler +  alias Pleroma.Web.OStatus.DeleteHandler    def is_representable?(%Activity{data: data}) do      object = Object.normalize(data["object"]) @@ -48,6 +56,9 @@ defmodule Pleroma.Web.OStatus do    def handle_incoming(xml_string) do      with doc when doc != :error <- parse_document(xml_string) do +      with {:ok, actor_user} <- find_make_or_update_user(doc), +           do: Pleroma.Instances.set_reachable(actor_user.ap_id) +        entries = :xmerl_xpath.string('//entry', doc)        activities = @@ -148,7 +159,7 @@ defmodule Pleroma.Web.OStatus do      Logger.debug("Trying to get entry from db")      with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry), -         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do        {:ok, activity}      else        _ -> diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 9b600737f..db4c8f4da 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -5,22 +5,30 @@  defmodule Pleroma.Web.OStatus.OStatusController do    use Pleroma.Web, :controller -  alias Pleroma.{User, Activity, Object} -  alias Pleroma.Web.OStatus.{FeedRepresenter, ActivityRepresenter} -  alias Pleroma.Repo -  alias Pleroma.Web.{OStatus, Federator} -  alias Pleroma.Web.XML -  alias Pleroma.Web.ActivityPub.ObjectView -  alias Pleroma.Web.ActivityPub.ActivityPubController +  alias Pleroma.Activity +  alias Pleroma.Object +  alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.ActivityPub.ActivityPubController +  alias Pleroma.Web.ActivityPub.ObjectView +  alias Pleroma.Web.OStatus.ActivityRepresenter +  alias Pleroma.Web.OStatus.FeedRepresenter +  alias Pleroma.Web.Federator +  alias Pleroma.Web.OStatus +  alias Pleroma.Web.XML    plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming]) +    action_fallback(:errors)    def feed_redirect(conn, %{"nickname" => nickname}) do      case get_format(conn) do        "html" -> -        Fallback.RedirectController.redirector(conn, nil) +        with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do +          Fallback.RedirectController.redirector_with_meta(conn, %{user: user}) +        else +          nil -> {:error, :not_found} +        end        "activity+json" ->          ActivityPubController.call(conn, :user) @@ -90,8 +98,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do        ActivityPubController.call(conn, :object)      else        with id <- o_status_url(conn, :object, uuid), -           {_, %Activity{} = activity} <- -             {:activity, Activity.get_create_activity_by_object_ap_id(id)}, +           {_, %Activity{} = activity} <- {:activity, Activity.get_create_by_object_ap_id(id)},             {_, true} <- {:public?, ActivityPub.is_public?(activity)},             %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do          case get_format(conn) do @@ -112,45 +119,65 @@ defmodule Pleroma.Web.OStatus.OStatusController do    end    def activity(conn, %{"uuid" => uuid}) do -    with id <- o_status_url(conn, :activity, uuid), -         {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, -         {_, true} <- {:public?, ActivityPub.is_public?(activity)}, -         %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do -      case format = get_format(conn) do -        "html" -> redirect(conn, to: "/notice/#{activity.id}") -        _ -> represent_activity(conn, format, activity, user) -      end +    if get_format(conn) == "activity+json" do +      ActivityPubController.call(conn, :activity)      else -      {:public?, false} -> -        {:error, :not_found} +      with id <- o_status_url(conn, :activity, uuid), +           {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, +           {_, true} <- {:public?, ActivityPub.is_public?(activity)}, +           %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do +        case format = get_format(conn) do +          "html" -> redirect(conn, to: "/notice/#{activity.id}") +          _ -> represent_activity(conn, format, activity, user) +        end +      else +        {:public?, false} -> +          {:error, :not_found} -      {:activity, nil} -> -        {:error, :not_found} +        {:activity, nil} -> +          {:error, :not_found} -      e -> -        e +        e -> +          e +      end      end    end    def notice(conn, %{"id" => id}) do -    with {_, %Activity{} = activity} <- {:activity, Repo.get(Activity, id)}, +    with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(id)},           {_, true} <- {:public?, ActivityPub.is_public?(activity)},           %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do        case format = get_format(conn) do          "html" -> -          conn -          |> put_resp_content_type("text/html") -          |> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html")) +          if activity.data["type"] == "Create" do +            %Object{} = object = Object.normalize(activity.data["object"]) + +            Fallback.RedirectController.redirector_with_meta(conn, %{ +              object: object, +              url: +                Pleroma.Web.Router.Helpers.o_status_url( +                  Pleroma.Web.Endpoint, +                  :notice, +                  activity.id +                ), +              user: user +            }) +          else +            Fallback.RedirectController.redirector(conn, nil) +          end          _ ->            represent_activity(conn, format, activity, user)        end      else        {:public?, false} -> -        {:error, :not_found} +        conn +        |> put_status(404) +        |> Fallback.RedirectController.redirector(nil, 404)        {:activity, nil} -> -        {:error, :not_found} +        conn +        |> Fallback.RedirectController.redirector(nil, 404)        e ->          e diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex index ffd2aac91..ddd4fe037 100644 --- a/lib/pleroma/web/push/push.ex +++ b/lib/pleroma/web/push/push.ex @@ -5,7 +5,8 @@  defmodule Pleroma.Web.Push do    use GenServer -  alias Pleroma.{Repo, User} +  alias Pleroma.Repo +  alias Pleroma.User    alias Pleroma.Web.Push.Subscription    require Logger diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex index 82b30950c..242e30910 100644 --- a/lib/pleroma/web/push/subscription.ex +++ b/lib/pleroma/web/push/subscription.ex @@ -4,13 +4,16 @@  defmodule Pleroma.Web.Push.Subscription do    use Ecto.Schema +    import Ecto.Changeset -  alias Pleroma.{Repo, User} + +  alias Pleroma.Repo +  alias Pleroma.User    alias Pleroma.Web.OAuth.Token    alias Pleroma.Web.Push.Subscription    schema "push_subscriptions" do -    belongs_to(:user, User) +    belongs_to(:user, User, type: Pleroma.FlakeId)      belongs_to(:token, Token)      field(:endpoint, :string)      field(:key_p256dh, :string) diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex new file mode 100644 index 000000000..abb1cf7f2 --- /dev/null +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright _ 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Helpers do +  alias Pleroma.Activity +  alias Pleroma.Object +  alias Pleroma.HTML +  alias Pleroma.Web.RichMedia.Parser + +  def fetch_data_for_activity(%Activity{} = activity) do +    with true <- Pleroma.Config.get([:rich_media, :enabled]), +         %Object{} = object <- Object.normalize(activity.data["object"]), +         {:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]), +         {:ok, rich_media} <- Parser.parse(page_url) do +      %{page_url: page_url, rich_media: rich_media} +    else +      _ -> %{} +    end +  end +end diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex new file mode 100644 index 000000000..4341141df --- /dev/null +++ b/lib/pleroma/web/rich_media/parser.ex @@ -0,0 +1,75 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser do +  @parsers [ +    Pleroma.Web.RichMedia.Parsers.OGP, +    Pleroma.Web.RichMedia.Parsers.TwitterCard, +    Pleroma.Web.RichMedia.Parsers.OEmbed +  ] + +  @hackney_options [ +    pool: :media, +    timeout: 2_000, +    recv_timeout: 2_000, +    max_body: 2_000_000 +  ] + +  def parse(nil), do: {:error, "No URL provided"} + +  if Mix.env() == :test do +    def parse(url), do: parse_url(url) +  else +    def parse(url) do +      try do +        Cachex.fetch!(:rich_media_cache, url, fn _ -> +          {:commit, parse_url(url)} +        end) +      rescue +        e -> +          {:error, "Cachex error: #{inspect(e)}"} +      end +    end +  end + +  defp parse_url(url) do +    try do +      {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options) + +      html |> maybe_parse() |> clean_parsed_data() |> check_parsed_data() +    rescue +      e -> +        {:error, "Parsing error: #{inspect(e)}"} +    end +  end + +  defp maybe_parse(html) do +    Enum.reduce_while(@parsers, %{}, fn parser, acc -> +      case parser.parse(html, acc) do +        {:ok, data} -> {:halt, data} +        {:error, _msg} -> {:cont, acc} +      end +    end) +  end + +  defp check_parsed_data(%{title: title} = data) when is_binary(title) and byte_size(title) > 0 do +    {:ok, data} +  end + +  defp check_parsed_data(data) do +    {:error, "Found metadata was invalid or incomplete: #{inspect(data)}"} +  end + +  defp clean_parsed_data(data) do +    data +    |> Enum.reject(fn {key, val} -> +      with {:ok, _} <- Jason.encode(%{key => val}) do +        false +      else +        _ -> true +      end +    end) +    |> Map.new() +  end +end diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex new file mode 100644 index 000000000..4a7c5eae0 --- /dev/null +++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex @@ -0,0 +1,30 @@ +defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do +  def parse(html, data, prefix, error_message, key_name, value_name \\ "content") do +    with elements = [_ | _] <- get_elements(html, key_name, prefix), +         meta_data = +           Enum.reduce(elements, data, fn el, acc -> +             attributes = normalize_attributes(el, prefix, key_name, value_name) + +             Map.merge(acc, attributes) +           end) do +      {:ok, meta_data} +    else +      _e -> {:error, error_message} +    end +  end + +  defp get_elements(html, key_name, prefix) do +    html |> Floki.find("meta[#{key_name}^='#{prefix}:']") +  end + +  defp normalize_attributes(html_node, prefix, key_name, value_name) do +    {_tag, attributes, _children} = html_node + +    data = +      Enum.into(attributes, %{}, fn {name, value} -> +        {name, String.trim_leading(value, "#{prefix}:")} +      end) + +    %{String.to_atom(data[key_name]) => data[value_name]} +  end +end diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex new file mode 100644 index 000000000..2530b8c9d --- /dev/null +++ b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex @@ -0,0 +1,31 @@ +defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do +  def parse(html, _data) do +    with elements = [_ | _] <- get_discovery_data(html), +         {:ok, oembed_url} <- get_oembed_url(elements), +         {:ok, oembed_data} <- get_oembed_data(oembed_url) do +      {:ok, oembed_data} +    else +      _e -> {:error, "No OEmbed data found"} +    end +  end + +  defp get_discovery_data(html) do +    html |> Floki.find("link[type='application/json+oembed']") +  end + +  defp get_oembed_url(nodes) do +    {"link", attributes, _children} = nodes |> hd() + +    {:ok, Enum.into(attributes, %{})["href"]} +  end + +  defp get_oembed_data(url) do +    {:ok, %Tesla.Env{body: json}} = Pleroma.HTTP.get(url, [], adapter: [pool: :media]) + +    {:ok, data} = Jason.decode(json) + +    data = data |> Map.new(fn {k, v} -> {String.to_atom(k), v} end) + +    {:ok, data} +  end +end diff --git a/lib/pleroma/web/rich_media/parsers/ogp.ex b/lib/pleroma/web/rich_media/parsers/ogp.ex new file mode 100644 index 000000000..0e1a0e719 --- /dev/null +++ b/lib/pleroma/web/rich_media/parsers/ogp.ex @@ -0,0 +1,11 @@ +defmodule Pleroma.Web.RichMedia.Parsers.OGP do +  def parse(html, data) do +    Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse( +      html, +      data, +      "og", +      "No OGP metadata found", +      "property" +    ) +  end +end diff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex new file mode 100644 index 000000000..a317c3e78 --- /dev/null +++ b/lib/pleroma/web/rich_media/parsers/twitter_card.ex @@ -0,0 +1,11 @@ +defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do +  def parse(html, data) do +    Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse( +      html, +      data, +      "twitter", +      "No twitter card metadata found", +      "name" +    ) +  end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 1f929ee21..d66a1c2a1 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -107,6 +107,11 @@ defmodule Pleroma.Web.Router do      get("/captcha", UtilController, :captcha)    end +  scope "/api/pleroma", Pleroma.Web do +    pipe_through(:pleroma_api) +    post("/uploader_callback/:upload_path", UploaderController, :callback) +  end +    scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do      pipe_through(:admin_api)      delete("/user", AdminAPIController, :user_delete) @@ -180,6 +185,7 @@ defmodule Pleroma.Web.Router do      get("/timelines/direct", MastodonAPIController, :dm_timeline)      get("/favourites", MastodonAPIController, :favourites) +    get("/bookmarks", MastodonAPIController, :bookmarks)      post("/statuses", MastodonAPIController, :post_status)      delete("/statuses/:id", MastodonAPIController, :delete_status) @@ -188,6 +194,12 @@ defmodule Pleroma.Web.Router do      post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)      post("/statuses/:id/favourite", MastodonAPIController, :fav_status)      post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status) +    post("/statuses/:id/pin", MastodonAPIController, :pin_status) +    post("/statuses/:id/unpin", MastodonAPIController, :unpin_status) +    post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status) +    post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status) +    post("/statuses/:id/mute", MastodonAPIController, :mute_conversation) +    post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)      post("/notifications/clear", MastodonAPIController, :clear_notifications)      post("/notifications/dismiss", MastodonAPIController, :dismiss_notification) @@ -245,7 +257,7 @@ defmodule Pleroma.Web.Router do      get("/statuses/:id", MastodonAPIController, :get_status)      get("/statuses/:id/context", MastodonAPIController, :get_context) -    get("/statuses/:id/card", MastodonAPIController, :empty_object) +    get("/statuses/:id/card", MastodonAPIController, :status_card)      get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)      get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by) @@ -271,6 +283,7 @@ defmodule Pleroma.Web.Router do      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    scope "/api", Pleroma.Web do @@ -347,6 +360,9 @@ defmodule Pleroma.Web.Router do      post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)      post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post) +    post("/statuses/pin/:id", TwitterAPI.Controller, :pin) +    post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin) +      get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)      post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)      post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request) @@ -380,7 +396,11 @@ defmodule Pleroma.Web.Router do    end    pipeline :ostatus do -    plug(:accepts, ["xml", "atom", "html", "activity+json"]) +    plug(:accepts, ["html", "xml", "atom", "activity+json"]) +  end + +  pipeline :oembed do +    plug(:accepts, ["json", "xml"])    end    scope "/", Pleroma.Web do @@ -398,6 +418,12 @@ defmodule Pleroma.Web.Router do      post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)    end +  scope "/", Pleroma.Web do +    pipe_through(:oembed) + +    get("/oembed", OEmbed.OEmbedController, :url) +  end +    pipeline :activitypub do      plug(:accepts, ["activity+json"])      plug(Pleroma.Web.Plugs.HTTPSignaturePlug) @@ -410,6 +436,7 @@ defmodule Pleroma.Web.Router do      get("/users/:nickname/followers", ActivityPubController, :followers)      get("/users/:nickname/following", ActivityPubController, :following)      get("/users/:nickname/outbox", ActivityPubController, :outbox) +    get("/objects/:uuid/likes", ActivityPubController, :object_likes)    end    pipeline :activitypub_client do @@ -429,6 +456,7 @@ defmodule Pleroma.Web.Router do    scope "/", Pleroma.Web.ActivityPub do      pipe_through([:activitypub_client]) +    get("/api/ap/whoami", ActivityPubController, :whoami)      get("/users/:nickname/inbox", ActivityPubController, :read_inbox)      post("/users/:nickname/outbox", ActivityPubController, :update_outbox)    end @@ -440,8 +468,8 @@ defmodule Pleroma.Web.Router do    scope "/", Pleroma.Web.ActivityPub do      pipe_through(:activitypub) -    post("/users/:nickname/inbox", ActivityPubController, :inbox)      post("/inbox", ActivityPubController, :inbox) +    post("/users/:nickname/inbox", ActivityPubController, :inbox)    end    scope "/.well-known", Pleroma.Web do @@ -484,6 +512,7 @@ defmodule Pleroma.Web.Router do    scope "/", Fallback do      get("/registration/:token", RedirectController, :registration_page) +    get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)      get("/*path", RedirectController, :redirector)      options("/*path", RedirectController, :empty) @@ -492,11 +521,36 @@ end  defmodule Fallback.RedirectController do    use Pleroma.Web, :controller +  alias Pleroma.Web.Metadata +  alias Pleroma.User -  def redirector(conn, _params) do +  def redirector(conn, _params, code \\ 200) do      conn      |> put_resp_content_type("text/html") -    |> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html")) +    |> send_file(code, index_file_path()) +  end + +  def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do +    with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do +      redirector_with_meta(conn, %{user: user}) +    else +      nil -> +        redirector(conn, params) +    end +  end + +  def redirector_with_meta(conn, params) do +    {:ok, index_content} = File.read(index_file_path()) +    tags = Metadata.build_tags(params) +    response = String.replace(index_content, "<!--server-generated-meta-->", tags) + +    conn +    |> put_resp_content_type("text/html") +    |> send_resp(200, response) +  end + +  def index_file_path do +    Pleroma.Plugs.InstanceStatic.file_path("index.html")    end    def registration_page(conn, params) do diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index e41657da1..a5a9e16c6 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -6,9 +6,12 @@ defmodule Pleroma.Web.Salmon do    @httpoison Application.get_env(:pleroma, :httpoison)    use Bitwise + +  alias Pleroma.Instances +  alias Pleroma.User    alias Pleroma.Web.XML    alias Pleroma.Web.OStatus.ActivityRepresenter -  alias Pleroma.User +    require Logger    def decode(salmon) do @@ -161,25 +164,31 @@ defmodule Pleroma.Web.Salmon do      |> Enum.filter(fn user -> user && !user.local end)    end -  # push an activity to remote accounts -  # -  defp send_to_user(%{info: %{salmon: salmon}}, feed, poster), -    do: send_to_user(salmon, feed, poster) +  @doc "Pushes an activity to remote account." +  def send_to_user(%{recipient: %{info: %{salmon: salmon}}} = params), +    do: send_to_user(Map.put(params, :recipient, salmon)) -  defp send_to_user(url, feed, poster) when is_binary(url) do -    with {:ok, %{status: code}} <- +  def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is_binary(url) do +    with {:ok, %{status: code}} when code in 200..299 <-             poster.(               url,               feed,               [{"Content-Type", "application/magic-envelope+xml"}]             ) do +      if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], +        do: Instances.set_reachable(url) +        Logger.debug(fn -> "Pushed to #{url}, code #{code}" end) +      :ok      else -      e -> Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end) +      e -> +        unless params[:unreachable_since], do: Instances.set_reachable(url) +        Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end) +        :error      end    end -  defp send_to_user(_, _, _), do: nil +  def send_to_user(_), do: :noop    @supported_activities [      "Create", @@ -209,12 +218,23 @@ defmodule Pleroma.Web.Salmon do        {:ok, private, _} = keys_from_pem(keys)        {:ok, feed} = encode(private, feed) -      remote_users(activity) +      remote_users = remote_users(activity) + +      salmon_urls = Enum.map(remote_users, & &1.info.salmon) +      reachable_urls_metadata = Instances.filter_reachable(salmon_urls) +      reachable_urls = Map.keys(reachable_urls_metadata) + +      remote_users +      |> Enum.filter(&(&1.info.salmon in reachable_urls))        |> Enum.each(fn remote_user -> -        Task.start(fn -> -          Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end) -          send_to_user(remote_user, feed, poster) -        end) +        Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end) + +        Pleroma.Web.Federator.enqueue(:publish_single_salmon, %{ +          recipient: remote_user, +          feed: feed, +          poster: poster, +          unreachable_since: reachable_urls_metadata[remote_user.info.salmon] +        })        end)      end    end diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 3136b1b9d..4de7608e4 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -5,7 +5,11 @@  defmodule Pleroma.Web.Streamer do    use GenServer    require Logger -  alias Pleroma.{User, Notification, Activity, Object, Repo} +  alias Pleroma.User +  alias Pleroma.Notification +  alias Pleroma.Activity +  alias Pleroma.Object +  alias Pleroma.Repo    alias Pleroma.Web.ActivityPub.ActivityPub    @keepalive_interval :timer.seconds(30) @@ -205,6 +209,15 @@ defmodule Pleroma.Web.Streamer do      end)    end +  def push_to_socket(topics, topic, %Activity{id: id, data: %{"type" => "Delete"}}) do +    Enum.each(topics[topic] || [], fn socket -> +      send( +        socket.transport_pid, +        {:text, %{event: "delete", payload: to_string(id)} |> Jason.encode!()} +      ) +    end) +  end +    def push_to_socket(topics, topic, item) do      Enum.each(topics[topic] || [], fn socket ->        # Get the current user so we have up-to-date blocks etc. diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex index 2e96c1509..520e4b3d5 100644 --- a/lib/pleroma/web/templates/layout/app.html.eex +++ b/lib/pleroma/web/templates/layout/app.html.eex @@ -1,7 +1,8 @@  <!DOCTYPE html>  <html>    <head> -    <meta charset=utf-8 /> +    <meta charset="utf-8" /> +    <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />      <title>      <%= Application.get_env(:pleroma, :instance)[:name] %>      </title> @@ -66,6 +67,32 @@          font-weight: 500;          font-size: 16px;        } + +      .alert-danger { +        box-sizing: border-box; +        width: 100%; +        color: #D8000C; +        background-color: #FFD2D2; +        border-radius: 4px; +        border: none; +        padding: 10px; +        margin-top: 20px; +        font-weight: 500; +        font-size: 16px; +      } + +      .alert-info { +        box-sizing: border-box; +        width: 100%; +        color: #00529B; +        background-color: #BDE5F8; +        border-radius: 4px; +        border: none; +        padding: 10px; +        margin-top: 20px; +        font-weight: 500; +        font-size: 16px; +      }      </style>    </head>    <body> diff --git a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex b/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex index 0862412ea..9a725e420 100644 --- a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex +++ b/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex @@ -1,23 +1,28 @@  <!DOCTYPE html>  <html lang='en'>  <head> +<meta charset='utf-8'> +<meta content='width=device-width, initial-scale=1' name='viewport'>  <title>  <%= Application.get_env(:pleroma, :instance)[:name] %>  </title> -<meta charset='utf-8'> -<meta content='width=device-width, initial-scale=1' name='viewport'>  <link rel="icon" type="image/png" href="/favicon.png"/> -<link rel="stylesheet" media="all" href="/packs/common.css" /> -<link rel="stylesheet" media="all" href="/packs/default.css" /> +<script crossorigin='anonymous' src="/packs/locales.js"></script> +<script crossorigin='anonymous' src="/packs/locales/glitch/en.js"></script> -<script src="/packs/common.js"></script> -<script src="/packs/locale_en.js"></script> -<link as='script' crossorigin='anonymous' href='/packs/features/getting_started.js' rel='preload'> -<link as='script' crossorigin='anonymous' href='/packs/features/compose.js' rel='preload'> -<link as='script' crossorigin='anonymous' href='/packs/features/home_timeline.js' rel='preload'> -<link as='script' crossorigin='anonymous' href='/packs/features/notifications.js' rel='preload'> +<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/getting_started.js'> +<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/compose.js'> +<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/home_timeline.js'> +<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/notifications.js'>  <script id='initial-state' type='application/json'><%= raw @initial_state %></script> -<script src="/packs/application.js"></script> + +<script src="/packs/core/common.js"></script> +<link rel="stylesheet" media="all" href="/packs/core/common.css" /> + +<script src="/packs/flavours/glitch/common.js"></script> +<link rel="stylesheet" media="all" href="/packs/flavours/glitch/common.css" /> + +<script src="/packs/flavours/glitch/home.js"></script>  </head>  <body class='app-body no-reduce-motion system-font'>    <div class='app-holder' data-props='{"locale":"en"}' id='mastodon'> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index de2241ec9..32c458f0c 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -1,5 +1,9 @@ +<%= if get_flash(@conn, :info) do %>  <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p> +<% end %> +<%= if get_flash(@conn, :error) do %>  <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p> +<% end %>  <h2>OAuth Authorization</h2>  <%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>  <%= label f, :name, "Name or email" %> diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index a79072f3d..e2fdedb25 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -4,14 +4,19 @@  defmodule Pleroma.Web.TwitterAPI.UtilController do    use Pleroma.Web, :controller +    require Logger + +  alias Comeonin.Pbkdf2 +  alias Pleroma.Emoji +  alias Pleroma.PasswordResetToken +  alias Pleroma.User +  alias Pleroma.Repo    alias Pleroma.Web +  alias Pleroma.Web.CommonAPI    alias Pleroma.Web.OStatus    alias Pleroma.Web.WebFinger -  alias Pleroma.Web.CommonAPI -  alias Comeonin.Pbkdf2    alias Pleroma.Web.ActivityPub.ActivityPub -  alias Pleroma.{Repo, PasswordResetToken, User, Emoji}    def show_password_reset(conn, %{"token" => token}) do      with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), @@ -183,25 +188,31 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do            invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0")          } -        pleroma_fe = %{ -          theme: Keyword.get(instance_fe, :theme), -          background: Keyword.get(instance_fe, :background), -          logo: Keyword.get(instance_fe, :logo), -          logoMask: Keyword.get(instance_fe, :logo_mask), -          logoMargin: Keyword.get(instance_fe, :logo_margin), -          redirectRootNoLogin: Keyword.get(instance_fe, :redirect_root_no_login), -          redirectRootLogin: Keyword.get(instance_fe, :redirect_root_login), -          chatDisabled: !Keyword.get(instance_chat, :enabled), -          showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel), -          scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled), -          formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled), -          collapseMessageWithSubject: Keyword.get(instance_fe, :collapse_message_with_subject), -          hidePostStats: Keyword.get(instance_fe, :hide_post_stats), -          hideUserStats: Keyword.get(instance_fe, :hide_user_stats), -          scopeCopy: Keyword.get(instance_fe, :scope_copy), -          subjectLineBehavior: Keyword.get(instance_fe, :subject_line_behavior), -          alwaysShowSubjectInput: Keyword.get(instance_fe, :always_show_subject_input) -        } +        pleroma_fe = +          if instance_fe do +            %{ +              theme: Keyword.get(instance_fe, :theme), +              background: Keyword.get(instance_fe, :background), +              logo: Keyword.get(instance_fe, :logo), +              logoMask: Keyword.get(instance_fe, :logo_mask), +              logoMargin: Keyword.get(instance_fe, :logo_margin), +              redirectRootNoLogin: Keyword.get(instance_fe, :redirect_root_no_login), +              redirectRootLogin: Keyword.get(instance_fe, :redirect_root_login), +              chatDisabled: !Keyword.get(instance_chat, :enabled), +              showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel), +              scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled), +              formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled), +              collapseMessageWithSubject: +                Keyword.get(instance_fe, :collapse_message_with_subject), +              hidePostStats: Keyword.get(instance_fe, :hide_post_stats), +              hideUserStats: Keyword.get(instance_fe, :hide_user_stats), +              scopeCopy: Keyword.get(instance_fe, :scope_copy), +              subjectLineBehavior: Keyword.get(instance_fe, :subject_line_behavior), +              alwaysShowSubjectInput: Keyword.get(instance_fe, :always_show_subject_input) +            } +          else +            Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) +          end          managed_config = Keyword.get(instance, :managed_config) @@ -216,6 +227,14 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do      end    end +  def frontend_configurations(conn, _params) do +    config = +      Pleroma.Config.get(:frontend_configurations, %{}) +      |> Enum.into(%{}) + +    json(conn, config) +  end +    def version(conn, _params) do      version = Pleroma.Application.named_version() diff --git a/lib/pleroma/web/twitter_api/representers/activity_representer.ex b/lib/pleroma/web/twitter_api/representers/activity_representer.ex index 2a221cc66..192ab7334 100644 --- a/lib/pleroma/web/twitter_api/representers/activity_representer.ex +++ b/lib/pleroma/web/twitter_api/representers/activity_representer.ex @@ -2,16 +2,21 @@  # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>  # SPDX-License-Identifier: AGPL-3.0-only +# FIXME: Remove this module?  # THIS MODULE IS DEPRECATED! DON'T USE IT!  # USE THE Pleroma.Web.TwitterAPI.Views.ActivityView MODULE!  defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do    use Pleroma.Web.TwitterAPI.Representers.BaseRepresenter    alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter -  alias Pleroma.{Activity, User} -  alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView} -  alias Pleroma.Web.CommonAPI.Utils +  alias Pleroma.Activity    alias Pleroma.Formatter    alias Pleroma.HTML +  alias Pleroma.User +  alias Pleroma.Web.TwitterAPI.ActivityView +  alias Pleroma.Web.TwitterAPI.TwitterAPI +  alias Pleroma.Web.TwitterAPI.UserView +  alias Pleroma.Web.CommonAPI.Utils +  alias Pleroma.Web.MastodonAPI.StatusView    defp user_by_ap_id(user_list, ap_id) do      Enum.find(user_list, fn %{ap_id: user_id} -> ap_id == user_id end) @@ -153,11 +158,14 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do      announcement_count = object["announcement_count"] || 0      favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])      repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || []) +    pinned = activity.id in user.info.pinned_activities      mentions = opts[:mentioned] || []      attentions = -      activity.recipients +      [] +      |> Utils.maybe_notify_to_recipients(activity) +      |> Utils.maybe_notify_mentioned_recipients(activity)        |> Enum.map(fn ap_id -> Enum.find(mentions, fn user -> ap_id == user.ap_id end) end)        |> Enum.filter(& &1)        |> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end) @@ -181,6 +189,14 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do      reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor) +    summary = HTML.strip_tags(object["summary"]) + +    card = +      StatusView.render( +        "card.json", +        Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) +      ) +      %{        "id" => activity.id,        "uri" => activity.data["object"]["id"], @@ -202,12 +218,15 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do        "repeat_num" => announcement_count,        "favorited" => to_boolean(favorited),        "repeated" => to_boolean(repeated), +      "pinned" => pinned,        "external_url" => object["external_url"] || object["id"],        "tags" => tags,        "activity_type" => "post",        "possibly_sensitive" => possibly_sensitive,        "visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object), -      "summary" => object["summary"] +      "summary" => summary, +      "summary_html" => summary |> Formatter.emojify(object["emoji"]), +      "card" => card      }    end diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index ecf81d492..db521a3ad 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -3,8 +3,13 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.TwitterAPI.TwitterAPI do -  alias Pleroma.{UserInviteToken, User, Activity, Repo, Object} -  alias Pleroma.{UserEmail, Mailer} +  alias Pleroma.UserInviteToken +  alias Pleroma.User +  alias Pleroma.Activity +  alias Pleroma.Repo +  alias Pleroma.Object +  alias Pleroma.UserEmail +  alias Pleroma.Mailer    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.TwitterAPI.UserView    alias Pleroma.Web.CommonAPI @@ -70,28 +75,36 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do    def repeat(%User{} = user, ap_id_or_id) do      with {:ok, _announce, %{data: %{"id" => id}}} <- CommonAPI.repeat(ap_id_or_id, user), -         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do        {:ok, activity}      end    end    def unrepeat(%User{} = user, ap_id_or_id) do      with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user), -         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do        {:ok, activity}      end    end +  def pin(%User{} = user, ap_id_or_id) do +    CommonAPI.pin(ap_id_or_id, user) +  end + +  def unpin(%User{} = user, ap_id_or_id) do +    CommonAPI.unpin(ap_id_or_id, user) +  end +    def fav(%User{} = user, ap_id_or_id) do      with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user), -         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do        {:ok, activity}      end    end    def unfav(%User{} = user, ap_id_or_id) do      with {:ok, _unfav, _fav, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user), -         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do        {:ok, activity}      end    end diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 1e04b8c4b..c2f0dc2a9 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -7,12 +7,19 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    import Pleroma.Web.ControllerHelper, only: [json_response: 3] -  alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView} -  alias Pleroma.Web.CommonAPI -  alias Pleroma.{Repo, Activity, Object, User, Notification} +  alias Ecto.Changeset    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Utils -  alias Ecto.Changeset +  alias Pleroma.Web.CommonAPI +  alias Pleroma.Web.TwitterAPI.ActivityView +  alias Pleroma.Web.TwitterAPI.NotificationView +  alias Pleroma.Web.TwitterAPI.TwitterAPI +  alias Pleroma.Web.TwitterAPI.UserView +  alias Pleroma.Activity +  alias Pleroma.Object +  alias Pleroma.Notification +  alias Pleroma.Repo +  alias Pleroma.User    require Logger @@ -24,7 +31,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do      conn      |> put_view(UserView) -    |> render("show.json", %{user: user, token: token}) +    |> render("show.json", %{user: user, token: token, for: user})    end    def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do @@ -265,8 +272,6 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    id = String.to_integer(id) -      with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id),           activities <-             ActivityPub.fetch_activities_for_context(context, %{ @@ -330,48 +335,74 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def get_by_id_or_ap_id(id) do -    activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id) +    activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)      if activity.data["type"] == "Create" do        activity      else -      Activity.get_create_activity_by_object_ap_id(activity.data["object"]) +      Activity.get_create_by_object_ap_id(activity.data["object"])      end    end    def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)}, -         {:ok, activity} <- TwitterAPI.fav(user, id) do +    with {:ok, activity} <- TwitterAPI.fav(user, id) do        conn        |> put_view(ActivityView)        |> render("activity.json", %{activity: activity, for: user}) +    else +      _ -> json_reply(conn, 400, Jason.encode!(%{}))      end    end    def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)}, -         {:ok, activity} <- TwitterAPI.unfav(user, id) do +    with {:ok, activity} <- TwitterAPI.unfav(user, id) do        conn        |> put_view(ActivityView)        |> render("activity.json", %{activity: activity, for: user}) +    else +      _ -> json_reply(conn, 400, Jason.encode!(%{}))      end    end    def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)}, -         {:ok, activity} <- TwitterAPI.repeat(user, id) do +    with {:ok, activity} <- TwitterAPI.repeat(user, id) do        conn        |> put_view(ActivityView)        |> render("activity.json", %{activity: activity, for: user}) +    else +      _ -> json_reply(conn, 400, Jason.encode!(%{}))      end    end    def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)}, -         {:ok, activity} <- TwitterAPI.unrepeat(user, id) do +    with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do        conn        |> put_view(ActivityView)        |> render("activity.json", %{activity: activity, for: user}) +    else +      _ -> json_reply(conn, 400, Jason.encode!(%{})) +    end +  end + +  def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do +    with {:ok, activity} <- TwitterAPI.pin(user, id) do +      conn +      |> put_view(ActivityView) +      |> render("activity.json", %{activity: activity, for: user}) +    else +      {:error, message} -> bad_request_reply(conn, message) +      err -> err +    end +  end + +  def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do +    with {:ok, activity} <- TwitterAPI.unpin(user, id) do +      conn +      |> put_view(ActivityView) +      |> render("activity.json", %{activity: activity, for: user}) +    else +      {:error, message} -> bad_request_reply(conn, message) +      err -> err      end    end @@ -472,12 +503,14 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def followers(%{assigns: %{user: for_user}} = conn, params) do +    {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1) +      with {:ok, user} <- TwitterAPI.get_user(for_user, params), -         {:ok, followers} <- User.get_followers(user) do +         {:ok, followers} <- User.get_followers(user, page) do        followers =          cond do            for_user && user.id == for_user.id -> followers -          user.info.hide_network -> [] +          user.info.hide_followers -> []            true -> followers          end @@ -490,12 +523,14 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def friends(%{assigns: %{user: for_user}} = conn, params) do +    {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1) +      with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params), -         {:ok, friends} <- User.get_friends(user) do +         {:ok, friends} <- User.get_friends(user, page) do        friends =          cond do            for_user && user.id == for_user.id -> friends -          user.info.hide_network -> [] +          user.info.hide_follows -> []            true -> friends          end @@ -528,7 +563,6 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    def approve_friend_request(conn, %{"user_id" => uid} = _params) do      with followed <- conn.assigns[:user], -         uid when is_number(uid) <- String.to_integer(uid),           %User{} = follower <- Repo.get(User, uid),           {:ok, follower} <- User.maybe_follow(follower, followed),           %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), @@ -550,7 +584,6 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    def deny_friend_request(conn, %{"user_id" => uid} = _params) do      with followed <- conn.assigns[:user], -         uid when is_number(uid) <- String.to_integer(uid),           %User{} = follower <- Repo.get(User, uid),           %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),           {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"), @@ -592,7 +625,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    defp build_info_cng(user, params) do      info_params = -      ["no_rich_text", "locked", "hide_network"] +      ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]        |> Enum.reduce(%{}, fn key, res ->          if value = params[key] do            Map.put(res, key, value == "true") @@ -647,7 +680,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do -    users = User.search(query, true) +    users = User.search(query, true, user)      conn      |> put_view(UserView) diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index 84f35ebf9..661022afa 100644 --- a/lib/pleroma/web/twitter_api/views/activity_view.ex +++ b/lib/pleroma/web/twitter_api/views/activity_view.ex @@ -4,18 +4,18 @@  defmodule Pleroma.Web.TwitterAPI.ActivityView do    use Pleroma.Web, :view -  alias Pleroma.Web.CommonAPI.Utils -  alias Pleroma.User -  alias Pleroma.Web.TwitterAPI.UserView -  alias Pleroma.Web.TwitterAPI.ActivityView -  alias Pleroma.Web.TwitterAPI.TwitterAPI -  alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter    alias Pleroma.Activity +  alias Pleroma.Formatter    alias Pleroma.HTML    alias Pleroma.Object -  alias Pleroma.User    alias Pleroma.Repo -  alias Pleroma.Formatter +  alias Pleroma.User +  alias Pleroma.Web.CommonAPI.Utils +  alias Pleroma.Web.MastodonAPI.StatusView +  alias Pleroma.Web.TwitterAPI.ActivityView +  alias Pleroma.Web.TwitterAPI.TwitterAPI +  alias Pleroma.Web.TwitterAPI.UserView +  alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter    import Ecto.Query    require Logger @@ -94,8 +94,14 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do        ap_id == "https://www.w3.org/ns/activitystreams#Public" ->          nil +      user = User.get_cached_by_ap_id(ap_id) -> +        user + +      user = User.get_by_guessed_nickname(ap_id) -> +        user +        true -> -        User.get_cached_by_ap_id(ap_id) +        User.error_user(ap_id)      end    end @@ -108,7 +114,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do        |> Map.put(:context_ids, context_ids)        |> Map.put(:users, users) -    render_many( +    safe_render_many(        opts.activities,        ActivityView,        "activity.json", @@ -162,7 +168,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do    def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activity} = opts) do      user = get_user(activity.data["actor"], opts)      created_at = activity.data["published"] |> Utils.date_to_asctime() -    announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) +    announced_activity = Activity.get_create_by_object_ap_id(activity.data["object"])      text = "#{user.nickname} retweeted a status." @@ -186,7 +192,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do    def render("activity.json", %{activity: %{data: %{"type" => "Like"}} = activity} = opts) do      user = get_user(activity.data["actor"], opts) -    liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) +    liked_activity = Activity.get_create_by_object_ap_id(activity.data["object"])      liked_activity_id = if liked_activity, do: liked_activity.id, else: nil      created_at = @@ -227,9 +233,12 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do      announcement_count = object["announcement_count"] || 0      favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])      repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || []) +    pinned = activity.id in user.info.pinned_activities      attentions = -      activity.recipients +      [] +      |> Utils.maybe_notify_to_recipients(activity) +      |> Utils.maybe_notify_mentioned_recipients(activity)        |> Enum.map(fn ap_id -> get_user(ap_id, opts) end)        |> Enum.filter(& &1)        |> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end) @@ -245,20 +254,32 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do      html =        content -      |> HTML.get_cached_scrubbed_html_for_object(User.html_filter_policy(opts[:for]), activity) +      |> HTML.get_cached_scrubbed_html_for_object( +        User.html_filter_policy(opts[:for]), +        activity, +        __MODULE__ +      )        |> Formatter.emojify(object["emoji"])      text =        if content do          content          |> String.replace(~r/<br\s?\/?>/, "\n") -        |> HTML.get_cached_stripped_html_for_object(activity) +        |> HTML.get_cached_stripped_html_for_object(activity, __MODULE__)        end      reply_parent = Activity.get_in_reply_to_activity(activity)      reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor) +    summary = HTML.strip_tags(summary) + +    card = +      StatusView.render( +        "card.json", +        Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) +      ) +      %{        "id" => activity.id,        "uri" => activity.data["object"]["id"], @@ -280,12 +301,15 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do        "repeat_num" => announcement_count,        "favorited" => !!favorited,        "repeated" => !!repeated, +      "pinned" => pinned,        "external_url" => object["external_url"] || object["id"],        "tags" => tags,        "activity_type" => "post",        "possibly_sensitive" => possibly_sensitive, -      "visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object), -      "summary" => summary +      "visibility" => StatusView.get_visibility(object), +      "summary" => summary, +      "summary_html" => summary |> Formatter.emojify(object["emoji"]), +      "card" => card      }    end diff --git a/lib/pleroma/web/twitter_api/views/notification_view.ex b/lib/pleroma/web/twitter_api/views/notification_view.ex index d6a1c0a4d..e7c7a7496 100644 --- a/lib/pleroma/web/twitter_api/views/notification_view.ex +++ b/lib/pleroma/web/twitter_api/views/notification_view.ex @@ -4,10 +4,11 @@  defmodule Pleroma.Web.TwitterAPI.NotificationView do    use Pleroma.Web, :view -  alias Pleroma.{Notification, User} +  alias Pleroma.Notification +  alias Pleroma.User    alias Pleroma.Web.CommonAPI.Utils -  alias Pleroma.Web.TwitterAPI.UserView    alias Pleroma.Web.TwitterAPI.ActivityView +  alias Pleroma.Web.TwitterAPI.UserView    defp get_user(ap_id, opts) do      cond do diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index a8cf83613..a09450df7 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -4,11 +4,11 @@  defmodule Pleroma.Web.TwitterAPI.UserView do    use Pleroma.Web, :view -  alias Pleroma.User    alias Pleroma.Formatter +  alias Pleroma.HTML +  alias Pleroma.User    alias Pleroma.Web.CommonAPI.Utils    alias Pleroma.Web.MediaProxy -  alias Pleroma.HTML    def render("show.json", %{user: user = %User{}} = assigns) do      render_one(user, Pleroma.Web.TwitterAPI.UserView, "user.json", assigns) @@ -108,6 +108,8 @@ defmodule Pleroma.Web.TwitterAPI.UserView do        "locked" => user.info.locked,        "default_scope" => user.info.default_scope,        "no_rich_text" => user.info.no_rich_text, +      "hide_followers" => user.info.hide_followers, +      "hide_follows" => user.info.hide_follows,        "fields" => fields,        # Pleroma extension @@ -117,6 +119,12 @@ defmodule Pleroma.Web.TwitterAPI.UserView do        }      } +    data = +      if(user.info.is_admin || user.info.is_moderator, +        do: maybe_with_role(data, user, for_user), +        else: data +      ) +      if assigns[:token] do        Map.put(data, "token", token_string(assigns[:token]))      else @@ -124,6 +132,20 @@ defmodule Pleroma.Web.TwitterAPI.UserView do      end    end +  defp maybe_with_role(data, %User{id: id} = user, %User{id: id}) do +    Map.merge(data, %{"role" => role(user), "show_role" => user.info.show_role}) +  end + +  defp maybe_with_role(data, %User{info: %{show_role: true}} = user, _user) do +    Map.merge(data, %{"role" => role(user)}) +  end + +  defp maybe_with_role(data, _, _), do: data + +  defp role(%User{info: %{:is_admin => true}}), do: "admin" +  defp role(%User{info: %{:is_moderator => true}}), do: "moderator" +  defp role(_), do: "member" +    defp image_url(%{"url" => [%{"href" => href} | _]}), do: href    defp image_url(_), do: nil diff --git a/lib/pleroma/web/uploader_controller.ex b/lib/pleroma/web/uploader_controller.ex new file mode 100644 index 000000000..5d8a77346 --- /dev/null +++ b/lib/pleroma/web/uploader_controller.ex @@ -0,0 +1,25 @@ +defmodule Pleroma.Web.UploaderController do +  use Pleroma.Web, :controller + +  alias Pleroma.Uploaders.Uploader + +  def callback(conn, %{"upload_path" => upload_path} = params) do +    process_callback(conn, :global.whereis_name({Uploader, upload_path}), params) +  end + +  def callbacks(conn, _) do +    send_resp(conn, 400, "bad request") +  end + +  defp process_callback(conn, pid, params) when is_pid(pid) do +    send(pid, {Uploader, self(), conn, params}) + +    receive do +      {Uploader, conn} -> conn +    end +  end + +  defp process_callback(conn, _, _) do +    send_resp(conn, 400, "bad request") +  end +end diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index 74b13f929..853aa2a87 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -24,7 +24,8 @@ defmodule Pleroma.Web do      quote do        use Phoenix.Controller, namespace: Pleroma.Web        import Plug.Conn -      import Pleroma.Web.{Gettext, Router.Helpers} +      import Pleroma.Web.Gettext +      import Pleroma.Web.Router.Helpers      end    end @@ -37,13 +38,43 @@ defmodule Pleroma.Web do        # Import convenience functions from controllers        import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] -      import Pleroma.Web.{ErrorHelpers, Gettext, Router.Helpers} +      import Pleroma.Web.ErrorHelpers +      import Pleroma.Web.Gettext +      import Pleroma.Web.Router.Helpers + +      require Logger + +      @doc "Same as `render/3` but wrapped in a rescue block" +      def safe_render(view, template, assigns \\ %{}) do +        Phoenix.View.render(view, template, assigns) +      rescue +        error -> +          Logger.error( +            "#{__MODULE__} failed to render #{inspect({view, template})}: #{inspect(error)}" +          ) + +          Logger.error(inspect(__STACKTRACE__)) +          nil +      end + +      @doc """ +      Same as `render_many/4` but wrapped in rescue block. +      """ +      def safe_render_many(collection, view, template, assigns \\ %{}) do +        Enum.map(collection, fn resource -> +          as = Map.get(assigns, :as) || view.__resource__ +          assigns = Map.put(assigns, as, resource) +          safe_render(view, template, assigns) +        end) +        |> Enum.filter(& &1) +      end      end    end    def router do      quote do        use Phoenix.Router +      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse        import Plug.Conn        import Phoenix.Controller      end @@ -51,6 +82,7 @@ defmodule Pleroma.Web do    def channel do      quote do +      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse        use Phoenix.Channel        import Pleroma.Web.Gettext      end diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index 0a6338312..5ea5ae48e 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -5,9 +5,12 @@  defmodule Pleroma.Web.WebFinger do    @httpoison Application.get_env(:pleroma, :httpoison) -  alias Pleroma.{User, XmlBuilder} +  alias Pleroma.User +  alias Pleroma.XmlBuilder    alias Pleroma.Web -  alias Pleroma.Web.{XML, Salmon, OStatus} +  alias Pleroma.Web.XML +  alias Pleroma.Web.Salmon +  alias Pleroma.Web.OStatus    require Jason    require Logger diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex index 3a287edd9..a08d7993d 100644 --- a/lib/pleroma/web/websub/websub.ex +++ b/lib/pleroma/web/websub/websub.ex @@ -4,10 +4,14 @@  defmodule Pleroma.Web.Websub do    alias Ecto.Changeset +  alias Pleroma.Instances    alias Pleroma.Repo -  alias Pleroma.Web.Websub.{WebsubServerSubscription, WebsubClientSubscription} +  alias Pleroma.Web.Websub.WebsubServerSubscription +  alias Pleroma.Web.Websub.WebsubClientSubscription    alias Pleroma.Web.OStatus.FeedRepresenter -  alias Pleroma.Web.{XML, Endpoint, OStatus} +  alias Pleroma.Web.XML +  alias Pleroma.Web.Endpoint +  alias Pleroma.Web.OStatus    alias Pleroma.Web.Router.Helpers    require Logger @@ -53,28 +57,34 @@ defmodule Pleroma.Web.Websub do    ]    def publish(topic, user, %{data: %{"type" => type}} = activity)        when type in @supported_activities do -    # TODO: Only send to still valid subscriptions. +    response = +      user +      |> FeedRepresenter.to_simple_form([activity], [user]) +      |> :xmerl.export_simple(:xmerl_xml) +      |> to_string +      query =        from(          sub in WebsubServerSubscription,          where: sub.topic == ^topic and sub.state == "active", -        where: fragment("? > NOW()", sub.valid_until) +        where: fragment("? > (NOW() at time zone 'UTC')", sub.valid_until)        )      subscriptions = Repo.all(query) -    Enum.each(subscriptions, fn sub -> -      response = -        user -        |> FeedRepresenter.to_simple_form([activity], [user]) -        |> :xmerl.export_simple(:xmerl_xml) -        |> to_string +    callbacks = Enum.map(subscriptions, & &1.callback) +    reachable_callbacks_metadata = Instances.filter_reachable(callbacks) +    reachable_callbacks = Map.keys(reachable_callbacks_metadata) +    subscriptions +    |> Enum.filter(&(&1.callback in reachable_callbacks)) +    |> Enum.each(fn sub ->        data = %{          xml: response,          topic: topic,          callback: sub.callback, -        secret: sub.secret +        secret: sub.secret, +        unreachable_since: reachable_callbacks_metadata[sub.callback]        }        Pleroma.Web.Federator.enqueue(:publish_single_websub, data) @@ -121,6 +131,12 @@ defmodule Pleroma.Web.Websub do      end    end +  def incoming_subscription_request(user, params) do +    Logger.info("Unhandled WebSub request for #{user.nickname}: #{inspect(params)}") + +    {:error, "Invalid WebSub request"} +  end +    defp get_subscription(topic, callback) do      Repo.get_by(WebsubServerSubscription, topic: topic, callback: callback) ||        %WebsubServerSubscription{} @@ -257,11 +273,11 @@ defmodule Pleroma.Web.Websub do      end)    end -  def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret}) do +  def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret} = params) do      signature = sign(secret || "", xml)      Logger.info(fn -> "Pushing #{topic} to #{callback}" end) -    with {:ok, %{status: code}} <- +    with {:ok, %{status: code}} when code in 200..299 <-             @httpoison.post(               callback,               xml, @@ -270,12 +286,16 @@ defmodule Pleroma.Web.Websub do                 {"X-Hub-Signature", "sha1=#{signature}"}               ]             ) do +      if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], +        do: Instances.set_reachable(callback) +        Logger.info(fn -> "Pushed to #{callback}, code #{code}" end)        {:ok, code}      else -      e -> -        Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(e)}" end) -        {:error, e} +      {_post_result, response} -> +        unless params[:unreachable_since], do: Instances.set_reachable(callback) +        Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(response)}" end) +        {:error, response}      end    end  end diff --git a/lib/pleroma/web/websub/websub_client_subscription.ex b/lib/pleroma/web/websub/websub_client_subscription.ex index 105b0069f..969ee0684 100644 --- a/lib/pleroma/web/websub/websub_client_subscription.ex +++ b/lib/pleroma/web/websub/websub_client_subscription.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.Websub.WebsubClientSubscription do      field(:state, :string)      field(:subscribers, {:array, :string}, default: [])      field(:hub, :string) -    belongs_to(:user, User) +    belongs_to(:user, User, type: Pleroma.FlakeId)      timestamps()    end diff --git a/lib/pleroma/web/websub/websub_controller.ex b/lib/pleroma/web/websub/websub_controller.ex index 27304d988..1ad18a8a4 100644 --- a/lib/pleroma/web/websub/websub_controller.ex +++ b/lib/pleroma/web/websub/websub_controller.ex @@ -4,9 +4,13 @@  defmodule Pleroma.Web.Websub.WebsubController do    use Pleroma.Web, :controller -  alias Pleroma.{Repo, User} -  alias Pleroma.Web.{Websub, Federator} + +  alias Pleroma.Repo +  alias Pleroma.User +  alias Pleroma.Web.Websub +  alias Pleroma.Web.Federator    alias Pleroma.Web.Websub.WebsubClientSubscription +    require Logger    plug( @@ -67,6 +71,13 @@ defmodule Pleroma.Web.Websub.WebsubController do      end    end +  def websub_subscription_confirmation(conn, params) do +    Logger.info("Invalid WebSub confirmation request: #{inspect(params)}") + +    conn +    |> send_resp(500, "Invalid parameters") +  end +    def websub_incoming(conn, %{"id" => id}) do      with "sha1=" <> signature <- hd(get_req_header(conn, "x-hub-signature")),           signature <- String.downcase(signature),  | 
