diff options
Diffstat (limited to 'lib')
163 files changed, 4676 insertions, 1966 deletions
diff --git a/lib/healthcheck.ex b/lib/healthcheck.ex index 646fb3b9d..f97d14432 100644 --- a/lib/healthcheck.ex +++ b/lib/healthcheck.ex @@ -1,3 +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.Healthcheck do    @moduledoc """    Module collects metrics about app and assign healthy status. @@ -29,13 +33,13 @@ defmodule Pleroma.Healthcheck do    end    defp assign_db_info(healthcheck) do -    database = Application.get_env(:pleroma, Repo)[:database] +    database = Pleroma.Config.get([Repo, :database])      query =        "select state, count(pid) from pg_stat_activity where datname = '#{database}' group by state;"      result = Repo.query!(query) -    pool_size = Application.get_env(:pleroma, Repo)[:pool_size] +    pool_size = Pleroma.Config.get([Repo, :pool_size])      db_info =        Enum.reduce(result.rows, %{active: 0, idle: 0}, fn [state, cnt], states -> diff --git a/lib/jason_types.ex b/lib/jason_types.ex index d1a7bc7ac..c558aef57 100644 --- a/lib/jason_types.ex +++ b/lib/jason_types.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only +  Postgrex.Types.define(    Pleroma.PostgresTypes,    [] ++ Ecto.Adapters.Postgres.extensions(), diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex new file mode 100644 index 000000000..1b758ea33 --- /dev/null +++ b/lib/mix/pleroma.ex @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Pleroma do +  @doc "Common functions to be reused in mix tasks" +  def start_pleroma do +    Application.put_env(:phoenix, :serve_endpoints, false, persistent: true) +    {:ok, _} = Application.ensure_all_started(:pleroma) +  end + +  def load_pleroma do +    Application.load(:pleroma) +  end + +  def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do +    Keyword.get(options, opt) || shell_prompt(prompt, defval, defname) +  end + +  def shell_prompt(prompt, defval \\ nil, defname \\ nil) do +    prompt_message = "#{prompt} [#{defname || defval}] " + +    input = +      if mix_shell?(), +        do: Mix.shell().prompt(prompt_message), +        else: :io.get_line(prompt_message) + +    case input do +      "\n" -> +        case defval do +          nil -> +            shell_prompt(prompt, defval, defname) + +          defval -> +            defval +        end + +      input -> +        String.trim(input) +    end +  end + +  def shell_yes?(message) do +    if mix_shell?(), +      do: Mix.shell().yes?("Continue?"), +      else: shell_prompt(message, "Continue?") in ~w(Yn Y y) +  end + +  def shell_info(message) do +    if mix_shell?(), +      do: Mix.shell().info(message), +      else: IO.puts(message) +  end + +  def shell_error(message) do +    if mix_shell?(), +      do: Mix.shell().error(message), +      else: IO.puts(:stderr, message) +  end + +  @doc "Performs a safe check whether `Mix.shell/0` is available (does not raise if Mix is not loaded)" +  def mix_shell?, do: :erlang.function_exported(Mix, :shell, 0) + +  def escape_sh_path(path) do +    ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(') +  end +end diff --git a/lib/mix/tasks/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex index 0fbb4dbb1..5222cce80 100644 --- a/lib/mix/tasks/benchmark.ex +++ b/lib/mix/tasks/pleroma/benchmark.ex @@ -1,19 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only +  defmodule Mix.Tasks.Pleroma.Benchmark do +  import Mix.Pleroma    use Mix.Task -  alias Mix.Tasks.Pleroma.Common    def run(["search"]) do -    Common.start_pleroma() +    start_pleroma()      Benchee.run(%{        "search" => fn -> -        Pleroma.Web.MastodonAPI.MastodonAPIController.status_search(nil, "cofe") +        Pleroma.Activity.search(nil, "cofe")        end      })    end    def run(["tag"]) do -    Common.start_pleroma() +    start_pleroma()      Benchee.run(%{        "tag" => fn -> diff --git a/lib/mix/tasks/pleroma/common.ex b/lib/mix/tasks/pleroma/common.ex deleted file mode 100644 index 48c0c1346..000000000 --- a/lib/mix/tasks/pleroma/common.ex +++ /dev/null @@ -1,28 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.Common do -  @doc "Common functions to be reused in mix tasks" -  def start_pleroma do -    Mix.Task.run("app.start") -  end - -  def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do -    Keyword.get(options, opt) || -      case Mix.shell().prompt("#{prompt} [#{defname || defval}]") do -        "\n" -> -          case defval do -            nil -> get_option(options, opt, prompt, defval) -            defval -> defval -          end - -        opt -> -          opt |> String.trim() -      end -  end - -  def escape_sh_path(path) do -    ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(') -  end -end diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex new file mode 100644 index 000000000..a7d0fac5d --- /dev/null +++ b/lib/mix/tasks/pleroma/config.ex @@ -0,0 +1,83 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Config do +  use Mix.Task +  import Mix.Pleroma +  alias Pleroma.Repo +  alias Pleroma.Web.AdminAPI.Config +  @shortdoc "Manages the location of the config" +  @moduledoc """ +  Manages the location of the config. + +  ## Transfers config from file to DB. + +      mix pleroma.config migrate_to_db + +  ## Transfers config from DB to file. + +      mix pleroma.config migrate_from_db ENV +  """ + +  def run(["migrate_to_db"]) do +    start_pleroma() + +    if Pleroma.Config.get([:instance, :dynamic_configuration]) do +      Application.get_all_env(:pleroma) +      |> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end) +      |> Enum.each(fn {k, v} -> +        key = to_string(k) |> String.replace("Elixir.", "") + +        key = +          if String.starts_with?(key, "Pleroma.") do +            key +          else +            ":" <> key +          end + +        {:ok, _} = Config.update_or_create(%{group: "pleroma", key: key, value: v}) +        Mix.shell().info("#{key} is migrated.") +      end) + +      Mix.shell().info("Settings migrated.") +    else +      Mix.shell().info( +        "Migration is not allowed by config. You can change this behavior in instance settings." +      ) +    end +  end + +  def run(["migrate_from_db", env, delete?]) do +    start_pleroma() + +    delete? = if delete? == "true", do: true, else: false + +    if Pleroma.Config.get([:instance, :dynamic_configuration]) do +      config_path = "config/#{env}.exported_from_db.secret.exs" + +      {:ok, file} = File.open(config_path, [:write]) +      IO.write(file, "use Mix.Config\r\n") + +      Repo.all(Config) +      |> Enum.each(fn config -> +        IO.write( +          file, +          "config :#{config.group}, #{config.key}, #{inspect(Config.from_binary(config.value))}\r\n\r\n" +        ) + +        if delete? do +          {:ok, _} = Repo.delete(config) +          Mix.shell().info("#{config.key} deleted from DB.") +        end +      end) + +      File.close(file) +      System.cmd("mix", ["format", config_path]) +    else +      Mix.shell().info( +        "Migration is not allowed by config. You can change this behavior in instance settings." +      ) +    end +  end +end diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 4d480ac3f..e91fb31d1 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -3,12 +3,12 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Mix.Tasks.Pleroma.Database do -  alias Mix.Tasks.Pleroma.Common    alias Pleroma.Conversation    alias Pleroma.Object    alias Pleroma.Repo    alias Pleroma.User    require Logger +  import Mix.Pleroma    use Mix.Task    @shortdoc "A collection of database related tasks" @@ -45,7 +45,7 @@ defmodule Mix.Tasks.Pleroma.Database do          ]        ) -    Common.start_pleroma() +    start_pleroma()      Logger.info("Removing embedded objects")      Repo.query!( @@ -66,12 +66,12 @@ defmodule Mix.Tasks.Pleroma.Database do    end    def run(["bump_all_conversations"]) do -    Common.start_pleroma() +    start_pleroma()      Conversation.bump_for_all_activities()    end    def run(["update_users_following_followers_counts"]) do -    Common.start_pleroma() +    start_pleroma()      users = Repo.all(User)      Enum.each(users, &User.remove_duplicated_following/1) @@ -89,7 +89,7 @@ defmodule Mix.Tasks.Pleroma.Database do          ]        ) -    Common.start_pleroma() +    start_pleroma()      deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) diff --git a/lib/mix/tasks/pleroma/ecto/ecto.ex b/lib/mix/tasks/pleroma/ecto/ecto.ex new file mode 100644 index 000000000..b66f63376 --- /dev/null +++ b/lib/mix/tasks/pleroma/ecto/ecto.ex @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-onl + +defmodule Mix.Tasks.Pleroma.Ecto do +  @doc """ +  Ensures the given repository's migrations path exists on the file system. +  """ +  @spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t() +  def ensure_migrations_path(repo, opts) do +    path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations") + +    path = +      case Path.type(path) do +        :relative -> +          Path.join(Application.app_dir(:pleroma), path) + +        :absolute -> +          path +      end + +    if not File.dir?(path) do +      raise_missing_migrations(Path.relative_to_cwd(path), repo) +    end + +    path +  end + +  @doc """ +  Returns the private repository path relative to the source. +  """ +  def source_repo_priv(repo) do +    config = repo.config() +    priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" +    Path.join(Application.app_dir(:pleroma), priv) +  end + +  defp raise_missing_migrations(path, repo) do +    raise(""" +    Could not find migrations directory #{inspect(path)} +    for repo #{inspect(repo)}. +    This may be because you are in a new project and the +    migration directory has not been created yet. Creating an +    empty directory at the path above will fix this error. +    If you expected existing migrations to be found, please +    make sure your repository has been properly configured +    and the configured path exists. +    """) +  end +end diff --git a/lib/mix/tasks/pleroma/ecto/migrate.ex b/lib/mix/tasks/pleroma/ecto/migrate.ex new file mode 100644 index 000000000..855c977f6 --- /dev/null +++ b/lib/mix/tasks/pleroma/ecto/migrate.ex @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-onl + +defmodule Mix.Tasks.Pleroma.Ecto.Migrate do +  use Mix.Task +  import Mix.Pleroma +  require Logger + +  @shortdoc "Wrapper on `ecto.migrate` task." + +  @aliases [ +    n: :step, +    v: :to +  ] + +  @switches [ +    all: :boolean, +    step: :integer, +    to: :integer, +    quiet: :boolean, +    log_sql: :boolean, +    strict_version_order: :boolean, +    migrations_path: :string +  ] + +  @moduledoc """ +  Changes `Logger` level to `:info` before start migration. +  Changes level back when migration ends. + +  ## Start migration + +      mix pleroma.ecto.migrate [OPTIONS] + +  Options: +    - see https://hexdocs.pm/ecto/2.0.0/Mix.Tasks.Ecto.Migrate.html +  """ + +  @impl true +  def run(args \\ []) do +    load_pleroma() +    {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) + +    opts = +      if opts[:to] || opts[:step] || opts[:all], +        do: opts, +        else: Keyword.put(opts, :all, true) + +    opts = +      if opts[:quiet], +        do: Keyword.merge(opts, log: false, log_sql: false), +        else: opts + +    path = Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo, opts) + +    level = Logger.level() +    Logger.configure(level: :info) + +    {:ok, _, _} = Ecto.Migrator.with_repo(Pleroma.Repo, &Ecto.Migrator.run(&1, path, :up, opts)) + +    Logger.configure(level: level) +  end +end diff --git a/lib/mix/tasks/pleroma/ecto/rollback.ex b/lib/mix/tasks/pleroma/ecto/rollback.ex new file mode 100644 index 000000000..2ffb0901c --- /dev/null +++ b/lib/mix/tasks/pleroma/ecto/rollback.ex @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-onl + +defmodule Mix.Tasks.Pleroma.Ecto.Rollback do +  use Mix.Task +  import Mix.Pleroma +  require Logger +  @shortdoc "Wrapper on `ecto.rollback` task" + +  @aliases [ +    n: :step, +    v: :to +  ] + +  @switches [ +    all: :boolean, +    step: :integer, +    to: :integer, +    start: :boolean, +    quiet: :boolean, +    log_sql: :boolean, +    migrations_path: :string +  ] + +  @moduledoc """ +  Changes `Logger` level to `:info` before start rollback. +  Changes level back when rollback ends. + +  ## Start rollback + +      mix pleroma.ecto.rollback + +  Options: +    - see https://hexdocs.pm/ecto/2.0.0/Mix.Tasks.Ecto.Rollback.html +  """ + +  @impl true +  def run(args \\ []) do +    load_pleroma() +    {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) + +    opts = +      if opts[:to] || opts[:step] || opts[:all], +        do: opts, +        else: Keyword.put(opts, :step, 1) + +    opts = +      if opts[:quiet], +        do: Keyword.merge(opts, log: false, log_sql: false), +        else: opts + +    path = Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo, opts) + +    level = Logger.level() +    Logger.configure(level: :info) + +    if Pleroma.Config.get(:env) == :test do +      Logger.info("Rollback succesfully") +    else +      {:ok, _, _} = +        Ecto.Migrator.with_repo(Pleroma.Repo, &Ecto.Migrator.run(&1, path, :down, opts)) +    end + +    Logger.configure(level: level) +  end +end diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index d2ddf450a..c2225af7d 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -55,15 +55,13 @@ defmodule Mix.Tasks.Pleroma.Emoji do    are extracted).    """ -  @default_manifest Pleroma.Config.get!([:emoji, :default_manifest]) -    def run(["ls-packs" | args]) do      Application.ensure_all_started(:hackney)      {options, [], []} = parse_global_opts(args)      manifest = -      fetch_manifest(if options[:manifest], do: options[:manifest], else: @default_manifest) +      fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest())      Enum.each(manifest, fn {name, info} ->        to_print = [ @@ -88,7 +86,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do      {options, pack_names, []} = parse_global_opts(args) -    manifest_url = if options[:manifest], do: options[:manifest], else: @default_manifest +    manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest()      manifest = fetch_manifest(manifest_url) @@ -298,4 +296,6 @@ defmodule Mix.Tasks.Pleroma.Emoji do      Tesla.client(middleware)    end + +  defp default_manifest, do: Pleroma.Config.get!([:emoji, :default_manifest])  end diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 6cee8d630..9080adb52 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -4,7 +4,7 @@  defmodule Mix.Tasks.Pleroma.Instance do    use Mix.Task -  alias Mix.Tasks.Pleroma.Common +  import Mix.Pleroma    @shortdoc "Manages Pleroma instance"    @moduledoc """ @@ -29,7 +29,13 @@ defmodule Mix.Tasks.Pleroma.Instance do    - `--dbname DBNAME` - the name of the database to use    - `--dbuser DBUSER` - the user (aka role) to use for the database connection    - `--dbpass DBPASS` - the password to use for the database connection +  - `--rum Y/N` - Whether to enable RUM indexes    - `--indexable Y/N` - Allow/disallow indexing site by search engines +  - `--db-configurable Y/N` - Allow/disallow configuring instance from admin part +  - `--uploads-dir` - the directory uploads go in when using a local uploader +  - `--static-dir` - the directory custom public files should be read from (custom emojis, frontend bundle overrides, robots.txt, etc.) +  - `--listen-ip` - the ip the app should listen to, defaults to 127.0.0.1 +  - `--listen-port` - the port the app should listen to, defaults to 4000    """    def run(["gen" | rest]) do @@ -48,7 +54,13 @@ defmodule Mix.Tasks.Pleroma.Instance do            dbname: :string,            dbuser: :string,            dbpass: :string, -          indexable: :string +          rum: :string, +          indexable: :string, +          db_configurable: :string, +          uploads_dir: :string, +          static_dir: :string, +          listen_ip: :string, +          listen_port: :string          ],          aliases: [            o: :output, @@ -68,7 +80,7 @@ defmodule Mix.Tasks.Pleroma.Instance do      if proceed? do        [domain, port | _] =          String.split( -          Common.get_option( +          get_option(              options,              :domain,              "What domain will your instance use? (e.g pleroma.soykaf.com)" @@ -77,16 +89,16 @@ defmodule Mix.Tasks.Pleroma.Instance do          ) ++ [443]        name = -        Common.get_option( +        get_option(            options,            :instance_name,            "What is the name of your instance? (e.g. Pleroma/Soykaf)"          ) -      email = Common.get_option(options, :admin_email, "What is your admin email address?") +      email = get_option(options, :admin_email, "What is your admin email address?")        notify_email = -        Common.get_option( +        get_option(            options,            :notify_email,            "What email address do you want to use for sending email notifications?", @@ -94,21 +106,27 @@ defmodule Mix.Tasks.Pleroma.Instance do          )        indexable = -        Common.get_option( +        get_option(            options,            :indexable,            "Do you want search engines to index your site? (y/n)",            "y"          ) === "y" -      dbhost = -        Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost") +      db_configurable? = +        get_option( +          options, +          :db_configurable, +          "Do you want to store the configuration in the database (allows controlling it from admin-fe)? (y/n)", +          "n" +        ) === "y" + +      dbhost = get_option(options, :dbhost, "What is the hostname of your database?", "localhost") -      dbname = -        Common.get_option(options, :dbname, "What is the name of your database?", "pleroma_dev") +      dbname = get_option(options, :dbname, "What is the name of your database?", "pleroma")        dbuser = -        Common.get_option( +        get_option(            options,            :dbuser,            "What is the user used to connect to your database?", @@ -116,7 +134,7 @@ defmodule Mix.Tasks.Pleroma.Instance do          )        dbpass = -        Common.get_option( +        get_option(            options,            :dbpass,            "What is the password used to connect to your database?", @@ -124,13 +142,54 @@ defmodule Mix.Tasks.Pleroma.Instance do            "autogenerated"          ) +      rum_enabled = +        get_option( +          options, +          :rum, +          "Would you like to use RUM indices?", +          "n" +        ) === "y" + +      listen_port = +        get_option( +          options, +          :listen_port, +          "What port will the app listen to (leave it if you are using the default setup with nginx)?", +          4000 +        ) + +      listen_ip = +        get_option( +          options, +          :listen_ip, +          "What ip will the app listen to (leave it if you are using the default setup with nginx)?", +          "127.0.0.1" +        ) + +      uploads_dir = +        get_option( +          options, +          :uploads_dir, +          "What directory should media uploads go in (when using the local uploader)?", +          Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads]) +        ) + +      static_dir = +        get_option( +          options, +          :static_dir, +          "What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?", +          Pleroma.Config.get([:instance, :static_dir]) +        ) +        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) +      template_dir = Application.app_dir(:pleroma, "priv") <> "/templates"        result_config =          EEx.eval_file( -          "sample_config.eex" |> Path.expand(__DIR__), +          template_dir <> "/sample_config.eex",            domain: domain,            port: port,            email: email, @@ -140,46 +199,40 @@ defmodule Mix.Tasks.Pleroma.Instance do            dbname: dbname,            dbuser: dbuser,            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) +          web_push_private_key: Base.url_encode64(web_push_private_key, padding: false), +          db_configurable?: db_configurable?, +          static_dir: static_dir, +          uploads_dir: uploads_dir, +          rum_enabled: rum_enabled, +          listen_ip: listen_ip, +          listen_port: listen_port          )        result_psql =          EEx.eval_file( -          "sample_psql.eex" |> Path.expand(__DIR__), +          template_dir <> "/sample_psql.eex",            dbname: dbname,            dbuser: dbuser, -          dbpass: dbpass +          dbpass: dbpass, +          rum_enabled: rum_enabled          ) -      Mix.shell().info( -        "Writing config to #{config_path}. You should rename it to config/prod.secret.exs or config/dev.secret.exs." -      ) +      shell_info("Writing config to #{config_path}.")        File.write(config_path, result_config) -      Mix.shell().info("Writing #{psql_path}.") +      shell_info("Writing the postgres script to #{psql_path}.")        File.write(psql_path, result_psql) -      write_robots_txt(indexable) - -      Mix.shell().info( -        "\n" <> -          """ -          To get started: -          1. Verify the contents of the generated files. -          2. Run `sudo -u postgres psql -f #{Common.escape_sh_path(psql_path)}`. -          """ <> -          if config_path in ["config/dev.secret.exs", "config/prod.secret.exs"] do -            "" -          else -            "3. Run `mv #{Common.escape_sh_path(config_path)} 'config/prod.secret.exs'`." -          end +      write_robots_txt(indexable, template_dir) + +      shell_info( +        "\n All files successfully written! Refer to the installation instructions for your platform for next steps"        )      else -      Mix.shell().error( +      shell_error(          "The task would have overwritten the following files:\n" <>            (Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <>            "Rerun with `--force` to overwrite them." @@ -187,10 +240,10 @@ defmodule Mix.Tasks.Pleroma.Instance do      end    end -  defp write_robots_txt(indexable) do +  defp write_robots_txt(indexable, template_dir) do      robots_txt =        EEx.eval_file( -        Path.expand("robots_txt.eex", __DIR__), +        template_dir <> "/robots_txt.eex",          indexable: indexable        ) @@ -204,10 +257,10 @@ defmodule Mix.Tasks.Pleroma.Instance do      if File.exists?(robots_txt_path) do        File.cp!(robots_txt_path, "#{robots_txt_path}.bak") -      Mix.shell().info("Backing up existing robots.txt to #{robots_txt_path}.bak") +      shell_info("Backing up existing robots.txt to #{robots_txt_path}.bak")      end      File.write(robots_txt_path, robots_txt) -    Mix.shell().info("Writing #{robots_txt_path}.") +    shell_info("Writing #{robots_txt_path}.")    end  end diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex index fbec473c5..83ed0ed02 100644 --- a/lib/mix/tasks/pleroma/relay.ex +++ b/lib/mix/tasks/pleroma/relay.ex @@ -4,7 +4,7 @@  defmodule Mix.Tasks.Pleroma.Relay do    use Mix.Task -  alias Mix.Tasks.Pleroma.Common +  import Mix.Pleroma    alias Pleroma.Web.ActivityPub.Relay    @shortdoc "Manages remote relays" @@ -24,24 +24,24 @@ defmodule Mix.Tasks.Pleroma.Relay do    Example: ``mix pleroma.relay unfollow https://example.org/relay``    """    def run(["follow", target]) do -    Common.start_pleroma() +    start_pleroma()      with {:ok, _activity} <- Relay.follow(target) do        # put this task to sleep to allow the genserver to push out the messages        :timer.sleep(500)      else -      {:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}") +      {:error, e} -> shell_error("Error while following #{target}: #{inspect(e)}")      end    end    def run(["unfollow", target]) do -    Common.start_pleroma() +    start_pleroma()      with {:ok, _activity} <- Relay.unfollow(target) do        # put this task to sleep to allow the genserver to push out the messages        :timer.sleep(500)      else -      {:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}") +      {:error, e} -> shell_error("Error while following #{target}: #{inspect(e)}")      end    end  end diff --git a/lib/mix/tasks/pleroma/robots_txt.eex b/lib/mix/tasks/pleroma/robots_txt.eex deleted file mode 100644 index 1af3c47ee..000000000 --- a/lib/mix/tasks/pleroma/robots_txt.eex +++ /dev/null @@ -1,2 +0,0 @@ -User-Agent: * -Disallow: <%= if indexable, do: "", else: "/" %> diff --git a/lib/mix/tasks/pleroma/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex deleted file mode 100644 index 52bd57cb7..000000000 --- a/lib/mix/tasks/pleroma/sample_config.eex +++ /dev/null @@ -1,78 +0,0 @@ -# Pleroma instance configuration - -# NOTE: This file should not be committed to a repo or otherwise made public -# without removing sensitive information. - -use Mix.Config - -config :pleroma, Pleroma.Web.Endpoint, -   url: [host: "<%= domain %>", scheme: "https", port: <%= port %>], -   secret_key_base: "<%= secret %>", -   signing_salt: "<%= signing_salt %>" - -config :pleroma, :instance, -  name: "<%= name %>", -  email: "<%= email %>", -  notify_email: "<%= notify_email %>", -  limit: 5000, -  registrations_open: true, -  dedupe_media: false - -config :pleroma, :media_proxy, -  enabled: false, -  redirect_on_failure: true -  #base_url: "https://cache.pleroma.social" - -config :pleroma, Pleroma.Repo, -  adapter: Ecto.Adapters.Postgres, -  username: "<%= dbuser %>", -  password: "<%= dbpass %>", -  database: "<%= dbname %>", -  hostname: "<%= dbhost %>", -  pool_size: 10 - -# Configure web push notifications -config :web_push_encryption, :vapid_details, -  subject: "mailto:<%= email %>", -  public_key: "<%= web_push_public_key %>", -  private_key: "<%= web_push_private_key %>" - -# Enable Strict-Transport-Security once SSL is working: -# config :pleroma, :http_security, -#   sts: true - -# Configure S3 support if desired. -# The public S3 endpoint is different depending on region and provider, -# consult your S3 provider's documentation for details on what to use. -# -# config :pleroma, Pleroma.Uploaders.S3, -#   bucket: "some-bucket", -#   public_endpoint: "https://s3.amazonaws.com" -# -# Configure S3 credentials: -# config :ex_aws, :s3, -#   access_key_id: "xxxxxxxxxxxxx", -#   secret_access_key: "yyyyyyyyyyyy", -#   region: "us-east-1", -#   scheme: "https://" -# -# For using third-party S3 clones like wasabi, also do: -# config :ex_aws, :s3, -#   host: "s3.wasabisys.com" - - -# Configure Openstack Swift support if desired. -# -# Many openstack deployments are different, so config is left very open with -# no assumptions made on which provider you're using. This should allow very -# wide support without needing separate handlers for OVH, Rackspace, etc. -# -# config :pleroma, Pleroma.Uploaders.Swift, -#  container: "some-container", -#  username: "api-username-yyyy", -#  password: "api-key-xxxx", -#  tenant_id: "<openstack-project/tenant-id>", -#  auth_url: "https://keystone-endpoint.provider.com", -#  storage_url: "https://swift-endpoint.prodider.com/v1/AUTH_<tenant>/<container>", -#  object_url: "https://cdn-endpoint.provider.com/<container>" -# diff --git a/lib/mix/tasks/pleroma/sample_psql.eex b/lib/mix/tasks/pleroma/sample_psql.eex deleted file mode 100644 index f0ac05e57..000000000 --- a/lib/mix/tasks/pleroma/sample_psql.eex +++ /dev/null @@ -1,7 +0,0 @@ -CREATE USER <%= dbuser %> WITH ENCRYPTED PASSWORD '<%= dbpass %>'; -CREATE DATABASE <%= dbname %> OWNER <%= dbuser %>; -\c <%= dbname %>; ---Extensions made by ecto.migrate that need superuser access -CREATE EXTENSION IF NOT EXISTS citext; -CREATE EXTENSION IF NOT EXISTS pg_trgm; -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; diff --git a/lib/mix/tasks/pleroma/uploads.ex b/lib/mix/tasks/pleroma/uploads.ex index 106fcf443..be45383ee 100644 --- a/lib/mix/tasks/pleroma/uploads.ex +++ b/lib/mix/tasks/pleroma/uploads.ex @@ -4,7 +4,7 @@  defmodule Mix.Tasks.Pleroma.Uploads do    use Mix.Task -  alias Mix.Tasks.Pleroma.Common +  import Mix.Pleroma    alias Pleroma.Upload    alias Pleroma.Uploaders.Local    require Logger @@ -24,7 +24,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do    """    def run(["migrate_local", target_uploader | args]) do      delete? = Enum.member?(args, "--delete") -    Common.start_pleroma() +    start_pleroma()      local_path = Pleroma.Config.get!([Local, :uploads])      uploader = Module.concat(Pleroma.Uploaders, target_uploader) @@ -38,10 +38,10 @@ defmodule Mix.Tasks.Pleroma.Uploads do        Pleroma.Config.put([Upload, :uploader], uploader)      end -    Mix.shell().info("Migrating files from local #{local_path} to #{to_string(uploader)}") +    shell_info("Migrating files from local #{local_path} to #{to_string(uploader)}")      if delete? do -      Mix.shell().info( +      shell_info(          "Attention: uploaded files will be deleted, hope you have backups! (--delete ; cancel with ^C)"        ) @@ -78,7 +78,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do        |> Enum.filter(& &1)      total_count = length(uploads) -    Mix.shell().info("Found #{total_count} uploads") +    shell_info("Found #{total_count} uploads")      uploads      |> Task.async_stream( @@ -90,7 +90,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do              :ok            error -> -            Mix.shell().error("failed to upload #{inspect(upload.path)}: #{inspect(error)}") +            shell_error("failed to upload #{inspect(upload.path)}: #{inspect(error)}")          end        end,        timeout: 150_000 @@ -99,10 +99,10 @@ defmodule Mix.Tasks.Pleroma.Uploads do      # 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") +      shell_info("Uploaded #{count}/#{total_count} files")        count      end) -    Mix.shell().info("Done!") +    shell_info("Done!")    end  end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 25fc40ea7..8a78b4fe6 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -5,9 +5,10 @@  defmodule Mix.Tasks.Pleroma.User do    use Mix.Task    import Ecto.Changeset -  alias Mix.Tasks.Pleroma.Common +  import Mix.Pleroma    alias Pleroma.User    alias Pleroma.UserInviteToken +  alias Pleroma.Web.OAuth    @shortdoc "Manages Pleroma users"    @moduledoc """ @@ -49,6 +50,10 @@ defmodule Mix.Tasks.Pleroma.User do        mix pleroma.user delete_activities NICKNAME +  ## Sign user out from all applications (delete user's OAuth tokens and authorizations). + +      mix pleroma.user sign_out NICKNAME +    ## Deactivate or activate the user's account.        mix pleroma.user toggle_activated NICKNAME @@ -115,7 +120,7 @@ defmodule Mix.Tasks.Pleroma.User do      admin? = Keyword.get(options, :admin, false)      assume_yes? = Keyword.get(options, :assume_yes, false) -    Mix.shell().info(""" +    shell_info("""      A user will be created with the following information:        - nickname: #{nickname}        - email: #{email} @@ -128,10 +133,10 @@ defmodule Mix.Tasks.Pleroma.User do        - admin: #{if(admin?, do: "true", else: "false")}      """) -    proceed? = assume_yes? or Mix.shell().yes?("Continue?") +    proceed? = assume_yes? or shell_yes?("Continue?")      if proceed? do -      Common.start_pleroma() +      start_pleroma()        params = %{          nickname: nickname, @@ -145,7 +150,7 @@ defmodule Mix.Tasks.Pleroma.User do        changeset = User.register_changeset(%User{}, params, need_confirmation: false)        {:ok, _user} = User.register(changeset) -      Mix.shell().info("User #{nickname} created") +      shell_info("User #{nickname} created")        if moderator? do          run(["set", nickname, "--moderator"]) @@ -159,64 +164,64 @@ defmodule Mix.Tasks.Pleroma.User do          run(["reset_password", nickname])        end      else -      Mix.shell().info("User will not be created.") +      shell_info("User will not be created.")      end    end    def run(["rm", nickname]) do -    Common.start_pleroma() +    start_pleroma()      with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do        User.perform(:delete, user) -      Mix.shell().info("User #{nickname} deleted.") +      shell_info("User #{nickname} deleted.")      else        _ -> -        Mix.shell().error("No local user #{nickname}") +        shell_error("No local user #{nickname}")      end    end    def run(["toggle_activated", nickname]) do -    Common.start_pleroma() +    start_pleroma()      with %User{} = user <- User.get_cached_by_nickname(nickname) do        {:ok, user} = User.deactivate(user, !user.info.deactivated) -      Mix.shell().info( +      shell_info(          "Activation status of #{nickname}: #{if(user.info.deactivated, do: "de", else: "")}activated"        )      else        _ -> -        Mix.shell().error("No user #{nickname}") +        shell_error("No user #{nickname}")      end    end    def run(["reset_password", nickname]) do -    Common.start_pleroma() +    start_pleroma()      with %User{local: true} = user <- User.get_cached_by_nickname(nickname),           {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do -      Mix.shell().info("Generated password reset token for #{user.nickname}") +      shell_info("Generated password reset token for #{user.nickname}")        IO.puts(          "URL: #{ -          Pleroma.Web.Router.Helpers.util_url( +          Pleroma.Web.Router.Helpers.reset_password_url(              Pleroma.Web.Endpoint, -            :show_password_reset, +            :reset,              token.token            )          }"        )      else        _ -> -        Mix.shell().error("No local user #{nickname}") +        shell_error("No local user #{nickname}")      end    end    def run(["unsubscribe", nickname]) do -    Common.start_pleroma() +    start_pleroma()      with %User{} = user <- User.get_cached_by_nickname(nickname) do -      Mix.shell().info("Deactivating #{user.nickname}") +      shell_info("Deactivating #{user.nickname}")        User.deactivate(user)        {:ok, friends} = User.get_friends(user) @@ -224,7 +229,7 @@ defmodule Mix.Tasks.Pleroma.User do        Enum.each(friends, fn friend ->          user = User.get_cached_by_id(user.id) -        Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}") +        shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")          User.unfollow(user, friend)        end) @@ -233,16 +238,16 @@ defmodule Mix.Tasks.Pleroma.User do        user = User.get_cached_by_id(user.id)        if Enum.empty?(user.following) do -        Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}") +        shell_info("Successfully unsubscribed all followers from #{user.nickname}")        end      else        _ -> -        Mix.shell().error("No user #{nickname}") +        shell_error("No user #{nickname}")      end    end    def run(["set", nickname | rest]) do -    Common.start_pleroma() +    start_pleroma()      {options, [], []} =        OptionParser.parse( @@ -274,33 +279,33 @@ defmodule Mix.Tasks.Pleroma.User do          end      else        _ -> -        Mix.shell().error("No local user #{nickname}") +        shell_error("No local user #{nickname}")      end    end    def run(["tag", nickname | tags]) do -    Common.start_pleroma() +    start_pleroma()      with %User{} = user <- User.get_cached_by_nickname(nickname) do        user = user |> User.tag(tags) -      Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") +      shell_info("Tags of #{user.nickname}: #{inspect(tags)}")      else        _ -> -        Mix.shell().error("Could not change user tags for #{nickname}") +        shell_error("Could not change user tags for #{nickname}")      end    end    def run(["untag", nickname | tags]) do -    Common.start_pleroma() +    start_pleroma()      with %User{} = user <- User.get_cached_by_nickname(nickname) do        user = user |> User.untag(tags) -      Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") +      shell_info("Tags of #{user.nickname}: #{inspect(tags)}")      else        _ -> -        Mix.shell().error("Could not change user tags for #{nickname}") +        shell_error("Could not change user tags for #{nickname}")      end    end @@ -321,14 +326,12 @@ defmodule Mix.Tasks.Pleroma.User do        end)        |> Enum.into(%{}) -    Common.start_pleroma() +    start_pleroma()      with {:ok, val} <- options[:expires_at],           options = Map.put(options, :expires_at, val),           {:ok, invite} <- UserInviteToken.create_invite(options) do -      Mix.shell().info( -        "Generated user invite token " <> String.replace(invite.invite_type, "_", " ") -      ) +      shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " "))        url =          Pleroma.Web.Router.Helpers.redirect_url( @@ -340,14 +343,14 @@ defmodule Mix.Tasks.Pleroma.User do        IO.puts(url)      else        error -> -        Mix.shell().error("Could not create invite token: #{inspect(error)}") +        shell_error("Could not create invite token: #{inspect(error)}")      end    end    def run(["invites"]) do -    Common.start_pleroma() +    start_pleroma() -    Mix.shell().info("Invites list:") +    shell_info("Invites list:")      UserInviteToken.list_invites()      |> Enum.each(fn invite -> @@ -361,7 +364,7 @@ defmodule Mix.Tasks.Pleroma.User do            " | Max use: #{max_use}    Left use: #{max_use - invite.uses}"          end -      Mix.shell().info( +      shell_info(          "ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{            invite.used          }#{expire_info}#{using_info}" @@ -370,40 +373,54 @@ defmodule Mix.Tasks.Pleroma.User do    end    def run(["revoke_invite", token]) do -    Common.start_pleroma() +    start_pleroma()      with {:ok, invite} <- UserInviteToken.find_by_token(token),           {:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do -      Mix.shell().info("Invite for token #{token} was revoked.") +      shell_info("Invite for token #{token} was revoked.")      else -      _ -> Mix.shell().error("No invite found with token #{token}") +      _ -> shell_error("No invite found with token #{token}")      end    end    def run(["delete_activities", nickname]) do -    Common.start_pleroma() +    start_pleroma()      with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do        {:ok, _} = User.delete_user_activities(user) -      Mix.shell().info("User #{nickname} statuses deleted.") +      shell_info("User #{nickname} statuses deleted.")      else        _ -> -        Mix.shell().error("No local user #{nickname}") +        shell_error("No local user #{nickname}")      end    end    def run(["toggle_confirmed", nickname]) do -    Common.start_pleroma() +    start_pleroma()      with %User{} = user <- User.get_cached_by_nickname(nickname) do        {:ok, user} = User.toggle_confirmation(user)        message = if user.info.confirmation_pending, do: "needs", else: "doesn't need" -      Mix.shell().info("#{nickname} #{message} confirmation.") +      shell_info("#{nickname} #{message} confirmation.") +    else +      _ -> +        shell_error("No local user #{nickname}") +    end +  end + +  def run(["sign_out", nickname]) do +    start_pleroma() + +    with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do +      OAuth.Token.delete_user_tokens(user) +      OAuth.Authorization.delete_user_authorizations(user) + +      shell_info("#{nickname} signed out from all apps.")      else        _ -> -        Mix.shell().error("No local user #{nickname}") +        shell_error("No local user #{nickname}")      end    end @@ -416,7 +433,7 @@ defmodule Mix.Tasks.Pleroma.User do      {:ok, user} = User.update_and_set_cache(user_cng) -    Mix.shell().info("Moderator status of #{user.nickname}: #{user.info.is_moderator}") +    shell_info("Moderator status of #{user.nickname}: #{user.info.is_moderator}")      user    end @@ -429,7 +446,7 @@ defmodule Mix.Tasks.Pleroma.User do      {:ok, user} = User.update_and_set_cache(user_cng) -    Mix.shell().info("Admin status of #{user.nickname}: #{user.info.is_admin}") +    shell_info("Admin status of #{user.nickname}: #{user.info.is_admin}")      user    end @@ -442,7 +459,7 @@ defmodule Mix.Tasks.Pleroma.User do      {:ok, user} = User.update_and_set_cache(user_cng) -    Mix.shell().info("Locked status of #{user.nickname}: #{user.info.locked}") +    shell_info("Locked status of #{user.nickname}: #{user.info.locked}")      user    end  end diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 99589590c..46552c7be 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -343,4 +343,6 @@ defmodule Pleroma.Activity do          )      )    end + +  defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search  end diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex new file mode 100644 index 000000000..0cc3770a7 --- /dev/null +++ b/lib/pleroma/activity/search.ex @@ -0,0 +1,92 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Activity.Search do +  alias Pleroma.Activity +  alias Pleroma.Object.Fetcher +  alias Pleroma.Pagination +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.Visibility + +  import Ecto.Query + +  def search(user, search_query, options \\ []) do +    index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin +    limit = Enum.min([Keyword.get(options, :limit), 40]) +    offset = Keyword.get(options, :offset, 0) +    author = Keyword.get(options, :author) + +    Activity +    |> Activity.with_preloaded_object() +    |> Activity.restrict_deactivated_users() +    |> restrict_public() +    |> query_with(index_type, search_query) +    |> maybe_restrict_local(user) +    |> maybe_restrict_author(author) +    |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => limit}, :offset) +    |> maybe_fetch(user, search_query) +  end + +  def maybe_restrict_author(query, %User{} = author) do +    from([a, o] in query, +      where: a.actor == ^author.ap_id +    ) +  end + +  def maybe_restrict_author(query, _), do: query + +  defp restrict_public(q) do +    from([a, o] in q, +      where: fragment("?->>'type' = 'Create'", a.data), +      where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients +    ) +  end + +  defp query_with(q, :gin, search_query) do +    from([a, o] in q, +      where: +        fragment( +          "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)", +          o.data, +          ^search_query +        ) +    ) +  end + +  defp query_with(q, :rum, search_query) do +    from([a, o] in q, +      where: +        fragment( +          "? @@ plainto_tsquery('english', ?)", +          o.fts_content, +          ^search_query +        ), +      order_by: [fragment("? <=> now()::date", o.inserted_at)] +    ) +  end + +  defp maybe_restrict_local(q, user) do +    limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated) + +    case {limit, user} do +      {:all, _} -> restrict_local(q) +      {:unauthenticated, %User{}} -> q +      {:unauthenticated, _} -> restrict_local(q) +      {false, _} -> q +    end +  end + +  defp restrict_local(q), do: where(q, local: true) + +  defp maybe_fetch(activities, user, search_query) do +    with true <- Regex.match?(~r/https?:/, search_query), +         {:ok, object} <- Fetcher.fetch_object_from_id(search_query), +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), +         true <- Visibility.visible_for_user?(activity, user) do +      activities ++ [activity] +    else +      _ -> activities +    end +  end +end diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 76df3945e..ba4cf8486 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -4,7 +4,6 @@  defmodule Pleroma.Application do    use Application -  import Supervisor.Spec    @name Mix.Project.config()[:name]    @version Mix.Project.config()[:version] @@ -31,96 +30,128 @@ defmodule Pleroma.Application do      children =        [          # Start the Ecto repository -        supervisor(Pleroma.Repo, []), -        worker(Pleroma.Emoji, []), -        worker(Pleroma.Captcha, []), -        worker( -          Cachex, -          [ -            :used_captcha_cache, -            [ -              ttl_interval: :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid])) -            ] -          ], -          id: :cachex_used_captcha_cache -        ), -        worker( -          Cachex, -          [ -            :user_cache, -            [ -              default_ttl: 25_000, -              ttl_interval: 1000, -              limit: 2500 -            ] -          ], -          id: :cachex_user -        ), -        worker( -          Cachex, -          [ -            :object_cache, -            [ -              default_ttl: 25_000, -              ttl_interval: 1000, -              limit: 2500 -            ] -          ], -          id: :cachex_object -        ), -        worker( -          Cachex, -          [ -            :rich_media_cache, -            [ -              default_ttl: :timer.minutes(120), -              limit: 5000 -            ] -          ], -          id: :cachex_rich_media -        ), -        worker( -          Cachex, -          [ -            :scrubber_cache, -            [ -              limit: 2500 -            ] -          ], -          id: :cachex_scrubber -        ), -        worker( -          Cachex, -          [ -            :idempotency_cache, -            [ -              expiration: -                expiration( -                  default: :timer.seconds(6 * 60 * 60), -                  interval: :timer.seconds(60) -                ), -              limit: 2500 -            ] -          ], -          id: :cachex_idem -        ), -        worker(Pleroma.FlakeId, []), -        worker(Pleroma.ScheduledActivityWorker, []) +        %{id: Pleroma.Repo, start: {Pleroma.Repo, :start_link, []}, type: :supervisor}, +        %{id: Pleroma.Config.TransferTask, start: {Pleroma.Config.TransferTask, :start_link, []}}, +        %{id: Pleroma.Emoji, start: {Pleroma.Emoji, :start_link, []}}, +        %{id: Pleroma.Captcha, start: {Pleroma.Captcha, :start_link, []}}, +        %{ +          id: :cachex_used_captcha_cache, +          start: +            {Cachex, :start_link, +             [ +               :used_captcha_cache, +               [ +                 ttl_interval: +                   :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid])) +               ] +             ]} +        }, +        %{ +          id: :cachex_user, +          start: +            {Cachex, :start_link, +             [ +               :user_cache, +               [ +                 default_ttl: 25_000, +                 ttl_interval: 1000, +                 limit: 2500 +               ] +             ]} +        }, +        %{ +          id: :cachex_object, +          start: +            {Cachex, :start_link, +             [ +               :object_cache, +               [ +                 default_ttl: 25_000, +                 ttl_interval: 1000, +                 limit: 2500 +               ] +             ]} +        }, +        %{ +          id: :cachex_rich_media, +          start: +            {Cachex, :start_link, +             [ +               :rich_media_cache, +               [ +                 default_ttl: :timer.minutes(120), +                 limit: 5000 +               ] +             ]} +        }, +        %{ +          id: :cachex_scrubber, +          start: +            {Cachex, :start_link, +             [ +               :scrubber_cache, +               [ +                 limit: 2500 +               ] +             ]} +        }, +        %{ +          id: :cachex_idem, +          start: +            {Cachex, :start_link, +             [ +               :idempotency_cache, +               [ +                 expiration: +                   expiration( +                     default: :timer.seconds(6 * 60 * 60), +                     interval: :timer.seconds(60) +                   ), +                 limit: 2500 +               ] +             ]} +        }, +        %{id: Pleroma.FlakeId, start: {Pleroma.FlakeId, :start_link, []}}, +        %{ +          id: Pleroma.ScheduledActivityWorker, +          start: {Pleroma.ScheduledActivityWorker, :start_link, []} +        }        ] ++          hackney_pool_children() ++          [ -          worker(Pleroma.Web.Federator.RetryQueue, []), -          worker(Pleroma.Web.OAuth.Token.CleanWorker, []), -          worker(Pleroma.Stats, []), -          worker(Task, [&Pleroma.Web.Push.init/0], restart: :temporary, id: :web_push_init), -          worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary, id: :federator_init) +          %{ +            id: Pleroma.Web.Federator.RetryQueue, +            start: {Pleroma.Web.Federator.RetryQueue, :start_link, []} +          }, +          %{ +            id: Pleroma.Web.OAuth.Token.CleanWorker, +            start: {Pleroma.Web.OAuth.Token.CleanWorker, :start_link, []} +          }, +          %{ +            id: Pleroma.Stats, +            start: {Pleroma.Stats, :start_link, []} +          }, +          %{ +            id: :web_push_init, +            start: {Task, :start_link, [&Pleroma.Web.Push.init/0]}, +            restart: :temporary +          }, +          %{ +            id: :federator_init, +            start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]}, +            restart: :temporary +          }          ] ++          streamer_child() ++          chat_child() ++          [            # Start the endpoint when the application starts -          supervisor(Pleroma.Web.Endpoint, []), -          worker(Pleroma.Gopher.Server, []) +          %{ +            id: Pleroma.Web.Endpoint, +            start: {Pleroma.Web.Endpoint, :start_link, []}, +            type: :supervisor +          }, +          %{id: Pleroma.Gopher.Server, start: {Pleroma.Gopher.Server, :start_link, []}}          ]      # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html @@ -144,7 +175,6 @@ defmodule Pleroma.Application do        Pleroma.Repo.Instrumenter.setup()      end -    Prometheus.Registry.register_collector(:prometheus_process_collector)      Pleroma.Web.Endpoint.MetricsExporter.setup()      Pleroma.Web.Endpoint.PipelineInstrumenter.setup()      Pleroma.Web.Endpoint.Instrumenter.setup() @@ -157,24 +187,29 @@ defmodule Pleroma.Application do        else          []        end ++ -      if Pleroma.Config.get([Pleroma.Uploader, :proxy_remote]) do +      if Pleroma.Config.get([Pleroma.Upload, :proxy_remote]) do          [:upload]        else          []        end    end -  if Mix.env() == :test do +  if Pleroma.Config.get(:env) == :test do      defp streamer_child, do: []      defp chat_child, do: []    else      defp streamer_child do -      [worker(Pleroma.Web.Streamer, [])] +      [%{id: Pleroma.Web.Streamer, start: {Pleroma.Web.Streamer, :start_link, []}}]      end      defp chat_child do        if Pleroma.Config.get([:chat, :enabled]) do -        [worker(Pleroma.Web.ChatChannel.ChatChannelState, [])] +        [ +          %{ +            id: Pleroma.Web.ChatChannel.ChatChannelState, +            start: {Pleroma.Web.ChatChannel.ChatChannelState, :start_link, []} +          } +        ]        else          []        end diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex index a2c153720..79f133ea6 100644 --- a/lib/pleroma/bbs/authenticator.ex +++ b/lib/pleroma/bbs/authenticator.ex @@ -1,3 +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.BBS.Authenticator do    use Sshd.PasswordAuthenticator    alias Comeonin.Pbkdf2 diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex index f34be961f..0a381f592 100644 --- a/lib/pleroma/bbs/handler.ex +++ b/lib/pleroma/bbs/handler.ex @@ -1,3 +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.BBS.Handler do    use Sshd.ShellHandler    alias Pleroma.Activity diff --git a/lib/pleroma/bookmark.ex b/lib/pleroma/bookmark.ex index 7f8fd43b6..d976f949c 100644 --- a/lib/pleroma/bookmark.ex +++ b/lib/pleroma/bookmark.ex @@ -1,3 +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.Bookmark do    use Ecto.Schema diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha/captcha.ex index f105cbb25..a73b87251 100644 --- a/lib/pleroma/captcha/captcha.ex +++ b/lib/pleroma/captcha/captcha.ex @@ -3,6 +3,8 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Captcha do +  import Pleroma.Web.Gettext +    alias Calendar.DateTime    alias Plug.Crypto.KeyGenerator    alias Plug.Crypto.MessageEncryptor @@ -83,10 +85,11 @@ defmodule Pleroma.Captcha do        with {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),             %{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do          try do -          if DateTime.before?(at, valid_if_after), do: throw({:error, "CAPTCHA expired"}) +          if DateTime.before?(at, valid_if_after), +            do: throw({:error, dgettext("errors", "CAPTCHA expired")})            if not is_nil(Cachex.get!(:used_captcha_cache, token)), -            do: throw({:error, "CAPTCHA already used"}) +            do: throw({:error, dgettext("errors", "CAPTCHA already used")})            res = method().validate(token, captcha, answer_md5)            # Throw if an error occurs @@ -101,7 +104,7 @@ defmodule Pleroma.Captcha do            :throw, e -> e          end        else -        _ -> {:error, "Invalid answer data"} +        _ -> {:error, dgettext("errors", "Invalid answer data")}        end      {:reply, result, state} diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex index 18931d5a0..4e1a07c59 100644 --- a/lib/pleroma/captcha/kocaptcha.ex +++ b/lib/pleroma/captcha/kocaptcha.ex @@ -3,6 +3,7 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Captcha.Kocaptcha do +  import Pleroma.Web.Gettext    alias Pleroma.Captcha.Service    @behaviour Service @@ -12,7 +13,7 @@ defmodule Pleroma.Captcha.Kocaptcha do      case Tesla.get(endpoint <> "/new") do        {:error, _} -> -        %{error: "Kocaptcha service unavailable"} +        %{error: dgettext("errors", "Kocaptcha service unavailable")}        {:ok, res} ->          json_resp = Jason.decode!(res.body) @@ -32,6 +33,6 @@ defmodule Pleroma.Captcha.Kocaptcha do      if not is_nil(captcha) and           :crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),         do: :ok, -       else: {:error, "Invalid CAPTCHA"} +       else: {:error, dgettext("errors", "Invalid CAPTCHA")}    end  end diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex index 71a47b9fb..fcc039710 100644 --- a/lib/pleroma/config.ex +++ b/lib/pleroma/config.ex @@ -38,7 +38,7 @@ defmodule Pleroma.Config do    def put([parent_key | keys], value) do      parent = -      Application.get_env(:pleroma, parent_key) +      Application.get_env(:pleroma, parent_key, [])        |> put_in(keys, value)      Application.put_env(:pleroma, parent_key, parent) diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex new file mode 100644 index 000000000..7799b2a78 --- /dev/null +++ b/lib/pleroma/config/transfer_task.ex @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Config.TransferTask do +  use Task +  alias Pleroma.Web.AdminAPI.Config + +  def start_link do +    load_and_update_env() +    if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Pleroma.Repo) +    :ignore +  end + +  def load_and_update_env do +    if Pleroma.Config.get([:instance, :dynamic_configuration]) and +         Ecto.Adapters.SQL.table_exists?(Pleroma.Repo, "config") do +      for_restart = +        Pleroma.Repo.all(Config) +        |> Enum.map(&update_env(&1)) + +      # We need to restart applications for loaded settings take effect +      for_restart +      |> Enum.reject(&(&1 in [:pleroma, :ok])) +      |> Enum.each(fn app -> +        Application.stop(app) +        :ok = Application.start(app) +      end) +    end +  end + +  defp update_env(setting) do +    try do +      key = +        if String.starts_with?(setting.key, "Pleroma.") do +          "Elixir." <> setting.key +        else +          String.trim_leading(setting.key, ":") +        end + +      group = String.to_existing_atom(setting.group) + +      Application.put_env( +        group, +        String.to_existing_atom(key), +        Config.from_binary(setting.value) +      ) + +      group +    rescue +      e -> +        require Logger + +        Logger.warn( +          "updating env causes error, key: #{inspect(setting.key)}, error: #{inspect(e)}" +        ) +    end +  end +end diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex index 238c1acf2..bc97b39ca 100644 --- a/lib/pleroma/conversation.ex +++ b/lib/pleroma/conversation.ex @@ -49,7 +49,7 @@ defmodule Pleroma.Conversation do      with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),           "Create" <- activity.data["type"],           object <- Pleroma.Object.normalize(activity), -         "Note" <- object.data["type"], +         true <- object.data["type"] in ["Note", "Question"],           ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do        {:ok, conversation} = create_for_ap_id(ap_id) diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 2a11f9069..5883e4183 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -59,10 +59,10 @@ defmodule Pleroma.Conversation.Participation do    def for_user(user, params \\ %{}) do      from(p in __MODULE__,        where: p.user_id == ^user.id, -      order_by: [desc: p.updated_at] +      order_by: [desc: p.updated_at], +      preload: [conversation: [:users]]      )      |> Pleroma.Pagination.fetch_paginated(params) -    |> Repo.preload(conversation: [:users])    end    def for_user_with_last_activity_id(user, params \\ %{}) do @@ -79,5 +79,6 @@ defmodule Pleroma.Conversation.Participation do          | last_activity_id: activity_id        }      end) +    |> Enum.filter(& &1.last_activity_id)    end  end diff --git a/lib/pleroma/emails/mailer.ex b/lib/pleroma/emails/mailer.ex index 53f5a661c..2e4657b7c 100644 --- a/lib/pleroma/emails/mailer.ex +++ b/lib/pleroma/emails/mailer.ex @@ -3,11 +3,58 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Emails.Mailer do -  use Swoosh.Mailer, otp_app: :pleroma +  @moduledoc """ +  Defines the Pleroma mailer. +  The module contains functions to delivery email using Swoosh.Mailer. +  """ + +  alias Swoosh.DeliveryError + +  @otp_app :pleroma +  @mailer_config [otp: :pleroma] + +  @spec enabled?() :: boolean() +  def enabled?, do: Pleroma.Config.get([__MODULE__, :enabled]) + +  @doc "add email to queue"    def deliver_async(email, config \\ []) do      PleromaJobQueue.enqueue(:mailer, __MODULE__, [:deliver_async, email, config])    end +  @doc "callback to perform send email from queue"    def perform(:deliver_async, email, config), do: deliver(email, config) + +  @spec deliver(Swoosh.Email.t(), Keyword.t()) :: {:ok, term} | {:error, term} +  def deliver(email, config \\ []) + +  def deliver(email, config) do +    case enabled?() do +      true -> Swoosh.Mailer.deliver(email, parse_config(config)) +      false -> {:error, :deliveries_disabled} +    end +  end + +  @spec deliver!(Swoosh.Email.t(), Keyword.t()) :: term | no_return +  def deliver!(email, config \\ []) + +  def deliver!(email, config) do +    case deliver(email, config) do +      {:ok, result} -> result +      {:error, reason} -> raise DeliveryError, reason: reason +    end +  end + +  @on_load :validate_dependency + +  @doc false +  def validate_dependency do +    parse_config([]) +    |> Keyword.get(:adapter) +    |> Swoosh.Mailer.validate_dependency() +  end + +  defp parse_config(config) do +    Swoosh.Mailer.parse_config(@otp_app, __MODULE__, @mailer_config, config) +  end  end diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index 8502a0d0c..934620765 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -23,13 +23,8 @@ defmodule Pleroma.Emails.UserEmail do    defp recipient(email, name), do: {name, email}    defp recipient(%Pleroma.User{} = user), do: recipient(user.email, user.name) -  def password_reset_email(user, password_reset_token) when is_binary(password_reset_token) do -    password_reset_url = -      Router.Helpers.util_url( -        Endpoint, -        :show_password_reset, -        password_reset_token -      ) +  def password_reset_email(user, token) when is_binary(token) do +    password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)      html_body = """      <h3>Reset your password at #{instance_name()}</h3> diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index 6390cce4c..052501642 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -22,7 +22,6 @@ defmodule Pleroma.Emoji do    @ets __MODULE__.Ets    @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}] -  @groups Application.get_env(:pleroma, :emoji)[:groups]    @doc false    def start_link do @@ -87,6 +86,8 @@ defmodule Pleroma.Emoji do          "emoji"        ) +    emoji_groups = Pleroma.Config.get([:emoji, :groups]) +      case File.ls(emoji_dir_path) do        {:error, :enoent} ->          # The custom emoji directory doesn't exist, @@ -97,14 +98,28 @@ defmodule Pleroma.Emoji do          # There was some other error          Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}") -      {:ok, packs} -> +      {:ok, results} -> +        grouped = +          Enum.group_by(results, fn file -> File.dir?(Path.join(emoji_dir_path, file)) end) + +        packs = grouped[true] || [] +        files = grouped[false] || [] +          # Print the packs we've found          Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}") +        if not Enum.empty?(files) do +          Logger.warn( +            "Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{ +              Enum.join(files, ", ") +            }" +          ) +        end +          emojis =            Enum.flat_map(              packs, -            fn pack -> load_pack(Path.join(emoji_dir_path, pack)) end +            fn pack -> load_pack(Path.join(emoji_dir_path, pack), emoji_groups) end            )          true = :ets.insert(@ets, emojis) @@ -112,12 +127,12 @@ defmodule Pleroma.Emoji do      # Compat thing for old custom emoji handling & default emoji,      # it should run even if there are no emoji packs -    shortcode_globs = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || [] +    shortcode_globs = Pleroma.Config.get([:emoji, :shortcode_globs], [])      emojis = -      (load_from_file("config/emoji.txt") ++ -         load_from_file("config/custom_emoji.txt") ++ -         load_from_globs(shortcode_globs)) +      (load_from_file("config/emoji.txt", emoji_groups) ++ +         load_from_file("config/custom_emoji.txt", emoji_groups) ++ +         load_from_globs(shortcode_globs, emoji_groups))        |> Enum.reject(fn value -> value == nil end)      true = :ets.insert(@ets, emojis) @@ -125,23 +140,25 @@ defmodule Pleroma.Emoji do      :ok    end -  defp load_pack(pack_dir) do +  defp load_pack(pack_dir, emoji_groups) do      pack_name = Path.basename(pack_dir)      emoji_txt = Path.join(pack_dir, "emoji.txt")      if File.exists?(emoji_txt) do -      load_from_file(emoji_txt) +      load_from_file(emoji_txt, emoji_groups)      else +      extensions = Pleroma.Config.get([:emoji, :pack_extensions]) +        Logger.info( -        "No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji" +        "No emoji.txt found for pack \"#{pack_name}\", assuming all #{Enum.join(extensions, ", ")} files are emoji"        ) -      make_shortcode_to_file_map(pack_dir, [".png"]) +      make_shortcode_to_file_map(pack_dir, extensions)        |> Enum.map(fn {shortcode, rel_file} ->          filename = Path.join("/emoji/#{pack_name}", rel_file) -        {shortcode, filename, [to_string(match_extra(@groups, filename))]} +        {shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}        end)      end    end @@ -170,21 +187,21 @@ defmodule Pleroma.Emoji do      |> Enum.filter(fn f -> Path.extname(f) in exts end)    end -  defp load_from_file(file) do +  defp load_from_file(file, emoji_groups) do      if File.exists?(file) do -      load_from_file_stream(File.stream!(file)) +      load_from_file_stream(File.stream!(file), emoji_groups)      else        []      end    end -  defp load_from_file_stream(stream) do +  defp load_from_file_stream(stream, emoji_groups) do      stream      |> Stream.map(&String.trim/1)      |> Stream.map(fn line ->        case String.split(line, ~r/,\s*/) do          [name, file] -> -          {name, file, [to_string(match_extra(@groups, file))]} +          {name, file, [to_string(match_extra(emoji_groups, file))]}          [name, file | tags] ->            {name, file, tags} @@ -196,7 +213,7 @@ defmodule Pleroma.Emoji do      |> Enum.to_list()    end -  defp load_from_globs(globs) do +  defp load_from_globs(globs, emoji_groups) do      static_path = Path.join(:code.priv_dir(:pleroma), "static")      paths = @@ -207,7 +224,7 @@ defmodule Pleroma.Emoji do        |> Enum.concat()      Enum.map(paths, fn path -> -      tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path))) +      tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path)))        shortcode = Path.basename(path, Path.extname(path))        external_path = Path.join("/", Path.relative_to(path, static_path))        {shortcode, external_path, [to_string(tag)]} diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex new file mode 100644 index 000000000..8a79b44c4 --- /dev/null +++ b/lib/pleroma/helpers/uri_helper.ex @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Helpers.UriHelper do +  def append_uri_params(uri, appended_params) do +    uri = URI.parse(uri) +    appended_params = for {k, v} <- appended_params, into: %{}, do: {to_string(k), v} +    existing_params = URI.query_decoder(uri.query || "") |> Enum.into(%{}) +    updated_params_keys = Enum.uniq(Map.keys(existing_params) ++ Map.keys(appended_params)) + +    updated_params = +      for k <- updated_params_keys, do: {k, appended_params[k] || existing_params[k]} + +    uri +    |> Map.put(:query, URI.encode_query(updated_params)) +    |> URI.to_string() +  end + +  def append_param_if_present(%{} = params, param_name, param_value) do +    if param_value do +      Map.put(params, param_name, param_value) +    else +      params +    end +  end +end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index d1da746de..2fae7281c 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -89,7 +89,7 @@ defmodule Pleroma.HTML do      Cachex.fetch!(:scrubber_cache, key, fn _key ->        result =          content -        |> Floki.filter_out("a.mention") +        |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]")          |> Floki.attribute("a", "href")          |> Enum.at(0) @@ -104,7 +104,6 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do    paragraphs, breaks and links are allowed through the filter.    """ -  @markup Application.get_env(:pleroma, :markup)    @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])    require HtmlSanitizeEx.Scrubber.Meta @@ -142,9 +141,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do    Meta.allow_tag_with_these_attributes("span", [])    # allow inline images for custom emoji -  @allow_inline_images Keyword.get(@markup, :allow_inline_images) - -  if @allow_inline_images do +  if Pleroma.Config.get([:markup, :allow_inline_images]) do      # restrict img tags to http/https only, because of MediaProxy.      Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"]) @@ -168,7 +165,6 @@ defmodule Pleroma.HTML.Scrubber.Default do    # credo:disable-for-previous-line    # No idea how to fix this one… -  @markup Application.get_env(:pleroma, :markup)    @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])    Meta.remove_cdata_sections_before_scrub() @@ -213,7 +209,7 @@ defmodule Pleroma.HTML.Scrubber.Default do    Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])    Meta.allow_tag_with_these_attributes("span", []) -  @allow_inline_images Keyword.get(@markup, :allow_inline_images) +  @allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images])    if @allow_inline_images do      # restrict img tags to http/https only, because of MediaProxy. @@ -228,9 +224,7 @@ defmodule Pleroma.HTML.Scrubber.Default do      ])    end -  @allow_tables Keyword.get(@markup, :allow_tables) - -  if @allow_tables do +  if Pleroma.Config.get([:markup, :allow_tables]) do      Meta.allow_tag_with_these_attributes("table", [])      Meta.allow_tag_with_these_attributes("tbody", [])      Meta.allow_tag_with_these_attributes("td", []) @@ -239,9 +233,7 @@ defmodule Pleroma.HTML.Scrubber.Default do      Meta.allow_tag_with_these_attributes("tr", [])    end -  @allow_headings Keyword.get(@markup, :allow_headings) - -  if @allow_headings do +  if Pleroma.Config.get([:markup, :allow_headings]) do      Meta.allow_tag_with_these_attributes("h1", [])      Meta.allow_tag_with_these_attributes("h2", [])      Meta.allow_tag_with_these_attributes("h3", []) @@ -249,9 +241,7 @@ defmodule Pleroma.HTML.Scrubber.Default do      Meta.allow_tag_with_these_attributes("h5", [])    end -  @allow_fonts Keyword.get(@markup, :allow_fonts) - -  if @allow_fonts do +  if Pleroma.Config.get([:markup, :allow_fonts]) do      Meta.allow_tag_with_these_attributes("font", ["face"])    end diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index c216cdcb1..a1460d303 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -29,7 +29,7 @@ defmodule Pleroma.HTTP.Connection do    # fetch Hackney options    # -  defp hackney_options(opts) do +  def hackney_options(opts) do      options = Keyword.get(opts, :adapter, [])      adapter_options = Pleroma.Config.get([:http, :adapter], [])      proxy_url = Pleroma.Config.get([:http, :proxy_url], nil) diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index c5f720bc9..dec24458a 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -65,13 +65,7 @@ defmodule Pleroma.HTTP do    end    def process_request_options(options) do -    config = Application.get_env(:pleroma, :http, []) -    proxy = Keyword.get(config, :proxy_url, nil) - -    case proxy do -      nil -> options -      _ -> options ++ [proxy: proxy] -    end +    Keyword.merge(Pleroma.HTTP.Connection.hackney_options([]), options)    end    @doc """ diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex index 5e107f4c9..1b05d573c 100644 --- a/lib/pleroma/instances.ex +++ b/lib/pleroma/instances.ex @@ -1,3 +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.Instances do    @moduledoc "Instances context." @@ -13,7 +17,7 @@ defmodule Pleroma.Instances do    def reachability_datetime_threshold do      federation_reachability_timeout_days = -      Pleroma.Config.get(:instance)[:federation_reachability_timeout_days] || 0 +      Pleroma.Config.get([:instance, :federation_reachability_timeout_days], 0)      if federation_reachability_timeout_days > 0 do        NaiveDateTime.add( diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 420803a8f..4d7ed4ca1 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -1,3 +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.Instances.Instance do    @moduledoc "Instance." diff --git a/lib/pleroma/keys.ex b/lib/pleroma/keys.ex index b7bc7a4da..6dd31d3bd 100644 --- a/lib/pleroma/keys.ex +++ b/lib/pleroma/keys.ex @@ -35,10 +35,12 @@ defmodule Pleroma.Keys do    end    def keys_from_pem(pem) do -    [private_key_code] = :public_key.pem_decode(pem) -    private_key = :public_key.pem_entry_decode(private_key_code) -    {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key -    public_key = {:RSAPublicKey, modulus, exponent} -    {:ok, private_key, public_key} +    with [private_key_code] <- :public_key.pem_decode(pem), +         private_key <- :public_key.pem_entry_decode(private_key_code), +         {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do +      {:ok, private_key, {:RSAPublicKey, modulus, exponent}} +    else +      error -> {:error, error} +    end    end  end diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index a5b1cad68..1d320206e 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -16,6 +16,7 @@ defmodule Pleroma.List do      belongs_to(:user, User, type: Pleroma.FlakeId)      field(:title, :string)      field(:following, {:array, :string}, default: []) +    field(:ap_id, :string)      timestamps()    end @@ -55,6 +56,10 @@ defmodule Pleroma.List do      Repo.one(query)    end +  def get_by_ap_id(ap_id) do +    Repo.get_by(__MODULE__, ap_id: ap_id) +  end +    def get_following(%Pleroma.List{following: following} = _list) do      q =        from( @@ -105,7 +110,14 @@ defmodule Pleroma.List do    def create(title, %User{} = creator) do      list = %Pleroma.List{user_id: creator.id, title: title} -    Repo.insert(list) + +    Repo.transaction(fn -> +      list = Repo.insert!(list) + +      list +      |> change(ap_id: "#{creator.ap_id}/lists/#{list.id}") +      |> Repo.update!() +    end)    end    def follow(%Pleroma.List{following: following} = list, %User{} = followed) do @@ -125,4 +137,19 @@ defmodule Pleroma.List do      |> follow_changeset(attrs)      |> Repo.update()    end + +  def memberships(%User{follower_address: follower_address}) do +    Pleroma.List +    |> where([l], ^follower_address in l.following) +    |> select([l], l.ap_id) +    |> Repo.all() +  end + +  def memberships(_), do: [] + +  def member?(%Pleroma.List{following: following}, %User{follower_address: follower_address}) do +    Enum.member?(following, follower_address) +  end + +  def member?(_, _), do: false  end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 844264307..d47229258 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -11,8 +11,9 @@ defmodule Pleroma.Notification do    alias Pleroma.Pagination    alias Pleroma.Repo    alias Pleroma.User -  alias Pleroma.Web.CommonAPI    alias Pleroma.Web.CommonAPI.Utils +  alias Pleroma.Web.Push +  alias Pleroma.Web.Streamer    import Ecto.Query    import Ecto.Changeset @@ -30,31 +31,47 @@ defmodule Pleroma.Notification do      |> cast(attrs, [:seen])    end -  def for_user_query(user) do -    Notification -    |> where(user_id: ^user.id) -    |> where( -      [n, a], -      fragment( -        "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')", -        a.actor -      ) -    ) -    |> join(:inner, [n], activity in assoc(n, :activity)) -    |> join(:left, [n, a], object in Object, -      on: +  def for_user_query(user, opts) do +    query = +      Notification +      |> where(user_id: ^user.id) +      |> where( +        [n, a],          fragment( -          "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)", -          object.data, -          a.data +          "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')", +          a.actor          ) -    ) -    |> preload([n, a, o], activity: {a, object: o}) +      ) +      |> join(:inner, [n], activity in assoc(n, :activity)) +      |> join(:left, [n, a], object in Object, +        on: +          fragment( +            "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)", +            object.data, +            a.data +          ) +      ) +      |> preload([n, a, o], activity: {a, object: o}) + +    if opts[:with_muted] do +      query +    else +      where(query, [n, a], a.actor not in ^user.info.muted_notifications) +      |> where([n, a], a.actor not in ^user.info.blocks) +      |> where( +        [n, a], +        fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks +      ) +      |> join(:left, [n, a], tm in Pleroma.ThreadMute, +        on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data) +      ) +      |> where([n, a, o, tm], is_nil(tm.user_id)) +    end    end    def for_user(user, opts \\ %{}) do      user -    |> for_user_query() +    |> for_user_query(opts)      |> Pagination.fetch_paginated(opts)    end @@ -125,10 +142,21 @@ defmodule Pleroma.Notification do      end    end +  def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do +    object = Object.normalize(activity) + +    unless object && object.data["type"] == "Answer" do +      users = get_notified_from_activity(activity) +      notifications = Enum.map(users, fn user -> create_notification(activity, user) end) +      {:ok, notifications} +    else +      {:ok, []} +    end +  end +    def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity) -      when type in ["Create", "Like", "Announce", "Follow"] do +      when type in ["Like", "Announce", "Follow"] do      users = get_notified_from_activity(activity) -      notifications = Enum.map(users, fn user -> create_notification(activity, user) end)      {:ok, notifications}    end @@ -140,8 +168,9 @@ defmodule Pleroma.Notification do      unless skip?(activity, user) do        notification = %Notification{user_id: user.id, activity: activity}        {:ok, notification} = Repo.insert(notification) -      Pleroma.Web.Streamer.stream("user", notification) -      Pleroma.Web.Push.send(notification) +      Streamer.stream("user", notification) +      Streamer.stream("user:notification", notification) +      Push.send(notification)        notification      end    end @@ -165,32 +194,24 @@ defmodule Pleroma.Notification do    def get_notified_from_activity(_, _local_only), do: [] +  @spec skip?(Activity.t(), User.t()) :: boolean()    def skip?(activity, user) do -    [:self, :blocked, :local, :muted, :followers, :follows, :recently_followed] +    [ +      :self, +      :followers, +      :follows, +      :non_followers, +      :non_follows, +      :recently_followed +    ]      |> Enum.any?(&skip?(&1, activity, user))    end +  @spec skip?(atom(), Activity.t(), User.t()) :: boolean()    def skip?(:self, activity, user) do      activity.data["actor"] == user.ap_id    end -  def skip?(:blocked, activity, user) do -    actor = activity.data["actor"] -    User.blocks?(user, %{ap_id: actor}) -  end - -  def skip?(:local, %{local: true}, %{info: %{notification_settings: %{"local" => false}}}), -    do: true - -  def skip?(:local, %{local: false}, %{info: %{notification_settings: %{"remote" => false}}}), -    do: true - -  def skip?(:muted, activity, user) do -    actor = activity.data["actor"] - -    User.mutes?(user, %{ap_id: actor}) or CommonAPI.thread_muted?(user, activity) -  end -    def skip?(          :followers,          activity, @@ -201,12 +222,32 @@ defmodule Pleroma.Notification do      User.following?(follower, user)    end +  def skip?( +        :non_followers, +        activity, +        %{info: %{notification_settings: %{"non_followers" => false}}} = user +      ) do +    actor = activity.data["actor"] +    follower = User.get_cached_by_ap_id(actor) +    !User.following?(follower, user) +  end +    def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do      actor = activity.data["actor"]      followed = User.get_cached_by_ap_id(actor)      User.following?(user, followed)    end +  def skip?( +        :non_follows, +        activity, +        %{info: %{notification_settings: %{"non_follows" => false}}} = user +      ) do +    actor = activity.data["actor"] +    followed = User.get_cached_by_ap_id(actor) +    !User.following?(user, followed) +  end +    def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do      actor = activity.data["actor"] diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index cc6fc9c5d..c8d339c19 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -35,50 +35,55 @@ defmodule Pleroma.Object do      |> unique_constraint(:ap_id, name: :objects_unique_apid_index)    end +  def get_by_id(nil), do: nil +  def get_by_id(id), do: Repo.get(Object, id) +    def get_by_ap_id(nil), do: nil    def get_by_ap_id(ap_id) do      Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))    end -  def normalize(_, fetch_remote \\ true) +  defp warn_on_no_object_preloaded(ap_id) do +    "Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object" +    |> Logger.debug() + +    Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}") +  end + +  def normalize(_, fetch_remote \\ true, options \\ []) +    # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.    # Use this whenever possible, especially when walking graphs in an O(N) loop! -  def normalize(%Object{} = object, _), do: object -  def normalize(%Activity{object: %Object{} = object}, _), do: object +  def normalize(%Object{} = object, _, _), do: object +  def normalize(%Activity{object: %Object{} = object}, _, _), do: object    # A hack for fake activities -  def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do +  def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _, _) do      %Object{id: "pleroma:fake_object_id", data: data}    end -  # Catch and log Object.normalize() calls where the Activity's child object is not -  # preloaded. -  def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote) do -    Logger.debug( -      "Object.normalize() called without preloaded object (#{ap_id}).  Consider preloading the object!" -    ) - -    Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}") - +  # No preloaded object +  def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote, _) do +    warn_on_no_object_preloaded(ap_id)      normalize(ap_id, fetch_remote)    end -  def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote) do -    Logger.debug( -      "Object.normalize() called without preloaded object (#{ap_id}).  Consider preloading the object!" -    ) - -    Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}") - +  # No preloaded object +  def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote, _) do +    warn_on_no_object_preloaded(ap_id)      normalize(ap_id, fetch_remote)    end    # Old way, try fetching the object through cache. -  def normalize(%{"id" => ap_id}, fetch_remote), do: normalize(ap_id, fetch_remote) -  def normalize(ap_id, false) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id) -  def normalize(ap_id, true) when is_binary(ap_id), do: Fetcher.fetch_object_from_id!(ap_id) -  def normalize(_, _), do: nil +  def normalize(%{"id" => ap_id}, fetch_remote, _), do: normalize(ap_id, fetch_remote) +  def normalize(ap_id, false, _) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id) + +  def normalize(ap_id, true, options) when is_binary(ap_id) do +    Fetcher.fetch_object_from_id!(ap_id, options) +  end + +  def normalize(_, _, _), do: nil    # Owned objects can only be mutated by their owner    def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}), @@ -195,4 +200,34 @@ defmodule Pleroma.Object do        _ -> {:error, "Not found"}      end    end + +  def increase_vote_count(ap_id, name) do +    with %Object{} = object <- Object.normalize(ap_id), +         "Question" <- object.data["type"] do +      multiple = Map.has_key?(object.data, "anyOf") + +      options = +        (object.data["anyOf"] || object.data["oneOf"] || []) +        |> Enum.map(fn +          %{"name" => ^name} = option -> +            Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1)) + +          option -> +            option +        end) + +      data = +        if multiple do +          Map.put(object.data, "anyOf", options) +        else +          Map.put(object.data, "oneOf", options) +        end + +      object +      |> Object.change(%{data: data}) +      |> update_and_set_cache() +    else +      _ -> :noop +    end +  end  end diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index 2f4687fa2..f077a9f32 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -1,3 +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.Object.Containment do    @moduledoc """    This module contains some useful functions for containing objects to specific @@ -44,6 +48,9 @@ defmodule Pleroma.Object.Containment do      end    end +  def contain_origin(id, %{"attributedTo" => actor} = params), +    do: contain_origin(id, Map.put(params, "actor", actor)) +    def contain_origin_from_id(_id, %{"id" => nil}), do: :error    def contain_origin_from_id(id, %{"id" => other_id} = _params) do @@ -56,4 +63,9 @@ defmodule Pleroma.Object.Containment do        :error      end    end + +  def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}), +    do: contain_origin(id, object) + +  def contain_child(_), do: :ok  end diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index ca980c629..96b34ae9f 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -1,3 +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.Object.Fetcher do    alias Pleroma.HTTP    alias Pleroma.Object @@ -22,39 +26,45 @@ defmodule Pleroma.Object.Fetcher do    # TODO:    # This will create a Create activity, which we need internally at the moment. -  def fetch_object_from_id(id) do +  def fetch_object_from_id(id, options \\ []) do      if object = Object.get_cached_by_ap_id(id) do        {:ok, object}      else        Logger.info("Fetching #{id} via AP") -      with {:ok, data} <- fetch_and_contain_remote_object_from_id(id), -           nil <- Object.normalize(data, false), +      with {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)}, +           {:normalize, nil} <- {:normalize, Object.normalize(data, false)},             params <- %{               "type" => "Create",               "to" => data["to"],               "cc" => data["cc"], +             # Should we seriously keep this attributedTo thing?               "actor" => data["actor"] || data["attributedTo"],               "object" => data             }, -           :ok <- Containment.contain_origin(id, params), -           {:ok, activity} <- Transmogrifier.handle_incoming(params), +           {:containment, :ok} <- {:containment, Containment.contain_origin(id, params)}, +           {:ok, activity} <- Transmogrifier.handle_incoming(params, options),             {:object, _data, %Object{} = object} <-               {:object, data, Object.normalize(activity, false)} do          {:ok, object}        else +        {:containment, _} -> +          {:error, "Object containment failed."} +          {:error, {:reject, nil}} ->            {:reject, nil}          {:object, data, nil} ->            reinject_object(data) -        object = %Object{} -> +        {:normalize, object = %Object{}} ->            {:ok, object}          _e -> +          # Only fallback when receiving a fetch/normalization error with ActivityPub            Logger.info("Couldn't get object via AP, trying out OStatus fetching...") +          # FIXME: OStatus Object Containment?            case OStatus.fetch_activity_from_url(id) do              {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}              e -> e @@ -63,8 +73,8 @@ defmodule Pleroma.Object.Fetcher do      end    end -  def fetch_object_from_id!(id) do -    with {:ok, object} <- fetch_object_from_id(id) do +  def fetch_object_from_id!(id, options \\ []) do +    with {:ok, object} <- fetch_object_from_id(id, options) do        object      else        _e -> @@ -85,6 +95,9 @@ defmodule Pleroma.Object.Fetcher do           :ok <- Containment.contain_origin_from_id(id, data) do        {:ok, data}      else +      {:ok, %{status: code}} when code in [404, 410] -> +        {:error, "Object has been deleted"} +        e ->          {:error, e}      end diff --git a/lib/pleroma/object_tombstone.ex b/lib/pleroma/object_tombstone.ex index 64d836d3e..fe947ffd3 100644 --- a/lib/pleroma/object_tombstone.ex +++ b/lib/pleroma/object_tombstone.ex @@ -1,3 +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.ObjectTombstone do    @enforce_keys [:id, :formerType, :deleted]    defstruct [:id, :formerType, :deleted, type: "Tombstone"] diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex index f435e5c9c..2b869ccdc 100644 --- a/lib/pleroma/pagination.ex +++ b/lib/pleroma/pagination.ex @@ -1,3 +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.Pagination do    @moduledoc """    Implements Mastodon-compatible pagination. @@ -10,16 +14,28 @@ defmodule Pleroma.Pagination do    @default_limit 20 -  def fetch_paginated(query, params) do +  def fetch_paginated(query, params, type \\ :keyset) + +  def fetch_paginated(query, params, :keyset) do      options = cast_params(params)      query -    |> paginate(options) +    |> paginate(options, :keyset)      |> Repo.all()      |> enforce_order(options)    end -  def paginate(query, options) do +  def fetch_paginated(query, params, :offset) do +    options = cast_params(params) + +    query +    |> paginate(options, :offset) +    |> Repo.all() +  end + +  def paginate(query, options, method \\ :keyset) + +  def paginate(query, options, :keyset) do      query      |> restrict(:min_id, options)      |> restrict(:since_id, options) @@ -28,11 +44,18 @@ defmodule Pleroma.Pagination do      |> restrict(:limit, options)    end +  def paginate(query, options, :offset) do +    query +    |> restrict(:offset, options) +    |> restrict(:limit, options) +  end +    defp cast_params(params) do      param_types = %{        min_id: :string,        since_id: :string,        max_id: :string, +      offset: :integer,        limit: :integer      } @@ -66,6 +89,10 @@ defmodule Pleroma.Pagination do      order_by(query, [u], fragment("? desc nulls last", u.id))    end +  defp restrict(query, :offset, %{offset: offset}) do +    offset(query, ^offset) +  end +    defp restrict(query, :limit, options) do      limit = Map.get(options, :limit, @default_limit) diff --git a/lib/pleroma/PasswordResetToken.ex b/lib/pleroma/password_reset_token.ex index f31ea5bc5..4a833f6a5 100644 --- a/lib/pleroma/PasswordResetToken.ex +++ b/lib/pleroma/password_reset_token.ex @@ -37,6 +37,7 @@ defmodule Pleroma.PasswordResetToken do      |> put_change(:used, true)    end +  @spec reset_password(binary(), map()) :: {:ok, User.t()} | {:error, binary()}    def reset_password(token, data) do      with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),           %User{} = user <- User.get_cached_by_id(token.user_id), diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex index da4ed4226..eec514892 100644 --- a/lib/pleroma/plugs/authentication_plug.ex +++ b/lib/pleroma/plugs/authentication_plug.ex @@ -6,11 +6,26 @@ defmodule Pleroma.Plugs.AuthenticationPlug do    alias Comeonin.Pbkdf2    import Plug.Conn    alias Pleroma.User +  require Logger    def init(options) do      options    end +  def checkpw(password, password_hash) do +    cond do +      String.starts_with?(password_hash, "$pbkdf2") -> +        Pbkdf2.checkpw(password, password_hash) + +      String.starts_with?(password_hash, "$6") -> +        :crypt.crypt(password, password_hash) == password_hash + +      true -> +        Logger.error("Password hash not recognized") +        false +    end +  end +    def call(%{assigns: %{user: %User{}}} = conn, _), do: conn    def call( diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex index 11c4342c4..27cd41aec 100644 --- a/lib/pleroma/plugs/ensure_authenticated_plug.ex +++ b/lib/pleroma/plugs/ensure_authenticated_plug.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do    import Plug.Conn +  import Pleroma.Web.TranslationHelpers    alias Pleroma.User    def init(options) do @@ -16,8 +17,7 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do    def call(conn, _) do      conn -    |> put_resp_content_type("application/json") -    |> send_resp(403, Jason.encode!(%{error: "Invalid credentials."})) +    |> render_error(:forbidden, "Invalid credentials.")      |> halt    end  end diff --git a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex index 317fd5445..a16f61435 100644 --- a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex +++ b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex @@ -3,6 +3,7 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do +  import Pleroma.Web.TranslationHelpers    import Plug.Conn    alias Pleroma.Config    alias Pleroma.User @@ -23,8 +24,7 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do        {false, _} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{error: "This resource requires authentication."})) +        |> render_error(:forbidden, "This resource requires authentication.")          |> halt      end    end diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex index effc154bf..4dc4e9279 100644 --- a/lib/pleroma/plugs/federating_plug.ex +++ b/lib/pleroma/plugs/federating_plug.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.FederatingPlug do    end    def call(conn, _opts) do -    if Keyword.get(Application.get_env(:pleroma, :instance), :federating) do +    if Pleroma.Config.get([:instance, :federating]) do        conn      else        conn diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 485ddfbc7..a7cc22831 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -56,14 +56,14 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do      connect_src = "connect-src 'self' #{static_url} #{websocket_url}"      connect_src = -      if Mix.env() == :dev do +      if Pleroma.Config.get(:env) == :dev do          connect_src <> " http://localhost:3035/"        else          connect_src        end      script_src = -      if Mix.env() == :dev do +      if Pleroma.Config.get(:env) == :dev do          "script-src 'self' 'unsafe-eval'"        else          "script-src 'self'" diff --git a/lib/pleroma/plugs/idempotency_plug.ex b/lib/pleroma/plugs/idempotency_plug.ex new file mode 100644 index 000000000..e99c5d279 --- /dev/null +++ b/lib/pleroma/plugs/idempotency_plug.ex @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.IdempotencyPlug do +  import Phoenix.Controller, only: [json: 2] +  import Plug.Conn + +  @behaviour Plug + +  @impl true +  def init(opts), do: opts + +  # Sending idempotency keys in `GET` and `DELETE` requests has no effect +  # and should be avoided, as these requests are idempotent by definition. + +  @impl true +  def call(%{method: method} = conn, _) when method in ["POST", "PUT", "PATCH"] do +    case get_req_header(conn, "idempotency-key") do +      [key] -> process_request(conn, key) +      _ -> conn +    end +  end + +  def call(conn, _), do: conn + +  def process_request(conn, key) do +    case Cachex.get(:idempotency_cache, key) do +      {:ok, nil} -> +        cache_resposnse(conn, key) + +      {:ok, record} -> +        send_cached(conn, key, record) + +      {atom, message} when atom in [:ignore, :error] -> +        render_error(conn, message) +    end +  end + +  defp cache_resposnse(conn, key) do +    register_before_send(conn, fn conn -> +      [request_id] = get_resp_header(conn, "x-request-id") +      content_type = get_content_type(conn) + +      record = {request_id, content_type, conn.status, conn.resp_body} +      {:ok, _} = Cachex.put(:idempotency_cache, key, record) + +      conn +      |> put_resp_header("idempotency-key", key) +      |> put_resp_header("x-original-request-id", request_id) +    end) +  end + +  defp send_cached(conn, key, record) do +    {request_id, content_type, status, body} = record + +    conn +    |> put_resp_header("idempotency-key", key) +    |> put_resp_header("idempotent-replayed", "true") +    |> put_resp_header("x-original-request-id", request_id) +    |> put_resp_content_type(content_type) +    |> send_resp(status, body) +    |> halt() +  end + +  defp render_error(conn, message) do +    conn +    |> put_status(:unprocessable_entity) +    |> json(%{error: message}) +    |> halt() +  end + +  defp get_content_type(conn) do +    [content_type] = get_resp_header(conn, "content-type") + +    if String.contains?(content_type, ";") do +      content_type +      |> String.split(";") +      |> hd() +    else +      content_type +    end +  end +end diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index f2bfa2b1a..b508628a9 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Plugs.OAuthScopesPlug do    import Plug.Conn +  import Pleroma.Web.Gettext    @behaviour Plug @@ -30,11 +31,14 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do        true ->          missing_scopes = scopes -- token.scopes -        error_message = "Insufficient permissions: #{Enum.join(missing_scopes, " #{op} ")}." +        permissions = Enum.join(missing_scopes, " #{op} ") + +        error_message = +          dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions)          conn          |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{error: error_message})) +        |> send_resp(:forbidden, Jason.encode!(%{error: error_message}))          |> halt()      end    end diff --git a/lib/pleroma/plugs/rate_limit_plug.ex b/lib/pleroma/plugs/rate_limit_plug.ex deleted file mode 100644 index 466f64a79..000000000 --- a/lib/pleroma/plugs/rate_limit_plug.ex +++ /dev/null @@ -1,36 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RateLimitPlug do -  import Phoenix.Controller, only: [json: 2] -  import Plug.Conn - -  def init(opts), do: opts - -  def call(conn, opts) do -    enabled? = Pleroma.Config.get([:app_account_creation, :enabled]) - -    case check_rate(conn, Map.put(opts, :enabled, enabled?)) do -      {:ok, _count} -> conn -      {:error, _count} -> render_error(conn) -      %Plug.Conn{} = conn -> conn -    end -  end - -  defp check_rate(conn, %{enabled: true} = opts) do -    max_requests = opts[:max_requests] -    bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".") - -    ExRated.check_rate(bucket_name, opts[:interval] * 1000, max_requests) -  end - -  defp check_rate(conn, _), do: conn - -  defp render_error(conn) do -    conn -    |> put_status(:forbidden) -    |> json(%{error: "Rate limit exceeded."}) -    |> halt() -  end -end diff --git a/lib/pleroma/plugs/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter.ex new file mode 100644 index 000000000..31388f574 --- /dev/null +++ b/lib/pleroma/plugs/rate_limiter.ex @@ -0,0 +1,131 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.RateLimiter do +  @moduledoc """ + +  ## Configuration + +  A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where: + +  * The first element: `scale` (Integer). The time scale in milliseconds. +  * The second element: `limit` (Integer). How many requests to limit in the time scale provided. + +  It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated. + +  To disable a limiter set its value to `nil`. + +  ### Example + +      config :pleroma, :rate_limit, +        one: {1000, 10}, +        two: [{10_000, 10}, {10_000, 50}], +        foobar: nil + +  Here we have three limiters: + +  * `one` which is not over 10req/1s +  * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users +  * `foobar` which is disabled + +  ## Usage + +  AllowedSyntax: + +      plug(Pleroma.Plugs.RateLimiter, :limiter_name) +      plug(Pleroma.Plugs.RateLimiter, {:limiter_name, options}) + +  Allowed options: + +      * `bucket_name` overrides bucket name (e.g. to have a separate limit for a set of actions) +      * `params` appends values of specified request params (e.g. ["id"]) to bucket name + +  Inside a controller: + +      plug(Pleroma.Plugs.RateLimiter, :one when action == :one) +      plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three]) + +      plug( +        Pleroma.Plugs.RateLimiter, +        {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} +        when action in ~w(fav_status unfav_status)a +      ) + +  or inside a router pipeline: + +      pipeline :api do +        ... +        plug(Pleroma.Plugs.RateLimiter, :one) +        ... +      end +  """ +  import Pleroma.Web.TranslationHelpers +  import Plug.Conn + +  alias Pleroma.User + +  def init(limiter_name) when is_atom(limiter_name) do +    init({limiter_name, []}) +  end + +  def init({limiter_name, opts}) do +    case Pleroma.Config.get([:rate_limit, limiter_name]) do +      nil -> nil +      config -> {limiter_name, config, opts} +    end +  end + +  # Do not limit if there is no limiter configuration +  def call(conn, nil), do: conn + +  def call(conn, settings) do +    case check_rate(conn, settings) do +      {:ok, _count} -> +        conn + +      {:error, _count} -> +        render_throttled_error(conn) +    end +  end + +  defp bucket_name(conn, limiter_name, opts) do +    bucket_name = opts[:bucket_name] || limiter_name + +    if params_names = opts[:params] do +      params_values = for p <- Enum.sort(params_names), do: conn.params[p] +      Enum.join([bucket_name] ++ params_values, ":") +    else +      bucket_name +    end +  end + +  defp check_rate( +         %{assigns: %{user: %User{id: user_id}}} = conn, +         {limiter_name, [_, {scale, limit}], opts} +       ) do +    bucket_name = bucket_name(conn, limiter_name, opts) +    ExRated.check_rate("#{bucket_name}:#{user_id}", scale, limit) +  end + +  defp check_rate(conn, {limiter_name, [{scale, limit} | _], opts}) do +    bucket_name = bucket_name(conn, limiter_name, opts) +    ExRated.check_rate("#{bucket_name}:#{ip(conn)}", scale, limit) +  end + +  defp check_rate(conn, {limiter_name, {scale, limit}, opts}) do +    check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts}) +  end + +  def ip(%{remote_ip: remote_ip}) do +    remote_ip +    |> Tuple.to_list() +    |> Enum.join(".") +  end + +  defp render_throttled_error(conn) do +    conn +    |> render_error(:too_many_requests, "Throttled") +    |> halt() +  end +end diff --git a/lib/pleroma/plugs/set_locale_plug.ex b/lib/pleroma/plugs/set_locale_plug.ex new file mode 100644 index 000000000..8646cb30d --- /dev/null +++ b/lib/pleroma/plugs/set_locale_plug.ex @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +# NOTE: this module is based on https://github.com/smeevil/set_locale +defmodule Pleroma.Plugs.SetLocalePlug do +  import Plug.Conn, only: [get_req_header: 2, assign: 3] + +  def init(_), do: nil + +  def call(conn, _) do +    locale = get_locale_from_header(conn) || Gettext.get_locale() +    Gettext.put_locale(locale) +    assign(conn, :locale, locale) +  end + +  defp get_locale_from_header(conn) do +    conn +    |> extract_accept_language() +    |> Enum.find(&supported_locale?/1) +  end + +  defp extract_accept_language(conn) do +    case get_req_header(conn, "accept-language") do +      [value | _] -> +        value +        |> String.split(",") +        |> Enum.map(&parse_language_option/1) +        |> Enum.sort(&(&1.quality > &2.quality)) +        |> Enum.map(& &1.tag) +        |> Enum.reject(&is_nil/1) +        |> ensure_language_fallbacks() + +      _ -> +        [] +    end +  end + +  defp supported_locale?(locale) do +    Pleroma.Web.Gettext +    |> Gettext.known_locales() +    |> Enum.member?(locale) +  end + +  defp parse_language_option(string) do +    captures = Regex.named_captures(~r/^\s?(?<tag>[\w\-]+)(?:;q=(?<quality>[\d\.]+))?$/i, string) + +    quality = +      case Float.parse(captures["quality"] || "1.0") do +        {val, _} -> val +        :error -> 1.0 +      end + +    %{tag: captures["tag"], quality: quality} +  end + +  defp ensure_language_fallbacks(tags) do +    Enum.flat_map(tags, fn tag -> +      [language | _] = String.split(tag, "-") +      if Enum.member?(tags, language), do: [tag], else: [tag, language] +    end) +  end +end diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex index fd77b8d8f..69c1ab942 100644 --- a/lib/pleroma/plugs/uploaded_media.ex +++ b/lib/pleroma/plugs/uploaded_media.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Plugs.UploadedMedia do    """    import Plug.Conn +  import Pleroma.Web.Gettext    require Logger    @behaviour Plug @@ -36,7 +37,7 @@ defmodule Pleroma.Plugs.UploadedMedia do            conn        end -    config = Pleroma.Config.get([Pleroma.Upload]) +    config = Pleroma.Config.get(Pleroma.Upload)      with uploader <- Keyword.fetch!(config, :uploader),           proxy_remote = Keyword.get(config, :proxy_remote, false), @@ -45,7 +46,7 @@ defmodule Pleroma.Plugs.UploadedMedia do      else        _ ->          conn -        |> send_resp(500, "Failed") +        |> send_resp(:internal_server_error, dgettext("errors", "Failed"))          |> halt()      end    end @@ -64,7 +65,7 @@ defmodule Pleroma.Plugs.UploadedMedia do        conn      else        conn -      |> send_resp(404, "Not found") +      |> send_resp(:not_found, dgettext("errors", "Not found"))        |> halt()      end    end @@ -84,7 +85,7 @@ defmodule Pleroma.Plugs.UploadedMedia do      Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}")      conn -    |> send_resp(500, "Internal Error") +    |> send_resp(:internal_server_error, dgettext("errors", "Internal Error"))      |> halt()    end  end diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index 04329e919..4c4b3d610 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -3,6 +3,7 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Plugs.UserIsAdminPlug do +  import Pleroma.Web.TranslationHelpers    import Plug.Conn    alias Pleroma.User @@ -16,8 +17,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do    def call(conn, _) do      conn -    |> put_resp_content_type("application/json") -    |> send_resp(403, Jason.encode!(%{error: "User is not admin."})) +    |> render_error(:forbidden, "User is not admin.")      |> halt    end  end diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex new file mode 100644 index 000000000..8afabf463 --- /dev/null +++ b/lib/pleroma/release_tasks.ex @@ -0,0 +1,66 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReleaseTasks do +  @repo Pleroma.Repo + +  def run(args) do +    [task | args] = String.split(args) + +    case task do +      "migrate" -> migrate(args) +      "create" -> create() +      "rollback" -> rollback(args) +      task -> mix_task(task, args) +    end +  end + +  defp mix_task(task, args) do +    Application.load(:pleroma) +    {:ok, modules} = :application.get_key(:pleroma, :modules) + +    module = +      Enum.find(modules, fn module -> +        module = Module.split(module) + +        match?(["Mix", "Tasks", "Pleroma" | _], module) and +          String.downcase(List.last(module)) == task +      end) + +    if module do +      module.run(args) +    else +      IO.puts("The task #{task} does not exist") +    end +  end + +  def migrate(args) do +    Mix.Tasks.Pleroma.Ecto.Migrate.run(args) +  end + +  def rollback(args) do +    Mix.Tasks.Pleroma.Ecto.Rollback.run(args) +  end + +  def create do +    Application.load(:pleroma) + +    case @repo.__adapter__.storage_up(@repo.config) do +      :ok -> +        IO.puts("The database for #{inspect(@repo)} has been created") + +      {:error, :already_up} -> +        IO.puts("The database for #{inspect(@repo)} has already been created") + +      {:error, term} when is_binary(term) -> +        IO.puts(:stderr, "The database for #{inspect(@repo)} couldn't be created: #{term}") + +      {:error, term} -> +        IO.puts( +          :stderr, +          "The database for #{inspect(@repo)} couldn't be created: #{inspect(term)}" +        ) +    end +  end +end diff --git a/lib/pleroma/repo_streamer.ex b/lib/pleroma/repo_streamer.ex new file mode 100644 index 000000000..a4b71a1bb --- /dev/null +++ b/lib/pleroma/repo_streamer.ex @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RepoStreamer do +  alias Pleroma.Repo +  import Ecto.Query + +  def chunk_stream(query, chunk_size) do +    Stream.unfold(0, fn +      :halt -> +        {[], :halt} + +      last_id -> +        query +        |> order_by(asc: :id) +        |> where([r], r.id > ^last_id) +        |> limit(^chunk_size) +        |> Repo.all() +        |> case do +          [] -> +            {[], :halt} + +          records -> +            last_id = List.last(records).id +            {records, last_id} +        end +    end) +    |> Stream.take_while(fn +      [] -> false +      _ -> true +    end) +  end +end diff --git a/lib/pleroma/reverse_proxy/client.ex b/lib/pleroma/reverse_proxy/client.ex new file mode 100644 index 000000000..776c4794c --- /dev/null +++ b/lib/pleroma/reverse_proxy/client.ex @@ -0,0 +1,28 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReverseProxy.Client do +  @callback request(atom(), String.t(), [tuple()], String.t(), list()) :: +              {:ok, pos_integer(), [tuple()], reference() | map()} +              | {:ok, pos_integer(), [tuple()]} +              | {:ok, reference()} +              | {:error, term()} + +  @callback stream_body(reference() | pid() | map()) :: +              {:ok, binary()} | :done | {:error, String.t()} + +  @callback close(reference() | pid() | map()) :: :ok + +  def request(method, url, headers, "", opts \\ []) do +    client().request(method, url, headers, "", opts) +  end + +  def stream_body(ref), do: client().stream_body(ref) + +  def close(ref), do: client().close(ref) + +  defp client do +    Pleroma.Config.get([Pleroma.ReverseProxy.Client], :hackney) +  end +end diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index 6e5feb4c3..1f98f215c 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -61,9 +61,7 @@ defmodule Pleroma.ReverseProxy do    * `http`: options for [hackney](https://github.com/benoitc/hackney).    """ -  @hackney Application.get_env(:pleroma, :hackney, :hackney) - -  @default_hackney_options [] +  @default_hackney_options [pool: :media]    @inline_content_types [      "image/gif", @@ -96,7 +94,8 @@ defmodule Pleroma.ReverseProxy do    def call(conn = %{method: method}, url, opts) when method in @methods do      hackney_opts = -      @default_hackney_options +      Pleroma.HTTP.Connection.hackney_options([]) +      |> Keyword.merge(@default_hackney_options)        |> Keyword.merge(Keyword.get(opts, :http, []))        |> HTTP.process_request_options() @@ -148,7 +147,7 @@ defmodule Pleroma.ReverseProxy do      Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")      method = method |> String.downcase() |> String.to_existing_atom() -    case @hackney.request(method, url, headers, "", hackney_opts) do +    case client().request(method, url, headers, "", hackney_opts) do        {:ok, code, headers, client} when code in @valid_resp_codes ->          {:ok, code, downcase_headers(headers), client} @@ -175,7 +174,7 @@ defmodule Pleroma.ReverseProxy do          halt(conn)        {:error, :closed, conn} -> -        :hackney.close(client) +        client().close(client)          halt(conn)        {:error, error, conn} -> @@ -183,7 +182,7 @@ defmodule Pleroma.ReverseProxy do            "#{__MODULE__} request to #{url} failed while reading/chunking: #{inspect(error)}"          ) -        :hackney.close(client) +        client().close(client)          halt(conn)      end    end @@ -198,7 +197,7 @@ defmodule Pleroma.ReverseProxy do               duration,               Keyword.get(opts, :max_read_duration, @max_read_duration)             ), -         {:ok, data} <- @hackney.stream_body(client), +         {:ok, data} <- client().stream_body(client),           {:ok, duration} <- increase_read_duration(duration),           sent_so_far = sent_so_far + byte_size(data),           :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)), @@ -379,4 +378,6 @@ defmodule Pleroma.ReverseProxy do    defp increase_read_duration(_) do      {:ok, :no_duration_limit, :no_duration_limit}    end + +  defp client, do: Pleroma.ReverseProxy.Client  end diff --git a/lib/pleroma/upload/filter/anonymize_filename.ex b/lib/pleroma/upload/filter/anonymize_filename.ex index 5ca53a79b..a8516811c 100644 --- a/lib/pleroma/upload/filter/anonymize_filename.ex +++ b/lib/pleroma/upload/filter/anonymize_filename.ex @@ -10,10 +10,19 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilename do    """    @behaviour Pleroma.Upload.Filter -  def filter(upload) do -    extension = List.last(String.split(upload.name, ".")) -    name = Pleroma.Config.get([__MODULE__, :text], random(extension)) -    {:ok, %Pleroma.Upload{upload | name: name}} +  alias Pleroma.Config +  alias Pleroma.Upload + +  def filter(%Upload{name: name} = upload) do +    extension = List.last(String.split(name, ".")) +    name = predefined_name(extension) || random(extension) +    {:ok, %Upload{upload | name: name}} +  end + +  @spec predefined_name(String.t()) :: String.t() | nil +  defp predefined_name(extension) do +    with name when not is_nil(name) <- Config.get([__MODULE__, :text]), +         do: String.replace(name, "{extension}", extension)    end    defp random(extension) do diff --git a/lib/pleroma/uploaders/swift/keystone.ex b/lib/pleroma/uploaders/swift/keystone.ex deleted file mode 100644 index dd44c7561..000000000 --- a/lib/pleroma/uploaders/swift/keystone.ex +++ /dev/null @@ -1,51 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Uploaders.Swift.Keystone do -  use HTTPoison.Base - -  def process_url(url) do -    Enum.join( -      [Pleroma.Config.get!([Pleroma.Uploaders.Swift, :auth_url]), url], -      "/" -    ) -  end - -  def process_response_body(body) do -    body -    |> Jason.decode!() -  end - -  def get_token do -    settings = Pleroma.Config.get(Pleroma.Uploaders.Swift) -    username = Keyword.fetch!(settings, :username) -    password = Keyword.fetch!(settings, :password) -    tenant_id = Keyword.fetch!(settings, :tenant_id) - -    case post( -           "/tokens", -           make_auth_body(username, password, tenant_id), -           ["Content-Type": "application/json"], -           hackney: [:insecure] -         ) do -      {:ok, %Tesla.Env{status: 200, body: body}} -> -        body["access"]["token"]["id"] - -      {:ok, %Tesla.Env{status: _}} -> -        "" -    end -  end - -  def make_auth_body(username, password, tenant) do -    Jason.encode!(%{ -      :auth => %{ -        :passwordCredentials => %{ -          :username => username, -          :password => password -        }, -        :tenantId => tenant -      } -    }) -  end -end diff --git a/lib/pleroma/uploaders/swift/swift.ex b/lib/pleroma/uploaders/swift/swift.ex deleted file mode 100644 index 2b0f2ad04..000000000 --- a/lib/pleroma/uploaders/swift/swift.ex +++ /dev/null @@ -1,29 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Uploaders.Swift.Client do -  use HTTPoison.Base - -  def process_url(url) do -    Enum.join( -      [Pleroma.Config.get!([Pleroma.Uploaders.Swift, :storage_url]), url], -      "/" -    ) -  end - -  def upload_file(filename, body, content_type) do -    token = Pleroma.Uploaders.Swift.Keystone.get_token() - -    case put("#{filename}", body, "X-Auth-Token": token, "Content-Type": content_type) do -      {:ok, %Tesla.Env{status: 201}} -> -        {:ok, {:file, filename}} - -      {:ok, %Tesla.Env{status: 401}} -> -        {:error, "Unauthorized, Bad Token"} - -      {:error, _} -> -        {:error, "Swift Upload Error"} -    end -  end -end diff --git a/lib/pleroma/uploaders/swift/uploader.ex b/lib/pleroma/uploaders/swift/uploader.ex deleted file mode 100644 index d122b09e7..000000000 --- a/lib/pleroma/uploaders/swift/uploader.ex +++ /dev/null @@ -1,19 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Uploaders.Swift do -  @behaviour Pleroma.Uploaders.Uploader - -  def get_file(name) do -    {:ok, {:url, Path.join([Pleroma.Config.get!([__MODULE__, :object_url]), name])}} -  end - -  def put_file(upload) do -    Pleroma.Uploaders.Swift.Client.upload_file( -      upload.path, -      File.read!(upload.tmpfile), -      upload.content_type -    ) -  end -end diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex index bf15389fc..0af76bc59 100644 --- a/lib/pleroma/uploaders/uploader.ex +++ b/lib/pleroma/uploaders/uploader.ex @@ -3,6 +3,8 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Uploaders.Uploader do +  import Pleroma.Web.Gettext +    @moduledoc """    Defines the contract to put and get an uploaded file to any backend.    """ @@ -66,7 +68,7 @@ defmodule Pleroma.Uploaders.Uploader do              {:error, error}          end      after -      30_000 -> {:error, "Uploader callback timeout"} +      30_000 -> {:error, dgettext("errors", "Uploader callback timeout")}      end    end  end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 653dec95f..ffba3f390 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -9,12 +9,14 @@ defmodule Pleroma.User do    import Ecto.Query    alias Comeonin.Pbkdf2 +  alias Ecto.Multi    alias Pleroma.Activity    alias Pleroma.Keys    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Registration    alias Pleroma.Repo +  alias Pleroma.RepoStreamer    alias Pleroma.User    alias Pleroma.Web    alias Pleroma.Web.ActivityPub.ActivityPub @@ -50,6 +52,7 @@ defmodule Pleroma.User do      field(:avatar, :map)      field(:local, :boolean, default: true)      field(:follower_address, :string) +    field(:following_address, :string)      field(:search_rank, :float, virtual: true)      field(:search_type, :integer, virtual: true)      field(:tags, {:array, :string}, default: []) @@ -105,17 +108,32 @@ defmodule Pleroma.User do    def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa    def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" -  def user_info(%User{} = user) do +  @spec ap_following(User.t()) :: Sring.t() +  def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa +  def ap_following(%User{} = user), do: "#{ap_id(user)}/following" + +  def user_info(%User{} = user, args \\ %{}) do +    following_count = +      if args[:following_count], do: args[:following_count], else: following_count(user) + +    follower_count = +      if args[:follower_count], do: args[:follower_count], else: user.info.follower_count +      %{ -      following_count: following_count(user),        note_count: user.info.note_count, -      follower_count: user.info.follower_count,        locked: user.info.locked,        confirmation_pending: user.info.confirmation_pending,        default_scope: user.info.default_scope      } +    |> Map.put(:following_count, following_count) +    |> Map.put(:follower_count, follower_count)    end +  def set_info_cache(user, args) do +    Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args)) +  end + +  @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()    def restrict_deactivated(query) do      from(u in query,        where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info) @@ -150,9 +168,10 @@ defmodule Pleroma.User do      if changes.valid? do        case info_cng.changes[:source_data] do -        %{"followers" => followers} -> +        %{"followers" => followers, "following" => following} ->            changes            |> put_change(:follower_address, followers) +          |> put_change(:following_address, following)          _ ->            followers = User.ap_followers(%User{nickname: changes.changes[:nickname]}) @@ -184,7 +203,14 @@ defmodule Pleroma.User do        |> User.Info.user_upgrade(params[:info])      struct -    |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at]) +    |> cast(params, [ +      :bio, +      :name, +      :follower_address, +      :following_address, +      :avatar, +      :last_refreshed_at +    ])      |> unique_constraint(:nickname)      |> validate_format(:nickname, local_nickname_regex())      |> validate_length(:bio, max: 5000) @@ -193,29 +219,26 @@ defmodule Pleroma.User do    end    def password_update_changeset(struct, params) do -    changeset = -      struct -      |> cast(params, [:password, :password_confirmation]) -      |> validate_required([:password, :password_confirmation]) -      |> validate_confirmation(:password) - -    OAuth.Token.delete_user_tokens(struct) -    OAuth.Authorization.delete_user_authorizations(struct) - -    if changeset.valid? do -      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password]) - -      changeset -      |> put_change(:password_hash, hashed) -    else -      changeset +    struct +    |> cast(params, [:password, :password_confirmation]) +    |> validate_required([:password, :password_confirmation]) +    |> validate_confirmation(:password) +    |> put_password_hash +  end + +  def reset_password(%User{id: user_id} = user, data) do +    multi = +      Multi.new() +      |> Multi.update(:user, password_update_changeset(user, data)) +      |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id)) +      |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user)) + +    case Repo.transaction(multi) do +      {:ok, %{user: user} = _} -> set_cache(user) +      {:error, _, changeset, _} -> {:error, changeset}      end    end -  def reset_password(user, data) do -    update_and_set_cache(password_update_changeset(user, data)) -  end -    def register_changeset(struct, params \\ %{}, opts \\ []) do      need_confirmation? =        if is_nil(opts[:need_confirmation]) do @@ -249,12 +272,11 @@ defmodule Pleroma.User do        end      if changeset.valid? do -      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])        ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})        followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})        changeset -      |> put_change(:password_hash, hashed) +      |> put_password_hash        |> put_change(:ap_id, ap_id)        |> unique_constraint(:ap_id)        |> put_change(:following, [followers]) @@ -324,14 +346,6 @@ defmodule Pleroma.User do      end    end -  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do -    if not following?(follower, followed) do -      follow(follower, followed) -    else -      {:ok, follower} -    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 @@ -366,14 +380,12 @@ defmodule Pleroma.User do    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) - +    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])      ap_followers = followed.follower_address      cond do -      following?(follower, followed) or info.deactivated -> -        {:error, "Could not follow user: #{followed.nickname} is already on your list."} +      info.deactivated -> +        {:error, "Could not follow user: You are deactivated."}        deny_follow_blocked and blocks?(followed, follower) ->          {:error, "Could not follow user: #{followed.nickname} blocked you."} @@ -737,126 +749,13 @@ defmodule Pleroma.User do      |> Repo.all()    end -  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: get_or_fetch(query) - -    {:ok, results} = -      Repo.transaction(fn -> -        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", []) -        Repo.all(search_query(query, for_user)) -      end) - -    results -  end - -  def search_query(query, for_user) do -    fts_subquery = fts_search_subquery(query) -    trigram_subquery = trigram_search_subquery(query) -    union_query = from(s in trigram_subquery, union_all: ^fts_subquery) -    distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id) - -    from(s in subquery(boost_search_rank_query(distinct_query, for_user)), -      order_by: [desc: s.search_rank], -      limit: 20 -    ) -  end - -  defp boost_search_rank_query(query, nil), do: query - -  defp boost_search_rank_query(query, for_user) do -    friends_ids = get_friends_ids(for_user) -    followers_ids = get_followers_ids(for_user) +  @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()} +  def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do +    info = muter.info -    from(u in subquery(query), -      select_merge: %{ -        search_rank: -          fragment( -            """ -             CASE WHEN (?) THEN (?) * 1.3 -             WHEN (?) THEN (?) * 1.2 -             WHEN (?) THEN (?) * 1.1 -             ELSE (?) END -            """, -            u.id in ^friends_ids and u.id in ^followers_ids, -            u.search_rank, -            u.id in ^friends_ids, -            u.search_rank, -            u.id in ^followers_ids, -            u.search_rank, -            u.search_rank -          ) -      } -    ) -  end - -  defp fts_search_subquery(term, query \\ User) do -    processed_query = -      term -      |> String.replace(~r/\W+/, " ") -      |> String.trim() -      |> String.split() -      |> Enum.map(&(&1 <> ":*")) -      |> Enum.join(" | ") - -    from( -      u in query, -      select_merge: %{ -        search_type: ^0, -        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 -        ) -    ) -    |> restrict_deactivated() -  end - -  defp trigram_search_subquery(term) do -    from( -      u in User, -      select_merge: %{ -        # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason -        search_type: fragment("?", 1), -        search_rank: -          fragment( -            "similarity(?, trim(? || ' ' || coalesce(?, '')))", -            ^term, -            u.nickname, -            u.name -          ) -      }, -      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term) -    ) -    |> restrict_deactivated() -  end - -  def mute(muter, %User{ap_id: ap_id}) do      info_cng = -      muter.info -      |> User.Info.add_to_mutes(ap_id) +      User.Info.add_to_mutes(info, ap_id) +      |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)      cng =        change(muter) @@ -866,9 +765,11 @@ defmodule Pleroma.User do    end    def unmute(muter, %{ap_id: ap_id}) do +    info = muter.info +      info_cng = -      muter.info -      |> User.Info.remove_from_mutes(ap_id) +      User.Info.remove_from_mutes(info, ap_id) +      |> User.Info.remove_from_muted_notifications(info, ap_id)      cng =        change(muter) @@ -964,15 +865,18 @@ defmodule Pleroma.User do    def mutes?(nil, _), do: false    def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id) -  def blocks?(user, %{ap_id: ap_id}) do -    blocks = user.info.blocks -    domain_blocks = user.info.domain_blocks +  @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean() +  def muted_notifications?(nil, _), do: false + +  def muted_notifications?(user, %{ap_id: ap_id}), +    do: Enum.member?(user.info.muted_notifications, ap_id) + +  def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do +    blocks = info.blocks +    domain_blocks = info.domain_blocks      %{host: host} = URI.parse(ap_id) -    Enum.member?(blocks, ap_id) || -      Enum.any?(domain_blocks, fn domain -> -        host == domain -      end) +    Enum.member?(blocks, ap_id) || Enum.any?(domain_blocks, &(&1 == host))    end    def subscribed_to?(user, %{ap_id: ap_id}) do @@ -1058,18 +962,26 @@ defmodule Pleroma.User do    @spec perform(atom(), User.t()) :: {:ok, User.t()}    def perform(:delete, %User{} = user) do -    {:ok, user} = User.deactivate(user) +    {:ok, _user} = ActivityPub.delete(user)      # Remove all relationships      {:ok, followers} = User.get_followers(user) -    Enum.each(followers, fn follower -> User.unfollow(follower, user) end) +    Enum.each(followers, fn follower -> +      ActivityPub.unfollow(follower, user) +      User.unfollow(follower, user) +    end)      {:ok, friends} = User.get_friends(user) -    Enum.each(friends, fn followed -> User.unfollow(user, followed) end) +    Enum.each(friends, fn followed -> +      ActivityPub.unfollow(user, followed) +      User.unfollow(user, followed) +    end)      delete_user_activities(user) +    invalidate_cache(user) +    Repo.delete(user)    end    @spec perform(atom(), User.t()) :: {:ok, User.t()} @@ -1125,6 +1037,34 @@ defmodule Pleroma.User do      )    end +  @spec external_users_query() :: Ecto.Query.t() +  def external_users_query do +    User.Query.build(%{ +      external: true, +      active: true, +      order_by: :id +    }) +  end + +  @spec external_users(keyword()) :: [User.t()] +  def external_users(opts \\ []) do +    query = +      external_users_query() +      |> select([u], struct(u, [:id, :ap_id, :info])) + +    query = +      if opts[:max_id], +        do: where(query, [u], u.id > ^opts[:max_id]), +        else: query + +    query = +      if opts[:limit], +        do: limit(query, ^opts[:limit]), +        else: query + +    Repo.all(query) +  end +    def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),      do:        PleromaJobQueue.enqueue(:background, __MODULE__, [ @@ -1142,18 +1082,35 @@ defmodule Pleroma.User do        ])    def delete_user_activities(%User{ap_id: ap_id} = user) do -    stream = -      ap_id -      |> Activity.query_by_actor() -      |> Repo.stream() - -    Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity) +    ap_id +    |> Activity.query_by_actor() +    |> RepoStreamer.chunk_stream(50) +    |> Stream.each(fn activities -> +      Enum.each(activities, &delete_activity(&1)) +    end) +    |> Stream.run()      {:ok, user}    end    defp delete_activity(%{data: %{"type" => "Create"}} = activity) do -    Object.normalize(activity) |> ActivityPub.delete() +    activity +    |> Object.normalize() +    |> ActivityPub.delete() +  end + +  defp delete_activity(%{data: %{"type" => "Like"}} = activity) do +    user = get_cached_by_ap_id(activity.actor) +    object = Object.normalize(activity) + +    ActivityPub.unlike(user, object) +  end + +  defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do +    user = get_cached_by_ap_id(activity.actor) +    object = Object.normalize(activity) + +    ActivityPub.unannounce(user, object)    end    defp delete_activity(_activity), do: "Doing nothing" @@ -1162,9 +1119,7 @@ defmodule Pleroma.User do      Pleroma.HTML.Scrubber.TwitterText    end -  @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy]) - -  def html_filter_policy(_), do: @default_scrubbers +  def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])    def fetch_by_ap_id(ap_id) do      ap_try = ActivityPub.make_user_from_ap_id(ap_id) @@ -1235,10 +1190,12 @@ defmodule Pleroma.User do    end    # OStatus Magic Key -  def public_key_from_info(%{magic_key: magic_key}) do +  def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do      {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}    end +  def public_key_from_info(_), do: {:error, "not found key"} +    def get_public_key_for_ap_id(ap_id) do      with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),           {:ok, public_key} <- public_key_from_info(user.info) do @@ -1424,23 +1381,34 @@ defmodule Pleroma.User do      }    end -  def ensure_keys_present(user) do -    info = user.info - +  def ensure_keys_present(%User{info: info} = user) do      if info.keys do        {:ok, user}      else        {:ok, pem} = Keys.generate_rsa_pem() -      info_cng = -        info -        |> User.Info.set_keys(pem) +      user +      |> Ecto.Changeset.change() +      |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem)) +      |> update_and_set_cache() +    end +  end -      cng = -        Ecto.Changeset.change(user) -        |> Ecto.Changeset.put_embed(:info, info_cng) +  def get_ap_ids_by_nicknames(nicknames) do +    from(u in User, +      where: u.nickname in ^nicknames, +      select: u.ap_id +    ) +    |> Repo.all() +  end -      update_and_set_cache(cng) -    end +  defdelegate search(query, opts \\ []), to: User.Search + +  defp put_password_hash( +         %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset +       ) do +    change(changeset, password_hash: Pbkdf2.hashpwsalt(password))    end + +  defp put_password_hash(changeset), do: changeset  end diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 6397e2737..9beb3ddbd 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -24,6 +24,7 @@ defmodule Pleroma.User.Info do      field(:domain_blocks, {:array, :string}, default: [])      field(:mutes, {:array, :string}, default: [])      field(:muted_reblogs, {:array, :string}, default: []) +    field(:muted_notifications, {:array, :string}, default: [])      field(:subscribers, {:array, :string}, default: [])      field(:deactivated, :boolean, default: false)      field(:no_rich_text, :boolean, default: false) @@ -42,14 +43,21 @@ defmodule Pleroma.User.Info do      field(:hide_follows, :boolean, default: false)      field(:hide_favorites, :boolean, default: true)      field(:pinned_activities, {:array, :string}, default: []) -    field(:flavour, :string, default: nil)      field(:mascot, :map, default: nil)      field(:emoji, {:array, :map}, default: []) +    field(:pleroma_settings_store, :map, default: %{})      field(:notification_settings, :map, -      default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true} +      default: %{ +        "followers" => true, +        "follows" => true, +        "non_follows" => true, +        "non_followers" => true +      }      ) +    field(:skip_thread_containment, :boolean, default: false) +      # Found in the wild      # ap_id -> Where is this used?      # bio -> Where is this used? @@ -68,10 +76,15 @@ defmodule Pleroma.User.Info do    end    def update_notification_settings(info, settings) do +    settings = +      settings +      |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end) +      |> Map.new() +      notification_settings =        info.notification_settings        |> Map.merge(settings) -      |> Map.take(["remote", "local", "followers", "follows"]) +      |> Map.take(["followers", "follows", "non_follows", "non_followers"])      params = %{notification_settings: notification_settings} @@ -108,6 +121,16 @@ defmodule Pleroma.User.Info do      |> validate_required([:mutes])    end +  @spec set_notification_mutes(Changeset.t(), [String.t()], boolean()) :: Changeset.t() +  def set_notification_mutes(changeset, muted_notifications, notifications?) do +    if notifications? do +      put_change(changeset, :muted_notifications, muted_notifications) +      |> validate_required([:muted_notifications]) +    else +      changeset +    end +  end +    def set_blocks(info, blocks) do      params = %{blocks: blocks} @@ -124,14 +147,31 @@ defmodule Pleroma.User.Info do      |> validate_required([:subscribers])    end +  @spec add_to_mutes(Info.t(), String.t()) :: Changeset.t()    def add_to_mutes(info, muted) do      set_mutes(info, Enum.uniq([muted | info.mutes]))    end +  @spec add_to_muted_notifications(Changeset.t(), Info.t(), String.t(), boolean()) :: +          Changeset.t() +  def add_to_muted_notifications(changeset, info, muted, notifications?) do +    set_notification_mutes( +      changeset, +      Enum.uniq([muted | info.muted_notifications]), +      notifications? +    ) +  end + +  @spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t()    def remove_from_mutes(info, muted) do      set_mutes(info, List.delete(info.mutes, muted))    end +  @spec remove_from_muted_notifications(Changeset.t(), Info.t(), String.t()) :: Changeset.t() +  def remove_from_muted_notifications(changeset, info, muted) do +    set_notification_mutes(changeset, List.delete(info.muted_notifications, muted), true) +  end +    def add_to_block(info, blocked) do      set_blocks(info, Enum.uniq([blocked | info.blocks]))    end @@ -209,7 +249,9 @@ defmodule Pleroma.User.Info do        :hide_followers,        :hide_favorites,        :background, -      :show_role +      :show_role, +      :skip_thread_containment, +      :pleroma_settings_store      ])    end @@ -241,14 +283,6 @@ defmodule Pleroma.User.Info do      |> validate_required([:settings])    end -  def mastodon_flavour_update(info, flavour) do -    params = %{flavour: flavour} - -    info -    |> cast(params, [:flavour]) -    |> validate_required([:flavour]) -  end -    def mascot_update(info, url) do      params = %{mascot: url} diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index ace9c05f2..f9bcc9e19 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -7,7 +7,7 @@ defmodule Pleroma.User.Query do    User query builder module. Builds query from new query or another user query.      ## Example: -        query = Pleroma.User.Query(%{nickname: "nickname"}) +        query = Pleroma.User.Query.build(%{nickname: "nickname"})          another_query = Pleroma.User.Query.build(query, %{email: "email@example.com"})          Pleroma.Repo.all(query)          Pleroma.Repo.all(another_query) @@ -47,7 +47,10 @@ defmodule Pleroma.User.Query do              friends: User.t(),              recipients_from_activity: [String.t()],              nickname: [String.t()], -            ap_id: [String.t()] +            ap_id: [String.t()], +            order_by: term(), +            select: term(), +            limit: pos_integer()            }            | %{} @@ -141,6 +144,18 @@ defmodule Pleroma.User.Query do      where(query, [u], u.ap_id in ^to or fragment("? && ?", u.following, ^to))    end +  defp compose_query({:order_by, key}, query) do +    order_by(query, [u], field(u, ^key)) +  end + +  defp compose_query({:select, keys}, query) do +    select(query, [u], ^keys) +  end + +  defp compose_query({:limit, limit}, query) do +    limit(query, ^limit) +  end +    defp compose_query(_unsupported_param, query), do: query    defp prepare_tag_criteria(tag, query) do diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex new file mode 100644 index 000000000..46620b89a --- /dev/null +++ b/lib/pleroma/user/search.ex @@ -0,0 +1,223 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.Search do +  alias Pleroma.Pagination +  alias Pleroma.Repo +  alias Pleroma.User +  import Ecto.Query + +  @similarity_threshold 0.25 +  @limit 20 + +  def search(query_string, opts \\ []) do +    resolve = Keyword.get(opts, :resolve, false) +    following = Keyword.get(opts, :following, false) +    result_limit = Keyword.get(opts, :limit, @limit) +    offset = Keyword.get(opts, :offset, 0) + +    for_user = Keyword.get(opts, :for_user) + +    query_string = format_query(query_string) + +    maybe_resolve(resolve, for_user, query_string) + +    {:ok, results} = +      Repo.transaction(fn -> +        Ecto.Adapters.SQL.query( +          Repo, +          "select set_limit(#{@similarity_threshold})", +          [] +        ) + +        query_string +        |> search_query(for_user, following) +        |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset) +      end) + +    results +  end + +  defp format_query(query_string) do +    # Strip the beginning @ off if there is a query +    query_string = String.trim_leading(query_string, "@") + +    with [name, domain] <- String.split(query_string, "@"), +         formatted_domain <- String.replace(domain, ~r/[!-\-|@|[-`|{-~|\/|:]+/, "") do +      name <> "@" <> to_string(:idna.encode(formatted_domain)) +    else +      _ -> query_string +    end +  end + +  defp search_query(query_string, for_user, following) do +    for_user +    |> base_query(following) +    |> filter_blocked_user(for_user) +    |> filter_blocked_domains(for_user) +    |> search_subqueries(query_string) +    |> union_subqueries +    |> distinct_query() +    |> boost_search_rank_query(for_user) +    |> subquery() +    |> order_by(desc: :search_rank) +    |> maybe_restrict_local(for_user) +  end + +  defp base_query(_user, false), do: User +  defp base_query(user, true), do: User.get_followers_query(user) + +  defp filter_blocked_user(query, %User{info: %{blocks: blocks}}) +       when length(blocks) > 0 do +    from(q in query, where: not (q.ap_id in ^blocks)) +  end + +  defp filter_blocked_user(query, _), do: query + +  defp filter_blocked_domains(query, %User{info: %{domain_blocks: domain_blocks}}) +       when length(domain_blocks) > 0 do +    domains = Enum.join(domain_blocks, ",") + +    from( +      q in query, +      where: fragment("substring(ap_id from '.*://([^/]*)') NOT IN (?)", ^domains) +    ) +  end + +  defp filter_blocked_domains(query, _), do: query + +  defp union_subqueries({fts_subquery, trigram_subquery}) do +    from(s in trigram_subquery, union_all: ^fts_subquery) +  end + +  defp search_subqueries(base_query, query_string) do +    { +      fts_search_subquery(base_query, query_string), +      trigram_search_subquery(base_query, query_string) +    } +  end + +  defp distinct_query(q) do +    from(s in subquery(q), order_by: s.search_type, distinct: s.id) +  end + +  defp maybe_resolve(true, user, query) do +    case {limit(), user} do +      {:all, _} -> :noop +      {:unauthenticated, %User{}} -> User.get_or_fetch(query) +      {:unauthenticated, _} -> :noop +      {false, _} -> User.get_or_fetch(query) +    end +  end + +  defp maybe_resolve(_, _, _), do: :noop + +  defp maybe_restrict_local(q, user) do +    case {limit(), user} do +      {:all, _} -> restrict_local(q) +      {:unauthenticated, %User{}} -> q +      {:unauthenticated, _} -> restrict_local(q) +      {false, _} -> q +    end +  end + +  defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated) + +  defp restrict_local(q), do: where(q, [u], u.local == true) + +  defp boost_search_rank_query(query, nil), do: query + +  defp boost_search_rank_query(query, for_user) do +    friends_ids = User.get_friends_ids(for_user) +    followers_ids = User.get_followers_ids(for_user) + +    from(u in subquery(query), +      select_merge: %{ +        search_rank: +          fragment( +            """ +             CASE WHEN (?) THEN 0.5 + (?) * 1.3 +             WHEN (?) THEN 0.5 + (?) * 1.2 +             WHEN (?) THEN (?) * 1.1 +             ELSE (?) END +            """, +            u.id in ^friends_ids and u.id in ^followers_ids, +            u.search_rank, +            u.id in ^friends_ids, +            u.search_rank, +            u.id in ^followers_ids, +            u.search_rank, +            u.search_rank +          ) +      } +    ) +  end + +  @spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t() +  defp fts_search_subquery(query, term) do +    processed_query = +      String.trim_trailing(term, "@" <> local_domain()) +      |> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ") +      |> String.trim() +      |> String.split() +      |> Enum.map(&(&1 <> ":*")) +      |> Enum.join(" | ") + +    from( +      u in query, +      select_merge: %{ +        search_type: ^0, +        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 +        ) +    ) +    |> User.restrict_deactivated() +  end + +  @spec trigram_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t() +  defp trigram_search_subquery(query, term) do +    term = String.trim_trailing(term, "@" <> local_domain()) + +    from( +      u in query, +      select_merge: %{ +        # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason +        search_type: fragment("?", 1), +        search_rank: +          fragment( +            "similarity(?, trim(? || ' ' || coalesce(?, '')))", +            ^term, +            u.nickname, +            u.name +          ) +      }, +      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term) +    ) +    |> User.restrict_deactivated() +  end + +  defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) +end diff --git a/lib/pleroma/user/welcome_message.ex b/lib/pleroma/user/welcome_message.ex index 2ba65b75a..99fba729e 100644 --- a/lib/pleroma/user/welcome_message.ex +++ b/lib/pleroma/user/welcome_message.ex @@ -1,3 +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.User.WelcomeMessage do    alias Pleroma.User    alias Pleroma.Web.CommonAPI diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index aa0229db7..31397b09f 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -4,9 +4,11 @@  defmodule Pleroma.Web.ActivityPub.ActivityPub do    alias Pleroma.Activity +  alias Pleroma.Config    alias Pleroma.Conversation    alias Pleroma.Notification    alias Pleroma.Object +  alias Pleroma.Object.Containment    alias Pleroma.Object.Fetcher    alias Pleroma.Pagination    alias Pleroma.Repo @@ -25,19 +27,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    # For Announce activities, we filter the recipients based on following status for any actors    # that match actual users.  See issue #164 for more information about why this is necessary.    defp get_recipients(%{"type" => "Announce"} = data) do -    to = data["to"] || [] -    cc = data["cc"] || [] +    to = Map.get(data, "to", []) +    cc = Map.get(data, "cc", []) +    bcc = Map.get(data, "bcc", [])      actor = User.get_cached_by_ap_id(data["actor"])      recipients = -      (to ++ cc) -      |> Enum.filter(fn recipient -> +      Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->          case User.get_cached_by_ap_id(recipient) do -          nil -> -            true - -          user -> -            User.following?(user, actor) +          nil -> true +          user -> User.following?(user, actor)          end        end) @@ -45,17 +44,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    end    defp get_recipients(%{"type" => "Create"} = data) do -    to = data["to"] || [] -    cc = data["cc"] || [] -    actor = data["actor"] || [] -    recipients = (to ++ cc ++ [actor]) |> Enum.uniq() +    to = Map.get(data, "to", []) +    cc = Map.get(data, "cc", []) +    bcc = Map.get(data, "bcc", []) +    actor = Map.get(data, "actor", []) +    recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq()      {recipients, to, cc}    end    defp get_recipients(data) do -    to = data["to"] || [] -    cc = data["cc"] || [] -    recipients = to ++ cc +    to = Map.get(data, "to", []) +    cc = Map.get(data, "cc", []) +    bcc = Map.get(data, "bcc", []) +    recipients = Enum.concat([to, cc, bcc])      {recipients, to, cc}    end @@ -73,7 +74,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    end    defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do -    limit = Pleroma.Config.get([:instance, :remote_limit]) +    limit = Config.get([:instance, :remote_limit])      String.length(content) <= limit    end @@ -108,6 +109,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    def decrease_replies_count_if_reply(_object), do: :noop +  def increase_poll_votes_if_vote(%{ +        "object" => %{"inReplyTo" => reply_ap_id, "name" => name}, +        "type" => "Create" +      }) do +    Object.increase_vote_count(reply_ap_id, name) +  end + +  def increase_poll_votes_if_vote(_create_data), do: :noop +    def insert(map, local \\ true, fake \\ false) when is_map(map) do      with nil <- Activity.normalize(map),           map <- lazy_put_activity_defaults(map, fake), @@ -116,6 +126,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do           {:ok, map} <- MRF.filter(map),           {recipients, _, _} = get_recipients(map),           {:fake, false, map, recipients} <- {:fake, fake, map, recipients}, +         :ok <- Containment.contain_child(map),           {:ok, map, object} <- insert_full_object(map) do        {:ok, activity} =          Repo.insert(%Activity{ @@ -179,44 +190,62 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end)    end +  def stream_out_participations(%Object{data: %{"context" => context}}, user) do +    with %Conversation{} = conversation <- Conversation.get_for_ap_id(context), +         conversation = Repo.preload(conversation, :participations), +         last_activity_id = +           fetch_latest_activity_id_for_context(conversation.ap_id, %{ +             "user" => user, +             "blocking_user" => user +           }) do +      if last_activity_id do +        stream_out_participations(conversation.participations) +      end +    end +  end + +  def stream_out_participations(_, _), do: :noop +    def stream_out(activity) do      public = "https://www.w3.org/ns/activitystreams#Public"      if activity.data["type"] in ["Create", "Announce", "Delete"] do -      Pleroma.Web.Streamer.stream("user", activity) -      Pleroma.Web.Streamer.stream("list", activity) - -      if Enum.member?(activity.data["to"], public) do -        Pleroma.Web.Streamer.stream("public", activity) +      object = Object.normalize(activity) +      # Do not stream out poll replies +      unless object.data["type"] == "Answer" do +        Pleroma.Web.Streamer.stream("user", activity) +        Pleroma.Web.Streamer.stream("list", activity) -        if activity.local do -          Pleroma.Web.Streamer.stream("public:local", activity) -        end +        if Enum.member?(activity.data["to"], public) do +          Pleroma.Web.Streamer.stream("public", activity) -        if activity.data["type"] in ["Create"] do -          object = Object.normalize(activity) +          if activity.local do +            Pleroma.Web.Streamer.stream("public:local", activity) +          end -          object.data -          |> 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["type"] in ["Create"] do +            object.data +            |> Map.get("tag", []) +            |> Enum.filter(fn tag -> is_bitstring(tag) end) +            |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) -          if object.data["attachment"] != [] do -            Pleroma.Web.Streamer.stream("public:media", activity) +            if object.data["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 +          # TODO: Write test, replace with visibility test +          if !Enum.member?(activity.data["cc"] || [], public) && +               !Enum.member?( +                 activity.data["to"], +                 User.get_cached_by_ap_id(activity.data["actor"]).follower_address +               ), +             do: Pleroma.Web.Streamer.stream("direct", activity)          end -      else -        # TODO: Write test, replace with visibility test -        if !Enum.member?(activity.data["cc"] || [], public) && -             !Enum.member?( -               activity.data["to"], -               User.get_cached_by_ap_id(activity.data["actor"]).follower_address -             ), -           do: Pleroma.Web.Streamer.stream("direct", activity)        end      end    end @@ -235,6 +264,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do           {:ok, activity} <- insert(create_data, local, fake),           {:fake, false, activity} <- {:fake, fake, activity},           _ <- increase_replies_count_if_reply(create_data), +         _ <- increase_poll_votes_if_vote(create_data),           # Changing note count prior to enqueuing federation task in order to avoid           # race conditions on updating user.info           {:ok, _actor} <- increase_note_count_if_public(actor, activity), @@ -376,6 +406,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end +  def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do +    with data <- %{ +           "to" => [follower_address], +           "type" => "Delete", +           "actor" => ap_id, +           "object" => %{"type" => "Person", "id" => ap_id} +         }, +         {:ok, activity} <- insert(data, true, true), +         :ok <- maybe_federate(activity) do +      {:ok, user} +    end +  end +    def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do      user = User.get_cached_by_ap_id(actor)      to = (object.data["to"] || []) ++ (object.data["cc"] || []) @@ -388,7 +431,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do             "to" => to,             "deleted_activity_id" => activity && activity.id           }, -         {:ok, activity} <- insert(data, local), +         {:ok, activity} <- insert(data, local, false), +         stream_out_participations(object, user),           _ <- decrease_replies_count_if_reply(object),           # Changing note count prior to enqueuing federation task in order to avoid           # race conditions on updating user.info @@ -399,16 +443,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    end    def block(blocker, blocked, activity_id \\ nil, local \\ true) do -    ap_config = Application.get_env(:pleroma, :activitypub) -    unfollow_blocked = Keyword.get(ap_config, :unfollow_blocked) -    outgoing_blocks = Keyword.get(ap_config, :outgoing_blocks) +    outgoing_blocks = Config.get([:activitypub, :outgoing_blocks]) +    unfollow_blocked = Config.get([:activitypub, :unfollow_blocked]) -    with true <- unfollow_blocked do +    if unfollow_blocked do        follow_activity = fetch_latest_follow(blocker, blocked) - -      if follow_activity do -        unfollow(blocker, blocked, nil, local) -      end +      if follow_activity, do: unfollow(blocker, blocked, nil, local)      end      with true <- outgoing_blocks, @@ -480,6 +520,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public      from(activity in Activity) +    |> maybe_preload_objects(opts)      |> restrict_blocked(opts)      |> restrict_recipients(recipients, opts["user"])      |> where( @@ -492,6 +533,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do          ^context        )      ) +    |> exclude_poll_votes(opts)      |> order_by([activity], desc: activity.id)    end @@ -499,7 +541,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    def fetch_activities_for_context(context, opts \\ %{}) do      context      |> fetch_activities_for_context_query(opts) -    |> Activity.with_preloaded_object()      |> Repo.all()    end @@ -507,7 +548,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do            Pleroma.FlakeId.t() | nil    def fetch_latest_activity_id_for_context(context, opts \\ %{}) do      context -    |> fetch_activities_for_context_query(opts) +    |> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))      |> limit(1)      |> select([a], a.id)      |> Repo.one() @@ -548,14 +589,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    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) -      ) - -    query +    from( +      a in query, +      where: +        fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility) +    )    end    defp restrict_visibility(_query, %{visibility: visibility}) @@ -565,17 +603,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_visibility(query, _visibility), do: query -  defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}) do -    query = -      from( -        a in query, -        where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data) -      ) +  defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _), +    do: query -    query +  defp restrict_thread_visibility( +         query, +         %{"user" => %User{info: %{skip_thread_containment: true}}}, +         _ +       ), +       do: query + +  defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do +    from( +      a in query, +      where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data) +    )    end -  defp restrict_thread_visibility(query, _), do: query +  defp restrict_thread_visibility(query, _, _), do: query    def fetch_user_activities(user, reading_user, params \\ %{}) do      params = @@ -653,20 +698,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_tag(query, _), do: query -  defp restrict_to_cc(query, recipients_to, recipients_cc) do -    from( -      activity in query, -      where: -        fragment( -          "(?->'to' \\?| ?) or (?->'cc' \\?| ?)", -          activity.data, -          ^recipients_to, -          activity.data, -          ^recipients_cc -        ) -    ) -  end -    defp restrict_recipients(query, [], _user), do: query    defp restrict_recipients(query, recipients, nil) do @@ -820,6 +851,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_muted_reblogs(query, _), do: query +  defp exclude_poll_votes(query, %{"include_poll_votes" => "true"}), do: query + +  defp exclude_poll_votes(query, _) do +    if has_named_binding?(query, :object) do +      from([activity, object: o] in query, +        where: fragment("not(?->>'type' = ?)", o.data, "Answer") +      ) +    else +      query +    end +  end +    defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query    defp maybe_preload_objects(query, _) do @@ -854,9 +897,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp maybe_order(query, _), do: query    def fetch_activities_query(recipients, opts \\ %{}) do -    base_query = from(activity in Activity) +    config = %{ +      skip_thread_containment: Config.get([:instance, :skip_thread_containment]) +    } -    base_query +    Activity      |> maybe_preload_objects(opts)      |> maybe_preload_bookmarks(opts)      |> maybe_set_thread_muted_field(opts) @@ -875,23 +920,53 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> restrict_muted(opts)      |> restrict_media(opts)      |> restrict_visibility(opts) -    |> restrict_thread_visibility(opts) +    |> restrict_thread_visibility(opts, config)      |> restrict_replies(opts)      |> restrict_reblogs(opts)      |> restrict_pinned(opts)      |> restrict_muted_reblogs(opts)      |> Activity.restrict_deactivated_users() +    |> exclude_poll_votes(opts)    end    def fetch_activities(recipients, opts \\ %{}) do -    fetch_activities_query(recipients, opts) +    list_memberships = Pleroma.List.memberships(opts["user"]) + +    fetch_activities_query(recipients ++ list_memberships, opts)      |> Pagination.fetch_paginated(opts)      |> Enum.reverse() +    |> maybe_update_cc(list_memberships, opts["user"]) +  end + +  defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id}) +       when is_list(list_memberships) and length(list_memberships) > 0 do +    Enum.map(activities, fn +      %{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 -> +        if Enum.any?(bcc, &(&1 in list_memberships)) do +          update_in(activity.data["cc"], &[user_ap_id | &1]) +        else +          activity +        end + +      activity -> +        activity +    end) +  end + +  defp maybe_update_cc(activities, _, _), do: activities + +  def fetch_activities_bounded_query(query, recipients, recipients_with_public) do +    from(activity in query, +      where: +        fragment("? && ?", activity.recipients, ^recipients) or +          (fragment("? && ?", activity.recipients, ^recipients_with_public) and +             "https://www.w3.org/ns/activitystreams#Public" in activity.recipients) +    )    end -  def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do +  def fetch_activities_bounded(recipients, recipients_with_public, opts \\ %{}) do      fetch_activities_query([], opts) -    |> restrict_to_cc(recipients_to, recipients_cc) +    |> fetch_activities_bounded_query(recipients, recipients_with_public)      |> Pagination.fetch_paginated(opts)      |> Enum.reverse()    end @@ -938,6 +1013,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        avatar: avatar,        name: data["name"],        follower_address: data["followers"], +      following_address: data["following"],        bio: data["summary"]      } diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index ad2ca1e54..e2af4ad1a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -27,13 +27,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    plug(:relay_active? when action in [:relay])    def relay_active?(conn, _) do -    if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do +    if Pleroma.Config.get([:instance, :allow_relay]) do        conn      else        conn -      |> put_status(404) -      |> json(%{error: "not found"}) -      |> halt +      |> render_error(:not_found, "not found") +      |> halt()      end    end @@ -104,43 +103,57 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do      end    end -  def following(conn, %{"nickname" => nickname, "page" => page}) do +  def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do      with %User{} = user <- User.get_cached_by_nickname(nickname), -         {:ok, user} <- User.ensure_keys_present(user) do +         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), +         {:show_follows, true} <- +           {:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do        {page, _} = Integer.parse(page)        conn        |> put_resp_header("content-type", "application/activity+json") -      |> json(UserView.render("following.json", %{user: user, page: page})) +      |> json(UserView.render("following.json", %{user: user, page: page, for: for_user})) +    else +      {:show_follows, _} -> +        conn +        |> put_resp_header("content-type", "application/activity+json") +        |> send_resp(403, "")      end    end -  def following(conn, %{"nickname" => nickname}) do +  def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do      with %User{} = user <- User.get_cached_by_nickname(nickname), -         {:ok, user} <- User.ensure_keys_present(user) do +         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do        conn        |> put_resp_header("content-type", "application/activity+json") -      |> json(UserView.render("following.json", %{user: user})) +      |> json(UserView.render("following.json", %{user: user, for: for_user}))      end    end -  def followers(conn, %{"nickname" => nickname, "page" => page}) do +  def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do      with %User{} = user <- User.get_cached_by_nickname(nickname), -         {:ok, user} <- User.ensure_keys_present(user) do +         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), +         {:show_followers, true} <- +           {:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do        {page, _} = Integer.parse(page)        conn        |> put_resp_header("content-type", "application/activity+json") -      |> json(UserView.render("followers.json", %{user: user, page: page})) +      |> json(UserView.render("followers.json", %{user: user, page: page, for: for_user})) +    else +      {:show_followers, _} -> +        conn +        |> put_resp_header("content-type", "application/activity+json") +        |> send_resp(403, "")      end    end -  def followers(conn, %{"nickname" => nickname}) do +  def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do      with %User{} = user <- User.get_cached_by_nickname(nickname), -         {:ok, user} <- User.ensure_keys_present(user) do +         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do        conn        |> put_resp_header("content-type", "application/activity+json") -      |> json(UserView.render("followers.json", %{user: user})) +      |> json(UserView.render("followers.json", %{user: user, for: for_user}))      end    end @@ -190,7 +203,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do        Logger.info(inspect(conn.req_headers))      end -    json(conn, "error") +    json(conn, dgettext("errors", "error"))    end    def relay(conn, _params) do @@ -218,9 +231,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do        |> put_resp_header("content-type", "application/activity+json")        |> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]}))      else +      err = +        dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}", +          nickname: nickname, +          as_nickname: user.nickname +        ) +        conn        |> put_status(:forbidden) -      |> json("can't read inbox of #{nickname} as #{user.nickname}") +      |> json(err)      end    end @@ -246,7 +265,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do           {:ok, delete} <- ActivityPub.delete(object) do        {:ok, delete}      else -      _ -> {:error, "Can't delete object"} +      _ -> {:error, dgettext("errors", "Can't delete object")}      end    end @@ -255,12 +274,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do           {:ok, activity, _object} <- ActivityPub.like(user, object) do        {:ok, activity}      else -      _ -> {:error, "Can't like object"} +      _ -> {:error, dgettext("errors", "Can't like object")}      end    end    def handle_user_activity(_, _) do -    {:error, "Unhandled activity type"} +    {:error, dgettext("errors", "Unhandled activity type")}    end    def update_outbox( @@ -288,22 +307,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do            |> json(message)        end      else +      err = +        dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}", +          nickname: nickname, +          as_nickname: user.nickname +        ) +        conn        |> put_status(:forbidden) -      |> json("can't update outbox of #{nickname} as #{user.nickname}") +      |> json(err)      end    end    def errors(conn, {:error, :not_found}) do      conn -    |> put_status(404) -    |> json("Not found") +    |> put_status(:not_found) +    |> json(dgettext("errors", "Not found"))    end    def errors(conn, _e) do      conn -    |> put_status(500) -    |> json("error") +    |> put_status(:internal_server_error) +    |> json(dgettext("errors", "error"))    end    defp set_requester_reachable(%Plug.Conn{} = conn, _) do @@ -314,4 +339,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do      conn    end + +  defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do +    {:ok, new_user} = User.ensure_keys_present(user) + +    for_user = +      if new_user != user and match?(%User{}, for_user) do +        User.get_cached_by_nickname(for_user.nickname) +      else +        for_user +      end + +    {new_user, for_user} +  end  end diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index 1aaa20050..10ceef715 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -5,8 +5,8 @@  defmodule Pleroma.Web.ActivityPub.MRF do    @callback filter(Map.t()) :: {:ok | :reject, Map.t()} -  def filter(object) do -    get_policies() +  def filter(policies, %{} = object) do +    policies      |> Enum.reduce({:ok, object}, fn        policy, {:ok, object} ->          policy.filter(object) @@ -16,10 +16,10 @@ defmodule Pleroma.Web.ActivityPub.MRF do      end)    end +  def filter(%{} = object), do: get_policies() |> filter(object) +    def get_policies do -    Application.get_env(:pleroma, :instance, []) -    |> Keyword.get(:rewrite_policy, []) -    |> get_policies() +    Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies()    end    defp get_policies(policy) when is_atom(policy), do: [policy] diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex new file mode 100644 index 000000000..2da3eac2f --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex @@ -0,0 +1,48 @@ +# 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.AntiLinkSpamPolicy do +  alias Pleroma.User + +  require Logger + +  # has the user successfully posted before? +  defp old_user?(%User{} = u) do +    u.info.note_count > 0 || u.info.follower_count > 0 +  end + +  # does the post contain links? +  defp contains_links?(%{"content" => content} = _object) do +    content +    |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"],a.zrl") +    |> Floki.attribute("a", "href") +    |> length() > 0 +  end + +  defp contains_links?(_), do: false + +  def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do +    with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor), +         {:contains_links, true} <- {:contains_links, contains_links?(object)}, +         {:old_user, true} <- {:old_user, old_user?(u)} do +      {:ok, message} +    else +      {:contains_links, false} -> +        {:ok, message} + +      {:old_user, false} -> +        {:reject, nil} + +      {:error, _} -> +        {:reject, nil} + +      e -> +        Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}") +        {:reject, nil} +    end +  end + +  # in all other cases, pass through +  def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex index 15d8514be..2d03df68a 100644 --- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex +++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex @@ -9,8 +9,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do    @behaviour Pleroma.Web.ActivityPub.MRF    @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) +    def filter_by_summary( -        %{"summary" => parent_summary} = _parent, +        %{data: %{"summary" => parent_summary}} = _in_reply_to,          %{"summary" => child_summary} = child        )        when not is_nil(child_summary) and byte_size(child_summary) > 0 and @@ -24,17 +25,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do      end    end -  def filter_by_summary(_parent, child), do: child - -  def filter(%{"type" => activity_type} = object) when activity_type == "Create" do -    child = object["object"] -    in_reply_to = Object.normalize(child["inReplyTo"]) +  def filter_by_summary(_in_reply_to, child), do: child +  def filter(%{"type" => "Create", "object" => child_object} = object) do      child = -      if(in_reply_to, -        do: filter_by_summary(in_reply_to.data, child), -        else: child -      ) +      child_object["inReplyTo"] +      |> Object.normalize(child_object["inReplyTo"]) +      |> filter_by_summary(child_object)      object = Map.put(object, "object", child) diff --git a/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex new file mode 100644 index 000000000..01d21a299 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex @@ -0,0 +1,56 @@ +# 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.MediaProxyWarmingPolicy do +  @moduledoc "Preloads any attachments in the MediaProxy cache by prefetching them" +  @behaviour Pleroma.Web.ActivityPub.MRF + +  alias Pleroma.HTTP +  alias Pleroma.Web.MediaProxy + +  require Logger + +  @hackney_options [ +    pool: :media, +    recv_timeout: 10_000 +  ] + +  def perform(:prefetch, url) do +    Logger.info("Prefetching #{inspect(url)}") + +    url +    |> MediaProxy.url() +    |> HTTP.get([], adapter: @hackney_options) +  end + +  def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do +    Enum.each(attachments, fn +      %{"url" => url} when is_list(url) -> +        url +        |> Enum.each(fn +          %{"href" => href} -> +            PleromaJobQueue.enqueue(:background, __MODULE__, [:prefetch, href]) + +          x -> +            Logger.debug("Unhandled attachment URL object #{inspect(x)}") +        end) + +      x -> +        Logger.debug("Unhandled attachment #{inspect(x)}") +    end) +  end + +  @impl true +  def filter( +        %{"type" => "Create", "object" => %{"attachment" => attachments} = _object} = message +      ) +      when is_list(attachments) and length(attachments) > 0 do +    PleromaJobQueue.enqueue(:background, __MODULE__, [:preload, message]) + +    {:ok, message} +  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 index f30fee0d5..86a48bda5 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex @@ -10,19 +10,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do    def filter(          %{            "type" => "Create", -          "object" => %{"content" => content, "attachment" => _attachment} = child_object +          "object" => %{"content" => content, "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} +    {:ok, put_in(object, ["object", "content"], "")}    end    @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex index 9c87c6963..c269d0f89 100644 --- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex +++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex @@ -8,18 +8,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do    @behaviour Pleroma.Web.ActivityPub.MRF -  def filter(%{"type" => activity_type} = object) when activity_type == "Create" do +  def filter(%{"type" => "Create", "object" => child_object} = object) do      scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy]) -    child = object["object"] -      content = -      child["content"] +      child_object["content"]        |> HTML.filter_tags(scrub_policy) -    child = Map.put(child, "content", content) - -    object = Map.put(object, "object", child) +    object = put_in(object, ["object", "content"], content)      {:ok, object}    end diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index ea3df1b4d..da13fd7c7 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -3,46 +3,42 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do -  alias Pleroma.User    @moduledoc "Rejects non-public (followers-only, direct) activities" + +  alias Pleroma.Config +  alias Pleroma.User +    @behaviour Pleroma.Web.ActivityPub.MRF +  @public "https://www.w3.org/ns/activitystreams#Public" +    @impl true    def filter(%{"type" => "Create"} = object) do      user = User.get_cached_by_ap_id(object["actor"]) -    public = "https://www.w3.org/ns/activitystreams#Public"      # Determine visibility      visibility =        cond do -        public in object["to"] -> "public" -        public in object["cc"] -> "unlisted" +        @public in object["to"] -> "public" +        @public in object["cc"] -> "unlisted"          user.follower_address in object["to"] -> "followers"          true -> "direct"        end -    policy = Pleroma.Config.get(:mrf_rejectnonpublic) +    policy = Config.get(:mrf_rejectnonpublic) + +    cond do +      visibility in ["public", "unlisted"] -> +        {:ok, object} -    case visibility do -      "public" -> +      visibility == "followers" and Keyword.get(policy, :allow_followersonly) ->          {:ok, object} -      "unlisted" -> +      visibility == "direct" and Keyword.get(policy, :allow_direct) ->          {:ok, object} -      "followers" -> -        with true <- Keyword.get(policy, :allow_followersonly) do -          {:ok, object} -        else -          _e -> {:reject, nil} -        end - -      "direct" -> -        with true <- Keyword.get(policy, :allow_direct) do -          {:ok, object} -        else -          _e -> {:reject, nil} -        end +      true -> +        {:reject, nil}      end    end diff --git a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex new file mode 100644 index 000000000..765704389 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/subchain_policy.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.ActivityPub.MRF.SubchainPolicy do +  alias Pleroma.Config +  alias Pleroma.Web.ActivityPub.MRF + +  require Logger + +  @behaviour MRF + +  defp lookup_subchain(actor) do +    with matches <- Config.get([:mrf_subchain, :match_actor]), +         {match, subchain} <- Enum.find(matches, fn {k, _v} -> String.match?(actor, k) end) do +      {:ok, match, subchain} +    else +      _e -> {:error, :notfound} +    end +  end + +  @impl true +  def filter(%{"actor" => actor} = message) do +    with {:ok, match, subchain} <- lookup_subchain(actor) do +      Logger.debug( +        "[SubchainPolicy] Matched #{actor} against #{inspect(match)} with subchain #{ +          inspect(subchain) +        }" +      ) + +      subchain +      |> MRF.filter(message) +    else +      _e -> {:ok, message} +    end +  end + +  @impl true +  def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index 6683b8d8e..b42c4ed76 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -19,12 +19,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do       - `mrf_tag:disable-any-subscription`: Reject any follow requests    """ +  @public "https://www.w3.org/ns/activitystreams#Public" +    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 +         %{ +           "type" => "Create", +           "object" => %{"attachment" => child_attachment} = object +         } = message         )         when length(child_attachment) > 0 do      tags = (object["tag"] || []) ++ ["nsfw"] @@ -41,7 +46,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do    defp process_tag(           "mrf_tag:media-strip", -         %{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message +         %{ +           "type" => "Create", +           "object" => %{"attachment" => child_attachment} = object +         } = message         )         when length(child_attachment) > 0 do      object = Map.delete(object, "attachment") @@ -52,19 +60,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do    defp process_tag(           "mrf_tag:force-unlisted", -         %{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message +         %{ +           "type" => "Create", +           "to" => to, +           "cc" => cc, +           "actor" => actor, +           "object" => object +         } = 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"] +    if Enum.member?(to, @public) do +      to = List.delete(to, @public) ++ [user.follower_address] +      cc = List.delete(cc, user.follower_address) ++ [@public]        object = -        message["object"] +        object          |> Map.put("to", to)          |> Map.put("cc", cc) @@ -82,19 +93,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do    defp process_tag(           "mrf_tag:sandbox", -         %{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message +         %{ +           "type" => "Create", +           "to" => to, +           "cc" => cc, +           "actor" => actor, +           "object" => object +         } = 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") +    if Enum.member?(to, @public) or Enum.member?(cc, @public) do +      to = List.delete(to, @public) ++ [user.follower_address] +      cc = List.delete(cc, @public)        object = -        message["object"] +        object          |> Map.put("to", to)          |> Map.put("cc", cc) @@ -123,7 +137,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do      end    end -  defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), do: {:reject, nil} +  defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), +    do: {:reject, nil}    defp process_tag(_, message), do: {:ok, message} diff --git a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex b/lib/pleroma/web/activity_pub/mrf/user_allowlist_policy.ex index 47663414a..e35d2c422 100644 --- a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex +++ b/lib/pleroma/web/activity_pub/mrf/user_allowlist_policy.ex @@ -21,7 +21,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do    @impl true    def filter(%{"actor" => actor} = object) do      actor_info = URI.parse(actor) -    allow_list = Config.get([:mrf_user_allowlist, String.to_atom(actor_info.host)], []) + +    allow_list = +      Config.get( +        [:mrf_user_allowlist, String.to_atom(actor_info.host)], +        [] +      )      filter_by_list(object, allow_list)    end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 8f1399ce6..18145e45f 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -88,22 +88,72 @@ defmodule Pleroma.Web.ActivityPub.Publisher do        true      else        inbox_info = URI.parse(inbox) -      !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host) +      !Enum.member?(Config.get([:instance, :quarantined_instances], []), inbox_info.host)      end    end -  @doc """ -  Publishes an activity to all relevant peers. -  """ -  def publish(%User{} = actor, %Activity{} = activity) do -    remote_followers = +  defp recipients(actor, activity) do +    followers =        if actor.follower_address in activity.recipients do          {:ok, followers} = User.get_followers(actor) -        followers |> Enum.filter(&(!&1.local)) +        Enum.filter(followers, &(!&1.local))        else          []        end +    Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers +  end + +  defp get_cc_ap_ids(ap_id, recipients) do +    host = Map.get(URI.parse(ap_id), :host) + +    recipients +    |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end) +    |> Enum.map(& &1.ap_id) +  end + +  @doc """ +  Publishes an activity with BCC to all relevant peers. +  """ + +  def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bcc != [] do +    public = is_public?(activity) +    {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + +    recipients = recipients(actor, activity) + +    recipients +    |> Enum.filter(&User.ap_enabled?/1) +    |> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end) +    |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) +    |> Instances.filter_reachable() +    |> Enum.each(fn {inbox, unreachable_since} -> +      %User{ap_id: ap_id} = +        Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end) + +      # Get all the recipients on the same host and add them to cc. Otherwise it a remote +      # instance would only accept a first message for the first recipient and ignore the rest. +      cc = get_cc_ap_ids(ap_id, recipients) + +      json = +        data +        |> Map.put("cc", cc) +        |> Jason.encode!() + +      Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{ +        inbox: inbox, +        json: json, +        actor: actor, +        id: activity.data["id"], +        unreachable_since: unreachable_since +      }) +    end) +  end + +  @doc """ +  Publishes an activity to all relevant peers. +  """ +  def publish(%User{} = actor, %Activity{} = activity) do      public = is_public?(activity)      if public && Config.get([:instance, :allow_relay]) do @@ -114,7 +164,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do      {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)      json = Jason.encode!(data) -    (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers) +    recipients(actor, activity)      |> 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"] diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 5edd8ccc7..602ae48e1 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.ActivityPub.Visibility +  alias Pleroma.Web.Federator    import Ecto.Query @@ -22,19 +23,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    @doc """    Modifies an incoming AP object (mastodon format) to our internal format.    """ -  def fix_object(object) do +  def fix_object(object, options \\ []) do      object      |> fix_actor      |> fix_url      |> fix_attachments      |> fix_context -    |> fix_in_reply_to +    |> fix_in_reply_to(options)      |> fix_emoji      |> fix_tag      |> fix_content_map      |> fix_likes      |> fix_addressing      |> fix_summary +    |> fix_type(options)    end    def fix_summary(%{"summary" => nil} = object) do @@ -65,7 +67,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      end    end -  def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do +  def fix_explicit_addressing( +        %{"to" => to, "cc" => cc} = object, +        explicit_mentions, +        follower_collection +      ) do      explicit_to =        to        |> Enum.filter(fn x -> x in explicit_mentions end) @@ -76,6 +82,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      final_cc =        (cc ++ explicit_cc) +      |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)        |> Enum.uniq()      object @@ -83,7 +90,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> Map.put("cc", final_cc)    end -  def fix_explicit_addressing(object, _explicit_mentions), do: object +  def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object    # if directMessage flag is set to true, leave the addressing alone    def fix_explicit_addressing(%{"directMessage" => true} = object), do: object @@ -93,10 +100,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do        object        |> Utils.determine_explicit_mentions() -    explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"] +    follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address -    object -    |> fix_explicit_addressing(explicit_mentions) +    explicit_mentions = +      explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public", follower_collection] + +    fix_explicit_addressing(object, explicit_mentions, follower_collection)    end    # if as:Public is addressed, then make sure the followers collection is also addressed @@ -133,7 +142,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> fix_addressing_list("cc")      |> fix_addressing_list("bto")      |> fix_addressing_list("bcc") -    |> fix_explicit_addressing +    |> fix_explicit_addressing()      |> fix_implicit_addressing(followers_collection)    end @@ -156,7 +165,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      object    end -  def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object) +  def fix_in_reply_to(object, options \\ []) + +  def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)        when not is_nil(in_reply_to) do      in_reply_to_id =        cond do @@ -174,28 +185,34 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do            ""        end -    case get_obj_helper(in_reply_to_id) do -      {:ok, replied_object} -> -        with %Activity{} = _activity <- -               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) -          |> Map.put("conversation", replied_object.data["context"] || object["conversation"]) -          |> Map.put("context", replied_object.data["context"] || object["conversation"]) -        else -          e -> -            Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") +    object = Map.put(object, "inReplyToAtomUri", in_reply_to_id) + +    if Federator.allowed_incoming_reply_depth?(options[:depth]) do +      case get_obj_helper(in_reply_to_id, options) do +        {:ok, replied_object} -> +          with %Activity{} = _activity <- +                 Activity.get_create_by_object_ap_id(replied_object.data["id"]) do              object -        end +            |> Map.put("inReplyTo", replied_object.data["id"]) +            |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) +            |> Map.put("conversation", replied_object.data["context"] || object["conversation"]) +            |> Map.put("context", replied_object.data["context"] || object["conversation"]) +          else +            e -> +              Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") +              object +          end -      e -> -        Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") -        object +        e -> +          Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") +          object +      end +    else +      object      end    end -  def fix_in_reply_to(object), do: object +  def fix_in_reply_to(object, _options), do: object    def fix_context(object) do      context = object["context"] || object["conversation"] || Utils.generate_context_id() @@ -328,6 +345,23 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def fix_content_map(object), do: object +  def fix_type(object, options \\ []) + +  def fix_type(%{"inReplyTo" => reply_id} = object, options) when is_binary(reply_id) do +    reply = +      if Federator.allowed_incoming_reply_depth?(options[:depth]) do +        Object.normalize(reply_id, true) +      end + +    if reply && (reply.data["type"] == "Question" and object["name"]) do +      Map.put(object, "type", "Answer") +    else +      object +    end +  end + +  def fix_type(object, _), do: object +    defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do      with true <- id =~ "follows",           %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id), @@ -354,9 +388,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      end    end +  def handle_incoming(data, options \\ []) +    # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them    # with nil ID. -  def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do +  def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do      with context <- data["context"] || Utils.generate_context_id(),           content <- data["content"] || "",           %User{} = actor <- User.get_cached_by_ap_id(actor), @@ -389,16 +425,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    # disallow objects with bogus IDs -  def handle_incoming(%{"id" => nil}), do: :error -  def handle_incoming(%{"id" => ""}), do: :error +  def handle_incoming(%{"id" => nil}, _options), do: :error +  def handle_incoming(%{"id" => ""}, _options), do: :error    # length of https:// = 8, should validate better, but good enough for now. -  def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error +  def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8), +    do: :error    # TODO: validate those with a Ecto scheme    # - tags    # - emoji -  def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) -      when objtype in ["Article", "Note", "Video", "Page"] do +  def handle_incoming( +        %{"type" => "Create", "object" => %{"type" => objtype} = object} = data, +        options +      ) +      when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do      actor = Containment.get_actor(data)      data = @@ -407,7 +447,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      with nil <- Activity.get_create_by_object_ap_id(object["id"]),           {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do -      object = fix_object(data["object"]) +      options = Keyword.put(options, :depth, (options[:depth] || 0) + 1) +      object = fix_object(data["object"], options)        params = %{          to: data["to"], @@ -432,16 +473,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming( -        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data +        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data, +        _options        ) do      with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),           {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),           {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do        with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), -           {:user_blocked, false} <- +           {_, false} <-               {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, -           {:user_locked, false} <- {:user_locked, User.locked?(followed)}, -           {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do +           {_, false} <- {:user_locked, User.locked?(followed)}, +           {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, +           {_, {:ok, _}} <- +             {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do          ActivityPub.accept(%{            to: [follower.ap_id],            actor: followed, @@ -450,7 +494,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          })        else          {:user_blocked, true} -> -          {:ok, _} = Utils.update_follow_state(activity, "reject") +          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")            ActivityPub.reject(%{              to: [follower.ap_id], @@ -460,7 +504,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do            })          {:follow, {:error, _}} -> -          {:ok, _} = Utils.update_follow_state(activity, "reject") +          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")            ActivityPub.reject(%{              to: [follower.ap_id], @@ -481,38 +525,35 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming( -        %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data +        %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data, +        _options        ) do      with actor <- Containment.get_actor(data),           {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),           {:ok, follow_activity} <- get_follow_activity(follow_object, followed), -         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"), +         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),           %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), -         {:ok, activity} <- -           ActivityPub.accept(%{ -             to: follow_activity.data["to"], -             type: "Accept", -             actor: followed, -             object: follow_activity.data["id"], -             local: false -           }) do -      if not User.following?(follower, followed) do -        {:ok, _follower} = User.follow(follower, followed) -      end - -      {:ok, activity} +         {:ok, _follower} = User.follow(follower, followed) do +      ActivityPub.accept(%{ +        to: follow_activity.data["to"], +        type: "Accept", +        actor: followed, +        object: follow_activity.data["id"], +        local: false +      })      else        _e -> :error      end    end    def handle_incoming( -        %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data +        %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data, +        _options        ) do      with actor <- Containment.get_actor(data),           {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),           {:ok, follow_activity} <- get_follow_activity(follow_object, followed), -         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"), +         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),           %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),           {:ok, activity} <-             ActivityPub.reject(%{ @@ -531,7 +572,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming( -        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data +        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data, +        _options        ) do      with actor <- Containment.get_actor(data),           {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), @@ -544,7 +586,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming( -        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data +        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data, +        _options        ) do      with actor <- Containment.get_actor(data),           {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), @@ -559,7 +602,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def handle_incoming(          %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} = -          data +          data, +        _options        )        when object_type in ["Person", "Application", "Service", "Organization"] do      with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do @@ -597,7 +641,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    # an error or a tombstone.  This would allow us to verify that a deletion actually took    # place.    def handle_incoming( -        %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data +        %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data, +        _options        ) do      object_id = Utils.get_ap_id(object_id) @@ -608,7 +653,30 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do           {:ok, activity} <- ActivityPub.delete(object, false) do        {:ok, activity}      else -      _e -> :error +      nil -> +        case User.get_cached_by_ap_id(object_id) do +          %User{ap_id: ^actor} = user -> +            {:ok, followers} = User.get_followers(user) + +            Enum.each(followers, fn follower -> +              User.unfollow(follower, user) +            end) + +            {:ok, friends} = User.get_friends(user) + +            Enum.each(friends, fn followed -> +              User.unfollow(user, followed) +            end) + +            User.invalidate_cache(user) +            Repo.delete(user) + +          nil -> +            :error +        end + +      _e -> +        :error      end    end @@ -618,7 +686,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do            "object" => %{"type" => "Announce", "object" => object_id},            "actor" => _actor,            "id" => id -        } = data +        } = data, +        _options        ) do      with actor <- Containment.get_actor(data),           {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), @@ -636,7 +705,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do            "object" => %{"type" => "Follow", "object" => followed},            "actor" => follower,            "id" => id -        } = _data +        } = _data, +        _options        ) do      with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),           {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), @@ -654,7 +724,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do            "object" => %{"type" => "Block", "object" => blocked},            "actor" => blocker,            "id" => id -        } = _data +        } = _data, +        _options        ) do      with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),           %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked), @@ -668,7 +739,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming( -        %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data +        %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data, +        _options        ) do      with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),           %User{local: true} = blocked = User.get_cached_by_ap_id(blocked), @@ -688,7 +760,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do            "object" => %{"type" => "Like", "object" => object_id},            "actor" => _actor,            "id" => id -        } = data +        } = data, +        _options        ) do      with actor <- Containment.get_actor(data),           {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), @@ -700,10 +773,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      end    end -  def handle_incoming(_), do: :error +  def handle_incoming(_, _), do: :error -  def get_obj_helper(id) do -    if object = Object.normalize(id), do: {:ok, object}, else: nil +  def get_obj_helper(id, options \\ []) do +    if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil    end    def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do @@ -731,6 +804,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> set_reply_to_uri      |> strip_internal_fields      |> strip_internal_tags +    |> set_type    end    #  @doc @@ -740,13 +814,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do      object = -      Object.normalize(object_id).data +      object_id +      |> Object.normalize() +      |> Map.get(:data)        |> prepare_object      data =        data        |> Map.put("object", object)        |> Map.merge(Utils.make_json_ld_header()) +      |> Map.delete("bcc")      {:ok, data}    end @@ -895,6 +972,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      Map.put(object, "sensitive", "nsfw" in tags)    end +  def set_type(%{"type" => "Answer"} = object) do +    Map.put(object, "type", "Note") +  end + +  def set_type(object), do: object +    def add_attributed_to(object) do      attributed_to = object["attributedTo"] || object["actor"] @@ -1007,6 +1090,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])        end +      if Pleroma.Config.get([:instance, :external_user_synchronization]) do +        update_following_followers_counters(user) +      end +        {:ok, user}      else        %User{} = user -> {:ok, user} @@ -1039,4 +1126,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      data      |> maybe_fix_user_url    end + +  def update_following_followers_counters(user) do +    info = %{} + +    following = fetch_counter(user.following_address) +    info = if following, do: Map.put(info, :following_count, following), else: info + +    followers = fetch_counter(user.follower_address) +    info = if followers, do: Map.put(info, :follower_count, followers), else: info + +    User.set_info_cache(user, info) +  end + +  defp fetch_counter(url) do +    with {:ok, %{body: body, status: code}} when code in 200..299 <- +           Pleroma.HTTP.get( +             url, +             [{:Accept, "application/activity+json"}] +           ), +         {:ok, data} <- Jason.decode(body) do +      data["totalItems"] +    end +  end  end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index ca8a0844b..c146f59d4 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -19,18 +19,14 @@ defmodule Pleroma.Web.ActivityPub.Utils do    require Logger -  @supported_object_types ["Article", "Note", "Video", "Page"] +  @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"]    @supported_report_states ~w(open closed resolved)    @valid_visibilities ~w(public unlisted private direct)    # Some implementations send the actor URI as the actor field, others send the entire actor object,    # so figure out what the actor's URI is based on what we have. -  def get_ap_id(object) do -    case object do -      %{"id" => id} -> id -      id -> id -    end -  end +  def get_ap_id(%{"id" => id} = _), do: id +  def get_ap_id(id), do: id    def normalize_params(params) do      Map.put(params, "actor", get_ap_id(params["actor"])) @@ -151,16 +147,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do    def create_context(context) do      context = context || generate_id("contexts") -    changeset = Object.context_mapping(context) -    case Repo.insert(changeset) do -      {:ok, object} -> -        object +    # Ecto has problems accessing the constraint inside the jsonb, +    # so we explicitly check for the existed object before insert +    object = Object.get_cached_by_ap_id(context) -      # This should be solved by an upsert, but it seems ecto -      # has problems accessing the constraint inside the jsonb. -      {:error, _} -> -        Object.get_cached_by_ap_id(context) +    with true <- is_nil(object), +         changeset <- Object.context_mapping(context), +         {:ok, inserted_object} <- Repo.insert(changeset) do +      inserted_object +    else +      _ -> +        object      end    end @@ -168,14 +166,17 @@ defmodule Pleroma.Web.ActivityPub.Utils do    Enqueues an activity for federation if it's local    """    def maybe_federate(%Activity{local: true} = activity) do -    priority = -      case activity.data["type"] do -        "Delete" -> 10 -        "Create" -> 1 -        _ -> 5 -      end +    if Pleroma.Config.get!([:instance, :federating]) do +      priority = +        case activity.data["type"] do +          "Delete" -> 10 +          "Create" -> 1 +          _ -> 5 +        end + +      Pleroma.Web.Federator.publish(activity, priority) +    end -    Pleroma.Web.Federator.publish(activity, priority)      :ok    end @@ -376,8 +377,8 @@ 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, +  def update_follow_state_for_all( +        %Activity{data: %{"actor" => actor, "object" => object}} = activity,          state        ) do      try do @@ -789,4 +790,22 @@ defmodule Pleroma.Web.ActivityPub.Utils do          [to, cc, recipients]      end    end + +  def get_existing_votes(actor, %{data: %{"id" => id}}) do +    query = +      from( +        [activity, object: object] in Activity.with_preloaded_object(Activity), +        where: fragment("(?)->>'type' = 'Create'", activity.data), +        where: fragment("(?)->>'actor' = ?", activity.data, ^actor), +        where: +          fragment( +            "(?)->>'inReplyTo' = ?", +            object.data, +            ^to_string(id) +          ), +        where: fragment("(?)->>'type' = 'Answer'", object.data) +      ) + +    Repo.all(query) +  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 327e0e05b..d9c1bcb2c 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -98,29 +98,31 @@ defmodule Pleroma.Web.ActivityPub.UserView do      |> Map.merge(Utils.make_json_ld_header())    end -  def render("following.json", %{user: user, page: page}) do +  def render("following.json", %{user: user, page: page} = opts) do +    showing = (opts[:for] && opts[:for] == user) || !user.info.hide_follows      query = User.get_friends_query(user)      query = from(user in query, select: [:ap_id])      following = Repo.all(query)      total = -      if !user.info.hide_follows do +      if showing do          length(following)        else          0        end -    collection(following, "#{user.ap_id}/following", page, !user.info.hide_follows, total) +    collection(following, "#{user.ap_id}/following", page, showing, total)      |> Map.merge(Utils.make_json_ld_header())    end -  def render("following.json", %{user: user}) do +  def render("following.json", %{user: user} = opts) do +    showing = (opts[:for] && opts[:for] == user) || !user.info.hide_follows      query = User.get_friends_query(user)      query = from(user in query, select: [:ap_id])      following = Repo.all(query)      total = -      if !user.info.hide_follows do +      if showing do          length(following)        else          0 @@ -130,34 +132,43 @@ defmodule Pleroma.Web.ActivityPub.UserView do        "id" => "#{user.ap_id}/following",        "type" => "OrderedCollection",        "totalItems" => total, -      "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows) +      "first" => +        if showing do +          collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows) +        else +          "#{user.ap_id}/following?page=1" +        end      }      |> Map.merge(Utils.make_json_ld_header())    end -  def render("followers.json", %{user: user, page: page}) do +  def render("followers.json", %{user: user, page: page} = opts) do +    showing = (opts[:for] && opts[:for] == user) || !user.info.hide_followers +      query = User.get_followers_query(user)      query = from(user in query, select: [:ap_id])      followers = Repo.all(query)      total = -      if !user.info.hide_followers do +      if showing do          length(followers)        else          0        end -    collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_followers, total) +    collection(followers, "#{user.ap_id}/followers", page, showing, total)      |> Map.merge(Utils.make_json_ld_header())    end -  def render("followers.json", %{user: user}) do +  def render("followers.json", %{user: user} = opts) do +    showing = (opts[:for] && opts[:for] == user) || !user.info.hide_followers +      query = User.get_followers_query(user)      query = from(user in query, select: [:ap_id])      followers = Repo.all(query)      total = -      if !user.info.hide_followers do +      if showing do          length(followers)        else          0 @@ -168,7 +179,11 @@ defmodule Pleroma.Web.ActivityPub.UserView do        "type" => "OrderedCollection",        "totalItems" => total,        "first" => -        collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_followers, total) +        if showing do +          collection(followers, "#{user.ap_id}/followers", 1, showing, total) +        else +          "#{user.ap_id}/followers?page=1" +        end      }      |> Map.merge(Utils.make_json_ld_header())    end diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 93b50ee47..2666edc7c 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -1,3 +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.ActivityPub.Visibility do    alias Pleroma.Activity    alias Pleroma.Object @@ -30,6 +34,20 @@ defmodule Pleroma.Web.ActivityPub.Visibility do      !is_public?(activity) && !is_private?(activity)    end +  def is_list?(%{data: %{"listMessage" => _}}), do: true +  def is_list?(_), do: false + +  def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true + +  def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do +    user.ap_id in activity.data["to"] || +      list_ap_id +      |> Pleroma.List.get_by_ap_id() +      |> Pleroma.List.member?(user) +  end + +  def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false +    def visible_for_user?(activity, nil) do      is_public?(activity)    end @@ -66,6 +84,12 @@ defmodule Pleroma.Web.ActivityPub.Visibility do        Enum.any?(to, &String.contains?(&1, "/followers")) ->          "private" +      object.data["directMessage"] == true -> +        "direct" + +      is_binary(object.data["listMessage"]) -> +        "list" +        length(cc) > 0 ->          "private" diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index de2a13c01..4a0bf4823 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Relay    alias Pleroma.Web.AdminAPI.AccountView +  alias Pleroma.Web.AdminAPI.Config +  alias Pleroma.Web.AdminAPI.ConfigView    alias Pleroma.Web.AdminAPI.ReportView    alias Pleroma.Web.AdminAPI.Search    alias Pleroma.Web.CommonAPI @@ -72,7 +74,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    end    def user_show(conn, %{"nickname" => nickname}) do -    with %User{} = user <- User.get_cached_by_nickname(nickname) do +    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do        conn        |> json(AccountView.render("show.json", %{user: user}))      else @@ -158,9 +160,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    end    def right_add(conn, _) do -    conn -    |> put_status(404) -    |> json(%{error: "No such permission_group"}) +    render_error(conn, :not_found, "No such permission_group")    end    def right_get(conn, %{"nickname" => nickname}) do @@ -182,9 +182,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do        )        when permission_group in ["moderator", "admin"] do      if admin_nickname == nickname do -      conn -      |> put_status(403) -      |> json(%{error: "You can't revoke your own admin status."}) +      render_error(conn, :forbidden, "You can't revoke your own admin status.")      else        user = User.get_cached_by_nickname(nickname) @@ -205,9 +203,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    end    def right_delete(conn, _) do -    conn -    |> put_status(404) -    |> json(%{error: "No such permission_group"}) +    render_error(conn, :not_found, "No such permission_group")    end    def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do @@ -362,28 +358,63 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      end    end +  def config_show(conn, _params) do +    configs = Pleroma.Repo.all(Config) + +    conn +    |> put_view(ConfigView) +    |> render("index.json", %{configs: configs}) +  end + +  def config_update(conn, %{"configs" => configs}) do +    updated = +      if Pleroma.Config.get([:instance, :dynamic_configuration]) do +        updated = +          Enum.map(configs, fn +            %{"group" => group, "key" => key, "delete" => "true"} -> +              {:ok, _} = Config.delete(%{group: group, key: key}) +              nil + +            %{"group" => group, "key" => key, "value" => value} -> +              {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value}) +              config +          end) +          |> Enum.reject(&is_nil(&1)) + +        Pleroma.Config.TransferTask.load_and_update_env() +        Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"]) +        updated +      else +        [] +      end + +    conn +    |> put_view(ConfigView) +    |> render("index.json", %{configs: updated}) +  end +    def errors(conn, {:error, :not_found}) do      conn -    |> put_status(404) -    |> json("Not found") +    |> put_status(:not_found) +    |> json(dgettext("errors", "Not found"))    end    def errors(conn, {:error, reason}) do      conn -    |> put_status(400) +    |> put_status(:bad_request)      |> json(reason)    end    def errors(conn, {:param_cast, _}) do      conn -    |> put_status(400) -    |> json("Invalid parameters") +    |> put_status(:bad_request) +    |> json(dgettext("errors", "Invalid parameters"))    end    def errors(conn, _) do      conn -    |> put_status(500) -    |> json("Something went wrong") +    |> put_status(:internal_server_error) +    |> json(dgettext("errors", "Something went wrong"))    end    defp page_params(params) do diff --git a/lib/pleroma/web/admin_api/config.ex b/lib/pleroma/web/admin_api/config.ex new file mode 100644 index 000000000..b4eb8e002 --- /dev/null +++ b/lib/pleroma/web/admin_api/config.ex @@ -0,0 +1,152 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.Config do +  use Ecto.Schema +  import Ecto.Changeset +  import Pleroma.Web.Gettext +  alias __MODULE__ +  alias Pleroma.Repo + +  @type t :: %__MODULE__{} + +  schema "config" do +    field(:key, :string) +    field(:group, :string) +    field(:value, :binary) + +    timestamps() +  end + +  @spec get_by_params(map()) :: Config.t() | nil +  def get_by_params(params), do: Repo.get_by(Config, params) + +  @spec changeset(Config.t(), map()) :: Changeset.t() +  def changeset(config, params \\ %{}) do +    config +    |> cast(params, [:key, :group, :value]) +    |> validate_required([:key, :group, :value]) +    |> unique_constraint(:key, name: :config_group_key_index) +  end + +  @spec create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} +  def create(params) do +    %Config{} +    |> changeset(Map.put(params, :value, transform(params[:value]))) +    |> Repo.insert() +  end + +  @spec update(Config.t(), map()) :: {:ok, Config} | {:error, Changeset.t()} +  def update(%Config{} = config, %{value: value}) do +    config +    |> change(value: transform(value)) +    |> Repo.update() +  end + +  @spec update_or_create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} +  def update_or_create(params) do +    with %Config{} = config <- Config.get_by_params(Map.take(params, [:group, :key])) do +      Config.update(config, params) +    else +      nil -> Config.create(params) +    end +  end + +  @spec delete(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} +  def delete(params) do +    with %Config{} = config <- Config.get_by_params(params) do +      Repo.delete(config) +    else +      nil -> +        err = +          dgettext("errors", "Config with params %{params} not found", params: inspect(params)) + +        {:error, err} +    end +  end + +  @spec from_binary(binary()) :: term() +  def from_binary(binary), do: :erlang.binary_to_term(binary) + +  @spec from_binary_with_convert(binary()) :: any() +  def from_binary_with_convert(binary) do +    from_binary(binary) +    |> do_convert() +  end + +  defp do_convert(entity) when is_list(entity) do +    for v <- entity, into: [], do: do_convert(v) +  end + +  defp do_convert(entity) when is_map(entity) do +    for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)} +  end + +  defp do_convert({:dispatch, [entity]}), do: %{"tuple" => [":dispatch", [inspect(entity)]]} + +  defp do_convert(entity) when is_tuple(entity), +    do: %{"tuple" => do_convert(Tuple.to_list(entity))} + +  defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity), +    do: entity + +  defp do_convert(entity) when is_atom(entity) do +    string = to_string(entity) + +    if String.starts_with?(string, "Elixir."), +      do: do_convert(string), +      else: ":" <> string +  end + +  defp do_convert("Elixir." <> module_name), do: module_name + +  defp do_convert(entity) when is_binary(entity), do: entity + +  @spec transform(any()) :: binary() +  def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do +    :erlang.term_to_binary(do_transform(entity)) +  end + +  def transform(entity), do: :erlang.term_to_binary(entity) + +  defp do_transform(%Regex{} = entity) when is_map(entity), do: entity + +  defp do_transform(%{"tuple" => [":dispatch", [entity]]}) do +    cleaned_string = String.replace(entity, ~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "") +    {dispatch_settings, []} = Code.eval_string(cleaned_string, [], requires: [], macros: []) +    {:dispatch, [dispatch_settings]} +  end + +  defp do_transform(%{"tuple" => entity}) do +    Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end) +  end + +  defp do_transform(entity) when is_map(entity) do +    for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)} +  end + +  defp do_transform(entity) when is_list(entity) do +    for v <- entity, into: [], do: do_transform(v) +  end + +  defp do_transform(entity) when is_binary(entity) do +    String.trim(entity) +    |> do_transform_string() +  end + +  defp do_transform(entity), do: entity + +  defp do_transform_string("~r/" <> pattern) do +    pattern = String.trim_trailing(pattern, "/") +    ~r/#{pattern}/ +  end + +  defp do_transform_string(":" <> atom), do: String.to_atom(atom) + +  defp do_transform_string(value) do +    if String.starts_with?(value, "Pleroma") or String.starts_with?(value, "Phoenix"), +      do: String.to_existing_atom("Elixir." <> value), +      else: value +  end +end diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 28bb667d8..7e1b9c431 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -5,8 +5,11 @@  defmodule Pleroma.Web.AdminAPI.AccountView do    use Pleroma.Web, :view +  alias Pleroma.HTML +  alias Pleroma.User    alias Pleroma.User.Info    alias Pleroma.Web.AdminAPI.AccountView +  alias Pleroma.Web.MediaProxy    def render("index.json", %{users: users, count: count, page_size: page_size}) do      %{ @@ -17,9 +20,14 @@ defmodule Pleroma.Web.AdminAPI.AccountView do    end    def render("show.json", %{user: user}) do +    avatar = User.avatar_url(user) |> MediaProxy.url() +    display_name = HTML.strip_tags(user.name || user.nickname) +      %{        "id" => user.id, +      "avatar" => avatar,        "nickname" => user.nickname, +      "display_name" => display_name,        "deactivated" => user.info.deactivated,        "local" => user.local,        "roles" => Info.roles(user.info), diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex new file mode 100644 index 000000000..49add0b6e --- /dev/null +++ b/lib/pleroma/web/admin_api/views/config_view.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.AdminAPI.ConfigView do +  use Pleroma.Web, :view + +  def render("index.json", %{configs: configs}) do +    %{ +      configs: render_many(configs, __MODULE__, "show.json", as: :config) +    } +  end + +  def render("show.json", %{config: config}) do +    %{ +      key: config.key, +      group: config.group, +      value: Pleroma.Web.AdminAPI.Config.from_binary_with_convert(config.value) +    } +  end +end diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index 47a73dc7e..a25f3f1fe 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -5,9 +5,9 @@  defmodule Pleroma.Web.AdminAPI.ReportView do    use Pleroma.Web, :view    alias Pleroma.Activity +  alias Pleroma.HTML    alias Pleroma.User    alias Pleroma.Web.CommonAPI.Utils -  alias Pleroma.Web.MastodonAPI.AccountView    alias Pleroma.Web.MastodonAPI.StatusView    def render("index.json", %{reports: reports}) do @@ -23,6 +23,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do      [account_ap_id | status_ap_ids] = report.data["object"]      account = User.get_cached_by_ap_id(account_ap_id) +    content = +      unless is_nil(report.data["content"]) do +        HTML.filter_tags(report.data["content"]) +      else +        nil +      end +      statuses =        Enum.map(status_ap_ids, fn ap_id ->          Activity.get_by_ap_id_with_object(ap_id) @@ -30,12 +37,19 @@ defmodule Pleroma.Web.AdminAPI.ReportView do      %{        id: report.id, -      account: AccountView.render("account.json", %{user: account}), -      actor: AccountView.render("account.json", %{user: user}), -      content: report.data["content"], +      account: merge_account_views(account), +      actor: merge_account_views(user), +      content: content,        created_at: created_at,        statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}),        state: report.data["state"]      }    end + +  defp merge_account_views(%User{} = user) do +    Pleroma.Web.MastodonAPI.AccountView.render("account.json", %{user: user}) +    |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) +  end + +  defp merge_account_views(_), do: %{}  end diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index c4a6fce08..f4234b743 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -3,7 +3,7 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.Auth.PleromaAuthenticator do -  alias Comeonin.Pbkdf2 +  alias Pleroma.Plugs.AuthenticationPlug    alias Pleroma.Registration    alias Pleroma.Repo    alias Pleroma.User @@ -16,7 +16,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do    def get_user(%Plug.Conn{} = conn) do      with {:ok, {name, password}} <- fetch_credentials(conn),           {_, %User{} = user} <- {:user, fetch_user(name)}, -         {_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do +         {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do        {:ok, user}      else        error -> @@ -24,6 +24,14 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do      end    end +  @doc """ +  Gets or creates Pleroma.Registration record from Ueberauth assigns. +  Note: some strategies (like `keycloak`) might need extra configuration to fill `uid` from callback response — +    see [`docs/config.md`](docs/config.md). +  """ +  def get_registration(%Plug.Conn{assigns: %{ueberauth_auth: %{uid: nil}}}), +    do: {:error, :missing_uid} +    def get_registration(%Plug.Conn{          assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}        }) do @@ -51,9 +59,10 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do    def get_registration(%Plug.Conn{} = _conn), do: {:error, :missing_credentials} +  @doc "Creates Pleroma.User record basing on params and Pleroma.Registration record."    def create_from_registration(          %Plug.Conn{params: %{"authorization" => registration_attrs}}, -        registration +        %Registration{} = registration        ) do      nickname = value([registration_attrs["nickname"], Registration.nickname(registration)])      email = value([registration_attrs["email"], Registration.email(registration)]) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 5a312d673..44af6a773 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -4,14 +4,15 @@  defmodule Pleroma.Web.CommonAPI do    alias Pleroma.Activity -  alias Pleroma.Bookmark    alias Pleroma.Formatter    alias Pleroma.Object    alias Pleroma.ThreadMute    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Utils +  alias Pleroma.Web.ActivityPub.Visibility +  import Pleroma.Web.Gettext    import Pleroma.Web.CommonAPI.Utils    def follow(follower, followed) do @@ -29,15 +30,16 @@ defmodule Pleroma.Web.CommonAPI do    def unfollow(follower, unfollowed) do      with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), -         {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do +         {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed), +         {:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do        {:ok, follower}      end    end    def accept_follow_request(follower, followed) do -    with {:ok, follower} <- User.maybe_follow(follower, followed), +    with {:ok, follower} <- User.follow(follower, followed),           %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), -         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"), +         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),           {:ok, _activity} <-             ActivityPub.accept(%{               to: [follower.ap_id], @@ -51,7 +53,7 @@ defmodule Pleroma.Web.CommonAPI do    def reject_follow_request(follower, followed) do      with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), -         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"), +         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),           {:ok, _activity} <-             ActivityPub.reject(%{               to: [follower.ap_id], @@ -73,7 +75,7 @@ defmodule Pleroma.Web.CommonAPI do        {:ok, delete}      else        _ -> -        {:error, "Could not delete"} +        {:error, dgettext("errors", "Could not delete")}      end    end @@ -84,7 +86,7 @@ defmodule Pleroma.Web.CommonAPI do        ActivityPub.announce(user, object)      else        _ -> -        {:error, "Could not repeat"} +        {:error, dgettext("errors", "Could not repeat")}      end    end @@ -94,7 +96,7 @@ defmodule Pleroma.Web.CommonAPI do        ActivityPub.unannounce(user, object)      else        _ -> -        {:error, "Could not unrepeat"} +        {:error, dgettext("errors", "Could not unrepeat")}      end    end @@ -105,7 +107,7 @@ defmodule Pleroma.Web.CommonAPI do        ActivityPub.like(user, object)      else        _ -> -        {:error, "Could not favorite"} +        {:error, dgettext("errors", "Could not favorite")}      end    end @@ -115,14 +117,69 @@ defmodule Pleroma.Web.CommonAPI do        ActivityPub.unlike(user, object)      else        _ -> -        {:error, "Could not unfavorite"} +        {:error, dgettext("errors", "Could not unfavorite")}      end    end +  def vote(user, object, choices) do +    with "Question" <- object.data["type"], +         {:author, false} <- {:author, object.data["actor"] == user.ap_id}, +         {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)}, +         {options, max_count} <- get_options_and_max_count(object), +         option_count <- Enum.count(options), +         {:choice_check, {choices, true}} <- +           {:choice_check, normalize_and_validate_choice_indices(choices, option_count)}, +         {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do +      answer_activities = +        Enum.map(choices, fn index -> +          answer_data = make_answer_data(user, object, Enum.at(options, index)["name"]) + +          {:ok, activity} = +            ActivityPub.create(%{ +              to: answer_data["to"], +              actor: user, +              context: object.data["context"], +              object: answer_data, +              additional: %{"cc" => answer_data["cc"]} +            }) + +          activity +        end) + +      object = Object.get_cached_by_ap_id(object.data["id"]) +      {:ok, answer_activities, object} +    else +      {:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")} +      {:existing_votes, _} -> {:error, dgettext("errors", "Already voted")} +      {:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")} +      {:count_check, false} -> {:error, dgettext("errors", "Too many choices")} +    end +  end + +  defp get_options_and_max_count(object) do +    if Map.has_key?(object.data, "anyOf") do +      {object.data["anyOf"], Enum.count(object.data["anyOf"])} +    else +      {object.data["oneOf"], 1} +    end +  end + +  defp normalize_and_validate_choice_indices(choices, count) do +    Enum.map_reduce(choices, true, fn index, valid -> +      index = if is_binary(index), do: String.to_integer(index), else: index +      {index, if(valid, do: index < count, else: valid)} +    end) +  end +    def get_visibility(%{"visibility" => visibility}, in_reply_to)        when visibility in ~w{public unlisted private direct},        do: {visibility, get_replied_to_visibility(in_reply_to)} +  def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do +    visibility = {:list, String.to_integer(list_id)} +    {visibility, get_replied_to_visibility(in_reply_to)} +  end +    def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do      visibility = get_replied_to_visibility(in_reply_to)      {visibility, visibility} @@ -154,12 +211,15 @@ defmodule Pleroma.Web.CommonAPI do               data,               visibility             ), -         {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility), +         mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id), +         addressed_users <- get_addressed_users(mentioned_users, data["to"]), +         {poll, poll_emoji} <- make_poll_data(data), +         {to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility),           context <- make_context(in_reply_to),           cw <- data["spoiler_text"] || "",           sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),           full_payload <- String.trim(status <> cw), -         length when length in 1..limit <- String.length(full_payload), +         :ok <- validate_character_limit(full_payload, attachments, limit),           object <-             make_note_data(               user.ap_id, @@ -171,29 +231,36 @@ defmodule Pleroma.Web.CommonAPI do               tags,               cw,               cc, -             sensitive +             sensitive, +             poll             ),           object <-             Map.put(               object,               "emoji", -             Formatter.get_emoji_map(full_payload) +             Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)             ) do -      res = -        ActivityPub.create( -          %{ -            to: to, -            actor: user, -            context: context, -            object: object, -            additional: %{"cc" => cc, "directMessage" => visibility == "direct"} -          }, -          Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false -        ) - -      res +      preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false +      direct? = visibility == "direct" + +      %{ +        to: to, +        actor: user, +        context: context, +        object: object, +        additional: %{"cc" => cc, "directMessage" => direct?} +      } +      |> maybe_add_list_data(user, visibility) +      |> ActivityPub.create(preview?)      else -      e -> {:error, e} +      {:private_to_public, true} -> +        {:error, dgettext("errors", "The message visibility must be direct")} + +      {:error, _} = e -> +        e + +      e -> +        {:error, e}      end    end @@ -228,12 +295,11 @@ defmodule Pleroma.Web.CommonAPI do             },             object: %Object{               data: %{ -               "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"), +         true <- Visibility.is_public?(activity),           %{valid?: true} = info_changeset <-             User.Info.add_pinnned_activity(user.info, activity),           changeset <- @@ -245,7 +311,7 @@ defmodule Pleroma.Web.CommonAPI do          {:error, err}        _ -> -        {:error, "Could not pin"} +        {:error, dgettext("errors", "Could not pin")}      end    end @@ -262,7 +328,7 @@ defmodule Pleroma.Web.CommonAPI do          {:error, err}        _ -> -        {:error, "Could not unpin"} +        {:error, dgettext("errors", "Could not unpin")}      end    end @@ -270,7 +336,7 @@ defmodule Pleroma.Web.CommonAPI do      with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do        {:ok, activity}      else -      {:error, _} -> {:error, "conversation is already muted"} +      {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}      end    end @@ -289,15 +355,6 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  def bookmarked?(user, activity) do -    with %Bookmark{} <- Bookmark.get(user.id, activity.id) do -      true -    else -      _ -> -        false -    end -  end -    def report(user, data) do      with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},           {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)}, @@ -315,8 +372,8 @@ defmodule Pleroma.Web.CommonAPI do        {:ok, activity}      else        {:error, err} -> {:error, err} -      {:account_id, %{}} -> {:error, "Valid `account_id` required"} -      {:account, nil} -> {:error, "Account not found"} +      {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")} +      {:account, nil} -> {:error, dgettext("errors", "Account not found")}      end    end @@ -325,14 +382,9 @@ defmodule Pleroma.Web.CommonAPI do           {:ok, activity} <- Utils.update_report_state(activity, state) do        {:ok, activity}      else -      nil -> -        {:error, :not_found} - -      {:error, reason} -> -        {:error, reason} - -      _ -> -        {:error, "Could not update state"} +      nil -> {:error, :not_found} +      {:error, reason} -> {:error, reason} +      _ -> {:error, dgettext("errors", "Could not update state")}      end    end @@ -342,11 +394,8 @@ defmodule Pleroma.Web.CommonAPI do           {:ok, activity} <- set_visibility(activity, opts) do        {:ok, activity}      else -      nil -> -        {:error, :not_found} - -      {:error, reason} -> -        {:error, reason} +      nil -> {:error, :not_found} +      {:error, reason} -> {:error, reason}      end    end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index d93c0d46e..fcc000969 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -3,12 +3,14 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.CommonAPI.Utils do +  import Pleroma.Web.Gettext +    alias Calendar.Strftime -  alias Comeonin.Pbkdf2    alias Pleroma.Activity    alias Pleroma.Config    alias Pleroma.Formatter    alias Pleroma.Object +  alias Pleroma.Plugs.AuthenticationPlug    alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web.ActivityPub.Utils @@ -61,9 +63,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do      end)    end -  def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do -    mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end) - +  @spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) :: +          {list(String.t()), list(String.t())} +  def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do      to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users]      cc = [user.follower_address] @@ -74,9 +76,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do      end    end -  def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do -    mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end) - +  def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do      to = [user.follower_address | mentioned_users]      cc = ["https://www.w3.org/ns/activitystreams#Public"] @@ -87,14 +87,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do      end    end -  def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do -    {to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "direct") +  def get_to_and_cc(user, mentioned_users, inReplyTo, "private") do +    {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct")      {[user.follower_address | to], cc}    end -  def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do -    mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end) - +  def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do      if inReplyTo do        {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}      else @@ -102,6 +100,95 @@ defmodule Pleroma.Web.CommonAPI.Utils do      end    end +  def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []} + +  def get_addressed_users(_, to) when is_list(to) do +    User.get_ap_ids_by_nicknames(to) +  end + +  def get_addressed_users(mentioned_users, _), do: mentioned_users + +  def maybe_add_list_data(activity_params, user, {:list, list_id}) do +    case Pleroma.List.get(list_id, user) do +      %Pleroma.List{} = list -> +        activity_params +        |> put_in([:additional, "bcc"], [list.ap_id]) +        |> put_in([:additional, "listMessage"], list.ap_id) +        |> put_in([:object, "listMessage"], list.ap_id) + +      _ -> +        activity_params +    end +  end + +  def maybe_add_list_data(activity_params, _, _), do: activity_params + +  def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) +      when is_list(options) do +    %{max_expiration: max_expiration, min_expiration: min_expiration} = +      limits = Pleroma.Config.get([:instance, :poll_limits]) + +    # XXX: There is probably a cleaner way of doing this +    try do +      # In some cases mastofe sends out strings instead of integers +      expires_in = if is_binary(expires_in), do: String.to_integer(expires_in), else: expires_in + +      if Enum.count(options) > limits.max_options do +        raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options" +      end + +      {poll, emoji} = +        Enum.map_reduce(options, %{}, fn option, emoji -> +          if String.length(option) > limits.max_option_chars do +            raise ArgumentError, +              message: +                "Poll options cannot be longer than #{limits.max_option_chars} characters each" +          end + +          {%{ +             "name" => option, +             "type" => "Note", +             "replies" => %{"type" => "Collection", "totalItems" => 0} +           }, Map.merge(emoji, Formatter.get_emoji_map(option))} +        end) + +      case expires_in do +        expires_in when expires_in > max_expiration -> +          raise ArgumentError, message: "Expiration date is too far in the future" + +        expires_in when expires_in < min_expiration -> +          raise ArgumentError, message: "Expiration date is too soon" + +        _ -> +          :noop +      end + +      end_time = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(expires_in) +        |> NaiveDateTime.to_iso8601() + +      poll = +        if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do +          %{"type" => "Question", "anyOf" => poll, "closed" => end_time} +        else +          %{"type" => "Question", "oneOf" => poll, "closed" => end_time} +        end + +      {poll, emoji} +    rescue +      e in ArgumentError -> e.message +    end +  end + +  def make_poll_data(%{"poll" => poll}) when is_map(poll) do +    "Invalid poll" +  end + +  def make_poll_data(_data) do +    {%{}, %{}} +  end +    def make_content_html(          status,          attachments, @@ -224,7 +311,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do          tags,          cw \\ nil,          cc \\ [], -        sensitive \\ false +        sensitive \\ false, +        merge \\ %{}        ) do      object = %{        "type" => "Note", @@ -239,12 +327,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do        "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()      } -    with false <- is_nil(in_reply_to), -         %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do -      Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) -    else -      _ -> object -    end +    object = +      with false <- is_nil(in_reply_to), +           %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do +        Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) +      else +        _ -> object +      end + +    Map.merge(object, merge)    end    def format_naive_asctime(date) do @@ -297,10 +388,10 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def confirm_current_password(user, password) do      with %User{local: true} = db_user <- User.get_cached_by_id(user.id), -         true <- Pbkdf2.checkpw(password, db_user.password_hash) do +         true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do        {:ok, db_user}      else -      _ -> {:error, "Invalid password."} +      _ -> {:error, dgettext("errors", "Invalid password.")}      end    end @@ -383,7 +474,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do      if String.length(comment) <= max_size do        {:ok, format_input(comment, "text/plain")}      else -      {:error, "Comment must be up to #{max_size} characters"} +      {:error, +       dgettext("errors", "Comment must be up to %{max_size} characters", max_size: max_size)}      end    end @@ -418,7 +510,32 @@ defmodule Pleroma.Web.CommonAPI.Utils do        context      else        _e -> -        {:error, "No such conversation"} +        {:error, dgettext("errors", "No such conversation")} +    end +  end + +  def make_answer_data(%User{ap_id: ap_id}, object, name) do +    %{ +      "type" => "Answer", +      "actor" => ap_id, +      "cc" => [object.data["actor"]], +      "to" => [], +      "name" => name, +      "inReplyTo" => object.data["id"] +    } +  end + +  def validate_character_limit(full_payload, attachments, limit) do +    length = String.length(full_payload) + +    if length < limit do +      if length > 0 or Enum.count(attachments) > 0 do +        :ok +      else +        {:error, dgettext("errors", "Cannot post an empty status without attachments")} +      end +    else +      {:error, dgettext("errors", "The status is over the character limit")}      end    end  end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 55706eeb8..8a753bb4f 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -15,4 +15,22 @@ defmodule Pleroma.Web.ControllerHelper do      |> put_status(status)      |> json(json)    end + +  @spec fetch_integer_param(map(), String.t(), integer() | nil) :: integer() | nil +  def fetch_integer_param(params, name, default \\ nil) do +    params +    |> Map.get(name, default) +    |> param_to_integer(default) +  end + +  defp param_to_integer(val, _) when is_integer(val), do: val + +  defp param_to_integer(val, default) when is_binary(val) do +    case Integer.parse(val) do +      {res, _} -> res +      _ -> default +    end +  end + +  defp param_to_integer(_, default), do: default  end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 8cd7a2270..c123530dc 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -7,13 +7,9 @@ defmodule Pleroma.Web.Endpoint do    socket("/socket", Pleroma.Web.UserSocket) -  # Serve at "/" the static files from "priv/static" directory. -  # -  # You should set gzip to true if you are running phoenix.digest -  # when deploying your static files in production. +  plug(Pleroma.Plugs.SetLocalePlug)    plug(CORSPlug)    plug(Pleroma.Plugs.HTTPSecurityPlug) -    plug(Pleroma.Plugs.UploadedMedia)    @static_cache_control "public, no-cache" @@ -30,6 +26,10 @@ defmodule Pleroma.Web.Endpoint do      }    ) +  # Serve at "/" the static files from "priv/static" directory. +  # +  # You should set gzip to true if you are running phoenix.digest +  # when deploying your static files in production.    plug(      Plug.Static,      at: "/", @@ -66,7 +66,7 @@ defmodule Pleroma.Web.Endpoint do      parsers: [:urlencoded, :multipart, :json],      pass: ["*/*"],      json_decoder: Jason, -    length: Application.get_env(:pleroma, :instance) |> Keyword.get(:upload_limit), +    length: Pleroma.Config.get([:instance, :upload_limit]),      body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}    ) @@ -91,7 +91,7 @@ defmodule Pleroma.Web.Endpoint do      Plug.Session,      store: :cookie,      key: cookie_name, -    signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]}, +    signing_salt: Pleroma.Config.get([__MODULE__, :signing_salt], "CqaoopA2"),      http_only: true,      secure: secure_cookies,      extra: extra diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index f4c9fe284..f4f9e83e0 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -22,6 +22,18 @@ defmodule Pleroma.Web.Federator do      refresh_subscriptions()    end +  @doc "Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161)" +  # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength +  def allowed_incoming_reply_depth?(depth) do +    max_replies_depth = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth]) + +    if max_replies_depth do +      (depth || 1) <= max_replies_depth +    else +      true +    end +  end +    # Client API    def incoming_doc(doc) do diff --git a/lib/pleroma/web/federator/retry_queue.ex b/lib/pleroma/web/federator/retry_queue.ex index 71e49494f..3db948c2e 100644 --- a/lib/pleroma/web/federator/retry_queue.ex +++ b/lib/pleroma/web/federator/retry_queue.ex @@ -15,7 +15,9 @@ defmodule Pleroma.Web.Federator.RetryQueue do    def start_link do      enabled = -      if Mix.env() == :test, do: true, else: Pleroma.Config.get([__MODULE__, :enabled], false) +      if Pleroma.Config.get(:env) == :test, +        do: true, +        else: Pleroma.Config.get([__MODULE__, :enabled], false)      if enabled do        Logger.info("Starting retry queue") @@ -219,7 +221,7 @@ defmodule Pleroma.Web.Federator.RetryQueue do      {:noreply, state}    end -  if Mix.env() == :test do +  if Pleroma.Config.get(:env) == :test do      defp growth_function(_retries) do        _shutit = Pleroma.Config.get([__MODULE__, :initial_timeout])        DateTime.to_unix(DateTime.utc_now()) - 1 diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index 3a3ec7c2a..46944dcbc 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -1,3 +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.MastodonAPI.MastodonAPI do    import Ecto.Query    import Ecto.Changeset @@ -49,7 +53,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do      options = cast_params(params)      user -    |> Notification.for_user_query() +    |> Notification.for_user_query(options)      |> restrict(:exclude_types, options)      |> Pagination.fetch_paginated(params)    end @@ -63,7 +67,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do    defp cast_params(params) do      param_types = %{        exclude_types: {:array, :string}, -      reblogs: :boolean +      reblogs: :boolean, +      with_muted: :boolean      }      changeset = cast({%{}, param_types}, params, Map.keys(param_types)) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index bc75ab35a..29b1391d3 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -14,8 +14,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    alias Pleroma.HTTP    alias Pleroma.Notification    alias Pleroma.Object -  alias Pleroma.Object.Fetcher    alias Pleroma.Pagination +  alias Pleroma.Plugs.RateLimiter    alias Pleroma.Repo    alias Pleroma.ScheduledActivity    alias Pleroma.Stats @@ -47,15 +47,25 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    require Logger +  @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status +    post_status delete_status)a +    plug( -    Pleroma.Plugs.RateLimitPlug, -    %{ -      max_requests: Config.get([:app_account_creation, :max_requests]), -      interval: Config.get([:app_account_creation, :interval]) -    } -    when action in [:account_register] +    RateLimiter, +    {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]} +    when action in ~w(reblog_status unreblog_status)a +  ) + +  plug( +    RateLimiter, +    {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} +    when action in ~w(fav_status unfav_status)a    ) +  plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions) +  plug(RateLimiter, :app_account_creation when action == :account_register) +  plug(RateLimiter, :search when action in [:search, :search2, :account_search]) +    @local_mastodon_name "Mastodon-Local"    action_fallback(:errors) @@ -117,13 +127,24 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> Enum.dedup()      info_params = -      [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role] +      [ +        :no_rich_text, +        :locked, +        :hide_followers, +        :hide_follows, +        :hide_favorites, +        :show_role, +        :skip_thread_containment +      ]        |> Enum.reduce(%{}, fn key, acc ->          add_if_present(acc, params, to_string(key), key, fn value ->            {:ok, ControllerHelper.truthy_param?(value)}          end)        end)        |> add_if_present(params, "default_scope", :default_scope) +      |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> +        {:ok, Map.merge(user.info.pleroma_settings_store, value)} +      end)        |> add_if_present(params, "header", :banner, fn value ->          with %Plug.Upload{} <- value,               {:ok, object} <- ActivityPub.upload(value, type: :banner) do @@ -132,6 +153,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do            _ -> :error          end        end) +      |> add_if_present(params, "pleroma_background_image", :background, fn value -> +        with %Plug.Upload{} <- value, +             {:ok, object} <- ActivityPub.upload(value, type: :background) do +          {:ok, object.data} +        else +          _ -> :error +        end +      end)        |> Map.put(:emoji, user_info_emojis)      info_cng = User.Info.profile_update(user.info, info_params) @@ -143,17 +172,89 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          CommonAPI.update(user)        end -      json(conn, AccountView.render("account.json", %{user: user, for: user})) +      json( +        conn, +        AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) +      )      else -      _e -> -        conn -        |> put_status(403) -        |> json(%{error: "Invalid request"}) +      _e -> render_error(conn, :forbidden, "Invalid request") +    end +  end + +  def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do +    change = Changeset.change(user, %{avatar: nil}) +    {:ok, user} = User.update_and_set_cache(change) +    CommonAPI.update(user) + +    json(conn, %{url: nil}) +  end + +  def update_avatar(%{assigns: %{user: user}} = conn, params) do +    {:ok, object} = ActivityPub.upload(params, type: :avatar) +    change = Changeset.change(user, %{avatar: object.data}) +    {:ok, user} = User.update_and_set_cache(change) +    CommonAPI.update(user) +    %{"url" => [%{"href" => href} | _]} = object.data + +    json(conn, %{url: href}) +  end + +  def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do +    with new_info <- %{"banner" => %{}}, +         info_cng <- User.Info.profile_update(user.info, new_info), +         changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), +         {:ok, user} <- User.update_and_set_cache(changeset) do +      CommonAPI.update(user) + +      json(conn, %{url: nil}) +    end +  end + +  def update_banner(%{assigns: %{user: user}} = conn, params) do +    with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), +         new_info <- %{"banner" => object.data}, +         info_cng <- User.Info.profile_update(user.info, new_info), +         changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), +         {:ok, user} <- User.update_and_set_cache(changeset) do +      CommonAPI.update(user) +      %{"url" => [%{"href" => href} | _]} = object.data + +      json(conn, %{url: href}) +    end +  end + +  def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do +    with new_info <- %{"background" => %{}}, +         info_cng <- User.Info.profile_update(user.info, new_info), +         changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), +         {:ok, _user} <- User.update_and_set_cache(changeset) do +      json(conn, %{url: nil}) +    end +  end + +  def update_background(%{assigns: %{user: user}} = conn, params) do +    with {:ok, object} <- ActivityPub.upload(params, type: :background), +         new_info <- %{"background" => object.data}, +         info_cng <- User.Info.profile_update(user.info, new_info), +         changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), +         {:ok, _user} <- User.update_and_set_cache(changeset) do +      %{"url" => [%{"href" => href} | _]} = object.data + +      json(conn, %{url: href})      end    end    def verify_credentials(%{assigns: %{user: user}} = conn, _) do -    account = AccountView.render("account.json", %{user: user, for: user}) +    chat_token = Phoenix.Token.sign(conn, "user socket", user.id) + +    account = +      AccountView.render("account.json", %{ +        user: user, +        for: user, +        with_pleroma_settings: true, +        with_chat_token: chat_token +      }) +      json(conn, account)    end @@ -171,10 +272,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        account = AccountView.render("account.json", %{user: user, for: for_user})        json(conn, account)      else -      _e -> -        conn -        |> put_status(404) -        |> json(%{error: "Can't find user"}) +      _e -> render_error(conn, :not_found, "Can't find user")      end    end @@ -197,7 +295,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        languages: ["en"],        registrations: Pleroma.Config.get([:instance, :registrations_open]),        # Extra (not present in Mastodon): -      max_toot_chars: Keyword.get(instance, :limit) +      max_toot_chars: Keyword.get(instance, :limit), +      poll_limits: Keyword.get(instance, :poll_limits)      }      json(conn, response) @@ -217,7 +316,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          "static_url" => url,          "visible_in_picker" => true,          "url" => url, -        "tags" => tags +        "tags" => tags, +        # Assuming that a comma is authorized in the category name +        "category" => (tags -- ["Custom"]) |> Enum.join(",")        }      end)    end @@ -331,6 +432,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do      with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do +      params = +        params +        |> Map.put("tag", params["tagged"]) +        activities = ActivityPub.fetch_user_activities(user, reading_user, params)        conn @@ -409,6 +514,56 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end +  def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do +    with %Object{} = object <- Object.get_by_id(id), +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), +         true <- Visibility.visible_for_user?(activity, user) do +      conn +      |> put_view(StatusView) +      |> try_render("poll.json", %{object: object, for: user}) +    else +      nil -> render_error(conn, :not_found, "Record not found") +      false -> render_error(conn, :not_found, "Record not found") +    end +  end + +  defp get_cached_vote_or_vote(user, object, choices) do +    idempotency_key = "polls:#{user.id}:#{object.data["id"]}" + +    {_, res} = +      Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> +        case CommonAPI.vote(user, object, choices) do +          {:error, _message} = res -> {:ignore, res} +          res -> {:commit, res} +        end +      end) + +    res +  end + +  def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do +    with %Object{} = object <- Object.get_by_id(id), +         true <- object.data["type"] == "Question", +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), +         true <- Visibility.visible_for_user?(activity, user), +         {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do +      conn +      |> put_view(StatusView) +      |> try_render("poll.json", %{object: object, for: user}) +    else +      nil -> +        render_error(conn, :not_found, "Record not found") + +      false -> +        render_error(conn, :not_found, "Record not found") + +      {:error, message} -> +        conn +        |> put_status(:unprocessable_entity) +        |> json(%{error: message}) +    end +  end +    def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do      with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do        conn @@ -458,26 +613,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params) -      when length(media_ids) > 0 do -    params = -      params -      |> Map.put("status", ".") - -    post_status(conn, params) -  end -    def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do      params =        params        |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) -    idempotency_key = -      case get_req_header(conn, "idempotency-key") do -        [key] -> key -        _ -> Ecto.UUID.generate() -      end -      scheduled_at = params["scheduled_at"]      if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do @@ -490,14 +630,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        params = Map.drop(params, ["scheduled_at"]) -      {:ok, activity} = -        Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> -          CommonAPI.post(user, params) -        end) +      case CommonAPI.post(user, params) do +        {:error, message} -> +          conn +          |> put_status(:unprocessable_entity) +          |> json(%{error: message}) -      conn -      |> put_view(StatusView) -      |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +        {:ok, activity} -> +          conn +          |> put_view(StatusView) +          |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +      end      end    end @@ -505,10 +648,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do        json(conn, %{})      else -      _e -> -        conn -        |> put_status(403) -        |> json(%{error: "Can't delete this post"}) +      _e -> render_error(conn, :forbidden, "Can't delete this post")      end    end @@ -553,11 +693,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ -598,11 +733,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ -633,8 +763,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        {:error, reason} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{"error" => reason})) +        |> put_status(:forbidden) +        |> json(%{"error" => reason})      end    end @@ -649,8 +779,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        {:error, reason} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{"error" => reason})) +        |> put_status(:forbidden) +        |> json(%{"error" => reason})      end    end @@ -728,9 +858,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          conn          |> json(rendered)        else -        conn -        |> put_resp_content_type("application/json") -        |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"})) +        render_error(conn, :unsupported_media_type, "mascots can only be images")        end      end    end @@ -743,21 +871,21 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), +    with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),           %Object{data: %{"likes" => likes}} <- Object.normalize(object) do        q = from(u in User, where: u.ap_id in ^likes)        users = Repo.all(q)        conn        |> put_view(AccountView) -      |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user}) +      |> render("accounts.json", %{for: user, users: users, as: :user})      else        _ -> json(conn, [])      end    end    def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), +    with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),           %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do        q = from(u in User, where: u.ap_id in ^announces)        users = Repo.all(q) @@ -859,8 +987,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        {:error, message} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{"error" => message})) +        |> put_status(:forbidden) +        |> json(%{error: message})      end    end @@ -873,8 +1001,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        {:error, message} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{"error" => message})) +        |> put_status(:forbidden) +        |> json(%{error: message})      end    end @@ -891,8 +1019,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        {:error, message} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{"error" => message})) +        |> put_status(:forbidden) +        |> json(%{error: message})      end    end @@ -909,8 +1037,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        {:error, message} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{"error" => message})) +        |> put_status(:forbidden) +        |> json(%{error: message})      end    end @@ -930,17 +1058,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do +  def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do +    notifications = +      if Map.has_key?(params, "notifications"), +        do: params["notifications"] in [true, "True", "true", "1"], +        else: true +      with %User{} = muted <- User.get_cached_by_id(id), -         {:ok, muter} <- User.mute(muter, muted) do +         {:ok, muter} <- User.mute(muter, muted, notifications) do        conn        |> put_view(AccountView)        |> render("relationship.json", %{user: muter, target: muted})      else        {:error, message} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{"error" => message})) +        |> put_status(:forbidden) +        |> json(%{error: message})      end    end @@ -953,8 +1086,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        {:error, message} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{"error" => message})) +        |> put_status(:forbidden) +        |> json(%{error: message})      end    end @@ -975,8 +1108,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        {:error, message} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{"error" => message})) +        |> put_status(:forbidden) +        |> json(%{error: message})      end    end @@ -990,8 +1123,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        {:error, message} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{"error" => message})) +        |> put_status(:forbidden) +        |> json(%{error: message})      end    end @@ -1025,8 +1158,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        {:error, message} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{"error" => message})) +        |> put_status(:forbidden) +        |> json(%{error: message})      end    end @@ -1039,117 +1172,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        {:error, message} ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(403, Jason.encode!(%{"error" => message})) -    end -  end - -  def status_search_query_with_gin(q, query) do -    from([a, o] in q, -      where: -        fragment( -          "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)", -          o.data, -          ^query -        ), -      order_by: [desc: :id] -    ) -  end - -  def status_search_query_with_rum(q, query) do -    from([a, o] in q, -      where: -        fragment( -          "? @@ plainto_tsquery('english', ?)", -          o.fts_content, -          ^query -        ), -      order_by: [fragment("? <=> now()::date", o.inserted_at)] -    ) -  end - -  def status_search(user, query) do -    fetched = -      if Regex.match?(~r/https?:/, query) do -        with {:ok, object} <- Fetcher.fetch_object_from_id(query), -             %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), -             true <- Visibility.visible_for_user?(activity, user) do -          [activity] -        else -          _e -> [] -        end -      end || [] - -    q = -      from([a, o] in Activity.with_preloaded_object(Activity), -        where: fragment("?->>'type' = 'Create'", a.data), -        where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients, -        limit: 40 -      ) - -    q = -      if Pleroma.Config.get([:database, :rum_enabled]) do -        status_search_query_with_rum(q, query) -      else -        status_search_query_with_gin(q, query) -      end - -    Repo.all(q) ++ fetched -  end - -  def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do -    accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - -    statuses = status_search(user, query) - -    tags_path = Web.base_url() <> "/tag/" - -    tags = -      query -      |> String.split() -      |> Enum.uniq() -      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) -      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) -      |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) - -    res = %{ -      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), -      "statuses" => -        StatusView.render("index.json", activities: statuses, for: user, as: :activity), -      "hashtags" => tags -    } - -    json(conn, res) -  end - -  def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do -    accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - -    statuses = status_search(user, query) - -    tags = -      query -      |> String.split() -      |> Enum.uniq() -      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) -      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) - -    res = %{ -      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), -      "statuses" => -        StatusView.render("index.json", activities: statuses, for: user, as: :activity), -      "hashtags" => tags -    } - -    json(conn, res) -  end - -  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do -    accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - -    res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) - -    json(conn, res) +        |> put_status(:forbidden) +        |> json(%{error: message}) +    end    end    def favourites(%{assigns: %{user: user}} = conn, params) do @@ -1196,13 +1221,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> put_view(StatusView)        |> render("index.json", %{activities: activities, for: for_user, as: :activity})      else -      nil -> -        {:error, :not_found} - -      true -> -        conn -        |> put_status(403) -        |> json(%{error: "Can't get favorites"}) +      nil -> {:error, :not_found} +      true -> render_error(conn, :forbidden, "Can't get favorites")      end    end @@ -1234,10 +1254,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        res = ListView.render("list.json", list: list)        json(conn, res)      else -      _e -> -        conn -        |> put_status(404) -        |> json(%{error: "Record not found"}) +      _e -> render_error(conn, :not_found, "Record not found")      end    end @@ -1253,7 +1270,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        json(conn, %{})      else        _e -> -        json(conn, "error") +        json(conn, dgettext("errors", "error"))      end    end @@ -1304,7 +1321,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        json(conn, res)      else        _e -> -        json(conn, "error") +        json(conn, dgettext("errors", "error"))      end    end @@ -1328,10 +1345,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> put_view(StatusView)        |> render("index.json", %{activities: activities, for: user, as: :activity})      else -      _e -> -        conn -        |> put_status(403) -        |> json(%{error: "Error."}) +      _e -> render_error(conn, :forbidden, "Error.")      end    end @@ -1346,8 +1360,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        accounts =          Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) -      flavour = get_user_flavour(user) -        initial_state =          %{            meta: %{ @@ -1366,6 +1378,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do              max_toot_chars: limit,              mascot: User.get_mascot(user)["url"]            }, +          poll_limits: Config.get([:instance, :poll_limits]),            rights: %{              delete_others_notice: present?(user.info.is_moderator),              admin: present?(user.info.is_admin) @@ -1433,7 +1446,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        conn        |> put_layout(false)        |> put_view(MastodonView) -      |> render("index.html", %{initial_state: initial_state, flavour: flavour}) +      |> render("index.html", %{initial_state: initial_state})      else        conn        |> put_session(:return_to, conn.request_path) @@ -1451,48 +1464,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        e ->          conn -        |> put_resp_content_type("application/json") -        |> send_resp(500, Jason.encode!(%{"error" => inspect(e)})) +        |> put_status(:internal_server_error) +        |> json(%{error: inspect(e)})      end    end -  @supported_flavours ["glitch", "vanilla"] - -  def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params) -      when flavour in @supported_flavours do -    flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour) - -    with changeset <- Ecto.Changeset.change(user), -         changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng), -         {:ok, user} <- User.update_and_set_cache(changeset), -         flavour <- user.info.flavour do -      json(conn, flavour) -    else -      e -> -        conn -        |> put_resp_content_type("application/json") -        |> send_resp(500, Jason.encode!(%{"error" => inspect(e)})) -    end -  end - -  def set_flavour(conn, _params) do -    conn -    |> put_status(400) -    |> json(%{error: "Unsupported flavour"}) -  end - -  def get_flavour(%{assigns: %{user: user}} = conn, _params) do -    json(conn, get_user_flavour(user)) -  end - -  defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do -    flavour -  end - -  defp get_user_flavour(_) do -    "glitch" -  end -    def login(%{assigns: %{user: %User{}}} = conn, _params) do      redirect(conn, to: local_mastodon_root_path(conn))    end @@ -1657,20 +1633,24 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> Enum.map_join(", ", fn {_k, v} -> v end)      conn -    |> put_status(422) +    |> put_status(:unprocessable_entity)      |> json(%{error: error_message})    end    def errors(conn, {:error, :not_found}) do +    render_error(conn, :not_found, "Record not found") +  end + +  def errors(conn, {:error, error_message}) do      conn -    |> put_status(404) -    |> json(%{error: "Record not found"}) +    |> put_status(:bad_request) +    |> json(%{error: error_message})    end    def errors(conn, _) do      conn -    |> put_status(500) -    |> json("Something went wrong") +    |> put_status(:internal_server_error) +    |> json(dgettext("errors", "Something went wrong"))    end    def suggestions(%{assigns: %{user: user}} = conn, _) do @@ -1790,21 +1770,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        {:error, errors} ->          conn -        |> put_status(400) -        |> json(Jason.encode!(errors)) +        |> put_status(:bad_request) +        |> json(errors)      end    end    def account_register(%{assigns: %{app: _app}} = conn, _params) do -    conn -    |> put_status(400) -    |> json(%{error: "Missing parameters"}) +    render_error(conn, :bad_request, "Missing parameters")    end    def account_register(conn, _) do -    conn -    |> put_status(403) -    |> json(%{error: "Invalid credentials"}) +    render_error(conn, :forbidden, "Invalid credentials")    end    def conversations(%{assigns: %{user: user}} = conn, params) do @@ -1834,21 +1810,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def try_render(conn, target, params)        when is_binary(target) do -    res = render(conn, target, params) - -    if res == nil do -      conn -      |> put_status(501) -      |> json(%{error: "Can't display this activity"}) -    else -      res +    case render(conn, target, params) do +      nil -> render_error(conn, :not_implemented, "Can't display this activity") +      res -> res      end    end    def try_render(conn, _, _) do -    conn -    |> put_status(501) -    |> json(%{error: "Can't display this activity"}) +    render_error(conn, :not_implemented, "Can't display this activity")    end    defp present?(nil), do: false diff --git a/lib/pleroma/web/mastodon_api/search_controller.ex b/lib/pleroma/web/mastodon_api/search_controller.ex new file mode 100644 index 000000000..9072aa7a4 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/search_controller.ex @@ -0,0 +1,120 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SearchController do +  use Pleroma.Web, :controller + +  alias Pleroma.Activity +  alias Pleroma.Plugs.RateLimiter +  alias Pleroma.Repo +  alias Pleroma.User +  alias Pleroma.Web +  alias Pleroma.Web.ControllerHelper +  alias Pleroma.Web.MastodonAPI.AccountView +  alias Pleroma.Web.MastodonAPI.StatusView + +  require Logger +  plug(RateLimiter, :search when action in [:search, :search2, :account_search]) + +  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do +    accounts = User.search(query, search_options(params, user)) +    res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) + +    json(conn, res) +  end + +  def search2(conn, params), do: do_search(:v2, conn, params) +  def search(conn, params), do: do_search(:v1, conn, params) + +  defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = params) do +    options = search_options(params, user) +    timeout = Keyword.get(Repo.config(), :timeout, 15_000) +    default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} + +    result = +      default_values +      |> Enum.map(fn {resource, default_value} -> +        if params["type"] == nil or params["type"] == resource do +          {resource, fn -> resource_search(version, resource, query, options) end} +        else +          {resource, fn -> default_value end} +        end +      end) +      |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end, +        timeout: timeout, +        on_timeout: :kill_task +      ) +      |> Enum.reduce(default_values, fn +        {:ok, {resource, result}}, acc -> +          Map.put(acc, resource, result) + +        _error, acc -> +          acc +      end) + +    json(conn, result) +  end + +  defp search_options(params, user) do +    [ +      resolve: params["resolve"] == "true", +      following: params["following"] == "true", +      limit: ControllerHelper.fetch_integer_param(params, "limit"), +      offset: ControllerHelper.fetch_integer_param(params, "offset"), +      type: params["type"], +      author: get_author(params), +      for_user: user +    ] +    |> Enum.filter(&elem(&1, 1)) +  end + +  defp resource_search(_, "accounts", query, options) do +    accounts = with_fallback(fn -> User.search(query, options) end) +    AccountView.render("accounts.json", users: accounts, for: options[:for_user], as: :user) +  end + +  defp resource_search(_, "statuses", query, options) do +    statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end) +    StatusView.render("index.json", activities: statuses, for: options[:for_user], as: :activity) +  end + +  defp resource_search(:v2, "hashtags", query, _options) do +    tags_path = Web.base_url() <> "/tag/" + +    query +    |> prepare_tags() +    |> Enum.map(fn tag -> +      tag = String.trim_leading(tag, "#") +      %{name: tag, url: tags_path <> tag} +    end) +  end + +  defp resource_search(:v1, "hashtags", query, _options) do +    query +    |> prepare_tags() +    |> Enum.map(fn tag -> String.trim_leading(tag, "#") end) +  end + +  defp prepare_tags(query) do +    query +    |> String.split() +    |> Enum.uniq() +    |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) +  end + +  defp with_fallback(f, fallback \\ []) do +    try do +      f.() +    rescue +      error -> +        Logger.error("#{__MODULE__} search error: #{inspect(error)}") +        fallback +    end +  end + +  defp get_author(%{"account_id" => account_id}) when is_binary(account_id), +    do: User.get_cached_by_id(account_id) + +  defp get_author(_params), do: nil +end diff --git a/lib/pleroma/web/mastodon_api/subscription_controller.ex b/lib/pleroma/web/mastodon_api/subscription_controller.ex index b6c8ff808..255ee2f18 100644 --- a/lib/pleroma/web/mastodon_api/subscription_controller.ex +++ b/lib/pleroma/web/mastodon_api/subscription_controller.ex @@ -59,13 +59,13 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do    #    def errors(conn, {:error, :not_found}) do      conn -    |> put_status(404) -    |> json("Not found") +    |> put_status(:not_found) +    |> json(dgettext("errors", "Not found"))    end    def errors(conn, _) do      conn -    |> put_status(500) -    |> json("Something went wrong") +    |> put_status(:internal_server_error) +    |> json(dgettext("errors", "Something went wrong"))    end  end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index b82d3319b..65bab4062 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -52,7 +52,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do        followed_by: User.following?(target, user),        blocking: User.blocks?(user, target),        muting: User.mutes?(user, target), -      muting_notifications: false, +      muting_notifications: User.muted_notifications?(user, target),        subscribing: User.subscribed_to?(user, target),        requested: requested,        domain_blocking: false, @@ -66,6 +66,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    end    defp do_render("account.json", %{user: user} = opts) do +    display_name = HTML.strip_tags(user.name || user.nickname) +      image = User.avatar_url(user) |> MediaProxy.url()      header = User.banner_url(user) |> MediaProxy.url()      user_info = User.get_cached_user_info(user) @@ -96,7 +98,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do        id: to_string(user.id),        username: username_from_nickname(user.nickname),        acct: user.nickname, -      display_name: user.name || user.nickname, +      display_name: display_name,        locked: user_info.locked,        created_at: Utils.to_masto_date(user.inserted_at),        followers_count: user_info.follower_count, @@ -124,12 +126,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do          hide_followers: user.info.hide_followers,          hide_follows: user.info.hide_follows,          hide_favorites: user.info.hide_favorites, -        relationship: relationship +        relationship: relationship, +        skip_thread_containment: user.info.skip_thread_containment, +        background_image: image_url(user.info.background) |> MediaProxy.url()        }      }      |> maybe_put_role(user, opts[:for])      |> maybe_put_settings(user, opts[:for], user_info)      |> maybe_put_notification_settings(user, opts[:for]) +    |> maybe_put_settings_store(user, opts[:for], opts) +    |> maybe_put_chat_token(user, opts[:for], opts)    end    defp username_from_nickname(string) when is_binary(string) do @@ -152,6 +158,24 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    defp maybe_put_settings(data, _, _, _), do: data +  defp maybe_put_settings_store(data, %User{info: info, id: id}, %User{id: id}, %{ +         with_pleroma_settings: true +       }) do +    data +    |> Kernel.put_in([:pleroma, :settings_store], info.pleroma_settings_store) +  end + +  defp maybe_put_settings_store(data, _, _, _), do: data + +  defp maybe_put_chat_token(data, %User{id: id}, %User{id: id}, %{ +         with_chat_token: token +       }) do +    data +    |> Kernel.put_in([:pleroma, :chat_token], token) +  end + +  defp maybe_put_chat_token(data, _, _, _), do: data +    defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do      data      |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) @@ -171,4 +195,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    end    defp maybe_put_notification_settings(data, _, _), do: data + +  defp image_url(%{"url" => [%{"href" => href} | _]}), do: href +  defp image_url(_), do: nil  end diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 8e8f7cf31..38bdec737 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -1,3 +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.MastodonAPI.ConversationView do    use Pleroma.Web, :view @@ -22,9 +26,14 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do      last_status = StatusView.render("status.json", %{activity: activity, for: user}) +    # Conversations return all users except the current user. +    users = +      participation.conversation.users +      |> Enum.reject(&(&1.id == user.id)) +      accounts =        AccountView.render("accounts.json", %{ -        users: participation.conversation.users, +        users: users,          as: :user        }) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index e55f9b96e..06a7251d8 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -19,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]    # TODO: Add cached version. +  defp get_replied_to_activities([]), do: %{} +    defp get_replied_to_activities(activities) do      activities      |> Enum.map(fn @@ -104,7 +106,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        id: to_string(activity.id),        uri: activity_object.data["id"],        url: activity_object.data["id"], -      account: AccountView.render("account.json", %{user: user}), +      account: AccountView.render("account.json", %{user: user, for: opts[:for]}),        in_reply_to_id: nil,        in_reply_to_account_id: nil,        reblog: reblogged, @@ -147,8 +149,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      tags = object.data["tag"] || []      sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw") +    tag_mentions = +      tags +      |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end) +      |> Enum.map(fn tag -> tag["href"] end) +      mentions = -      activity.recipients +      (object.data["to"] ++ tag_mentions) +      |> Enum.uniq()        |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)        |> Enum.filter(& &1)        |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end) @@ -221,7 +229,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        id: to_string(activity.id),        uri: object.data["id"],        url: url, -      account: AccountView.render("account.json", %{user: user}), +      account: AccountView.render("account.json", %{user: user, for: opts[:for]}),        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, @@ -240,6 +248,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        spoiler_text: summary_html,        visibility: get_visibility(object),        media_attachments: attachments, +      poll: render("poll.json", %{object: object, for: opts[:for]}),        mentions: mentions,        tags: build_tags(tags),        application: %{ @@ -290,8 +299,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        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], +      title: rich_media[:title] || "", +      description: rich_media[:description] || "",        pleroma: %{          opengraph: rich_media        } @@ -329,6 +338,64 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      }    end +  def render("poll.json", %{object: object} = opts) do +    {multiple, options} = +      case object.data do +        %{"anyOf" => options} when is_list(options) -> {true, options} +        %{"oneOf" => options} when is_list(options) -> {false, options} +        _ -> {nil, nil} +      end + +    if options do +      end_time = +        (object.data["closed"] || object.data["endTime"]) +        |> NaiveDateTime.from_iso8601!() + +      expired = +        end_time +        |> NaiveDateTime.compare(NaiveDateTime.utc_now()) +        |> case do +          :lt -> true +          _ -> false +        end + +      voted = +        if opts[:for] do +          existing_votes = +            Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object) + +          existing_votes != [] or opts[:for].ap_id == object.data["actor"] +        else +          false +        end + +      {options, votes_count} = +        Enum.map_reduce(options, 0, fn %{"name" => name} = option, count -> +          current_count = option["replies"]["totalItems"] || 0 + +          {%{ +             title: HTML.strip_tags(name), +             votes_count: current_count +           }, current_count + count} +        end) + +      %{ +        # Mastodon uses separate ids for polls, but an object can't have +        # more than one poll embedded so object id is fine +        id: object.id, +        expires_at: Utils.to_masto_date(end_time), +        expired: expired, +        multiple: multiple, +        votes_count: votes_count, +        options: options, +        voted: voted, +        emojis: build_emojis(object.data["emoji"]) +      } +    else +      nil +    end +  end +    def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do      object = Object.normalize(activity) diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index abfa26754..dbd3542ea 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do      "public:media",      "public:local:media",      "user", +    "user:notification",      "direct",      "list",      "hashtag" @@ -28,9 +29,10 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do    def init(%{qs: qs} = req, state) do      with params <- :cow_qs.parse_qs(qs), +         sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),           access_token <- List.keyfind(params, "access_token", 0),           {_, stream} <- List.keyfind(params, "stream", 0), -         {:ok, user} <- allow_request(stream, access_token), +         {:ok, user} <- allow_request(stream, [access_token, sec_websocket]),           topic when is_binary(topic) <- expand_topic(stream, params) do        {:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}      else @@ -83,13 +85,21 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do    end    # Public streams without authentication. -  defp allow_request(stream, nil) when stream in @anonymous_streams do +  defp allow_request(stream, [nil, nil]) when stream in @anonymous_streams do      {:ok, nil}    end    # Authenticated streams. -  defp allow_request(stream, {"access_token", access_token}) when stream in @streams do -    with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token), +  defp allow_request(stream, [access_token, sec_websocket]) when stream in @streams do +    token = +      with {"access_token", token} <- access_token do +        token +      else +        _ -> sec_websocket +      end + +    with true <- is_bitstring(token), +         %Token{user_id: user_id} <- Repo.get_by(Token, token: token),           user = %User{} <- User.get_cached_by_id(user_id) do        {:ok, user}      else diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 5762e767b..a661e9bb7 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -3,87 +3,71 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.MediaProxy do -  @base64_opts [padding: false] - -  def url(nil), do: nil +  alias Pleroma.Config +  alias Pleroma.Web -  def url(""), do: nil +  @base64_opts [padding: false] +  def url(url) when is_nil(url) or url == "", do: nil    def url("/" <> _ = url), do: url    def url(url) do -    config = Application.get_env(:pleroma, :media_proxy, []) -    domain = URI.parse(url).host +    if disabled?() or local?(url) or whitelisted?(url) do +      url +    else +      encode_url(url) +    end +  end -    cond do -      !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) -> -        url +  defp disabled?, do: !Config.get([:media_proxy, :enabled], false) -      Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern -> -        String.equivalent?(domain, pattern) -      end) -> -        url +  defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) -      true -> -        encode_url(url) -    end +  defp whitelisted?(url) do +    %{host: domain} = URI.parse(url) + +    Enum.any?(Config.get([:media_proxy, :whitelist]), fn pattern -> +      String.equivalent?(domain, pattern) +    end)    end    def encode_url(url) do -    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:") +    base64 = Base.url_encode64(url, @base64_opts) -    # 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") +    sig64 = +      base64 +      |> signed_url        |> Base.url_encode64(@base64_opts) -    sig = :crypto.hmac(:sha, secret, base64) -    sig64 = sig |> Base.url_encode64(@base64_opts) -      build_url(sig64, base64, filename(url))    end    def decode_url(sig, url) do -    secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] -    sig = Base.url_decode64!(sig, @base64_opts) -    local_sig = :crypto.hmac(:sha, secret, url) - -    if local_sig == sig do +    with {:ok, sig} <- Base.url_decode64(sig, @base64_opts), +         signature when signature == sig <- signed_url(url) do        {:ok, Base.url_decode64!(url, @base64_opts)}      else -      {:error, :invalid_signature} +      _ -> {:error, :invalid_signature}      end    end +  defp signed_url(url) do +    :crypto.hmac(:sha, Config.get([Web.Endpoint, :secret_key_base]), url) +  end +    def filename(url_or_path) do      if path = URI.parse(url_or_path).path, do: Path.basename(path)    end    def build_url(sig_base64, url_base64, filename \\ nil) do      [ -      Pleroma.Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()), +      Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()),        "proxy",        sig_base64,        url_base64,        filename      ] -    |> Enum.filter(fn value -> value end) +    |> Enum.filter(& &1)      |> 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/media_proxy/controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index c0552d89f..1e9520d46 100644 --- a/lib/pleroma/web/media_proxy/controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do      with config <- Pleroma.Config.get([:media_proxy], []),           true <- Keyword.get(config, :enabled, false),           {:ok, url} <- MediaProxy.decode_url(sig64, url64), -         :ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do +         :ok <- filename_matches(params, conn.request_path, url) do        ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))      else        false -> @@ -27,18 +27,15 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do      end    end -  def filename_matches(has_filename, path, url) do -    filename = -      url -      |> MediaProxy.filename() -      |> URI.decode() +  def filename_matches(%{"filename" => _} = _, path, url) do +    filename = MediaProxy.filename(url) -    path = URI.decode(path) - -    if has_filename && filename && Path.basename(path) != filename do +    if filename && Path.basename(path) != filename do        {:wrong_filename, filename}      else        :ok      end    end + +  def filename_matches(_, _, _), do: :ok  end diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex index 357b80a2d..e7fa7f408 100644 --- a/lib/pleroma/web/metadata/opengraph.ex +++ b/lib/pleroma/web/metadata/opengraph.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do    alias Pleroma.Web.Metadata.Utils    @behaviour Provider +  @media_types ["image", "audio", "video"]    @impl Provider    def build_tags(%{ @@ -81,26 +82,19 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph 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 Whatsapp Rich Preview. -          case media_type do +          case Utils.fetch_media_type(@media_types, url["mediaType"]) do              "audio" ->                [ -                {:meta, -                 [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []} +                {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []}                  | acc                ]              "image" ->                [ -                {:meta, -                 [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}, +                {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []},                  {:meta, [property: "og:image:width", content: 150], []},                  {:meta, [property: "og:image:height", content: 150], []}                  | acc @@ -108,8 +102,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do              "video" ->                [ -                {:meta, -                 [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []} +                {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []}                  | acc                ] @@ -121,4 +114,6 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do        acc ++ rendered_tags      end)    end + +  defp build_attachments(_), do: []  end diff --git a/lib/pleroma/web/metadata/player_view.ex b/lib/pleroma/web/metadata/player_view.ex index e9a8cfc8d..4289ebdbd 100644 --- a/lib/pleroma/web/metadata/player_view.ex +++ b/lib/pleroma/web/metadata/player_view.ex @@ -1,3 +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.PlayerView do    use Pleroma.Web, :view    import Phoenix.HTML.Tag, only: [content_tag: 3, tag: 2] diff --git a/lib/pleroma/web/metadata/rel_me.ex b/lib/pleroma/web/metadata/rel_me.ex index 03af899c4..f87fc1973 100644 --- a/lib/pleroma/web/metadata/rel_me.ex +++ b/lib/pleroma/web/metadata/rel_me.ex @@ -1,3 +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.RelMe do    alias Pleroma.Web.Metadata.Providers.Provider    @behaviour Provider diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex index 040b872e7..d6a6049b3 100644 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -1,4 +1,5 @@  # Pleroma: A lightweight social networking server +  # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>  # SPDX-License-Identifier: AGPL-3.0-only @@ -9,13 +10,10 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do    alias Pleroma.Web.Metadata.Utils    @behaviour Provider +  @media_types ["image", "audio", "video"]    @impl Provider -  def build_tags(%{ -        activity_id: id, -        object: object, -        user: user -      }) do +  def build_tags(%{activity_id: id, object: object, user: user}) do      attachments = build_attachments(id, object)      scrubbed_content = Utils.scrub_html_and_truncate(object)      # Zero width space @@ -27,21 +25,12 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do        end      [ -      {:meta, -       [ -         property: "twitter:title", -         content: Utils.user_name_string(user) -       ], []}, -      {:meta, -       [ -         property: "twitter:description", -         content: content -       ], []} +      title_tag(user), +      {:meta, [property: "twitter:description", content: content], []}      ] ++        if attachments == [] or Metadata.activity_nsfw?(object) do          [ -          {:meta, -           [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []}, +          image_tag(user),            {:meta, [property: "twitter:card", content: "summary_large_image"], []}          ]        else @@ -53,30 +42,28 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do    def build_tags(%{user: user}) do      with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do        [ -        {:meta, -         [ -           property: "twitter:title", -           content: Utils.user_name_string(user) -         ], []}, +        title_tag(user),          {:meta, [property: "twitter:description", content: truncated_bio], []}, -        {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], -         []}, +        image_tag(user),          {:meta, [property: "twitter:card", content: "summary"], []}        ]      end    end +  defp title_tag(user) do +    {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} +  end + +  def image_tag(user) do +    {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []} +  end +    defp build_attachments(id, %{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. -          case media_type do +          case Utils.fetch_media_type(@media_types, url["mediaType"]) do              "audio" ->                [                  {:meta, [property: "twitter:card", content: "player"], []}, @@ -117,6 +104,8 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do      end)    end +  defp build_attachments(_id, _object), do: [] +    defp player_url(id) do      Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id)    end diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex index 58385a3d1..720bd4519 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -39,4 +39,11 @@ defmodule Pleroma.Web.Metadata.Utils do          "(@#{user.nickname})"        end    end + +  @spec fetch_media_type(list(String.t()), String.t()) :: String.t() | nil +  def fetch_media_type(supported_types, media_type) do +    Enum.find(supported_types, fn support_type -> +      String.starts_with?(media_type, support_type) +    end) +  end  end diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex index 489d5d3a5..b786a521b 100644 --- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex +++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex @@ -29,7 +29,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do      else        false ->          conn -        |> put_status(403) +        |> put_status(:forbidden)          |> json(false)        _ -> diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 45f90c579..a1d7fcc7d 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -32,20 +32,18 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController 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) -    chat = Application.get_env(:pleroma, :chat) -    gopher = Application.get_env(:pleroma, :gopher)      stats = Stats.get_stats() +    exclusions = Config.get([:instance, :mrf_transparency_exclusions]) +      mrf_simple = -      Application.get_env(:pleroma, :mrf_simple) +      Config.get(:mrf_simple) +      |> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end)        |> Enum.into(%{})      # This horror is needed to convert regex sigils to strings      mrf_keyword = -      Application.get_env(:pleroma, :mrf_keyword, []) +      Config.get(:mrf_keyword, [])        |> Enum.map(fn {key, value} ->          {key,           Enum.map(value, fn @@ -74,14 +72,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do        MRF.get_policies()        |> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end) -    quarantined = Keyword.get(instance, :quarantined_instances) - -    quarantined = -      if is_list(quarantined) do -        quarantined -      else -        [] -      end +    quarantined = Config.get([:instance, :quarantined_instances], [])      staff_accounts =        User.all_superusers() @@ -92,13 +83,14 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do        |> Enum.into(%{}, fn {k, v} -> {k, length(v)} end)      federation_response = -      if Keyword.get(instance, :mrf_transparency) do +      if Config.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 +          quarantined_instances: quarantined, +          exclusions: length(exclusions) > 0          }        else          %{} @@ -109,22 +101,24 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do          "pleroma_api",          "mastodon_api",          "mastodon_api_streaming", -        if Keyword.get(media_proxy, :enabled) do +        "polls", +        "pleroma_explicit_addressing", +        if Config.get([:media_proxy, :enabled]) do            "media_proxy"          end, -        if Keyword.get(gopher, :enabled) do +        if Config.get([:gopher, :enabled]) do            "gopher"          end, -        if Keyword.get(chat, :enabled) do +        if Config.get([:chat, :enabled]) do            "chat"          end, -        if Keyword.get(suggestions, :enabled) do +        if Config.get([:suggestions, :enabled]) do            "suggestions"          end, -        if Keyword.get(instance, :allow_relay) do +        if Config.get([:instance, :allow_relay]) do            "relay"          end, -        if Keyword.get(instance, :safe_dm_mentions) do +        if Config.get([:instance, :safe_dm_mentions]) do            "safe_dm_mentions"          end        ] @@ -141,7 +135,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do          inbound: [],          outbound: []        }, -      openRegistrations: Keyword.get(instance, :registrations_open), +      openRegistrations: Config.get([:instance, :registrations_open]),        usage: %{          users: %{            total: stats.user_count || 0 @@ -149,29 +143,31 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do          localPosts: stats.status_count || 0        },        metadata: %{ -        nodeName: Keyword.get(instance, :name), -        nodeDescription: Keyword.get(instance, :description), -        private: !Keyword.get(instance, :public, true), +        nodeName: Config.get([:instance, :name]), +        nodeDescription: Config.get([:instance, :description]), +        private: !Config.get([:instance, :public], true),          suggestions: %{ -          enabled: Keyword.get(suggestions, :enabled, false), -          thirdPartyEngine: Keyword.get(suggestions, :third_party_engine, ""), -          timeout: Keyword.get(suggestions, :timeout, 5000), -          limit: Keyword.get(suggestions, :limit, 23), -          web: Keyword.get(suggestions, :web, "") +          enabled: Config.get([:suggestions, :enabled], false), +          thirdPartyEngine: Config.get([:suggestions, :third_party_engine], ""), +          timeout: Config.get([:suggestions, :timeout], 5000), +          limit: Config.get([:suggestions, :limit], 23), +          web: Config.get([:suggestions, :web], "")          },          staffAccounts: staff_accounts,          federation: federation_response, -        postFormats: Keyword.get(instance, :allowed_post_formats), +        pollLimits: Config.get([:instance, :poll_limits]), +        postFormats: Config.get([:instance, :allowed_post_formats]),          uploadLimits: %{ -          general: Keyword.get(instance, :upload_limit), -          avatar: Keyword.get(instance, :avatar_upload_limit), -          banner: Keyword.get(instance, :banner_upload_limit), -          background: Keyword.get(instance, :background_upload_limit) +          general: Config.get([:instance, :upload_limit]), +          avatar: Config.get([:instance, :avatar_upload_limit]), +          banner: Config.get([:instance, :banner_upload_limit]), +          background: Config.get([:instance, :background_upload_limit])          }, -        accountActivationRequired: Keyword.get(instance, :account_activation_required, false), -        invitesEnabled: Keyword.get(instance, :invites_enabled, false), +        accountActivationRequired: Config.get([:instance, :account_activation_required], false), +        invitesEnabled: Config.get([:instance, :invites_enabled], false),          features: features, -        restrictedNicknames: Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) +        restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]), +        skipThreadContainment: Config.get([:instance, :skip_thread_containment], false)        }      }    end @@ -209,8 +205,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do    end    def nodeinfo(conn, _) do -    conn -    |> put_status(404) -    |> json(%{error: "Nodeinfo schema version not handled"}) +    render_error(conn, :not_found, "Nodeinfo schema version not handled")    end  end diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index 18973413e..d53e20d12 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -76,14 +76,16 @@ defmodule Pleroma.Web.OAuth.Authorization do    def use_token(%Authorization{used: true}), do: {:error, "already used"}    @spec delete_user_authorizations(User.t()) :: {integer(), any()} -  def delete_user_authorizations(%User{id: user_id}) do -    from( -      a in Pleroma.Web.OAuth.Authorization, -      where: a.user_id == ^user_id -    ) +  def delete_user_authorizations(%User{} = user) do +    user +    |> delete_by_user_query      |> Repo.delete_all()    end +  def delete_by_user_query(%User{id: user_id}) do +    from(a in __MODULE__, where: a.user_id == ^user_id) +  end +    @doc "gets auth for app by token"    @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}    def get_by_token(%App{id: app_id} = _app, token) do diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex index e3984f009..dd7f08bf1 100644 --- a/lib/pleroma/web/oauth/fallback_controller.ex +++ b/lib/pleroma/web/oauth/fallback_controller.ex @@ -9,21 +9,24 @@ defmodule Pleroma.Web.OAuth.FallbackController do    def call(conn, {:register, :generic_error}) do      conn      |> put_status(:internal_server_error) -    |> put_flash(:error, "Unknown error, please check the details and try again.") +    |> put_flash( +      :error, +      dgettext("errors", "Unknown error, please check the details and try again.") +    )      |> OAuthController.registration_details(conn.params)    end    def call(conn, {:register, _error}) do      conn      |> put_status(:unauthorized) -    |> put_flash(:error, "Invalid Username/Password") +    |> put_flash(:error, dgettext("errors", "Invalid Username/Password"))      |> OAuthController.registration_details(conn.params)    end    def call(conn, _error) do      conn      |> put_status(:unauthorized) -    |> put_flash(:error, "Invalid Username/Password") +    |> put_flash(:error, dgettext("errors", "Invalid Username/Password"))      |> OAuthController.authorize(conn.params)    end  end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index ae2b80d95..ef53b7ae3 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -5,6 +5,7 @@  defmodule Pleroma.Web.OAuth.OAuthController do    use Pleroma.Web, :controller +  alias Pleroma.Helpers.UriHelper    alias Pleroma.Registration    alias Pleroma.Repo    alias Pleroma.User @@ -17,6 +18,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do    alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken    alias Pleroma.Web.OAuth.Scopes +  require Logger +    if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)    plug(:fetch_session) @@ -24,34 +27,25 @@ defmodule Pleroma.Web.OAuth.OAuthController do    action_fallback(Pleroma.Web.OAuth.FallbackController) +  @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" +    # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg -  def authorize(conn, %{"authorization" => _} = params) do +  def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do      {auth_attrs, params} = Map.pop(params, "authorization")      authorize(conn, Map.merge(params, auth_attrs))    end -  def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do +  def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do      if ControllerHelper.truthy_param?(params["force_login"]) do        do_authorize(conn, params)      else -      redirect_uri = -        if is_binary(params["redirect_uri"]) do -          params["redirect_uri"] -        else -          app = Repo.preload(token, :app).app - -          app.redirect_uris -          |> String.split() -          |> Enum.at(0) -        end - -      redirect(conn, external: redirect_uri(conn, redirect_uri)) +      handle_existing_authorization(conn, params)      end    end -  def authorize(conn, params), do: do_authorize(conn, params) +  def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params) -  defp do_authorize(conn, params) do +  defp do_authorize(%Plug.Conn{} = conn, params) do      app = Repo.get_by(App, client_id: params["client_id"])      available_scopes = (app && app.scopes) || []      scopes = Scopes.fetch_scopes(params, available_scopes) @@ -68,8 +62,41 @@ defmodule Pleroma.Web.OAuth.OAuthController do      })    end +  defp handle_existing_authorization( +         %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, +         %{"redirect_uri" => @oob_token_redirect_uri} +       ) do +    render(conn, "oob_token_exists.html", %{token: token}) +  end + +  defp handle_existing_authorization( +         %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, +         %{} = params +       ) do +    app = Repo.preload(token, :app).app + +    redirect_uri = +      if is_binary(params["redirect_uri"]) do +        params["redirect_uri"] +      else +        default_redirect_uri(app) +      end + +    if redirect_uri in String.split(app.redirect_uris) do +      redirect_uri = redirect_uri(conn, redirect_uri) +      url_params = %{access_token: token.token} +      url_params = UriHelper.append_param_if_present(url_params, :state, params["state"]) +      url = UriHelper.append_uri_params(redirect_uri, url_params) +      redirect(conn, external: url) +    else +      conn +      |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) +      |> redirect(external: redirect_uri(conn, redirect_uri)) +    end +  end +    def create_authorization( -        conn, +        %Plug.Conn{} = conn,          %{"authorization" => _} = params,          opts \\ []        ) do @@ -81,35 +108,33 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end -  def after_create_authorization(conn, auth, %{ -        "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs +  def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ +        "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}        }) do -    redirect_uri = redirect_uri(conn, redirect_uri) - -    if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do -      render(conn, "results.html", %{ -        auth: auth -      }) -    else -      connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" -      url = "#{redirect_uri}#{connector}" -      url_params = %{:code => auth.token} - -      url_params = -        if auth_attrs["state"] do -          Map.put(url_params, :state, auth_attrs["state"]) -        else -          url_params -        end - -      url = "#{url}#{Plug.Conn.Query.encode(url_params)}" +    render(conn, "oob_authorization_created.html", %{auth: auth}) +  end +  def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ +        "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs +      }) do +    app = Repo.preload(auth, :app).app + +    # An extra safety measure before we redirect (also done in `do_create_authorization/2`) +    if redirect_uri in String.split(app.redirect_uris) do +      redirect_uri = redirect_uri(conn, redirect_uri) +      url_params = %{code: auth.token} +      url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"]) +      url = UriHelper.append_uri_params(redirect_uri, url_params)        redirect(conn, external: url) +    else +      conn +      |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) +      |> redirect(external: redirect_uri(conn, redirect_uri))      end    end    defp handle_create_authorization_error( -         conn, +         %Plug.Conn{} = conn,           {:error, scopes_issue},           %{"authorization" => _} = params         ) @@ -117,31 +142,31 @@ defmodule Pleroma.Web.OAuth.OAuthController do      # Per https://github.com/tootsuite/mastodon/blob/      #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39      conn -    |> put_flash(:error, "This action is outside the authorized scopes") +    |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))      |> put_status(:unauthorized)      |> authorize(params)    end    defp handle_create_authorization_error( -         conn, +         %Plug.Conn{} = conn,           {:auth_active, false},           %{"authorization" => _} = params         ) do      # Per https://github.com/tootsuite/mastodon/blob/      #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76      conn -    |> put_flash(:error, "Your login is missing a confirmed e-mail address") +    |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))      |> put_status(:forbidden)      |> authorize(params)    end -  defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do +  defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do      Authenticator.handle_error(conn, error)    end    @doc "Renew access_token with refresh_token"    def token_exchange( -        conn, +        %Plug.Conn{} = conn,          %{"grant_type" => "refresh_token", "refresh_token" => token} = _params        ) do      with {:ok, app} <- Token.Utils.fetch_app(conn), @@ -151,13 +176,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do        json(conn, Token.Response.build(user, token, response_attrs))      else -      _error -> -        put_status(conn, 400) -        |> json(%{error: "Invalid credentials"}) +      _error -> render_invalid_credentials_error(conn)      end    end -  def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do +  def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do      with {:ok, app} <- Token.Utils.fetch_app(conn),           fixed_token = Token.Utils.fix_padding(params["code"]),           {:ok, auth} <- Authorization.get_by_token(app, fixed_token), @@ -167,14 +190,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do        json(conn, Token.Response.build(user, token, response_attrs))      else -      _error -> -        put_status(conn, 400) -        |> json(%{error: "Invalid credentials"}) +      _error -> render_invalid_credentials_error(conn)      end    end    def token_exchange( -        conn, +        %Plug.Conn{} = conn,          %{"grant_type" => "password"} = params        ) do      with {:ok, %User{} = user} <- Authenticator.get_user(conn), @@ -189,23 +210,18 @@ defmodule Pleroma.Web.OAuth.OAuthController do        {:auth_active, false} ->          # Per https://github.com/tootsuite/mastodon/blob/          #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 -        conn -        |> put_status(:forbidden) -        |> json(%{error: "Your login is missing a confirmed e-mail address"}) +        render_error(conn, :forbidden, "Your login is missing a confirmed e-mail address")        {:user_active, false} -> -        conn -        |> put_status(:forbidden) -        |> json(%{error: "Your account is currently disabled"}) +        render_error(conn, :forbidden, "Your account is currently disabled")        _error -> -        put_status(conn, 400) -        |> json(%{error: "Invalid credentials"}) +        render_invalid_credentials_error(conn)      end    end    def token_exchange( -        conn, +        %Plug.Conn{} = conn,          %{"grant_type" => "password", "name" => name, "password" => _password} = params        ) do      params = @@ -216,22 +232,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do      token_exchange(conn, params)    end -  def token_exchange(conn, %{"grant_type" => "client_credentials"} = _params) do +  def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do      with {:ok, app} <- Token.Utils.fetch_app(conn),           {:ok, auth} <- Authorization.create_authorization(app, %User{}),           {:ok, token} <- Token.exchange_token(app, auth) do        json(conn, Token.Response.build_for_client_credentials(token))      else -      _error -> -        put_status(conn, 400) -        |> json(%{error: "Invalid credentials"}) +      _error -> render_invalid_credentials_error(conn)      end    end    # Bad request -  def token_exchange(conn, params), do: bad_request(conn, params) +  def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) -  def token_revoke(conn, %{"token" => _token} = params) do +  def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do      with {:ok, app} <- Token.Utils.fetch_app(conn),           {:ok, _token} <- RevokeToken.revoke(app, params) do        json(conn, %{}) @@ -242,17 +256,18 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end -  def token_revoke(conn, params), do: bad_request(conn, params) +  def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)    # Response for bad request -  defp bad_request(conn, _) do -    conn -    |> put_status(500) -    |> json(%{error: "Bad request"}) +  defp bad_request(%Plug.Conn{} = conn, _) do +    render_error(conn, :internal_server_error, "Bad request")    end    @doc "Prepares OAuth request to provider for Ueberauth" -  def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do +  def prepare_request(%Plug.Conn{} = conn, %{ +        "provider" => provider, +        "authorization" => auth_attrs +      }) do      scope =        auth_attrs        |> Scopes.fetch_scopes([]) @@ -273,12 +288,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do      redirect(conn, to: o_auth_path(conn, :request, provider, params))    end -  def request(conn, params) do +  def request(%Plug.Conn{} = conn, params) do      message =        if params["provider"] do -        "Unsupported OAuth provider: #{params["provider"]}." +        dgettext("errors", "Unsupported OAuth provider: %{provider}.", +          provider: params["provider"] +        )        else -        "Bad OAuth request." +        dgettext("errors", "Bad OAuth request.")        end      conn @@ -286,17 +303,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do      |> redirect(to: "/")    end -  def callback(%{assigns: %{ueberauth_failure: failure}} = conn, params) do +  def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do      params = callback_params(params)      messages = for e <- Map.get(failure, :errors, []), do: e.message      message = Enum.join(messages, "; ")      conn -    |> put_flash(:error, "Failed to authenticate: #{message}.") +    |> put_flash( +      :error, +      dgettext("errors", "Failed to authenticate: %{message}.", message: message) +    )      |> redirect(external: redirect_uri(conn, params["redirect_uri"]))    end -  def callback(conn, params) do +  def callback(%Plug.Conn{} = conn, params) do      params = callback_params(params)      with {:ok, registration} <- Authenticator.get_registration(conn) do @@ -314,13 +334,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do              })            conn -          |> put_session(:registration_id, registration.id) +          |> put_session_registration_id(registration.id)            |> registration_details(%{"authorization" => registration_params})        end      else -      _ -> +      error -> +        Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns])) +          conn -        |> put_flash(:error, "Failed to set up user account.") +        |> put_flash(:error, dgettext("errors", "Failed to set up user account."))          |> redirect(external: redirect_uri(conn, params["redirect_uri"]))      end    end @@ -329,7 +351,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      Map.merge(params, Jason.decode!(state))    end -  def registration_details(conn, %{"authorization" => auth_attrs}) do +  def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do      render(conn, "register.html", %{        client_id: auth_attrs["client_id"],        redirect_uri: auth_attrs["redirect_uri"], @@ -340,7 +362,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      })    end -  def register(conn, %{"authorization" => _, "op" => "connect"} = params) do +  def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do      with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),           %Registration{} = registration <- Repo.get(Registration, registration_id),           {_, {:ok, auth}} <- @@ -359,7 +381,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end -  def register(conn, %{"authorization" => _, "op" => "register"} = params) do +  def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do      with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),           %Registration{} = registration <- Repo.get(Registration, registration_id),           {:ok, user} <- Authenticator.create_from_registration(conn, registration) do @@ -395,7 +417,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do    end    defp do_create_authorization( -         conn, +         %Plug.Conn{} = conn,           %{             "authorization" =>               %{ @@ -416,13 +438,13 @@ defmodule Pleroma.Web.OAuth.OAuthController do    end    # Special case: Local MastodonFE -  defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login) +  defp redirect_uri(%Plug.Conn{} = conn, "."), do: mastodon_api_url(conn, :login) -  defp redirect_uri(_conn, redirect_uri), do: redirect_uri +  defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri -  defp get_session_registration_id(conn), do: get_session(conn, :registration_id) +  defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) -  defp put_session_registration_id(conn, registration_id), +  defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),      do: put_session(conn, :registration_id, registration_id)    @spec validate_scopes(App.t(), map()) :: @@ -432,4 +454,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do      |> Scopes.fetch_scopes(app.scopes)      |> Scopes.validates(app.scopes)    end + +  def default_redirect_uri(%App{} = app) do +    app.redirect_uris +    |> String.split() +    |> Enum.at(0) +  end + +  defp render_invalid_credentials_error(conn) do +    render_error(conn, :bad_request, "Invalid credentials") +  end  end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index f412f7eb2..90c304487 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -14,7 +14,6 @@ defmodule Pleroma.Web.OAuth.Token do    alias Pleroma.Web.OAuth.Token    alias Pleroma.Web.OAuth.Token.Query -  @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)    @type t :: %__MODULE__{}    schema "oauth_tokens" do @@ -78,7 +77,7 @@ defmodule Pleroma.Web.OAuth.Token do    defp put_valid_until(changeset, attrs) do      expires_in = -      Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), @expires_in)) +      Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in()))      changeset      |> change(%{valid_until: expires_in}) @@ -123,4 +122,6 @@ defmodule Pleroma.Web.OAuth.Token do    end    def is_expired?(_), do: false + +  defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)  end diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex index 64e78b183..266110814 100644 --- a/lib/pleroma/web/oauth/token/response.ex +++ b/lib/pleroma/web/oauth/token/response.ex @@ -1,18 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only +  defmodule Pleroma.Web.OAuth.Token.Response do    @moduledoc false    alias Pleroma.User    alias Pleroma.Web.OAuth.Token.Utils -  @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) -    @doc false    def build(%User{} = user, token, opts \\ %{}) do      %{        token_type: "Bearer",        access_token: token.token,        refresh_token: token.refresh_token, -      expires_in: @expires_in, +      expires_in: expires_in(),        scope: Enum.join(token.scopes, " "),        me: user.ap_id      } @@ -25,8 +27,10 @@ defmodule Pleroma.Web.OAuth.Token.Response do        access_token: token.token,        refresh_token: token.refresh_token,        created_at: Utils.format_created_at(token), -      expires_in: @expires_in, +      expires_in: expires_in(),        scope: Enum.join(token.scopes, " ")      }    end + +  defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)  end diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex index 7df0be14e..c620050c8 100644 --- a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex +++ b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex @@ -1,3 +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.OAuth.Token.Strategy.RefreshToken do    @moduledoc """    Functions for dealing with refresh token strategy. diff --git a/lib/pleroma/web/oauth/token/strategy/revoke.ex b/lib/pleroma/web/oauth/token/strategy/revoke.ex index dea63ca54..983f095b4 100644 --- a/lib/pleroma/web/oauth/token/strategy/revoke.ex +++ b/lib/pleroma/web/oauth/token/strategy/revoke.ex @@ -1,3 +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.OAuth.Token.Strategy.Revoke do    @moduledoc """    Functions for dealing with revocation. diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex index 7a4fddafd..1e8765e93 100644 --- a/lib/pleroma/web/oauth/token/utils.ex +++ b/lib/pleroma/web/oauth/token/utils.ex @@ -1,3 +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.OAuth.Token.Utils do    @moduledoc """    Auxiliary functions for dealing with tokens. diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex index ec6e5cfaf..8e0adad91 100644 --- a/lib/pleroma/web/ostatus/handlers/note_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/note_handler.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.CommonAPI +  alias Pleroma.Web.Federator    alias Pleroma.Web.OStatus    alias Pleroma.Web.XML @@ -88,14 +89,15 @@ defmodule Pleroma.Web.OStatus.NoteHandler do      Map.put(note, "external_url", url)    end -  def fetch_replied_to_activity(entry, in_reply_to) do +  def fetch_replied_to_activity(entry, in_reply_to, options \\ []) do      with %Activity{} = activity <- Activity.get_create_by_object_ap_id(in_reply_to) do        activity      else        _e -> -        with in_reply_to_href when not is_nil(in_reply_to_href) <- +        with true <- Federator.allowed_incoming_reply_depth?(options[:depth]), +             in_reply_to_href when not is_nil(in_reply_to_href) <-                 XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry), -             {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href) do +             {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href, options) do            activity          else            _e -> nil @@ -104,7 +106,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do    end    # TODO: Clean this up a bit. -  def handle_note(entry, doc \\ nil) do +  def handle_note(entry, doc \\ nil, options \\ []) do      with id <- XML.string_from_xpath("//id", entry),           activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id),           [author] <- :xmerl_xpath.string('//author[1]', doc), @@ -112,7 +114,8 @@ defmodule Pleroma.Web.OStatus.NoteHandler do           content_html <- OStatus.get_content(entry),           cw <- OStatus.get_cw(entry),           in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry), -         in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to), +         options <- Keyword.put(options, :depth, (options[:depth] || 0) + 1), +         in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to, options),           in_reply_to_object <-             (in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil,           in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to, diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index 6ed089d84..502410c83 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -54,7 +54,7 @@ defmodule Pleroma.Web.OStatus do      "#{Web.base_url()}/ostatus_subscribe?acct={uri}"    end -  def handle_incoming(xml_string) do +  def handle_incoming(xml_string, options \\ []) 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) @@ -91,10 +91,12 @@ defmodule Pleroma.Web.OStatus do                _ ->                  case object_type do                    'http://activitystrea.ms/schema/1.0/note' -> -                    with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity +                    with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options), +                         do: activity                    'http://activitystrea.ms/schema/1.0/comment' -> -                    with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity +                    with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options), +                         do: activity                    _ ->                      Logger.error("Couldn't parse incoming document") @@ -359,7 +361,7 @@ defmodule Pleroma.Web.OStatus do      end    end -  def fetch_activity_from_atom_url(url) do +  def fetch_activity_from_atom_url(url, options \\ []) do      with true <- String.starts_with?(url, "http"),           {:ok, %{body: body, status: code}} when code in 200..299 <-             HTTP.get( @@ -367,7 +369,7 @@ defmodule Pleroma.Web.OStatus do               [{:Accept, "application/atom+xml"}]             ) do        Logger.debug("Got document from #{url}, handling...") -      handle_incoming(body) +      handle_incoming(body, options)      else        e ->          Logger.debug("Couldn't get #{url}: #{inspect(e)}") @@ -375,13 +377,13 @@ defmodule Pleroma.Web.OStatus do      end    end -  def fetch_activity_from_html_url(url) do +  def fetch_activity_from_html_url(url, options \\ []) do      Logger.debug("Trying to fetch #{url}")      with true <- String.starts_with?(url, "http"),           {:ok, %{body: body}} <- HTTP.get(url, []),           {:ok, atom_url} <- get_atom_url(body) do -      fetch_activity_from_atom_url(atom_url) +      fetch_activity_from_atom_url(atom_url, options)      else        e ->          Logger.debug("Couldn't get #{url}: #{inspect(e)}") @@ -389,11 +391,11 @@ defmodule Pleroma.Web.OStatus do      end    end -  def fetch_activity_from_url(url) do -    with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url) do +  def fetch_activity_from_url(url, options \\ []) do +    with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url, options) do        {:ok, activities}      else -      _e -> fetch_activity_from_html_url(url) +      _e -> fetch_activity_from_html_url(url, options)      end    rescue      e -> diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 2fb6ce41b..372d52899 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -245,14 +245,10 @@ defmodule Pleroma.Web.OStatus.OStatusController do    end    def errors(conn, {:error, :not_found}) do -    conn -    |> put_status(404) -    |> text("Not found") +    render_error(conn, :not_found, "Not found")    end    def errors(conn, _) do -    conn -    |> put_status(500) -    |> text("Something went wrong") +    render_error(conn, :internal_server_error, "Something went wrong")    end  end diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex index 26eb614a6..d376e2069 100644 --- a/lib/pleroma/web/rel_me.ex +++ b/lib/pleroma/web/rel_me.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.RelMe do      with_body: true    ] -  if Mix.env() == :test do +  if Pleroma.Config.get(:env) == :test do      def parse(url) when is_binary(url), do: parse_url(url)    else      def parse(url) when is_binary(url) do diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 9bc8f2559..6506de46c 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -4,25 +4,53 @@  defmodule Pleroma.Web.RichMedia.Helpers do    alias Pleroma.Activity +  alias Pleroma.Config    alias Pleroma.HTML    alias Pleroma.Object    alias Pleroma.Web.RichMedia.Parser +  @spec validate_page_url(any()) :: :ok | :error    defp validate_page_url(page_url) when is_binary(page_url) do -    if AutoLinker.Parser.is_url?(page_url, true) do -      URI.parse(page_url) |> validate_page_url -    else -      :error +    validate_tld = Application.get_env(:auto_linker, :opts)[:validate_tld] + +    page_url +    |> AutoLinker.Parser.url?(scheme: true, validate_tld: validate_tld) +    |> parse_uri(page_url) +  end + +  defp validate_page_url(%URI{host: host, scheme: scheme, authority: authority}) +       when scheme == "https" and not is_nil(authority) do +    cond do +      host in Config.get([:rich_media, :ignore_hosts], []) -> +        :error + +      get_tld(host) in Config.get([:rich_media, :ignore_tld], []) -> +        :error + +      true -> +        :ok      end    end -  defp validate_page_url(%URI{authority: nil}), do: :error -  defp validate_page_url(%URI{scheme: nil}), do: :error -  defp validate_page_url(%URI{}), do: :ok    defp validate_page_url(_), do: :error +  defp parse_uri(true, url) do +    url +    |> URI.parse() +    |> validate_page_url +  end + +  defp parse_uri(_, _), do: :error + +  defp get_tld(host) do +    host +    |> String.split(".") +    |> Enum.reverse() +    |> hd +  end +    def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do -    with true <- Pleroma.Config.get([:rich_media, :enabled]), +    with true <- Config.get([:rich_media, :enabled]),           %Object{} = object <- Object.normalize(activity),           false <- object.data["sensitive"] || false,           {:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]), diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index 62e8fa610..0d2523338 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -3,12 +3,6 @@  # 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,      recv_timeout: 2_000, @@ -16,9 +10,13 @@ defmodule Pleroma.Web.RichMedia.Parser do      with_body: true    ] +  defp parsers do +    Pleroma.Config.get([:rich_media, :parsers]) +  end +    def parse(nil), do: {:error, "No URL provided"} -  if Mix.env() == :test do +  if Pleroma.Config.get(:env) == :test do      def parse(url), do: parse_url(url)    else      def parse(url) do @@ -37,7 +35,10 @@ defmodule Pleroma.Web.RichMedia.Parser do      try do        {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options) -      html |> maybe_parse() |> clean_parsed_data() |> check_parsed_data() +      html +      |> maybe_parse() +      |> clean_parsed_data() +      |> check_parsed_data()      rescue        e ->          {:error, "Parsing error: #{inspect(e)}"} @@ -45,7 +46,7 @@ defmodule Pleroma.Web.RichMedia.Parser do    end    defp maybe_parse(html) do -    Enum.reduce_while(@parsers, %{}, fn parser, acc -> +    Enum.reduce_while(parsers(), %{}, fn parser, acc ->        case parser.parse(html, acc) do          {:ok, data} -> {:halt, data}          {:error, _msg} -> {:cont, acc} diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex index 4a7c5eae0..913975616 100644 --- a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex @@ -1,15 +1,23 @@ +# 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.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) +    meta_data = +      html +      |> get_elements(key_name, prefix) +      |> Enum.reduce(data, fn el, acc -> +        attributes = normalize_attributes(el, prefix, key_name, value_name) -             Map.merge(acc, attributes) -           end) do -      {:ok, meta_data} +        Map.merge(acc, attributes) +      end) +      |> maybe_put_title(html) + +    if Enum.empty?(meta_data) do +      {:error, error_message}      else -      _e -> {:error, error_message} +      {:ok, meta_data}      end    end @@ -27,4 +35,19 @@ defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do      %{String.to_atom(data[key_name]) => data[value_name]}    end + +  defp maybe_put_title(%{title: _} = meta, _), do: meta + +  defp maybe_put_title(meta, html) when meta != %{} do +    case get_page_title(html) do +      "" -> meta +      title -> Map.put_new(meta, :title, title) +    end +  end + +  defp maybe_put_title(meta, _), do: meta + +  defp get_page_title(html) do +    Floki.find(html, "title") |> Floki.text() +  end  end diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex index 2530b8c9d..875637c4d 100644 --- a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex @@ -1,3 +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.RichMedia.Parsers.OEmbed do    def parse(html, _data) do      with elements = [_ | _] <- get_discovery_data(html), diff --git a/lib/pleroma/web/rich_media/parsers/ogp.ex b/lib/pleroma/web/rich_media/parsers/ogp.ex index 0e1a0e719..d40fa009f 100644 --- a/lib/pleroma/web/rich_media/parsers/ogp.ex +++ b/lib/pleroma/web/rich_media/parsers/ogp.ex @@ -1,3 +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.RichMedia.Parsers.OGP do    def parse(html, data) do      Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse( diff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex index a317c3e78..e4efe2dd0 100644 --- a/lib/pleroma/web/rich_media/parsers/twitter_card.ex +++ b/lib/pleroma/web/rich_media/parsers/twitter_card.ex @@ -1,3 +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.RichMedia.Parsers.TwitterCard do    def parse(html, data) do      Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse( diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 08c74a742..3e5142e8a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -27,6 +27,7 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.UserEnabledPlug)      plug(Pleroma.Plugs.SetUserSessionIdPlug)      plug(Pleroma.Plugs.EnsureUserKeyPlug) +    plug(Pleroma.Plugs.IdempotencyPlug)    end    pipeline :authenticated_api do @@ -41,6 +42,7 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.UserEnabledPlug)      plug(Pleroma.Plugs.SetUserSessionIdPlug)      plug(Pleroma.Plugs.EnsureAuthenticatedPlug) +    plug(Pleroma.Plugs.IdempotencyPlug)    end    pipeline :admin_api do @@ -57,6 +59,7 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.SetUserSessionIdPlug)      plug(Pleroma.Plugs.EnsureAuthenticatedPlug)      plug(Pleroma.Plugs.UserIsAdminPlug) +    plug(Pleroma.Plugs.IdempotencyPlug)    end    pipeline :mastodon_html do @@ -133,8 +136,8 @@ defmodule Pleroma.Web.Router do    scope "/api/pleroma", Pleroma.Web.TwitterAPI do      pipe_through(:pleroma_api) -    get("/password_reset/:token", UtilController, :show_password_reset) -    post("/password_reset", UtilController, :password_reset) +    get("/password_reset/:token", PasswordController, :reset, as: :reset_password) +    post("/password_reset", PasswordController, :do_reset, as: :reset_password)      get("/emoji", UtilController, :emoji)      get("/captcha", UtilController, :captcha)      get("/healthcheck", UtilController, :healthcheck) @@ -202,6 +205,9 @@ defmodule Pleroma.Web.Router do      put("/statuses/:id", AdminAPIController, :status_update)      delete("/statuses/:id", AdminAPIController, :status_delete) + +    get("/config", AdminAPIController, :config_show) +    post("/config", AdminAPIController, :config_update)    end    scope "/", Pleroma.Web.TwitterAPI do @@ -309,8 +315,6 @@ defmodule Pleroma.Web.Router do        post("/conversations/:id/read", MastodonAPIController, :conversation_read)        get("/endorsements", MastodonAPIController, :empty_array) - -      get("/pleroma/flavour", MastodonAPIController, :get_flavour)      end      scope [] do @@ -335,6 +339,8 @@ defmodule Pleroma.Web.Router do        put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)        delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) +      post("/polls/:id/votes", MastodonAPIController, :poll_vote) +        post("/media", MastodonAPIController, :upload)        put("/media/:id", MastodonAPIController, :update_media) @@ -350,7 +356,9 @@ defmodule Pleroma.Web.Router do        put("/filters/:id", MastodonAPIController, :update_filter)        delete("/filters/:id", MastodonAPIController, :delete_filter) -      post("/pleroma/flavour/:flavour", MastodonAPIController, :set_flavour) +      patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar) +      patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner) +      patch("/pleroma/accounts/update_background", MastodonAPIController, :update_background)        get("/pleroma/mascot", MastodonAPIController, :get_mascot)        put("/pleroma/mascot", MastodonAPIController, :set_mascot) @@ -414,12 +422,7 @@ defmodule Pleroma.Web.Router do      get("/trends", MastodonAPIController, :empty_array) -    scope [] do -      pipe_through(:oauth_read) - -      get("/search", MastodonAPIController, :search) -      get("/accounts/search", MastodonAPIController, :account_search) -    end +    get("/accounts/search", SearchController, :account_search)      scope [] do        pipe_through(:oauth_read_or_public) @@ -431,18 +434,22 @@ defmodule Pleroma.Web.Router do        get("/statuses/:id", MastodonAPIController, :get_status)        get("/statuses/:id/context", MastodonAPIController, :get_context) +      get("/polls/:id", MastodonAPIController, :get_poll) +        get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)        get("/accounts/:id/followers", MastodonAPIController, :followers)        get("/accounts/:id/following", MastodonAPIController, :following)        get("/accounts/:id", MastodonAPIController, :user) +      get("/search", SearchController, :search) +        get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)      end    end    scope "/api/v2", Pleroma.Web.MastodonAPI do -    pipe_through([:api, :oauth_read]) -    get("/search", MastodonAPIController, :search2) +    pipe_through([:api, :oauth_read_or_public]) +    get("/search", SearchController, :search2)    end    scope "/api", Pleroma.Web do @@ -483,13 +490,8 @@ defmodule Pleroma.Web.Router do        get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)        get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation) -      get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline) -    end - -    scope [] do -      pipe_through(:oauth_read) -        get("/search", TwitterAPI.Controller, :search) +      get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)      end    end @@ -508,7 +510,7 @@ defmodule Pleroma.Web.Router do    end    scope "/api", Pleroma.Web, as: :twitter_api_search do -    pipe_through([:api, :oauth_read]) +    pipe_through([:api, :oauth_read_or_public])      get("/pleroma/search_user", TwitterAPI.Controller, :search_user)    end @@ -612,12 +614,6 @@ 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", "json"])      plug(Pleroma.Web.Plugs.HTTPSignaturePlug) @@ -627,8 +623,6 @@ defmodule Pleroma.Web.Router do      # XXX: not really ostatus      pipe_through(:ostatus) -    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 @@ -660,6 +654,12 @@ defmodule Pleroma.Web.Router do        pipe_through(:oauth_write)        post("/users/:nickname/outbox", ActivityPubController, :update_outbox)      end + +    scope [] do +      pipe_through(:oauth_read_or_public) +      get("/users/:nickname/followers", ActivityPubController, :followers) +      get("/users/:nickname/following", ActivityPubController, :following) +    end    end    scope "/relay", Pleroma.Web.ActivityPub do @@ -707,7 +707,7 @@ defmodule Pleroma.Web.Router do      get("/:sig/:url/:filename", MediaProxyController, :remote)    end -  if Mix.env() == :dev do +  if Pleroma.Config.get(:env) == :dev do      scope "/dev" do        pipe_through([:mailbox_preview]) @@ -732,6 +732,7 @@ end  defmodule Fallback.RedirectController do    use Pleroma.Web, :controller +  require Logger    alias Pleroma.User    alias Pleroma.Web.Metadata @@ -758,7 +759,20 @@ defmodule Fallback.RedirectController do    def redirector_with_meta(conn, params) do      {:ok, index_content} = File.read(index_file_path()) -    tags = Metadata.build_tags(params) + +    tags = +      try do +        Metadata.build_tags(params) +      rescue +        e -> +          Logger.error( +            "Metadata rendering for #{conn.request_path} failed.\n" <> +              Exception.format(:error, e, __STACKTRACE__) +          ) + +          "" +      end +      response = String.replace(index_content, "<!--server-generated-meta-->", tags)      conn diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index 9e91a5a40..9b01ebcc6 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -123,11 +123,26 @@ defmodule Pleroma.Web.Salmon do      {:ok, salmon}    end -  def remote_users(%{data: %{"to" => to} = data}) do -    to = to ++ (data["cc"] || []) +  def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do +    cc = Map.get(data, "cc", []) + +    bcc = +      data +      |> Map.get("bcc", []) +      |> Enum.reduce([], fn ap_id, bcc -> +        case Pleroma.List.get_by_ap_id(ap_id) do +          %Pleroma.List{user_id: ^user_id} = list -> +            {:ok, following} = Pleroma.List.get_following(list) +            bcc ++ Enum.map(following, & &1.ap_id) + +          _ -> +            bcc +        end +      end) -    to -    |> Enum.map(fn id -> User.get_cached_by_ap_id(id) end) +    [to, cc, bcc] +    |> Enum.concat() +    |> Enum.map(&User.get_cached_by_ap_id/1)      |> Enum.filter(fn user -> user && !user.local end)    end @@ -146,7 +161,7 @@ defmodule Pleroma.Web.Salmon do          do: Instances.set_reachable(url)        Logger.debug(fn -> "Pushed to #{url}, code #{code}" end) -      :ok +      {:ok, code}      else        e ->          unless params[:unreachable_since], do: Instances.set_reachable(url) @@ -191,7 +206,7 @@ defmodule Pleroma.Web.Salmon do        {:ok, private, _} = Keys.keys_from_pem(keys)        {:ok, feed} = encode(private, feed) -      remote_users = remote_users(activity) +      remote_users = remote_users(user, activity)        salmon_urls = Enum.map(remote_users, & &1.info.salmon)        reachable_urls_metadata = Instances.filter_reachable(salmon_urls) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 133decfc4..4f325113a 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do    use GenServer    require Logger    alias Pleroma.Activity +  alias Pleroma.Config    alias Pleroma.Conversation.Participation    alias Pleroma.Notification    alias Pleroma.Object @@ -109,23 +110,18 @@ defmodule Pleroma.Web.Streamer do      {:noreply, topics}    end -  def handle_cast(%{action: :stream, topic: "user", item: %Notification{} = item}, topics) do -    topic = "user:#{item.user_id}" - -    Enum.each(topics[topic] || [], fn socket -> -      json = -        %{ -          event: "notification", -          payload: -            NotificationView.render("show.json", %{ -              notification: item, -              for: socket.assigns["user"] -            }) -            |> Jason.encode!() -        } -        |> Jason.encode!() - -      send(socket.transport_pid, {:text, json}) +  def handle_cast( +        %{action: :stream, topic: topic, item: %Notification{} = item}, +        topics +      ) +      when topic in ["user", "user:notification"] do +    topics +    |> Map.get("#{topic}:#{item.user_id}", []) +    |> Enum.each(fn socket -> +      send( +        socket.transport_pid, +        {:text, represent_notification(socket.assigns[:user], item)} +      )      end)      {:noreply, topics} @@ -215,6 +211,20 @@ defmodule Pleroma.Web.Streamer do      |> Jason.encode!()    end +  @spec represent_notification(User.t(), Notification.t()) :: binary() +  defp represent_notification(%User{} = user, %Notification{} = notify) do +    %{ +      event: "notification", +      payload: +        NotificationView.render( +          "show.json", +          %{notification: notify, for: user} +        ) +        |> Jason.encode!() +    } +    |> Jason.encode!() +  end +    def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do      Enum.each(topics[topic] || [], fn socket ->        # Get the current user so we have up-to-date blocks etc. @@ -224,11 +234,10 @@ defmodule Pleroma.Web.Streamer do          mutes = user.info.mutes || []          reblog_mutes = user.info.muted_reblogs || [] -        parent = Object.normalize(item) - -        unless is_nil(parent) or item.actor in blocks or item.actor in mutes or -                 item.actor in reblog_mutes or not ActivityPub.contain_activity(item, user) or -                 parent.data["actor"] in blocks or parent.data["actor"] in mutes do +        with parent when not is_nil(parent) <- Object.normalize(item), +             true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)), +             true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)), +             true <- thread_containment(item, user) do            send(socket.transport_pid, {:text, represent_update(item, user)})          end        else @@ -264,8 +273,8 @@ defmodule Pleroma.Web.Streamer do          blocks = user.info.blocks || []          mutes = user.info.mutes || [] -        unless item.actor in blocks or item.actor in mutes or -                 not ActivityPub.contain_activity(item, user) do +        with true <- Enum.all?([blocks, mutes], &(item.actor not in &1)), +             true <- thread_containment(item, user) do            send(socket.transport_pid, {:text, represent_update(item, user)})          end        else @@ -274,9 +283,20 @@ defmodule Pleroma.Web.Streamer do      end)    end -  defp internal_topic(topic, socket) when topic in ~w[user direct] do +  defp internal_topic(topic, socket) when topic in ~w[user user:notification direct] do      "#{topic}:#{socket.assigns[:user].id}"    end    defp internal_topic(topic, _), do: topic + +  @spec thread_containment(Activity.t(), User.t()) :: boolean() +  defp thread_containment(_activity, %User{info: %{skip_thread_containment: true}}), do: true + +  defp thread_containment(activity, user) do +    if Config.get([:instance, :skip_thread_containment]) do +      true +    else +      ActivityPub.contain_activity(activity, user) +    end +  end  end diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex index 3389c91cc..b3cf9ed11 100644 --- a/lib/pleroma/web/templates/layout/app.html.eex +++ b/lib/pleroma/web/templates/layout/app.html.eex @@ -4,7 +4,7 @@      <meta charset="utf-8" />      <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />      <title> -    <%= Application.get_env(:pleroma, :instance)[:name] %> +    <%= Pleroma.Config.get([:instance, :name]) %>      </title>      <style>        body { @@ -63,13 +63,14 @@        .scopes-input {          display: flex; +        flex-direction: column;          margin-top: 1em;          text-align: left;          color: #89898a;        }        .scopes-input label:first-child { -        flex-basis: 40%; +        height: 2em;        }        .scopes { @@ -80,13 +81,22 @@        }        .scope { -        flex-basis: 100%;          display: flex; +        flex-basis: 100%;          height: 2em;          align-items: center;        } +      .scope:before { +        color: #b9b9ba; +        content: "✔\fe0e"; +        margin-left: 1em; +        margin-right: 1em; +      } +        [type="checkbox"] + label { +        display: none; +        cursor: pointer;          margin: 0.5em;        } @@ -95,10 +105,12 @@        }        [type="checkbox"] + label:before { +        cursor: pointer;          display: inline-block;          color: white;          background-color: #121a24;          border: 4px solid #121a24; +        box-shadow: 0px 0px 1px 0 #d8a070;          box-sizing: border-box;          width: 1.2em;          height: 1.2em; @@ -128,7 +140,8 @@          border-radius: 4px;          border: none;          padding: 10px; -        margin-top: 30px; +        margin-top: 20px; +        margin-bottom: 20px;          text-transform: uppercase;          font-size: 16px;          box-shadow: 0px 0px 2px 0px black, @@ -147,8 +160,8 @@          box-sizing: border-box;          width: 100%;          background-color: #931014; +        border: 1px solid #a06060;          border-radius: 4px; -        border: none;          padding: 10px;          margin-top: 20px;          font-weight: 500; @@ -171,12 +184,27 @@            margin-top: 0          } -        .scopes-input { -          flex-direction: column; +        .scope { +          flex-basis: 0%;          } -        .scope { -          flex-basis: 50%; +        .scope:before { +          content: ""; +          margin-left: 0em; +          margin-right: 1em; +        } + +        .scope:first-child:before { +          margin-left: 1em; +          content: "✔\fe0e"; +        } + +        .scope:after { +          content: ","; +        } + +        .scope:last-child:after { +          content: "";          }        }        .form-row { @@ -194,7 +222,7 @@    </head>    <body>      <div class="container"> -      <h1><%= Application.get_env(:pleroma, :instance)[:name] %></h1> +      <h1><%= Pleroma.Config.get([:instance, :name]) %></h1>        <%= render @view_module, @view_template, assigns %>      </div>    </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 5659c7828..3325beca1 100644 --- a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex +++ b/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex @@ -4,11 +4,11 @@  <meta charset='utf-8'>  <meta content='width=device-width, initial-scale=1' name='viewport'>  <title> -<%= Application.get_env(:pleroma, :instance)[:name] %> +<%= Pleroma.Config.get([:instance, :name]) %>  </title>  <link rel="icon" type="image/png" href="/favicon.png"/>  <script crossorigin='anonymous' src="/packs/locales.js"></script> -<script crossorigin='anonymous' src="/packs/locales/<%= @flavour %>/en.js"></script> +<script crossorigin='anonymous' src="/packs/locales/glitch/en.js"></script>  <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'> @@ -19,10 +19,10 @@  <script src="/packs/core/common.js"></script>  <link rel="stylesheet" media="all" href="/packs/core/common.css" /> -<script src="/packs/flavours/<%= @flavour %>/common.js"></script> -<link rel="stylesheet" media="all" href="/packs/flavours/<%= @flavour %>/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/<%= @flavour %>/home.js"></script> +<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/_scopes.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex index e6cfe108b..c9ec1ecbf 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex @@ -1,13 +1,19 @@  <div class="scopes-input"> -  <%= label @form, :scope, "Permissions" %> - +  <%= label @form, :scope, "The following permissions will be granted" %>    <div class="scopes">      <%= for scope <- @available_scopes do %>        <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> -      <div class="scope"> +      <%= if scope in @scopes do %> +        <div class="scope"> +          <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> +          <%= label @form, :"scope_#{scope}", String.capitalize(scope) %> +          <%= if scope in @scopes && scope do %> +            <%= String.capitalize(scope) %> +          <% end %> +        </div> +      <% else %>          <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> -        <%= label @form, :"scope_#{scope}", String.capitalize(scope) %> -      </div> +      <% end %>      <% end %>    </div>  </div> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex index 4bcda7300..4a0718851 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex @@ -1,7 +1,9 @@  <h2>Sign in with external provider</h2>  <%= form_for @conn, o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %> -  <%= render @view_module, "_scopes.html", Map.put(assigns, :form, f) %> +  <div style="display: none"> +    <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> +  </div>    <%= hidden_input f, :client_id, value: @client_id %>    <%= hidden_input f, :redirect_uri, value: @redirect_uri %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/results.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex index 8443d906b..8443d906b 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/results.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex diff --git a/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex new file mode 100644 index 000000000..961aad976 --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex @@ -0,0 +1,2 @@ +<h1>Authorization exists</h1> +<h2>Access token is <%= @token.token %></h2> 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 3e360a52c..b17142ff8 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 @@ -6,26 +6,38 @@  <% end %>  <h2>OAuth Authorization</h2> -  <%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> -<div class="input"> -  <%= label f, :name, "Name or email" %> -  <%= text_input f, :name %> -</div> -<div class="input"> -  <%= label f, :password, "Password" %> -  <%= password_input f, :password %> -</div> -<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> +<%= if @params["registration"] in ["true", true] do %> +  <h3>This is the first time you visit! Please enter your Pleroma handle.</h3> +  <p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p> +  <div class="input"> +    <%= label f, :nickname, "Pleroma Handle" %> +    <%= text_input f, :nickname, placeholder: "lain" %> +  </div> +  <%= hidden_input f, :name, value: @params["name"] %> +  <%= hidden_input f, :password, value: @params["password"] %> +  <br> +<% else %> +  <div class="input"> +    <%= label f, :name, "Username" %> +    <%= text_input f, :name %> +  </div> +  <div class="input"> +    <%= label f, :password, "Password" %> +    <%= password_input f, :password %> +  </div> +  <%= submit "Log In" %> +  <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> +<% end %>  <%= hidden_input f, :client_id, value: @client_id %>  <%= hidden_input f, :response_type, value: @response_type %>  <%= hidden_input f, :redirect_uri, value: @redirect_uri %>  <%= hidden_input f, :state, value: @state %> -<%= submit "Authorize" %>  <% end %>  <%= if Pleroma.Config.oauth_consumer_enabled?() do %>    <%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %>  <% end %> + diff --git a/lib/pleroma/web/templates/twitter_api/util/invalid_token.html.eex b/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex index ee84750c7..ee84750c7 100644 --- a/lib/pleroma/web/templates/twitter_api/util/invalid_token.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset.html.eex index a3facf017..7d3ef6b0d 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset.html.eex @@ -1,5 +1,5 @@  <h2>Password Reset for <%= @user.nickname %></h2> -<%= form_for @conn, util_path(@conn, :password_reset), [as: "data"], fn f -> %> +<%= form_for @conn, reset_password_path(@conn, :do_reset), [as: "data"], fn f -> %>    <div class="form-row">      <%= label f, :password, "Password" %>      <%= password_input f, :password %> diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex index df037c01e..df037c01e 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex index f30ba3274..f30ba3274 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex diff --git a/lib/pleroma/web/translation_helpers.ex b/lib/pleroma/web/translation_helpers.ex new file mode 100644 index 000000000..8f5a43bf6 --- /dev/null +++ b/lib/pleroma/web/translation_helpers.ex @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TranslationHelpers do +  defmacro render_error(conn, status, msgid, bindings \\ Macro.escape(%{})) do +    quote do +      require Pleroma.Web.Gettext + +      unquote(conn) +      |> Plug.Conn.put_status(unquote(status)) +      |> Phoenix.Controller.json(%{ +        error: Pleroma.Web.Gettext.dgettext("errors", unquote(msgid), unquote(bindings)) +      }) +    end +  end +end diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/twitter_api/controllers/password_controller.ex new file mode 100644 index 000000000..1941e6143 --- /dev/null +++ b/lib/pleroma/web/twitter_api/controllers/password_controller.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.PasswordController do +  @moduledoc """ +  The module containts functions for reset password. +  """ + +  use Pleroma.Web, :controller + +  require Logger + +  alias Pleroma.PasswordResetToken +  alias Pleroma.Repo +  alias Pleroma.User + +  def reset(conn, %{"token" => token}) do +    with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), +         %User{} = user <- User.get_cached_by_id(token.user_id) do +      render(conn, "reset.html", %{ +        token: token, +        user: user +      }) +    else +      _e -> render(conn, "invalid_token.html") +    end +  end + +  def do_reset(conn, %{"data" => data}) do +    with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do +      render(conn, "reset_success.html") +    else +      _e -> render(conn, "reset_failed.html") +    end +  end +end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 489170d80..c10c66ff2 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -7,12 +7,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    require Logger -  alias Comeonin.Pbkdf2    alias Pleroma.Activity    alias Pleroma.Emoji    alias Pleroma.Notification -  alias Pleroma.PasswordResetToken -  alias Pleroma.Repo +  alias Pleroma.Plugs.AuthenticationPlug    alias Pleroma.User    alias Pleroma.Web    alias Pleroma.Web.ActivityPub.ActivityPub @@ -20,26 +18,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    alias Pleroma.Web.OStatus    alias Pleroma.Web.WebFinger -  def show_password_reset(conn, %{"token" => token}) do -    with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), -         %User{} = user <- User.get_cached_by_id(token.user_id) do -      render(conn, "password_reset.html", %{ -        token: token, -        user: user -      }) -    else -      _e -> render(conn, "invalid_token.html") -    end -  end - -  def password_reset(conn, %{"data" => data}) do -    with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do -      render(conn, "password_reset_success.html") -    else -      _e -> render(conn, "password_reset_failed.html") -    end -  end -    def help_test(conn, _params) do      json(conn, "ok")    end @@ -118,7 +96,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do      name = followee.nickname      with %User{} = user <- User.get_cached_by_nickname(username), -         true <- Pbkdf2.checkpw(password, user.password_hash), +         true <- AuthenticationPlug.checkpw(password, user.password_hash),           %User{} = _followed <- User.get_cached_by_id(id),           {:ok, follower} <- User.follow(user, followee),           {:ok, _activity} <- ActivityPub.follow(follower, followee) do diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 31e86685a..0313560a8 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -192,6 +192,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def notifications(%{assigns: %{user: user}} = conn, params) do +    params = +      if Map.has_key?(params, "with_muted") do +        Map.put(params, :with_muted, params["with_muted"] in [true, "True", "true", "1"]) +      else +        params +      end +      notifications = Notification.for_user(user, params)      conn @@ -456,6 +463,16 @@ defmodule Pleroma.Web.TwitterAPI.Controller do      end    end +  def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do +    change = Changeset.change(user, %{avatar: nil}) +    {:ok, user} = User.update_and_set_cache(change) +    CommonAPI.update(user) + +    conn +    |> put_view(UserView) +    |> render("show.json", %{user: user, for: user}) +  end +    def update_avatar(%{assigns: %{user: user}} = conn, params) do      {:ok, object} = ActivityPub.upload(params, type: :avatar)      change = Changeset.change(user, %{avatar: object.data}) @@ -467,6 +484,19 @@ defmodule Pleroma.Web.TwitterAPI.Controller do      |> render("show.json", %{user: user, for: user})    end +  def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do +    with new_info <- %{"banner" => %{}}, +         info_cng <- User.Info.profile_update(user.info, new_info), +         changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), +         {:ok, user} <- User.update_and_set_cache(changeset) do +      CommonAPI.update(user) +      response = %{url: nil} |> Jason.encode!() + +      conn +      |> json_reply(200, response) +    end +  end +    def update_banner(%{assigns: %{user: user}} = conn, params) do      with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),           new_info <- %{"banner" => object.data}, @@ -482,6 +512,18 @@ defmodule Pleroma.Web.TwitterAPI.Controller do      end    end +  def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do +    with new_info <- %{"background" => %{}}, +         info_cng <- User.Info.profile_update(user.info, new_info), +         changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), +         {:ok, _user} <- User.update_and_set_cache(changeset) do +      response = %{url: nil} |> Jason.encode!() + +      conn +      |> json_reply(200, response) +    end +  end +    def update_background(%{assigns: %{user: user}} = conn, params) do      with {:ok, object} <- ActivityPub.upload(params, type: :background),           new_info <- %{"background" => object.data}, @@ -632,7 +674,15 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    defp build_info_cng(user, params) do      info_params = -      ["no_rich_text", "locked", "hide_followers", "hide_follows", "hide_favorites", "show_role"] +      [ +        "no_rich_text", +        "locked", +        "hide_followers", +        "hide_follows", +        "hide_favorites", +        "show_role", +        "skip_thread_containment" +      ]        |> Enum.reduce(%{}, fn key, res ->          if value = params[key] do            Map.put(res, key, value == "true") @@ -728,7 +778,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn    def only_if_public_instance(conn, _) do -    if Keyword.get(Application.get_env(:pleroma, :instance), :public) do +    if Pleroma.Config.get([:instance, :public]) do        conn      else        conn diff --git a/lib/pleroma/web/twitter_api/views/password_view.ex b/lib/pleroma/web/twitter_api/views/password_view.ex new file mode 100644 index 000000000..b166b925d --- /dev/null +++ b/lib/pleroma/web/twitter_api/views/password_view.ex @@ -0,0 +1,8 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.PasswordView do +  use Pleroma.Web, :view +  import Phoenix.HTML.Form +end diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index f0a4ddbd3..8d8892068 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -118,9 +118,11 @@ defmodule Pleroma.Web.TwitterAPI.UserView do          "pleroma" =>            %{              "confirmation_pending" => user_info.confirmation_pending, -            "tags" => user.tags +            "tags" => user.tags, +            "skip_thread_containment" => user.info.skip_thread_containment            }            |> maybe_with_activation_status(user, for_user) +          |> with_notification_settings(user, for_user)        }        |> maybe_with_user_settings(user, for_user)        |> maybe_with_role(user, for_user) @@ -132,6 +134,12 @@ defmodule Pleroma.Web.TwitterAPI.UserView do      end    end +  defp with_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do +    Map.put(data, "notification_settings", user.info.notification_settings) +  end + +  defp with_notification_settings(data, _, _), do: data +    defp maybe_with_activation_status(data, user, %User{info: %{is_admin: true}}) do      Map.put(data, "deactivated", user.info.deactivated)    end diff --git a/lib/pleroma/web/uploader_controller.ex b/lib/pleroma/web/uploader_controller.ex index 5d8a77346..bf09775e6 100644 --- a/lib/pleroma/web/uploader_controller.ex +++ b/lib/pleroma/web/uploader_controller.ex @@ -1,3 +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.UploaderController do    use Pleroma.Web, :controller @@ -8,7 +12,7 @@ defmodule Pleroma.Web.UploaderController do    end    def callbacks(conn, _) do -    send_resp(conn, 400, "bad request") +    render_error(conn, :bad_request, "bad request")    end    defp process_callback(conn, pid, params) when is_pid(pid) do @@ -20,6 +24,6 @@ defmodule Pleroma.Web.UploaderController do    end    defp process_callback(conn, _, _) do -    send_resp(conn, 400, "bad request") +    render_error(conn, :bad_request, "bad request")    end  end diff --git a/lib/pleroma/web/views/error_view.ex b/lib/pleroma/web/views/error_view.ex index f4c04131c..5cb8669fe 100644 --- a/lib/pleroma/web/views/error_view.ex +++ b/lib/pleroma/web/views/error_view.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.ErrorView do    def render("500.json", assigns) do      Logger.error("Internal server error: #{inspect(assigns[:reason])}") -    if Mix.env() != :prod do +    if Pleroma.Config.get(:env) != :prod do        %{errors: %{detail: "Internal server error", reason: inspect(assigns[:reason])}}      else        %{errors: %{detail: "Internal server error"}} diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index 66813e4dd..b42f6887e 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -23,9 +23,11 @@ defmodule Pleroma.Web do    def controller do      quote do        use Phoenix.Controller, namespace: Pleroma.Web +        import Plug.Conn        import Pleroma.Web.Gettext        import Pleroma.Web.Router.Helpers +      import Pleroma.Web.TranslationHelpers        plug(:set_put_layout) diff --git a/lib/transports.ex b/lib/transports.ex index 42f645b21..9f3fc535d 100644 --- a/lib/transports.ex +++ b/lib/transports.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only +  defmodule Phoenix.Transports.WebSocket.Raw do    import Plug.Conn,      only: [ diff --git a/lib/xml_builder.ex b/lib/xml_builder.ex index b58602c7b..ceeef2755 100644 --- a/lib/xml_builder.ex +++ b/lib/xml_builder.ex @@ -1,3 +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.XmlBuilder do    def to_xml({tag, attributes, content}) do      open_tag = make_open_tag(tag, attributes)  | 
