diff options
Diffstat (limited to 'lib/mix')
| -rw-r--r-- | lib/mix/pleroma.ex | 47 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/app.ex | 49 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/benchmark.ex | 39 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/config.ex | 19 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/database.ex | 57 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/digest.ex | 7 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/ecto/migrate.ex | 4 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/ecto/rollback.ex | 4 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/emoji.ex | 129 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/frontend.ex | 140 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/instance.ex | 12 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/notification_settings.ex | 18 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/refresh_counter_cache.ex | 49 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/relay.ex | 10 | ||||
| -rw-r--r-- | lib/mix/tasks/pleroma/user.ex | 43 | 
15 files changed, 496 insertions, 131 deletions
| diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index 3ad6edbfb..fe9b0d16c 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -3,15 +3,53 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Mix.Pleroma do +  @apps [ +    :restarter, +    :ecto, +    :ecto_sql, +    :postgrex, +    :db_connection, +    :cachex, +    :flake_id, +    :swoosh, +    :timex +  ] +  @cachex_children ["object", "user", "scrubber"]    @doc "Common functions to be reused in mix tasks"    def start_pleroma do +    Pleroma.Config.Holder.save_default()      Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)      if Pleroma.Config.get(:env) != :test do        Application.put_env(:logger, :console, level: :debug)      end -    {:ok, _} = Application.ensure_all_started(:pleroma) +    adapter = Application.get_env(:tesla, :adapter) + +    apps = +      if adapter == Tesla.Adapter.Gun do +        [:gun | @apps] +      else +        [:hackney | @apps] +      end + +    Enum.each(apps, &Application.ensure_all_started/1) + +    children = +      [ +        Pleroma.Repo, +        {Pleroma.Config.TransferTask, false}, +        Pleroma.Web.Endpoint, +        {Oban, Pleroma.Config.get(Oban)} +      ] ++ +        http_children(adapter) + +    cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, [])) + +    Supervisor.start_link(children ++ cachex_children, +      strategy: :one_for_one, +      name: Pleroma.Supervisor +    )      if Pleroma.Config.get(:env) not in [:test, :benchmark] do        pleroma_rebooted?() @@ -82,4 +120,11 @@ defmodule Mix.Pleroma do    def escape_sh_path(path) do      ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')    end + +  defp http_children(Tesla.Adapter.Gun) do +    Pleroma.Gun.ConnectionPool.children() ++ +      [{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}] +  end + +  defp http_children(_), do: []  end diff --git a/lib/mix/tasks/pleroma/app.ex b/lib/mix/tasks/pleroma/app.ex new file mode 100644 index 000000000..463e2449f --- /dev/null +++ b/lib/mix/tasks/pleroma/app.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.App do +  @moduledoc File.read!("docs/administration/CLI_tasks/oauth_app.md") +  use Mix.Task + +  import Mix.Pleroma + +  @shortdoc "Creates trusted OAuth App" + +  def run(["create" | options]) do +    start_pleroma() + +    {opts, _} = +      OptionParser.parse!(options, +        strict: [name: :string, redirect_uri: :string, scopes: :string], +        aliases: [n: :name, r: :redirect_uri, s: :scopes] +      ) + +    scopes = +      if opts[:scopes] do +        String.split(opts[:scopes], ",") +      else +        ["read", "write", "follow", "push"] +      end + +    params = %{ +      client_name: opts[:name], +      redirect_uris: opts[:redirect_uri], +      trusted: true, +      scopes: scopes +    } + +    with {:ok, app} <- Pleroma.Web.OAuth.App.create(params) do +      shell_info("#{app.client_name} successfully created:") +      shell_info("App client_id: " <> app.client_id) +      shell_info("App client_secret: " <> app.client_secret) +    else +      {:error, changeset} -> +        shell_error("Creating failed:") + +        Enum.each(Pleroma.Web.OAuth.App.errors(changeset), fn {key, error} -> +          shell_error("#{key}: #{error}") +        end) +    end +  end +end diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex index a4885b70c..dd2b9c8f2 100644 --- a/lib/mix/tasks/pleroma/benchmark.ex +++ b/lib/mix/tasks/pleroma/benchmark.ex @@ -74,4 +74,43 @@ defmodule Mix.Tasks.Pleroma.Benchmark do        inputs: inputs      )    end + +  def run(["adapters"]) do +    start_pleroma() + +    :ok = +      Pleroma.Gun.Conn.open( +        "https://httpbin.org/stream-bytes/1500", +        :gun_connections +      ) + +    Process.sleep(1_500) + +    Benchee.run( +      %{ +        "Without conn and without pool" => fn -> +          {:ok, %Tesla.Env{}} = +            Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], +              adapter: [pool: :no_pool, receive_conn: false] +            ) +        end, +        "Without conn and with pool" => fn -> +          {:ok, %Tesla.Env{}} = +            Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], +              adapter: [receive_conn: false] +            ) +        end, +        "With reused conn and without pool" => fn -> +          {:ok, %Tesla.Env{}} = +            Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], +              adapter: [pool: :no_pool] +            ) +        end, +        "With reused conn and with pool" => fn -> +          {:ok, %Tesla.Env{}} = Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500") +        end +      }, +      parallel: 10 +    ) +  end  end diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 5c9ef6904..904c5a74b 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -52,6 +52,7 @@ defmodule Mix.Tasks.Pleroma.Config do    defp do_migrate_to_db(config_file) do      if File.exists?(config_file) do +      shell_info("Migrating settings from file: #{Path.expand(config_file)}")        Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;")        Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;") @@ -72,8 +73,7 @@ defmodule Mix.Tasks.Pleroma.Config do      group      |> Pleroma.Config.Loader.filter_group(settings)      |> Enum.each(fn {key, value} -> -      key = inspect(key) -      {:ok, _} = ConfigDB.update_or_create(%{group: inspect(group), key: key, value: value}) +      {:ok, _} = ConfigDB.update_or_create(%{group: group, key: key, value: value})        shell_info("Settings for key #{key} migrated.")      end) @@ -83,7 +83,7 @@ defmodule Mix.Tasks.Pleroma.Config do    defp migrate_from_db(opts) do      if Pleroma.Config.get([:configurable_from_database]) do -      env = opts[:env] || "prod" +      env = opts[:env] || Pleroma.Config.get(:env)        config_path =          if Pleroma.Config.get(:release) do @@ -105,6 +105,10 @@ defmodule Mix.Tasks.Pleroma.Config do        :ok = File.close(file)        System.cmd("mix", ["format", config_path]) + +      shell_info( +        "Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs" +      )      else        migration_error()      end @@ -112,7 +116,7 @@ defmodule Mix.Tasks.Pleroma.Config do    defp migration_error do      shell_error( -      "Migration is not allowed in config. You can change this behavior by setting `configurable_from_database` to true." +      "Migration is not allowed in config. You can change this behavior by setting `config :pleroma, configurable_from_database: true`"      )    end @@ -131,12 +135,9 @@ defmodule Mix.Tasks.Pleroma.Config do    end    defp write(config, file) do -    value = -      config.value -      |> ConfigDB.from_binary() -      |> inspect(limit: :infinity) +    value = inspect(config.value, limit: :infinity) -    IO.write(file, "config #{config.group}, #{config.key}, #{value}\r\n\r\n") +    IO.write(file, "config #{inspect(config.group)}, #{inspect(config.key)}, #{value}\r\n\r\n")      config    end diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 778de162f..7d8f00b08 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -4,11 +4,13 @@  defmodule Mix.Tasks.Pleroma.Database do    alias Pleroma.Conversation +  alias Pleroma.Maintenance    alias Pleroma.Object    alias Pleroma.Repo    alias Pleroma.User    require Logger    require Pleroma.Constants +  import Ecto.Query    import Mix.Pleroma    use Mix.Task @@ -34,13 +36,7 @@ defmodule Mix.Tasks.Pleroma.Database do      )      if Keyword.get(options, :vacuum) do -      Logger.info("Runnning VACUUM FULL") - -      Repo.query!( -        "vacuum full;", -        [], -        timeout: :infinity -      ) +      Maintenance.vacuum("full")      end    end @@ -58,8 +54,6 @@ defmodule Mix.Tasks.Pleroma.Database do    end    def run(["prune_objects" | args]) do -    import Ecto.Query -      {options, [], []} =        OptionParser.parse(          args, @@ -94,19 +88,11 @@ defmodule Mix.Tasks.Pleroma.Database do      |> Repo.delete_all(timeout: :infinity)      if Keyword.get(options, :vacuum) do -      Logger.info("Runnning VACUUM FULL") - -      Repo.query!( -        "vacuum full;", -        [], -        timeout: :infinity -      ) +      Maintenance.vacuum("full")      end    end    def run(["fix_likes_collections"]) do -    import Ecto.Query -      start_pleroma()      from(object in Object, @@ -135,4 +121,39 @@ defmodule Mix.Tasks.Pleroma.Database do      end)      |> Stream.run()    end + +  def run(["vacuum", args]) do +    start_pleroma() + +    Maintenance.vacuum(args) +  end + +  def run(["ensure_expiration"]) do +    start_pleroma() +    days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365) + +    Pleroma.Activity +    |> join(:left, [a], u in assoc(a, :expiration)) +    |> join(:inner, [a, _u], o in Object, +      on: +        fragment( +          "(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')", +          o.data, +          a.data, +          a.data +        ) +    ) +    |> where(local: true) +    |> where([a, u], is_nil(u)) +    |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data)) +    |> where([_a, _u, o], fragment("?->>'type' = 'Note'", o.data)) +    |> Pleroma.RepoStreamer.chunk_stream(100) +    |> Stream.each(fn activities -> +      Enum.each(activities, fn activity -> +        expires_at = Timex.shift(activity.inserted_at, days: days) +        Pleroma.ActivityExpiration.create(activity, expires_at, false) +      end) +    end) +    |> Stream.run() +  end  end diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex index 7d09e70c5..3595f912d 100644 --- a/lib/mix/tasks/pleroma/digest.ex +++ b/lib/mix/tasks/pleroma/digest.ex @@ -1,5 +1,6 @@  defmodule Mix.Tasks.Pleroma.Digest do    use Mix.Task +  import Mix.Pleroma    @shortdoc "Manages digest emails"    @moduledoc File.read!("docs/administration/CLI_tasks/digest.md") @@ -22,12 +23,10 @@ defmodule Mix.Tasks.Pleroma.Digest do      with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(patched_user) do        {:ok, _} = Pleroma.Emails.Mailer.deliver(email) -      Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})") +      shell_info("Digest email have been sent to #{nickname} (#{user.email})")      else        _ -> -        Mix.shell().info( -          "Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}" -        ) +        shell_info("Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}")      end    end  end diff --git a/lib/mix/tasks/pleroma/ecto/migrate.ex b/lib/mix/tasks/pleroma/ecto/migrate.ex index bc8ed29fb..e903bd171 100644 --- a/lib/mix/tasks/pleroma/ecto/migrate.ex +++ b/lib/mix/tasks/pleroma/ecto/migrate.ex @@ -41,6 +41,10 @@ defmodule Mix.Tasks.Pleroma.Ecto.Migrate do      load_pleroma()      {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) +    if Application.get_env(:pleroma, Pleroma.Repo)[:ssl] do +      Application.ensure_all_started(:ssl) +    end +      opts =        if opts[:to] || opts[:step] || opts[:all],          do: opts, diff --git a/lib/mix/tasks/pleroma/ecto/rollback.ex b/lib/mix/tasks/pleroma/ecto/rollback.ex index f43bd0b98..3dba952cb 100644 --- a/lib/mix/tasks/pleroma/ecto/rollback.ex +++ b/lib/mix/tasks/pleroma/ecto/rollback.ex @@ -40,6 +40,10 @@ defmodule Mix.Tasks.Pleroma.Ecto.Rollback do      load_pleroma()      {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) +    if Application.get_env(:pleroma, Pleroma.Repo)[:ssl] do +      Application.ensure_all_started(:ssl) +    end +      opts =        if opts[:to] || opts[:step] || opts[:all],          do: opts, diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 2b03a3009..8f52ee98d 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -4,18 +4,18 @@  defmodule Mix.Tasks.Pleroma.Emoji do    use Mix.Task +  import Mix.Pleroma    @shortdoc "Manages emoji packs"    @moduledoc File.read!("docs/administration/CLI_tasks/emoji.md")    def run(["ls-packs" | args]) do -    Mix.Pleroma.start_pleroma() -    Application.ensure_all_started(:hackney) +    start_pleroma()      {options, [], []} = parse_global_opts(args) -    manifest = -      fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest()) +    url_or_path = options[:manifest] || default_manifest() +    manifest = fetch_and_decode!(url_or_path)      Enum.each(manifest, fn {name, info} ->        to_print = [ @@ -36,19 +36,18 @@ defmodule Mix.Tasks.Pleroma.Emoji do    end    def run(["get-packs" | args]) do -    Mix.Pleroma.start_pleroma() -    Application.ensure_all_started(:hackney) +    start_pleroma()      {options, pack_names, []} = parse_global_opts(args) -    manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest() +    url_or_path = options[:manifest] || default_manifest() -    manifest = fetch_manifest(manifest_url) +    manifest = fetch_and_decode!(url_or_path)      for pack_name <- pack_names do        if Map.has_key?(manifest, pack_name) do          pack = manifest[pack_name] -        src_url = pack["src"] +        src = pack["src"]          IO.puts(            IO.ANSI.format([ @@ -58,11 +57,11 @@ defmodule Mix.Tasks.Pleroma.Emoji do              :normal,              " from ",              :underline, -            src_url +            src            ])          ) -        binary_archive = Tesla.get!(client(), src_url).body +        {:ok, binary_archive} = fetch(src)          archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()          sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright] @@ -75,8 +74,11 @@ defmodule Mix.Tasks.Pleroma.Emoji do            raise "Bad SHA256 for #{pack_name}"          end -        # The url specified in files should be in the same directory -        files_url = Path.join(Path.dirname(manifest_url), pack["files"]) +        # The location specified in files should be in the same directory +        files_loc = +          url_or_path +          |> Path.dirname() +          |> Path.join(pack["files"])          IO.puts(            IO.ANSI.format([ @@ -86,11 +88,11 @@ defmodule Mix.Tasks.Pleroma.Emoji do              :normal,              " from ",              :underline, -            files_url +            files_loc            ])          ) -        files = Tesla.get!(client(), files_url).body |> Jason.decode!() +        files = fetch_and_decode!(files_loc)          IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name])) @@ -134,38 +136,51 @@ defmodule Mix.Tasks.Pleroma.Emoji do      end    end -  def run(["gen-pack", src]) do -    Application.ensure_all_started(:hackney) +  def run(["gen-pack" | args]) do +    start_pleroma() + +    {opts, [src], []} = +      OptionParser.parse( +        args, +        strict: [ +          name: :string, +          license: :string, +          homepage: :string, +          description: :string, +          files: :string, +          extensions: :string +        ] +      )      proposed_name = Path.basename(src) |> Path.rootname() -    name = String.trim(IO.gets("Pack name [#{proposed_name}]: ")) -    # If there's no name, use the default one -    name = if String.length(name) > 0, do: name, else: proposed_name - -    license = String.trim(IO.gets("License: ")) -    homepage = String.trim(IO.gets("Homepage: ")) -    description = String.trim(IO.gets("Description: ")) +    name = get_option(opts, :name, "Pack name:", proposed_name) +    license = get_option(opts, :license, "License:") +    homepage = get_option(opts, :homepage, "Homepage:") +    description = get_option(opts, :description, "Description:") -    proposed_files_name = "#{name}.json" -    files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: ")) -    files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name +    proposed_files_name = "#{name}_files.json" +    files_name = get_option(opts, :files, "Save file list to:", proposed_files_name)      default_exts = [".png", ".gif"] -    default_exts_str = Enum.join(default_exts, " ") -    exts = -      String.trim( -        IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ") +    custom_exts = +      get_option( +        opts, +        :extensions, +        "Emoji file extensions (separated with spaces):", +        Enum.join(default_exts, " ")        ) +      |> String.split(" ", trim: true)      exts = -      if String.length(exts) > 0 do -        String.split(exts, " ") -        |> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end) -      else +      if MapSet.equal?(MapSet.new(default_exts), MapSet.new(custom_exts)) do          default_exts +      else +        custom_exts        end +    IO.puts("Using #{Enum.join(exts, " ")} extensions") +      IO.puts("Downloading the pack and generating SHA256")      binary_archive = Tesla.get!(client(), src).body @@ -195,14 +210,16 @@ defmodule Mix.Tasks.Pleroma.Emoji do      IO.puts("""      #{files_name} has been created and contains the list of all found emojis in the pack. -    Please review the files in the remove those not needed. +    Please review the files in the pack and remove those not needed.      """) -    if File.exists?("index.json") do -      existing_data = File.read!("index.json") |> Jason.decode!() +    pack_file = "#{name}.json" + +    if File.exists?(pack_file) do +      existing_data = File.read!(pack_file) |> Jason.decode!()        File.write!( -        "index.json", +        pack_file,          Jason.encode!(            Map.merge(              existing_data, @@ -212,24 +229,36 @@ defmodule Mix.Tasks.Pleroma.Emoji do          )        ) -      IO.puts("index.json file has been update with the #{name} pack") +      IO.puts("#{pack_file} has been updated with the #{name} pack")      else -      File.write!("index.json", Jason.encode!(pack_json, pretty: true)) +      File.write!(pack_file, Jason.encode!(pack_json, pretty: true)) -      IO.puts("index.json has been created with the #{name} pack") +      IO.puts("#{pack_file} has been created with the #{name} pack")      end    end -  defp fetch_manifest(from) do -    Jason.decode!( -      if String.starts_with?(from, "http") do -        Tesla.get!(client(), from).body -      else -        File.read!(from) -      end -    ) +  def run(["reload"]) do +    start_pleroma() +    Pleroma.Emoji.reload() +    IO.puts("Emoji packs have been reloaded.") +  end + +  defp fetch_and_decode!(from) do +    with {:ok, json} <- fetch(from) do +      Jason.decode!(json) +    else +      {:error, error} -> raise "#{from} cannot be fetched. Error: #{error} occur." +    end    end +  defp fetch("http" <> _ = from) do +    with {:ok, %{body: body}} <- Tesla.get(client(), from) do +      {:ok, body} +    end +  end + +  defp fetch(path), do: File.read(path) +    defp parse_global_opts(args) do      OptionParser.parse(        args, diff --git a/lib/mix/tasks/pleroma/frontend.ex b/lib/mix/tasks/pleroma/frontend.ex new file mode 100644 index 000000000..2adbf8d72 --- /dev/null +++ b/lib/mix/tasks/pleroma/frontend.ex @@ -0,0 +1,140 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Frontend do +  use Mix.Task + +  import Mix.Pleroma + +  @shortdoc "Manages bundled Pleroma frontends" + +  @moduledoc File.read!("docs/administration/CLI_tasks/frontend.md") + +  def run(["install", "none" | _args]) do +    shell_info("Skipping frontend installation because none was requested") +    "none" +  end + +  def run(["install", frontend | args]) do +    log_level = Logger.level() +    Logger.configure(level: :warn) +    start_pleroma() + +    {options, [], []} = +      OptionParser.parse( +        args, +        strict: [ +          ref: :string, +          static_dir: :string, +          build_url: :string, +          build_dir: :string, +          file: :string +        ] +      ) + +    instance_static_dir = +      with nil <- options[:static_dir] do +        Pleroma.Config.get!([:instance, :static_dir]) +      end + +    cmd_frontend_info = %{ +      "name" => frontend, +      "ref" => options[:ref], +      "build_url" => options[:build_url], +      "build_dir" => options[:build_dir] +    } + +    config_frontend_info = Pleroma.Config.get([:frontends, :available, frontend], %{}) + +    frontend_info = +      Map.merge(config_frontend_info, cmd_frontend_info, fn _key, config, cmd -> +        # This only overrides things that are actually set +        cmd || config +      end) + +    ref = frontend_info["ref"] + +    unless ref do +      raise "No ref given or configured" +    end + +    dest = +      Path.join([ +        instance_static_dir, +        "frontends", +        frontend, +        ref +      ]) + +    fe_label = "#{frontend} (#{ref})" + +    tmp_dir = Path.join(dest, "tmp") + +    with {_, :ok} <- +           {:download_or_unzip, download_or_unzip(frontend_info, tmp_dir, options[:file])}, +         shell_info("Installing #{fe_label} to #{dest}"), +         :ok <- install_frontend(frontend_info, tmp_dir, dest) do +      File.rm_rf!(tmp_dir) +      shell_info("Frontend #{fe_label} installed to #{dest}") + +      Logger.configure(level: log_level) +    else +      {:download_or_unzip, _} -> +        shell_info("Could not download or unzip the frontend") + +      _e -> +        shell_info("Could not install the frontend") +    end +  end + +  defp download_or_unzip(frontend_info, temp_dir, file) do +    if file do +      with {:ok, zip} <- File.read(Path.expand(file)) do +        unzip(zip, temp_dir) +      end +    else +      download_build(frontend_info, temp_dir) +    end +  end + +  def unzip(zip, dest) do +    with {:ok, unzipped} <- :zip.unzip(zip, [:memory]) do +      File.rm_rf!(dest) +      File.mkdir_p!(dest) + +      Enum.each(unzipped, fn {filename, data} -> +        path = filename + +        new_file_path = Path.join(dest, path) + +        new_file_path +        |> Path.dirname() +        |> File.mkdir_p!() + +        File.write!(new_file_path, data) +      end) + +      :ok +    end +  end + +  defp download_build(frontend_info, dest) do +    shell_info("Downloading pre-built bundle for #{frontend_info["name"]}") +    url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"]) + +    with {:ok, %{status: 200, body: zip_body}} <- +           Pleroma.HTTP.get(url, [], timeout: 120_000, recv_timeout: 120_000) do +      unzip(zip_body, dest) +    else +      e -> {:error, e} +    end +  end + +  defp install_frontend(frontend_info, source, dest) do +    from = frontend_info["build_dir"] || "dist" +    File.mkdir_p!(dest) +    File.cp_r!(Path.join([source, from]), dest) +    :ok +  end +end diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index bc842a59f..91440b453 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -145,16 +145,18 @@ defmodule Mix.Tasks.Pleroma.Instance do            options,            :uploads_dir,            "What directory should media uploads go in (when using the local uploader)?", -          Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads]) +          Config.get([Pleroma.Uploaders.Local, :uploads])          ) +        |> Path.expand()        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]) +          Config.get([:instance, :static_dir])          ) +        |> Path.expand()        Config.put([:instance, :static_dir], static_dir) @@ -204,7 +206,7 @@ defmodule Mix.Tasks.Pleroma.Instance do        shell_info("Writing the postgres script to #{psql_path}.")        File.write(psql_path, result_psql) -      write_robots_txt(indexable, template_dir) +      write_robots_txt(static_dir, indexable, template_dir)        shell_info(          "\n All files successfully written! Refer to the installation instructions for your platform for next steps." @@ -224,15 +226,13 @@ defmodule Mix.Tasks.Pleroma.Instance do      end    end -  defp write_robots_txt(indexable, template_dir) do +  defp write_robots_txt(static_dir, indexable, template_dir) do      robots_txt =        EEx.eval_file(          template_dir <> "/robots_txt.eex",          indexable: indexable        ) -    static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") -      unless File.exists?(static_dir) do        File.mkdir_p!(static_dir)      end diff --git a/lib/mix/tasks/pleroma/notification_settings.ex b/lib/mix/tasks/pleroma/notification_settings.ex index 7d65f0587..00f5ba7bf 100644 --- a/lib/mix/tasks/pleroma/notification_settings.ex +++ b/lib/mix/tasks/pleroma/notification_settings.ex @@ -3,8 +3,8 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do    @moduledoc """    Example: -  > mix pleroma.notification_settings --privacy-option=false --nickname-users="parallel588"  # set false only for parallel588 user -  > mix pleroma.notification_settings --privacy-option=true # set true for all users +  > mix pleroma.notification_settings --hide-notification-contents=false --nickname-users="parallel588"  # set false only for parallel588 user +  > mix pleroma.notification_settings --hide-notification-contents=true # set true for all users    """ @@ -19,16 +19,16 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do        OptionParser.parse(          args,          strict: [ -          privacy_option: :boolean, +          hide_notification_contents: :boolean,            email_users: :string,            nickname_users: :string          ]        ) -    privacy_option = Keyword.get(options, :privacy_option) +    hide_notification_contents = Keyword.get(options, :hide_notification_contents) -    if not is_nil(privacy_option) do -      privacy_option +    if not is_nil(hide_notification_contents) do +      hide_notification_contents        |> build_query(options)        |> Pleroma.Repo.update_all([])      end @@ -36,15 +36,15 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do      shell_info("Done")    end -  defp build_query(privacy_option, options) do +  defp build_query(hide_notification_contents, options) do      query =        from(u in Pleroma.User,          update: [            set: [              notification_settings:                fragment( -                "jsonb_set(notification_settings, '{privacy_option}', ?)", -                ^privacy_option +                "jsonb_set(notification_settings, '{hide_notification_contents}', ?)", +                ^hide_notification_contents                )            ]          ] diff --git a/lib/mix/tasks/pleroma/refresh_counter_cache.ex b/lib/mix/tasks/pleroma/refresh_counter_cache.ex index 15b4dbfa6..efcbaa3b1 100644 --- a/lib/mix/tasks/pleroma/refresh_counter_cache.ex +++ b/lib/mix/tasks/pleroma/refresh_counter_cache.ex @@ -17,30 +17,53 @@ defmodule Mix.Tasks.Pleroma.RefreshCounterCache do    def run([]) do      Mix.Pleroma.start_pleroma() -    ["public", "unlisted", "private", "direct"] -    |> Enum.each(fn visibility -> -      count = status_visibility_count_query(visibility) -      name = "status_visibility_#{visibility}" -      CounterCache.set(name, count) -      Mix.Pleroma.shell_info("Set #{name} to #{count}") +    instances = +      Activity +      |> distinct([a], true) +      |> select([a], fragment("split_part(?, '/', 3)", a.actor)) +      |> Repo.all() + +    instances +    |> Enum.with_index(1) +    |> Enum.each(fn {instance, i} -> +      counters = instance_counters(instance) +      CounterCache.set(instance, counters) + +      Mix.Pleroma.shell_info( +        "[#{i}/#{length(instances)}] Setting #{instance} counters: #{inspect(counters)}" +      )      end)      Mix.Pleroma.shell_info("Done")    end -  defp status_visibility_count_query(visibility) do +  defp instance_counters(instance) do +    counters = %{"public" => 0, "unlisted" => 0, "private" => 0, "direct" => 0} +      Activity -    |> where( +    |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data)) +    |> where([a], fragment("split_part(?, '/', 3) = ?", a.actor, ^instance)) +    |> select( +      [a], +      {fragment( +         "activity_visibility(?, ?, ?)", +         a.actor, +         a.recipients, +         a.data +       ), count(a.id)} +    ) +    |> group_by(        [a],        fragment( -        "activity_visibility(?, ?, ?) = ?", +        "activity_visibility(?, ?, ?)",          a.actor,          a.recipients, -        a.data, -        ^visibility +        a.data        )      ) -    |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data)) -    |> Repo.aggregate(:count, :id, timeout: :timer.minutes(30)) +    |> Repo.all(timeout: :timer.minutes(30)) +    |> Enum.reduce(counters, fn {visibility, count}, acc -> +      Map.put(acc, visibility, count) +    end)    end  end diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex index c3312507e..a6d8d6c1c 100644 --- a/lib/mix/tasks/pleroma/relay.ex +++ b/lib/mix/tasks/pleroma/relay.ex @@ -35,10 +35,16 @@ defmodule Mix.Tasks.Pleroma.Relay do    def run(["list"]) do      start_pleroma() -    with {:ok, list} <- Relay.list(true) do -      list |> Enum.each(&shell_info(&1)) +    with {:ok, list} <- Relay.list() do +      Enum.each(list, &print_relay_url/1)      else        {:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")      end    end + +  defp print_relay_url(%{followed_back: false} = relay) do +    shell_info("#{relay.actor} - no Accept received (relay didn't follow back)") +  end + +  defp print_relay_url(relay), do: shell_info(relay.actor)  end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 40dd9bdc0..01824aa18 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -8,6 +8,8 @@ defmodule Mix.Tasks.Pleroma.User do    alias Ecto.Changeset    alias Pleroma.User    alias Pleroma.UserInviteToken +  alias Pleroma.Web.ActivityPub.Builder +  alias Pleroma.Web.ActivityPub.Pipeline    @shortdoc "Manages Pleroma users"    @moduledoc File.read!("docs/administration/CLI_tasks/user.md") @@ -96,8 +98,9 @@ defmodule Mix.Tasks.Pleroma.User do    def run(["rm", nickname]) do      start_pleroma() -    with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do -      User.perform(:delete, user) +    with %User{local: true} = user <- User.get_cached_by_nickname(nickname), +         {:ok, delete_data, _} <- Builder.delete(user, user.ap_id), +         {:ok, _delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do        shell_info("User #{nickname} deleted.")      else        _ -> shell_error("No local user #{nickname}") @@ -141,28 +144,30 @@ defmodule Mix.Tasks.Pleroma.User do      end    end -  def run(["unsubscribe", nickname]) do +  def run(["reset_mfa", nickname]) do +    start_pleroma() + +    with %User{local: true} = user <- User.get_cached_by_nickname(nickname), +         {:ok, _token} <- Pleroma.MFA.disable(user) do +      shell_info("Multi-Factor Authentication disabled for #{user.nickname}") +    else +      _ -> +        shell_error("No local user #{nickname}") +    end +  end + +  def run(["deactivate", nickname]) do      start_pleroma()      with %User{} = user <- User.get_cached_by_nickname(nickname) do        shell_info("Deactivating #{user.nickname}")        User.deactivate(user) - -      user -      |> User.get_friends() -      |> Enum.each(fn friend -> -        user = User.get_cached_by_id(user.id) - -        shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}") -        User.unfollow(user, friend) -      end) -        :timer.sleep(500)        user = User.get_cached_by_id(user.id) -      if Enum.empty?(User.get_friends(user)) do -        shell_info("Successfully unsubscribed all followers from #{user.nickname}") +      if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do +        shell_info("Successfully unsubscribed all local followers from #{user.nickname}")        end      else        _ -> @@ -170,7 +175,7 @@ defmodule Mix.Tasks.Pleroma.User do      end    end -  def run(["unsubscribe_all_from_instance", instance]) do +  def run(["deactivate_all_from_instance", instance]) do      start_pleroma()      Pleroma.User.Query.build(%{nickname: "@#{instance}"}) @@ -178,7 +183,7 @@ defmodule Mix.Tasks.Pleroma.User do      |> Stream.each(fn users ->        users        |> Enum.each(fn user -> -        run(["unsubscribe", user.nickname]) +        run(["deactivate", user.nickname])        end)      end)      |> Stream.run() @@ -227,7 +232,7 @@ defmodule Mix.Tasks.Pleroma.User do      with %User{} = user <- User.get_cached_by_nickname(nickname) do        user = user |> User.tag(tags) -      shell_info("Tags of #{user.nickname}: #{inspect(tags)}") +      shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")      else        _ ->          shell_error("Could not change user tags for #{nickname}") @@ -240,7 +245,7 @@ defmodule Mix.Tasks.Pleroma.User do      with %User{} = user <- User.get_cached_by_nickname(nickname) do        user = user |> User.untag(tags) -      shell_info("Tags of #{user.nickname}: #{inspect(tags)}") +      shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")      else        _ ->          shell_error("Could not change user tags for #{nickname}") | 
