diff options
75 files changed, 1338 insertions, 145 deletions
| diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cd0561852..ff3308e31 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,7 @@  image: git.pleroma.social:5050/pleroma/pleroma/ci-base  variables: &global_variables +  ELIXIR_VER: 1.12.3    POSTGRES_DB: pleroma_test    POSTGRES_USER: postgres    POSTGRES_PASSWORD: postgres @@ -294,7 +295,7 @@ stop_review_app:  amd64:    stage: release -  image: elixir:1.11.4 +  image: elixir:$ELIXIR_VER    only: &release-only    - stable@pleroma/pleroma    - develop@pleroma/pleroma @@ -334,7 +335,7 @@ amd64-musl:    stage: release    artifacts: *release-artifacts    only: *release-only -  image: elixir:1.11.4-alpine +  image: elixir:$ELIXIR_VER-alpine    tags:      - amd64    cache: *release-cache @@ -352,7 +353,7 @@ arm:    only: *release-only    tags:      - arm32-specified -  image: arm32v7/elixir:1.11.4 +  image: arm32v7/elixir:$ELIXIR_VER    cache: *release-cache    variables: *release-variables    before_script: *before-release @@ -364,7 +365,7 @@ arm-musl:    only: *release-only    tags:      - arm32-specified -  image: arm32v7/elixir:1.11.4-alpine +  image: arm32v7/elixir:$ELIXIR_VER-alpine    cache: *release-cache    variables: *release-variables    before_script: *before-release-musl @@ -376,7 +377,7 @@ arm64:    only: *release-only    tags:      - arm -  image: arm64v8/elixir:1.11.4 +  image: arm64v8/elixir:$ELIXIR_VER    cache: *release-cache    variables: *release-variables    before_script: *before-release @@ -388,7 +389,7 @@ arm64-musl:    only: *release-only    tags:      - arm -  image: arm64v8/elixir:1.11.4-alpine +  image: arm64v8/elixir:$ELIXIR_VER-alpine    cache: *release-cache    variables: *release-variables    before_script: *before-release-musl diff --git a/Dockerfile b/Dockerfile index 72c227b76..d2a3e3573 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@  ARG ELIXIR_IMG=hexpm/elixir -ARG ELIXIR_VER=1.11.4 +ARG ELIXIR_VER=1.12.3  ARG ERLANG_VER=24.2.1  ARG ALPINE_VER=3.17.0 diff --git a/changelog.d/3900.change b/changelog.d/3900.change new file mode 100644 index 000000000..fe0cc2fbf --- /dev/null +++ b/changelog.d/3900.change @@ -0,0 +1 @@ +Update to Phoenix 1.7 diff --git a/changelog.d/docs-max-elixir-erlang.change b/changelog.d/docs-max-elixir-erlang.change new file mode 100644 index 000000000..a58b7fc17 --- /dev/null +++ b/changelog.d/docs-max-elixir-erlang.change @@ -0,0 +1 @@ +- Document maximum supported version of Erlang & Elixir diff --git a/changelog.d/favicon.add b/changelog.d/favicon.add new file mode 100644 index 000000000..cf12395e7 --- /dev/null +++ b/changelog.d/favicon.add @@ -0,0 +1 @@ +Add support for configuring favicon, embed favicon and PWA manifest in server-generated meta diff --git a/changelog.d/federation_status-access.change b/changelog.d/federation_status-access.change new file mode 100644 index 000000000..952254476 --- /dev/null +++ b/changelog.d/federation_status-access.change @@ -0,0 +1 @@ +- Make `/api/v1/pleroma/federation_status` publicly available diff --git a/changelog.d/healthcheck-disabled-error.fix b/changelog.d/healthcheck-disabled-error.fix new file mode 100644 index 000000000..984384a52 --- /dev/null +++ b/changelog.d/healthcheck-disabled-error.fix @@ -0,0 +1 @@ +TwitterAPI: Return proper error when healthcheck is disabled diff --git a/changelog.d/meilisearch.add b/changelog.d/meilisearch.add new file mode 100644 index 000000000..4856eea2e --- /dev/null +++ b/changelog.d/meilisearch.add @@ -0,0 +1 @@ +Add meilisearch, make search engines pluggable diff --git a/changelog.d/quotes-count.skip b/changelog.d/quotes-count.skip new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/changelog.d/quotes-count.skip diff --git a/changelog.d/system-cflags.fix b/changelog.d/system-cflags.fix new file mode 100644 index 000000000..84de5ad57 --- /dev/null +++ b/changelog.d/system-cflags.fix @@ -0,0 +1 @@ +- Fix eblurhash and elixir-captcha not using system cflags diff --git a/ci/Dockerfile b/ci/Dockerfile index ca28b7029..a2b566873 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -1,4 +1,4 @@ -FROM elixir:1.11.4 +FROM elixir:1.12.3  # Single RUN statement, otherwise intermediate images are created  # https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run diff --git a/config/config.exs b/config/config.exs index ad3a98f46..247e1f25a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -110,17 +110,6 @@ config :pleroma, :uri_schemes,      "xmpp"    ] -websocket_config = [ -  path: "/websocket", -  serializer: [ -    {Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"}, -    {Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"} -  ], -  timeout: 60_000, -  transport_log: false, -  compress: false -] -  # Configures the endpoint  config :pleroma, Pleroma.Web.Endpoint,    url: [host: "localhost"], @@ -130,10 +119,7 @@ config :pleroma, Pleroma.Web.Endpoint,        {:_,         [           {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, -         {"/websocket", Phoenix.Endpoint.CowboyWebSocket, -          {Phoenix.Transports.WebSocket, -           {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}}, -         {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}} +         {:_, Plug.Cowboy.Handler, {Pleroma.Web.Endpoint, []}}         ]}      ]    ], @@ -185,6 +171,7 @@ config :pleroma, :instance,    short_description: "",    background_image: "/images/city.jpg",    instance_thumbnail: "/instance/thumbnail.jpeg", +  favicon: "/favicon.png",    limit: 5_000,    description_limit: 5_000,    remote_limit: 100_000, @@ -360,6 +347,8 @@ config :pleroma, :manifest,    icons: [      %{        src: "/static/logo.svg", +      sizes: "144x144", +      purpose: "any",        type: "image/svg+xml"      }    ], @@ -590,7 +579,9 @@ config :pleroma, Oban,      background: 5,      remote_fetcher: 2,      attachments_cleanup: 1, -    mute_expire: 5 +    new_users_digest: 1, +    mute_expire: 5, +    search_indexing: 10    ],    plugins: [Oban.Plugins.Pruner],    crontab: [ @@ -601,7 +592,8 @@ config :pleroma, Oban,  config :pleroma, :workers,    retries: [      federator_incoming: 5, -    federator_outgoing: 5 +    federator_outgoing: 5, +    search_indexing: 2    ]  config :pleroma, Pleroma.Formatter, @@ -888,11 +880,19 @@ config :pleroma, Pleroma.User.Backup,  config :pleroma, ConcurrentLimiter, [    {Pleroma.Web.RichMedia.Helpers, [max_running: 5, max_waiting: 5]}, -  {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]} +  {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}, +  {Pleroma.Search, [max_running: 30, max_waiting: 50]}  ]  config :pleroma, Pleroma.Web.WebFinger, domain: nil, update_nickname_on_user_fetch: true +config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch + +config :pleroma, Pleroma.Search.Meilisearch, +  url: "http://127.0.0.1:7700/", +  private_key: nil, +  initial_indexing_chunk_size: 100_000 +  # Import environment specific config. This must remain at the bottom  # of this file so it overrides the configuration defined above.  import_config "#{Mix.env()}.exs" diff --git a/config/description.exs b/config/description.exs index d18649ae8..27e7f7e9b 100644 --- a/config/description.exs +++ b/config/description.exs @@ -988,6 +988,12 @@ config :pleroma, :config_description, [          suggestions: ["/instance/thumbnail.jpeg"]        },        %{ +        key: :favicon, +        type: {:string, :image}, +        description: "Favicon of the instance", +        suggestions: ["/favicon.png"] +      }, +      %{          key: :show_reactions,          type: :boolean,          description: "Let favourites and emoji reactions be viewed through the API." @@ -3466,5 +3472,48 @@ config :pleroma, :config_description, [          ]        }      ] +  }, +  %{ +    group: :pleroma, +    key: Pleroma.Search, +    type: :group, +    description: "General search settings.", +    children: [ +      %{ +        key: :module, +        type: :keyword, +        description: "Selected search module.", +        suggestion: [Pleroma.Search.DatabaseSearch, Pleroma.Search.Meilisearch] +      } +    ] +  }, +  %{ +    group: :pleroma, +    key: Pleroma.Search.Meilisearch, +    type: :group, +    description: "Meilisearch settings.", +    children: [ +      %{ +        key: :url, +        type: :string, +        description: "Meilisearch URL.", +        suggestion: ["http://127.0.0.1:7700/"] +      }, +      %{ +        key: :private_key, +        type: :string, +        description: +          "Private key for meilisearch authentication, or `nil` to disable private key authentication.", +        suggestion: [nil] +      }, +      %{ +        key: :initial_indexing_chunk_size, +        type: :int, +        description: +          "Amount of posts in a batch when running the initial indexing operation. Should probably not be more than 100000" <> +            " since there's a limit on maximum insert size", +        suggestion: [100_000] +      } +    ]    }  ] diff --git a/config/test.exs b/config/test.exs index 78303eb1d..5e8135a58 100644 --- a/config/test.exs +++ b/config/test.exs @@ -133,10 +133,16 @@ config :pleroma, :side_effects,    ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock,    logger: Pleroma.LoggerMock +config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch + +config :pleroma, Pleroma.Search.Meilisearch, url: "http://127.0.0.1:7700/", private_key: nil +  # Reduce recompilation time  # https://dashbit.co/blog/speeding-up-re-compilation-of-elixir-projects  config :phoenix, :plug_init_mode, :runtime +config :pleroma, :config_impl, Pleroma.UnstubbedConfigMock +  if File.exists?("./config/test.secret.exs") do    import_config "test.secret.exs"  else diff --git a/docs/configuration/search.md b/docs/configuration/search.md new file mode 100644 index 000000000..f131948a7 --- /dev/null +++ b/docs/configuration/search.md @@ -0,0 +1,123 @@ +# Configuring search + +{! backend/administration/CLI_tasks/general_cli_task_info.include !} + +## Built-in search + +To use built-in search that has no external dependencies, set the search module to `Pleroma.Activity`: + +> config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch + +While it has no external dependencies, it has problems with performance and relevancy. + +## Meilisearch + +Note that it's quite a bit more memory hungry than PostgreSQL (around 4-5G for ~1.2 million +posts while idle and up to 7G while indexing initially). The disk usage for this additional index is also +around 4 gigabytes. Like [RUM](./cheatsheet.md#rum-indexing-for-full-text-search) indexes, it offers considerably +higher performance and ordering by timestamp in a reasonable amount of time. +Additionally, the search results seem to be more accurate. + +Due to high memory usage, it may be best to set it up on a different machine, if running pleroma on a low-resource +computer, and use private key authentication to secure the remote search instance. + +To use [meilisearch](https://www.meilisearch.com/), set the search module to `Pleroma.Search.Meilisearch`: + +> config :pleroma, Pleroma.Search, module: Pleroma.Search.Meilisearch + +You then need to set the address of the meilisearch instance, and optionally the private key for authentication. You might +also want to change the `initial_indexing_chunk_size` to be smaller if you're server is not very powerful, but not higher than `100_000`, +because meilisearch will refuse to process it if it's too big. However, in general you want this to be as big as possible, because meilisearch +indexes faster when it can process many posts in a single batch. + +> config :pleroma, Pleroma.Search.Meilisearch, +>    url: "http://127.0.0.1:7700/", +>    private_key: "private key", +>    initial_indexing_chunk_size: 100_000 + +Information about setting up meilisearch can be found in the +[official documentation](https://docs.meilisearch.com/learn/getting_started/installation.html). +You probably want to start it with `MEILI_NO_ANALYTICS=true` environment variable to disable analytics. +At least version 0.25.0 is required, but you are strongly adviced to use at least 0.26.0, as it introduces +the `--enable-auto-batching` option which drastically improves performance. Without this option, the search +is hardly usable on a somewhat big instance. + +### Private key authentication (optional) + +To set the private key, use the `MEILI_MASTER_KEY` environment variable when starting. After setting the _master key_, +you have to get the _private key_, which is actually used for authentication. + +=== "OTP" +    ```sh +    ./bin/pleroma_ctl search.meilisearch show-keys <your master key here> +    ``` + +=== "From Source" +    ```sh +    mix pleroma.search.meilisearch show-keys <your master key here> +    ``` + +You will see a "Default Admin API Key", this is the key you actually put into your configuration file. + +### Initial indexing + +After setting up the configuration, you'll want to index all of your already existsing posts. Only public posts are indexed.  You'll only +have to do it one time, but it might take a while, depending on the amount of posts your instance has seen. This is also a fairly RAM +consuming process for `meilisearch`, and it will take a lot of RAM when running if you have a lot of posts (seems to be around 5G for ~1.2 +million posts while idle and up to 7G while indexing initially, but your experience may be different). + +The sequence of actions is as follows: + +1. First, change the configuration to use `Pleroma.Search.Meilisearch` as the search backend +2. Restart your instance, at this point it can be used while the search indexing is running, though search won't return anything +3. Start the initial indexing process (as described below with `index`), +   and wait until the task says it sent everything from the database to index +4. Wait until everything is actually indexed (by checking with `stats` as described below), +   at this point you don't have to do anything, just wait a while. + +To start the initial indexing, run the `index` command: + +=== "OTP" +    ```sh +    ./bin/pleroma_ctl search.meilisearch index +    ``` + +=== "From Source" +    ```sh +    mix pleroma.search.meilisearch index +    ``` + +This will show you the total amount of posts to index, and then show you the amount of posts indexed currently, until the numbers eventually +become the same. The posts are indexed in big batches and meilisearch will take some time to actually index them, even after you have +inserted all the posts into it. Depending on the amount of posts, this may be as long as several hours. To get information about the status +of indexing and how many posts have actually been indexed, use the `stats` command: + +=== "OTP" +    ```sh +    ./bin/pleroma_ctl search.meilisearch stats +    ``` + +=== "From Source" +    ```sh +    mix pleroma.search.meilisearch stats +    ``` + +### Clearing the index + +In case you need to clear the index (for example, to re-index from scratch, if that needs to happen for some reason), you can +use the `clear` command: + +=== "OTP" +    ```sh +    ./bin/pleroma_ctl search.meilisearch clear +    ``` + +=== "From Source" +    ```sh +    mix pleroma.search.meilisearch clear +    ``` + +This will clear **all** the posts from the search index. Note, that deleted posts are also removed from index by the instance itself, so +there is no need to actually clear the whole index, unless you want **all** of it gone. That said, the index does not hold any information +that cannot be re-created from the database, it should also generally be a lot smaller than the size of your database. Still, the size +depends on the amount of text in posts. diff --git a/docs/installation/generic_dependencies.include b/docs/installation/generic_dependencies.include index dcaacfdfd..3365a36a8 100644 --- a/docs/installation/generic_dependencies.include +++ b/docs/installation/generic_dependencies.include @@ -1,11 +1,11 @@  ## Required dependencies -* PostgreSQL 9.6+ -* Elixir 1.10+ -* Erlang OTP 22.2+ +* PostgreSQL >=9.6 +* Elixir >=1.11.0 <1.15 +* Erlang OTP >=22.2.0 <26  * git  * file / libmagic -* gcc (clang might also work) +* gcc or clang  * GNU make  * CMake diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex new file mode 100644 index 000000000..8379a0c25 --- /dev/null +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -0,0 +1,145 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Search.Meilisearch do +  require Pleroma.Constants + +  import Mix.Pleroma +  import Ecto.Query + +  import Pleroma.Search.Meilisearch, +    only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete: 1] + +  def run(["index"]) do +    start_pleroma() +    Pleroma.HTML.compile_scrubbers() + +    meili_version = +      ( +        {:ok, result} = meili_get("/version") + +        result["pkgVersion"] +      ) + +    # The ranking rule syntax was changed but nothing about that is mentioned in the changelog +    if not Version.match?(meili_version, ">= 0.25.0") do +      raise "Meilisearch <0.24.0 not supported" +    end + +    {:ok, _} = +      meili_post( +        "/indexes/objects/settings/ranking-rules", +        [ +          "published:desc", +          "words", +          "exactness", +          "proximity", +          "typo", +          "attribute", +          "sort" +        ] +      ) + +    {:ok, _} = +      meili_post( +        "/indexes/objects/settings/searchable-attributes", +        [ +          "content" +        ] +      ) + +    IO.puts("Created indices. Starting to insert posts.") + +    chunk_size = Pleroma.Config.get([Pleroma.Search.Meilisearch, :initial_indexing_chunk_size]) + +    Pleroma.Repo.transaction( +      fn -> +        query = +          from(Pleroma.Object, +            # Only index public and unlisted posts which are notes and have some text +            where: +              fragment("data->>'type' = 'Note'") and +                (fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()) or +                   fragment("data->'cc' \\? ?", ^Pleroma.Constants.as_public())), +            order_by: [desc: fragment("data->'published'")] +          ) + +        count = query |> Pleroma.Repo.aggregate(:count, :data) +        IO.puts("Entries to index: #{count}") + +        Pleroma.Repo.stream( +          query, +          timeout: :infinity +        ) +        |> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1) +        |> Stream.filter(fn o -> not is_nil(o) end) +        |> Stream.chunk_every(chunk_size) +        |> Stream.transform(0, fn objects, acc -> +          new_acc = acc + Enum.count(objects) + +          # Reset to the beginning of the line and rewrite it +          IO.write("\r") +          IO.write("Indexed #{new_acc} entries") + +          {[objects], new_acc} +        end) +        |> Stream.each(fn objects -> +          result = +            meili_put( +              "/indexes/objects/documents", +              objects +            ) + +          with {:ok, res} <- result do +            if not Map.has_key?(res, "uid") do +              IO.puts("\nFailed to index: #{inspect(result)}") +            end +          else +            e -> IO.puts("\nFailed to index due to network error: #{inspect(e)}") +          end +        end) +        |> Stream.run() +      end, +      timeout: :infinity +    ) + +    IO.write("\n") +  end + +  def run(["clear"]) do +    start_pleroma() + +    meili_delete("/indexes/objects/documents") +  end + +  def run(["show-keys", master_key]) do +    start_pleroma() + +    endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url]) + +    {:ok, result} = +      Pleroma.HTTP.get( +        Path.join(endpoint, "/keys"), +        [{"Authorization", "Bearer #{master_key}"}] +      ) + +    decoded = Jason.decode!(result.body) + +    if decoded["results"] do +      Enum.each(decoded["results"], fn %{"description" => desc, "key" => key} -> +        IO.puts("#{desc}: #{key}") +      end) +    else +      IO.puts("Error fetching the keys, check the master key is correct: #{inspect(decoded)}") +    end +  end + +  def run(["stats"]) do +    start_pleroma() + +    {:ok, result} = meili_get("/indexes/objects/stats") +    IO.puts("Number of entries: #{result["numberOfDocuments"]}") +    IO.puts("Indexing? #{result["isIndexing"]}") +  end +end diff --git a/lib/phoenix/transports/web_socket/raw.ex b/lib/phoenix/transports/web_socket/raw.ex index 8cf9c32a2..cf4fda79f 100644 --- a/lib/phoenix/transports/web_socket/raw.ex +++ b/lib/phoenix/transports/web_socket/raw.ex @@ -26,7 +26,6 @@ defmodule Phoenix.Transports.WebSocket.Raw do        conn        |> fetch_query_params        |> Transport.transport_log(opts[:transport_log]) -      |> Transport.force_ssl(handler, endpoint, opts)        |> Transport.check_origin(handler, endpoint, opts)      case conn do diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 3556aaf9e..8a512dc57 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -368,7 +368,7 @@ defmodule Pleroma.Activity do      )    end -  defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search +  defdelegate search(user, query, options \\ []), to: Pleroma.Search.DatabaseSearch    def direct_conversation_id(activity, for_user) do      alias Pleroma.Conversation.Participation diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index e68a3c57e..7bbc132f1 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -322,7 +322,11 @@ defmodule Pleroma.Application do    def limiters_setup do      config = Config.get(ConcurrentLimiter, []) -    [Pleroma.Web.RichMedia.Helpers, Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy] +    [ +      Pleroma.Web.RichMedia.Helpers, +      Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, +      Pleroma.Search +    ]      |> Enum.each(fn module ->        mod_config = Keyword.get(config, module, []) diff --git a/lib/pleroma/config/getting.ex b/lib/pleroma/config/getting.ex index f9b66bba6..ec93fd02a 100644 --- a/lib/pleroma/config/getting.ex +++ b/lib/pleroma/config/getting.ex @@ -5,4 +5,11 @@  defmodule Pleroma.Config.Getting do    @callback get(any()) :: any()    @callback get(any(), any()) :: any() + +  def get(key), do: get(key, nil) +  def get(key, default), do: impl().get(key, default) + +  def impl do +    Application.get_env(:pleroma, :config_impl, Pleroma.Config) +  end  end diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index aa137d250..fa5baf1a4 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -328,6 +328,52 @@ defmodule Pleroma.Object do      end    end +  def increase_quotes_count(ap_id) do +    Object +    |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id))) +    |> update([o], +      set: [ +        data: +          fragment( +            """ +            safe_jsonb_set(?, '{quotesCount}', +              (coalesce((?->>'quotesCount')::int, 0) + 1)::varchar::jsonb, true) +            """, +            o.data, +            o.data +          ) +      ] +    ) +    |> Repo.update_all([]) +    |> case do +      {1, [object]} -> set_cache(object) +      _ -> {:error, "Not found"} +    end +  end + +  def decrease_quotes_count(ap_id) do +    Object +    |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id))) +    |> update([o], +      set: [ +        data: +          fragment( +            """ +            safe_jsonb_set(?, '{quotesCount}', +              (greatest(0, (?->>'quotesCount')::int - 1))::varchar::jsonb, true) +            """, +            o.data, +            o.data +          ) +      ] +    ) +    |> Repo.update_all([]) +    |> case do +      {1, [object]} -> set_cache(object) +      _ -> {:error, "Not found"} +    end +  end +    def increase_vote_count(ap_id, name, actor) do      with %Object{} = object <- Object.normalize(ap_id, fetch: false),           "Question" <- object.data["type"] do diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex new file mode 100644 index 000000000..3b266e59b --- /dev/null +++ b/lib/pleroma/search.ex @@ -0,0 +1,17 @@ +defmodule Pleroma.Search do +  alias Pleroma.Workers.SearchIndexingWorker + +  def add_to_index(%Pleroma.Activity{id: activity_id}) do +    SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id}) +  end + +  def remove_from_index(%Pleroma.Object{id: object_id}) do +    SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id}) +  end + +  def search(query, options) do +    search_module = Pleroma.Config.get([Pleroma.Search, :module], Pleroma.Activity) + +    search_module.search(options[:for_user], query, options) +  end +end diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/search/database_search.ex index 0b9b24aa4..c6311e0c7 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/search/database_search.ex @@ -1,9 +1,10 @@  # Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>  # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Activity.Search do +defmodule Pleroma.Search.DatabaseSearch do    alias Pleroma.Activity +  alias Pleroma.Config    alias Pleroma.Object.Fetcher    alias Pleroma.Pagination    alias Pleroma.User @@ -13,8 +14,11 @@ defmodule Pleroma.Activity.Search do    import Ecto.Query +  @behaviour Pleroma.Search.SearchBackend + +  @impl true    def search(user, search_query, options \\ []) do -    index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin +    index_type = if 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) @@ -45,6 +49,12 @@ defmodule Pleroma.Activity.Search do      end    end +  @impl true +  def add_to_index(_activity), do: :ok + +  @impl true +  def remove_from_index(_object), do: :ok +    def maybe_restrict_author(query, %User{} = author) do      Activity.Queries.by_author(query, author)    end @@ -136,8 +146,8 @@ defmodule Pleroma.Activity.Search do      )    end -  defp maybe_restrict_local(q, user) do -    limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated) +  def maybe_restrict_local(q, user) do +    limit = Config.get([:instance, :limit_to_local_content], :unauthenticated)      case {limit, user} do        {:all, _} -> restrict_local(q) @@ -149,7 +159,7 @@ defmodule Pleroma.Activity.Search do    defp restrict_local(q), do: where(q, local: true) -  defp maybe_fetch(activities, user, search_query) do +  def 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"]), diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex new file mode 100644 index 000000000..2bff663e8 --- /dev/null +++ b/lib/pleroma/search/meilisearch.ex @@ -0,0 +1,181 @@ +defmodule Pleroma.Search.Meilisearch do +  require Logger +  require Pleroma.Constants + +  alias Pleroma.Activity +  alias Pleroma.Config.Getting, as: Config + +  import Pleroma.Search.DatabaseSearch +  import Ecto.Query + +  @behaviour Pleroma.Search.SearchBackend + +  defp meili_headers do +    private_key = Config.get([Pleroma.Search.Meilisearch, :private_key]) + +    [{"Content-Type", "application/json"}] ++ +      if is_nil(private_key), do: [], else: [{"Authorization", "Bearer #{private_key}"}] +  end + +  def meili_get(path) do +    endpoint = Config.get([Pleroma.Search.Meilisearch, :url]) + +    result = +      Pleroma.HTTP.get( +        Path.join(endpoint, path), +        meili_headers() +      ) + +    with {:ok, res} <- result do +      {:ok, Jason.decode!(res.body)} +    end +  end + +  def meili_post(path, params) do +    endpoint = Config.get([Pleroma.Search.Meilisearch, :url]) + +    result = +      Pleroma.HTTP.post( +        Path.join(endpoint, path), +        Jason.encode!(params), +        meili_headers() +      ) + +    with {:ok, res} <- result do +      {:ok, Jason.decode!(res.body)} +    end +  end + +  def meili_put(path, params) do +    endpoint = Config.get([Pleroma.Search.Meilisearch, :url]) + +    result = +      Pleroma.HTTP.request( +        :put, +        Path.join(endpoint, path), +        Jason.encode!(params), +        meili_headers(), +        [] +      ) + +    with {:ok, res} <- result do +      {:ok, Jason.decode!(res.body)} +    end +  end + +  def meili_delete(path) do +    endpoint = Config.get([Pleroma.Search.Meilisearch, :url]) + +    with {:ok, _} <- +           Pleroma.HTTP.request( +             :delete, +             Path.join(endpoint, path), +             "", +             meili_headers(), +             [] +           ) do +      :ok +    else +      _ -> {:error, "Could not remove from index"} +    end +  end + +  @impl true +  def search(user, query, options \\ []) do +    limit = Enum.min([Keyword.get(options, :limit), 40]) +    offset = Keyword.get(options, :offset, 0) +    author = Keyword.get(options, :author) + +    res = +      meili_post( +        "/indexes/objects/search", +        %{q: query, offset: offset, limit: limit} +      ) + +    with {:ok, result} <- res do +      hits = result["hits"] |> Enum.map(& &1["ap"]) + +      try do +        hits +        |> Activity.create_by_object_ap_id() +        |> Activity.with_preloaded_object() +        |> Activity.restrict_deactivated_users() +        |> maybe_restrict_local(user) +        |> maybe_restrict_author(author) +        |> maybe_restrict_blocked(user) +        |> maybe_fetch(user, query) +        |> order_by([object: obj], desc: obj.data["published"]) +        |> Pleroma.Repo.all() +      rescue +        _ -> maybe_fetch([], user, query) +      end +    end +  end + +  def object_to_search_data(object) do +    # Only index public or unlisted Notes +    if not is_nil(object) and object.data["type"] == "Note" and +         not is_nil(object.data["content"]) and +         (Pleroma.Constants.as_public() in object.data["to"] or +            Pleroma.Constants.as_public() in object.data["cc"]) and +         object.data["content"] not in ["", "."] do +      data = object.data + +      content_str = +        case data["content"] do +          [nil | rest] -> to_string(rest) +          str -> str +        end + +      content = +        with {:ok, scrubbed} <- +               FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing), +             trimmed <- String.trim(scrubbed) do +          trimmed +        end + +      # Make sure we have a non-empty string +      if content != "" do +        {:ok, published, _} = DateTime.from_iso8601(data["published"]) + +        %{ +          id: object.id, +          content: content, +          ap: data["id"], +          published: published |> DateTime.to_unix() +        } +      end +    end +  end + +  @impl true +  def add_to_index(activity) do +    maybe_search_data = object_to_search_data(activity.object) + +    if activity.data["type"] == "Create" and maybe_search_data do +      result = +        meili_put( +          "/indexes/objects/documents", +          [maybe_search_data] +        ) + +      with {:ok, %{"status" => "enqueued"}} <- result do +        # Added successfully +        :ok +      else +        _ -> +          # There was an error, report it +          Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}") +          {:error, result} +      end +    else +      # The post isn't something we can search, that's ok +      :ok +    end +  end + +  @impl true +  def remove_from_index(object) do +    meili_delete("/indexes/objects/documents/#{object.id}") +  end +end diff --git a/lib/pleroma/search/search_backend.ex b/lib/pleroma/search/search_backend.ex new file mode 100644 index 000000000..a42e2f5f6 --- /dev/null +++ b/lib/pleroma/search/search_backend.ex @@ -0,0 +1,24 @@ +defmodule Pleroma.Search.SearchBackend do +  @doc """ +  Search statuses with a query, restricting to only those the user should have access to. +  """ +  @callback search(user :: Pleroma.User.t(), query :: String.t(), options :: [any()]) :: [ +              Pleroma.Activity.t() +            ] + +  @doc """ +  Add the object associated with the activity to the search index. + +  The whole activity is passed, to allow filtering on things such as scope. +  """ +  @callback add_to_index(activity :: Pleroma.Activity.t()) :: :ok | {:error, any()} + +  @doc """ +  Remove the object from the index. + +  Just the object, as opposed to the whole activity, is passed, since the object +  is what contains the actual content and there is no need for fitlering when removing +  from index. +  """ +  @callback remove_from_index(object :: Pleroma.Object.t()) :: {:ok, any()} | {:error, any()} +end diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index aee41b0fe..7a8b176cd 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -136,7 +136,7 @@ defmodule Pleroma.Web do          namespace: Pleroma.Web        # Import convenience functions from controllers -      import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] +      import Phoenix.Controller, only: [get_csrf_token: 0, view_module: 1]        import Pleroma.Web.ErrorHelpers        import Pleroma.Web.Gettext diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 3979d418e..32d1a1037 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -96,6 +96,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp increase_replies_count_if_reply(_create_data), do: :noop +  defp increase_quotes_count_if_quote(%{ +         "object" => %{"quoteUrl" => quote_ap_id} = object, +         "type" => "Create" +       }) do +    if is_public?(object) do +      Object.increase_quotes_count(quote_ap_id) +    end +  end + +  defp increase_quotes_count_if_quote(_create_data), do: :noop +    @object_types ~w[ChatMessage Question Answer Audio Video Image Event Article Note Page]    @impl true    def persist(%{"type" => type} = object, meta) when type in @object_types do @@ -140,6 +151,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do          Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)        end) +      # Add local posts to search index +      if local, do: Pleroma.Search.add_to_index(activity) +        {:ok, activity}      else        %Activity{} = activity -> @@ -299,6 +313,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      with {:ok, activity} <- insert(create_data, local, fake),           {:fake, false, activity} <- {:fake, fake, activity},           _ <- increase_replies_count_if_reply(create_data), +         _ <- increase_quotes_count_if_quote(create_data),           {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},           {:ok, _actor} <- increase_note_count_if_public(actor, activity),           {:ok, _actor} <- update_last_status_at_if_public(actor, activity), @@ -1237,6 +1252,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_unauthenticated(query, _), do: query +  defp restrict_quote_url(query, %{quote_url: quote_url}) do +    from([_activity, object] in query, +      where: fragment("(?)->'quoteUrl' = ?", object.data, ^quote_url) +    ) +  end + +  defp restrict_quote_url(query, _), do: query +    defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query    defp exclude_poll_votes(query, _) do @@ -1399,6 +1422,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        |> restrict_instance(opts)        |> restrict_announce_object_actor(opts)        |> restrict_filtered(opts) +      |> restrict_quote_url(opts)        |> maybe_restrict_deactivated_users(opts)        |> exclude_poll_votes(opts)        |> exclude_chat_messages(opts) diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex index 835ed97b7..1a5d02601 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -57,6 +57,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do        field(:replies_count, :integer, default: 0)        field(:like_count, :integer, default: 0)        field(:announcement_count, :integer, default: 0) +      field(:quotes_count, :integer, default: 0)        field(:inReplyTo, ObjectValidators.ObjectID)        field(:quoteUrl, ObjectValidators.ObjectID)        field(:url, ObjectValidators.BareUri) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 098c177c7..10f268f05 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -197,6 +197,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    # - Increase replies count    # - Set up ActivityExpiration    # - Set up notifications +  # - Index incoming posts for search (if needed)    @impl true    def handle(%{data: %{"type" => "Create"}} = activity, meta) do      with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta), @@ -209,6 +210,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do          Object.increase_replies_count(in_reply_to)        end +      if quote_url = object.data["quoteUrl"] do +        Object.increase_quotes_count(quote_url) +      end +        reply_depth = (meta[:depth] || 0) + 1        # FIXME: Force inReplyTo to replies @@ -226,6 +231,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do          Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)        end) +      Pleroma.Search.add_to_index(Map.put(activity, :object, object)) +        meta =          meta          |> add_notifications(notifications) @@ -285,6 +292,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    # - Reduce the user note count    # - Reduce the reply count    # - Stream out the activity +  # - Removes posts from search index (if needed)    @impl true    def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do      deleted_object = @@ -305,6 +313,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do                Object.decrease_replies_count(in_reply_to)              end +            if quote_url = deleted_object.data["quoteUrl"] do +              Object.decrease_quotes_count(quote_url) +            end +              MessageReference.delete_for_object(deleted_object)              ap_streamer().stream_out(object) @@ -323,6 +335,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do        end      if result == :ok do +      # Only remove from index when deleting actual objects, not users or anything else +      with %Pleroma.Object{} <- deleted_object do +        Pleroma.Search.remove_from_index(deleted_object) +      end +        {:ok, object, meta}      else        {:error, result} diff --git a/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex new file mode 100644 index 000000000..6e69c5269 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaStatusOperation do +  alias OpenApiSpex.Operation +  alias Pleroma.Web.ApiSpec.Schemas.ApiError +  alias Pleroma.Web.ApiSpec.Schemas.FlakeID +  alias Pleroma.Web.ApiSpec.StatusOperation + +  import Pleroma.Web.ApiSpec.Helpers + +  def open_api_operation(action) do +    operation = String.to_existing_atom("#{action}_operation") +    apply(__MODULE__, operation, []) +  end + +  def quotes_operation do +    %Operation{ +      tags: ["Retrieve status information"], +      summary: "Quoted by", +      description: "View quotes for a given status", +      operationId: "PleromaAPI.StatusController.quotes", +      parameters: [id_param() | pagination_params()], +      security: [%{"oAuth" => ["read:statuses"]}], +      responses: %{ +        200 => +          Operation.response( +            "Array of Status", +            "application/json", +            StatusOperation.array_of_statuses() +          ), +        403 => Operation.response("Forbidden", "application/json", ApiError), +        404 => Operation.response("Not Found", "application/json", ApiError) +      } +    } +  end + +  def id_param do +    Operation.parameter(:id, :path, FlakeID, "Status ID", +      example: "9umDrYheeY451cQnEe", +      required: true +    ) +  end +end diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 07f03134a..a4052803b 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -213,6 +213,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do              type: :boolean,              description: "`true` if the quoted post is visible to the user"            }, +          quotes_count: %Schema{ +            type: :integer, +            description: "How many statuses quoted this status" +          },            local: %Schema{              type: :boolean,              description: "`true` if the post was made on the local instance" @@ -367,7 +371,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do          "in_reply_to_account_acct" => nil,          "local" => true,          "spoiler_text" => %{"text/plain" => ""}, -        "thread_muted" => false +        "thread_muted" => false, +        "quotes_count" => 0        },        "poll" => nil,        "reblog" => nil, diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 574f3ab63..65dd72c49 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -9,7 +9,20 @@ defmodule Pleroma.Web.Endpoint do    alias Pleroma.Config -  socket("/socket", Pleroma.Web.UserSocket) +  socket("/socket", Pleroma.Web.UserSocket, +    websocket: [ +      path: "/websocket", +      serializer: [ +        {Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"}, +        {Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"} +      ], +      timeout: 60_000, +      transport_log: false, +      compress: false +    ], +    longpoll: false +  ) +    socket("/live", Phoenix.LiveView.Socket)    plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint]) diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index 1a86f7a53..4a0885fab 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -17,10 +17,28 @@ defmodule Pleroma.Web.Fallback.RedirectController do      |> json(%{error: "Not implemented"})    end +  def add_generated_metadata(page_content, extra \\ "") do +    title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>" +    favicon = "<link rel='icon' href='#{Pleroma.Config.get([:instance, :favicon])}'>" +    manifest = "<link rel='manifest' href='/manifest.json'>" + +    page_content +    |> String.replace( +      "<!--server-generated-meta-->", +      title <> favicon <> manifest <> extra +    ) +  end +    def redirector(conn, _params, code \\ 200) do +    {:ok, index_content} = File.read(index_file_path()) + +    response = +      index_content +      |> add_generated_metadata() +      conn      |> put_resp_content_type("text/html") -    |> send_file(code, index_file_path()) +    |> send_resp(code, response)    end    def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do @@ -34,14 +52,12 @@ defmodule Pleroma.Web.Fallback.RedirectController do    def redirector_with_meta(conn, params) do      {:ok, index_content} = File.read(index_file_path()) -      tags = build_tags(conn, params)      preloads = preload_data(conn, params) -    title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>"      response =        index_content -      |> String.replace("<!--server-generated-meta-->", tags <> preloads <> title) +      |> add_generated_metadata(tags <> preloads)      conn      |> put_resp_content_type("text/html") @@ -55,11 +71,10 @@ defmodule Pleroma.Web.Fallback.RedirectController do    def redirector_with_preload(conn, params) do      {:ok, index_content} = File.read(index_file_path())      preloads = preload_data(conn, params) -    title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>"      response =        index_content -      |> String.replace("<!--server-generated-meta-->", preloads <> title) +      |> add_generated_metadata(preloads)      conn      |> put_resp_content_type("text/html") diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 5e6e04734..e4acba226 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -5,7 +5,6 @@  defmodule Pleroma.Web.MastodonAPI.SearchController do    use Pleroma.Web, :controller -  alias Pleroma.Activity    alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web.ControllerHelper @@ -100,7 +99,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    end    defp resource_search(_, "statuses", query, options) do -    statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end) +    statuses = with_fallback(fn -> Pleroma.Search.search(query, options) end)      StatusView.render("index.json",        activities: statuses, diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index d070262cc..e3b5760fa 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -447,7 +447,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          thread_muted: thread_muted?,          emoji_reactions: emoji_reactions,          parent_visible: visible_for_user?(reply_to, opts[:for]), -        pinned_at: pinned_at +        pinned_at: pinned_at, +        quotes_count: object.data["quotesCount"] || 0        }      }    end diff --git a/lib/pleroma/web/pleroma_api/controllers/status_controller.ex b/lib/pleroma/web/pleroma_api/controllers/status_controller.ex new file mode 100644 index 000000000..482662fdd --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/status_controller.ex @@ -0,0 +1,66 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.StatusController do +  use Pleroma.Web, :controller + +  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] + +  require Ecto.Query +  require Pleroma.Constants + +  alias Pleroma.Activity +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.ActivityPub.Visibility +  alias Pleroma.Web.MastodonAPI.StatusView +  alias Pleroma.Web.Plugs.OAuthScopesPlug + +  plug(Pleroma.Web.ApiSpec.CastAndValidate) + +  action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + +  plug( +    OAuthScopesPlug, +    %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} when action == :quotes +  ) + +  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaStatusOperation + +  @doc "GET /api/v1/pleroma/statuses/:id/quotes" +  def quotes(%{assigns: %{user: user}} = conn, %{id: id} = params) do +    with %Activity{object: object} = activity <- Activity.get_by_id_with_object(id), +         true <- Visibility.visible_for_user?(activity, user) do +      params = +        params +        |> Map.put(:type, "Create") +        |> Map.put(:blocking_user, user) +        |> Map.put(:quote_url, object.data["id"]) + +      recipients = +        if user do +          [Pleroma.Constants.as_public()] ++ [user.ap_id | User.following(user)] +        else +          [Pleroma.Constants.as_public()] +        end + +      activities = +        recipients +        |> ActivityPub.fetch_activities(params) +        |> Enum.reverse() + +      conn +      |> add_link_headers(activities) +      |> put_view(StatusView) +      |> render("index.json", +        activities: activities, +        for: user, +        as: :activity +      ) +    else +      nil -> {:error, :not_found} +      false -> {:error, :not_found} +    end +  end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6b9e158a3..eb8576b02 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -224,6 +224,12 @@ defmodule Pleroma.Web.Router do      post("/remote_interaction", UtilController, :remote_interaction)    end +  scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do +    pipe_through(:pleroma_api) + +    get("/federation_status", InstancesController, :show) +  end +    scope "/api/v1/pleroma", Pleroma.Web do      pipe_through(:pleroma_api)      post("/uploader_callback/:upload_path", UploaderController, :callback) @@ -578,6 +584,8 @@ defmodule Pleroma.Web.Router do        pipe_through(:api)        get("/accounts/:id/favourites", AccountController, :favourites)        get("/accounts/:id/endorsements", AccountController, :endorsements) + +      get("/statuses/:id/quotes", StatusController, :quotes)      end      scope [] do @@ -602,7 +610,6 @@ defmodule Pleroma.Web.Router do    scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do      pipe_through(:api)      get("/accounts/:id/scrobbles", ScrobbleController, :index) -    get("/federation_status", InstancesController, :show)    end    scope "/api/v2/pleroma", Pleroma.Web.PleromaAPI do @@ -1003,9 +1010,8 @@ defmodule Pleroma.Web.Router do      options("/*path", RedirectController, :empty)    end -  # TODO: Change to Phoenix.Router.routes/1 for Phoenix 1.6.0+    def get_api_routes do -    __MODULE__.__routes__() +    Phoenix.Router.routes(__MODULE__)      |> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end)      |> Enum.map(fn r ->        r.path diff --git a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex index e45d13bdf..e3639aae7 100644 --- a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex +++ b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex @@ -1,8 +1,8 @@ -<%= if get_flash(@conn, :info) do %> -<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p> +<%= if Phoenix.Flash.get(@flash, :info) do %> +<p class="alert alert-info" role="alert"><%= Phoenix.Flash.get(@flash, :info) %></p>  <% end %> -<%= if get_flash(@conn, :error) do %> -<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p> +<%= if Phoenix.Flash.get(@flash, :error) do %> +<p class="alert alert-danger" role="alert"><%= Phoenix.Flash.get(@flash, :error) %></p>  <% end %>  <h2><%= Gettext.dpgettext("static_pages", "mfa recover page title", "Two-factor recovery") %></h2> diff --git a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex index 50e6c04b6..f995b8805 100644 --- a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex +++ b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex @@ -1,8 +1,8 @@ -<%= if get_flash(@conn, :info) do %> -<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p> +<%= if Phoenix.Flash.get(@flash, :info) do %> +<p class="alert alert-info" role="alert"><%= Phoenix.Flash.get(@flash, :info) %></p>  <% end %> -<%= if get_flash(@conn, :error) do %> -<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p> +<%= if Phoenix.Flash.get(@flash, :error) do %> +<p class="alert alert-danger" role="alert"><%= Phoenix.Flash.get(@flash, :error) %></p>  <% end %>  <h2><%= Gettext.dpgettext("static_pages", "mfa auth page title", "Two-factor authentication") %></h2> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex index 1f661efb2..e7f65266f 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex @@ -1,8 +1,8 @@ -<%= if get_flash(@conn, :info) do %> -  <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p> +<%= if Phoenix.Flash.get(@flash, :info) do %> +  <p class="alert alert-info" role="alert"><%= Phoenix.Flash.get(@flash, :info) %></p>  <% end %> -<%= if get_flash(@conn, :error) do %> -  <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p> +<%= if Phoenix.Flash.get(@flash, :error) do %> +  <p class="alert alert-danger" role="alert"><%= Phoenix.Flash.get(@flash, :error) %></p>  <% end %>  <h2><%= Gettext.dpgettext("static_pages", "oauth register page title", "Registration Details") %></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 b3654f3eb..5b38f7142 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -1,8 +1,8 @@ -<%= if get_flash(@conn, :info) do %> -<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p> +<%= if Phoenix.Flash.get(@flash, :info) do %> +<p class="alert alert-info" role="alert"><%= Phoenix.Flash.get(@flash, :info) %></p>  <% end %> -<%= if get_flash(@conn, :error) do %> -<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p> +<%= if Phoenix.Flash.get(@flash, :error) do %> +<p class="alert alert-danger" role="alert"><%= Phoenix.Flash.get(@flash, :error) %></p>  <% end %>  <%= form_for @conn, Routes.o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index d5a24ae6c..ca8a98960 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -345,13 +345,16 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    end    def healthcheck(conn, _params) do -    with true <- Config.get([:instance, :healthcheck]), +    with {:cfg, true} <- {:cfg, Config.get([:instance, :healthcheck])},           %{healthy: true} = info <- Healthcheck.system_info() do        json(conn, info)      else        %{healthy: false} = info ->          service_unavailable(conn, info) +      {:cfg, false} -> +        service_unavailable(conn, %{"error" => "Healthcheck disabled"}) +        _ ->          service_unavailable(conn, %{})      end diff --git a/lib/pleroma/workers/search_indexing_worker.ex b/lib/pleroma/workers/search_indexing_worker.ex new file mode 100644 index 000000000..8476a2be5 --- /dev/null +++ b/lib/pleroma/workers/search_indexing_worker.ex @@ -0,0 +1,23 @@ +defmodule Pleroma.Workers.SearchIndexingWorker do +  use Pleroma.Workers.WorkerHelper, queue: "search_indexing" + +  @impl Oban.Worker + +  alias Pleroma.Config.Getting, as: Config + +  def perform(%Job{args: %{"op" => "add_to_index", "activity" => activity_id}}) do +    activity = Pleroma.Activity.get_by_id_with_object(activity_id) + +    search_module = Config.get([Pleroma.Search, :module]) + +    search_module.add_to_index(activity) +  end + +  def perform(%Job{args: %{"op" => "remove_from_index", "object" => object_id}}) do +    object = Pleroma.Object.get_by_id(object_id) + +    search_module = Config.get([Pleroma.Search, :module]) + +    search_module.remove_from_index(object) +  end +end @@ -7,7 +7,7 @@ defmodule Pleroma.Mixfile do        version: version("2.6.50"),        elixir: "~> 1.11",        elixirc_paths: elixirc_paths(Mix.env()), -      compilers: [:phoenix] ++ Mix.compilers(), +      compilers: Mix.compilers(),        elixirc_options: [warnings_as_errors: warnings_as_errors()],        xref: [exclude: [:eldap]],        start_permanent: Mix.env() == :prod, @@ -113,18 +113,19 @@ defmodule Pleroma.Mixfile do    # Type `mix help deps` for examples and options.    defp deps do      [ -      {:phoenix, "~> 1.6.0"}, -      {:phoenix_ecto, "~> 4.4.0"}, +      {:phoenix, "~> 1.7.3"}, +      {:phoenix_ecto, "~> 4.4"}, +      {:ecto_sql, "~> 3.10"},        {:ecto_enum, "~> 1.4"},        {:postgrex, ">= 0.0.0"}, -      {:phoenix_html, "~> 3.1"}, +      {:phoenix_html, "~> 3.3"},        {:phoenix_live_reload, "~> 1.3.3", only: :dev}, -      {:phoenix_live_view, "~> 0.17.1"}, -      {:phoenix_live_dashboard, "~> 0.6.2"}, -      {:telemetry_metrics, "~> 0.6.1"}, +      {:phoenix_live_view, "~> 0.19.0"}, +      {:phoenix_live_dashboard, "~> 0.8.0"}, +      {:telemetry_metrics, "~> 0.6"},        {:telemetry_poller, "~> 1.0"},        {:tzdata, "~> 1.0.3"}, -      {:plug_cowboy, "~> 2.3"}, +      {:plug_cowboy, "~> 2.5"},        # oban 2.14 requires Elixir 1.12+        {:oban, "~> 2.13.4"},        {:gettext, "~> 0.20"}, @@ -186,10 +187,12 @@ defmodule Pleroma.Mixfile do         ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"},        {:captcha,         git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", -       ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, +       ref: "90f6ce7672f70f56708792a98d98bd05176c9176"},        {:restarter, path: "./restarter"},        {:majic, "~> 1.0"}, -      {:eblurhash, "~> 1.2.2"}, +      {:eblurhash, +       git: "https://github.com/zotonic/eblurhash.git", +       ref: "bc37ceb426ef021ee9927fb249bb93f7059194ab"},        {:open_api_spex, "~> 3.16"},        {:ecto_psql_extras, "~> 0.6"}, @@ -7,7 +7,7 @@    "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},    "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"},    "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, -  "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e0f16822d578866e186a0974d65ad58cddc1e2ab", [ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"]}, +  "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "90f6ce7672f70f56708792a98d98bd05176c9176", [ref: "90f6ce7672f70f56708792a98d98bd05176c9176"]},    "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},    "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},    "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, @@ -22,16 +22,16 @@    "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},    "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},    "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, -  "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, +  "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},    "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},    "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},    "earmark": {:hex, :earmark, "1.4.22", "ea3e45c6359446dc308be0a64ce82a03260d973de7d0625a762e6d352ff57958", [:mix], [{:earmark_parser, "~> 1.4.23", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "1caf5145665a42fd76d5317286b0c171861fb1c04f86ab103dde76868814fdfb"},    "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, -  "eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"}, -  "ecto": {:hex, :ecto, "3.10.2", "6b887160281a61aa16843e47735b8a266caa437f80588c3ab80a8a960e6abe37", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6a895778f0d7648a4b34b486af59a1c8009041fbdf2b17f1ac215eb829c60235"}, +  "eblurhash": {:git, "https://github.com/zotonic/eblurhash.git", "bc37ceb426ef021ee9927fb249bb93f7059194ab", [ref: "bc37ceb426ef021ee9927fb249bb93f7059194ab"]}, +  "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},    "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, -  "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.11", "6e20144c1446dcccfcdb4c142c9d8b7992a90a569b1d5958cbea5458550b25f0", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0 or ~> 0.17.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "def61f1f92d4f40d51c80bbae2157212d6c0a459eb604be446e47369cbd40b23"}, -  "ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"}, +  "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.14", "7a20cfe913b0476542b43870e67386461258734896035e3f284039fd18bd4c4c", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "22f5f98592dd597db9416fcef00effae0787669fdcb6faf447e982b553798e98"}, +  "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"},    "eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"},    "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},    "esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"}, @@ -59,7 +59,7 @@    "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},    "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},    "inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"}, -  "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, +  "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},    "joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"},    "jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"},    "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, @@ -85,23 +85,23 @@    "open_api_spex": {:hex, :open_api_spex, "3.17.3", "ada8e352eb786050dd639db2439d3316e92f3798eb2abd051f55bb9af825b37e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "165db21a85ca83cffc8e7c8890f35b354eddda8255de7404a2848ed652b9f0fe"},    "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},    "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"}, -  "phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"}, +  "phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"},    "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"}, -  "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"}, -  "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"}, +  "phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"}, +  "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"},    "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, -  "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.14", "5ec615d4d61bf9d4755f158bd6c80372b715533fe6d6219e12d74fb5eedbeac1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "afeb6ba43ce329a6f7fc1c9acdfc6d3039995345f025febb7f409a92f6faebd3"}, +  "phoenix_live_view": {:hex, :phoenix_live_view, "0.19.5", "6e730595e8e9b8c5da230a814e557768828fd8dfeeb90377d2d8dbb52d4ec00a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2eaa0dd3cfb9bd7fb949b88217df9f25aed915e986a28ad5c8a0d054e7ca9d3"},    "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},    "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.0", "a544d83fde4a767efb78f45404a74c9e37b2a9c5ea3339692e65a6966731f935", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e88d117251e89a16b92222415a6d87b99a96747ddf674fc5c7631de734811dba"}, -  "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, -  "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, -  "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, +  "phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"}, +  "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"}, +  "plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"},    "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, -  "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, +  "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},    "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},    "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},    "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, -  "postgrex": {:hex, :postgrex, "0.17.1", "01c29fd1205940ee55f7addb8f1dc25618ca63a8817e56fac4f6846fc2cddcbe", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "14b057b488e73be2beee508fb1955d8db90d6485c6466428fe9ccf1d6692a555"}, +  "postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"},    "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"},    "prom_ex": {:hex, :prom_ex, "1.7.1", "39331ee3fe6f9a8587d8208bf9274a253bb80281700e127dd18786cda5e08c37", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.10.2", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "4c978872b88a929833925a0f4d0561824804c671fdd04581e765509ed0a6ed08"},    "prometheus": {:hex, :prometheus, "4.10.0", "792adbf0130ff61b5fa8826f013772af24b6e57b984445c8d602c8a0355704a1", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "2a99bb6dce85e238c7236fde6b0064f9834dc420ddbd962aac4ea2a3c3d59384"}, @@ -133,5 +133,7 @@    "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},    "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},    "web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"}, +  "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, +  "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"},    "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},  } diff --git a/priv/repo/migrations/20220527134341_add_quote_url_index_to_objects.exs b/priv/repo/migrations/20220527134341_add_quote_url_index_to_objects.exs new file mode 100644 index 000000000..c746f12a1 --- /dev/null +++ b/priv/repo/migrations/20220527134341_add_quote_url_index_to_objects.exs @@ -0,0 +1,11 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.AddQuoteUrlIndexToObjects do +  use Ecto.Migration + +  def change do +    create_if_not_exists(index(:objects, ["(data->'quoteUrl')"], name: :objects_quote_url)) +  end +end diff --git a/priv/scrubbers/search_indexing.ex b/priv/scrubbers/search_indexing.ex new file mode 100644 index 000000000..02756ab79 --- /dev/null +++ b/priv/scrubbers/search_indexing.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTML.Scrubber.SearchIndexing do +  @moduledoc """ +  An HTML scrubbing policy that scrubs things for searching. +  """ + +  require FastSanitize.Sanitizer.Meta +  alias FastSanitize.Sanitizer.Meta + +  # Explicitly remove mentions +  def scrub({:a, attrs, children}) do +    if(Enum.any?(attrs, fn {att, val} -> att == "class" and String.contains?(val, "mention") end), +      do: nil, +      # Strip the tag itself, leave only children (text, presumably) +      else: children +    ) +  end + +  Meta.strip_comments() +  Meta.strip_everything_not_covered() +end diff --git a/test/mix/tasks/pleroma/digest_test.exs b/test/mix/tasks/pleroma/digest_test.exs index d2a8606c7..08482aadb 100644 --- a/test/mix/tasks/pleroma/digest_test.exs +++ b/test/mix/tasks/pleroma/digest_test.exs @@ -23,6 +23,11 @@ defmodule Mix.Tasks.Pleroma.DigestTest do    setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    describe "pleroma.digest test" do      test "Sends digest to the given user" do        user1 = insert(:user) diff --git a/test/mix/tasks/pleroma/user_test.exs b/test/mix/tasks/pleroma/user_test.exs index 4fdf6912b..c9bcf2951 100644 --- a/test/mix/tasks/pleroma/user_test.exs +++ b/test/mix/tasks/pleroma/user_test.exs @@ -20,6 +20,11 @@ defmodule Mix.Tasks.Pleroma.UserTest do    import Mock    import Pleroma.Factory +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    setup_all do      Mix.shell(Mix.Shell.Process) diff --git a/test/pleroma/conversation_test.exs b/test/pleroma/conversation_test.exs index 94897e7ea..809c1951a 100644 --- a/test/pleroma/conversation_test.exs +++ b/test/pleroma/conversation_test.exs @@ -13,6 +13,11 @@ defmodule Pleroma.ConversationTest do    setup_all do: clear_config([:instance, :federating], true) +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    test "it goes through old direct conversations" do      user = insert(:user)      other_user = insert(:user) diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs index 71af9acb8..4cf14e65b 100644 --- a/test/pleroma/notification_test.exs +++ b/test/pleroma/notification_test.exs @@ -21,6 +21,11 @@ defmodule Pleroma.NotificationTest do    alias Pleroma.Web.Push    alias Pleroma.Web.Streamer +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    describe "create_notifications" do      test "never returns nil" do        user = insert(:user) diff --git a/test/pleroma/activity/search_test.exs b/test/pleroma/search/database_search_test.exs index 3b5fd2c3c..6c47ff425 100644 --- a/test/pleroma/activity/search_test.exs +++ b/test/pleroma/search/database_search_test.exs @@ -2,8 +2,8 @@  # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>  # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Activity.SearchTest do -  alias Pleroma.Activity.Search +defmodule Pleroma.Search.DatabaseSearchTest do +  alias Pleroma.Search.DatabaseSearch, as: Search    alias Pleroma.Web.CommonAPI    import Pleroma.Factory diff --git a/test/pleroma/search/meilisearch_test.exs b/test/pleroma/search/meilisearch_test.exs new file mode 100644 index 000000000..eea454323 --- /dev/null +++ b/test/pleroma/search/meilisearch_test.exs @@ -0,0 +1,160 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Search.MeilisearchTest do +  require Pleroma.Constants + +  use Pleroma.DataCase, async: true +  use Oban.Testing, repo: Pleroma.Repo + +  import Pleroma.Factory +  import Tesla.Mock +  import Mox + +  alias Pleroma.Search.Meilisearch +  alias Pleroma.UnstubbedConfigMock, as: Config +  alias Pleroma.Web.CommonAPI +  alias Pleroma.Workers.SearchIndexingWorker + +  describe "meilisearch" do +    test "indexes a local post on creation" do +      user = insert(:user) + +      Tesla.Mock.mock(fn +        %{ +          method: :put, +          url: "http://127.0.0.1:7700/indexes/objects/documents", +          body: body +        } -> +          assert match?( +                   [%{"content" => "guys i just don't wanna leave the swamp"}], +                   Jason.decode!(body) +                 ) + +          # To make sure that the worker is called +          send(self(), "posted_to_meilisearch") + +          %{ +            "enqueuedAt" => "2023-11-12T12:36:46.927517Z", +            "indexUid" => "objects", +            "status" => "enqueued", +            "taskUid" => 6, +            "type" => "documentAdditionOrUpdate" +          } +          |> json() +      end) + +      Config +      |> expect(:get, 3, fn +        [Pleroma.Search, :module], nil -> +          Meilisearch + +        [Pleroma.Search.Meilisearch, :url], nil -> +          "http://127.0.0.1:7700" + +        [Pleroma.Search.Meilisearch, :private_key], nil -> +          "secret" +      end) + +      {:ok, activity} = +        CommonAPI.post(user, %{ +          status: "guys i just don't wanna leave the swamp", +          visibility: "public" +        }) + +      args = %{"op" => "add_to_index", "activity" => activity.id} + +      assert_enqueued( +        worker: SearchIndexingWorker, +        args: args +      ) + +      assert :ok = perform_job(SearchIndexingWorker, args) +      assert_received("posted_to_meilisearch") +    end + +    test "doesn't index posts that are not public" do +      user = insert(:user) + +      Enum.each(["private", "direct"], fn visibility -> +        {:ok, activity} = +          CommonAPI.post(user, %{ +            status: "guys i just don't wanna leave the swamp", +            visibility: visibility +          }) + +        args = %{"op" => "add_to_index", "activity" => activity.id} + +        Config +        |> expect(:get, fn +          [Pleroma.Search, :module], nil -> +            Meilisearch +        end) + +        assert_enqueued(worker: SearchIndexingWorker, args: args) +        assert :ok = perform_job(SearchIndexingWorker, args) +      end) +    end + +    test "deletes posts from index when deleted locally" do +      user = insert(:user) + +      Tesla.Mock.mock(fn +        %{ +          method: :put, +          url: "http://127.0.0.1:7700/indexes/objects/documents", +          body: body +        } -> +          assert match?( +                   [%{"content" => "guys i just don't wanna leave the swamp"}], +                   Jason.decode!(body) +                 ) + +          %{ +            "enqueuedAt" => "2023-11-12T12:36:46.927517Z", +            "indexUid" => "objects", +            "status" => "enqueued", +            "taskUid" => 6, +            "type" => "documentAdditionOrUpdate" +          } +          |> json() + +        %{method: :delete, url: "http://127.0.0.1:7700/indexes/objects/documents/" <> id} -> +          send(self(), "called_delete") +          assert String.length(id) > 1 +          json(%{}) +      end) + +      Config +      |> expect(:get, 6, fn +        [Pleroma.Search, :module], nil -> +          Meilisearch + +        [Pleroma.Search.Meilisearch, :url], nil -> +          "http://127.0.0.1:7700" + +        [Pleroma.Search.Meilisearch, :private_key], nil -> +          "secret" +      end) + +      {:ok, activity} = +        CommonAPI.post(user, %{ +          status: "guys i just don't wanna leave the swamp", +          visibility: "public" +        }) + +      args = %{"op" => "add_to_index", "activity" => activity.id} +      assert_enqueued(worker: SearchIndexingWorker, args: args) +      assert :ok = perform_job(SearchIndexingWorker, args) + +      {:ok, _} = CommonAPI.delete(activity.id, user) + +      delete_args = %{"op" => "remove_from_index", "object" => activity.object.id} +      assert_enqueued(worker: SearchIndexingWorker, args: delete_args) +      assert :ok = perform_job(SearchIndexingWorker, delete_args) + +      assert_received("called_delete") +    end +  end +end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 7f60b959a..b9df527a0 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -19,6 +19,11 @@ defmodule Pleroma.UserTest do    import ExUnit.CaptureLog    import Swoosh.TestAssertions +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    setup_all do      Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)      :ok diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 62eb9b5a3..0dc61c2e5 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -25,6 +25,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do    require Pleroma.Constants +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    setup_all do      Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)      :ok diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 1e8c14043..40482fef0 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -770,6 +770,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do        assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id)        assert object.data["repliesCount"] == 2      end + +    test "increates quotes count", %{user: user} do +      user2 = insert(:user) + +      {:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"}) +      ap_id = activity.data["id"] +      quote_data = %{status: "1", quote_id: activity.id} + +      # public +      {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "public")) +      assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) +      assert object.data["quotesCount"] == 1 + +      # unlisted +      {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "unlisted")) +      assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) +      assert object.data["quotesCount"] == 2 + +      # private +      {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "private")) +      assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) +      assert object.data["quotesCount"] == 2 + +      # direct +      {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "direct")) +      assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) +      assert object.data["quotesCount"] == 2 +    end    end    describe "fetch activities for recipients" do diff --git a/test/pleroma/web/admin_api/controllers/user_controller_test.exs b/test/pleroma/web/admin_api/controllers/user_controller_test.exs index bb9dcb4aa..8edfda54c 100644 --- a/test/pleroma/web/admin_api/controllers/user_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/user_controller_test.exs @@ -19,6 +19,11 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do    alias Pleroma.Web.Endpoint    alias Pleroma.Web.MediaProxy +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    setup_all do      Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) diff --git a/test/pleroma/web/fallback_test.exs b/test/pleroma/web/fallback_test.exs index 6d11d4f37..ed34d6490 100644 --- a/test/pleroma/web/fallback_test.exs +++ b/test/pleroma/web/fallback_test.exs @@ -6,20 +6,6 @@ defmodule Pleroma.Web.FallbackTest do    use Pleroma.Web.ConnCase    import Pleroma.Factory -  describe "neither preloaded data nor metadata attached to" do -    test "GET /registration/:token", %{conn: conn} do -      response = get(conn, "/registration/foo") - -      assert html_response(response, 200) =~ "<!--server-generated-meta-->" -    end - -    test "GET /*path", %{conn: conn} do -      assert conn -             |> get("/foo") -             |> html_response(200) =~ "<!--server-generated-meta-->" -    end -  end -    test "GET /*path adds a title", %{conn: conn} do      clear_config([:instance, :name], "a cool title") @@ -29,21 +15,28 @@ defmodule Pleroma.Web.FallbackTest do    end    describe "preloaded data and metadata attached to" do -    test "GET /:maybe_nickname_or_id", %{conn: conn} do +    test "GET /:maybe_nickname_or_id with existing user", %{conn: conn} do        clear_config([:instance, :name], "a cool title") -        user = insert(:user) -      user_missing = get(conn, "/foo") -      user_present = get(conn, "/#{user.nickname}") -      assert html_response(user_missing, 200) =~ "<!--server-generated-meta-->" -      refute html_response(user_present, 200) =~ "<!--server-generated-meta-->" -      assert html_response(user_present, 200) =~ "initial-results" -      assert html_response(user_present, 200) =~ "<title>a cool title</title>" +      resp = get(conn, "/#{user.nickname}") + +      assert html_response(resp, 200) =~ "<title>a cool title</title>" +      refute html_response(resp, 200) =~ "<!--server-generated-meta-->" +      assert html_response(resp, 200) =~ "initial-results" +    end + +    test "GET /:maybe_nickname_or_id with missing user", %{conn: conn} do +      clear_config([:instance, :name], "a cool title") + +      resp = get(conn, "/foo") + +      assert html_response(resp, 200) =~ "<title>a cool title</title>" +      refute html_response(resp, 200) =~ "initial-results"      end      test "GET /*path", %{conn: conn} do -      assert conn +      refute conn               |> get("/foo")               |> html_response(200) =~ "<!--server-generated-meta-->" @@ -65,10 +58,12 @@ defmodule Pleroma.Web.FallbackTest do      end      test "GET /main/all", %{conn: conn} do +      clear_config([:instance, :name], "a cool title")        public_page = get(conn, "/main/all")        refute html_response(public_page, 200) =~ "<!--server-generated-meta-->"        assert html_response(public_page, 200) =~ "initial-results" +      assert html_response(public_page, 200) =~ "<title>a cool title</title>"      end    end diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 128e60b0a..d8e5f9d39 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -18,6 +18,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do    import Pleroma.Factory +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    describe "account fetching" do      test "works by id" do        %User{id: user_id} = insert(:user) diff --git a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs index 1524df98f..350b935d7 100644 --- a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs @@ -12,6 +12,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do    import Pleroma.Factory +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    test "does NOT render account/pleroma/relationship by default" do      %{user: user, conn: conn} = oauth_access(["read:notifications"])      other_user = insert(:user) diff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs index 0a9240b70..19dee25d7 100644 --- a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs @@ -13,6 +13,11 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do    import Tesla.Mock    import Mock +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    setup_all do      mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)      :ok diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index de3b52e26..76c092529 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -27,6 +27,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do    setup do: clear_config([:mrf, :policies])    setup do: clear_config([:mrf_keyword, :reject]) +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    describe "posting statuses" do      setup do: oauth_access(["write:statuses"]) @@ -37,7 +42,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do        response =          conn          |> put_req_header("content-type", "application/json") -        |> post("api/v1/statuses", %{ +        |> post("/api/v1/statuses", %{            "content_type" => "text/plain",            "source" => "Pleroma FE",            "status" => "Hello world", @@ -50,7 +55,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do        response =          conn -        |> get("api/v1/statuses/#{response["id"]}", %{}) +        |> get("/api/v1/statuses/#{response["id"]}", %{})          |> json_response_and_validate_schema(200)        assert response["reblogs_count"] == 0 @@ -109,7 +114,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do        conn_four =          conn          |> put_req_header("content-type", "application/json") -        |> post("api/v1/statuses", %{ +        |> post("/api/v1/statuses", %{            "status" => "oolong",            "expires_in" => expires_in          }) @@ -156,7 +161,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do        assert %{"error" => "Expiry date is too soon"} =                 conn                 |> put_req_header("content-type", "application/json") -               |> post("api/v1/statuses", %{ +               |> post("/api/v1/statuses", %{                   "status" => "oolong",                   "expires_in" => expires_in                 }) @@ -168,7 +173,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do        assert %{"error" => "Expiry date is too soon"} =                 conn                 |> put_req_header("content-type", "application/json") -               |> post("api/v1/statuses", %{ +               |> post("/api/v1/statuses", %{                   "status" => "oolong",                   "expires_in" => expires_in                 }) @@ -182,7 +187,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do        assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} =                 conn                 |> put_req_header("content-type", "application/json") -               |> post("api/v1/statuses", %{"status" => "GNO/Linux"}) +               |> post("/api/v1/statuses", %{"status" => "GNO/Linux"})                 |> json_response_and_validate_schema(422)      end @@ -375,7 +380,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do        conn =          conn          |> put_req_header("content-type", "application/json") -        |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"}) +        |> post("/api/v1/statuses", %{"status" => content, "visibility" => "direct"})        assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200)        assert response["visibility"] == "direct" @@ -412,7 +417,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do        result =          conn -        |> get("api/v1/statuses/#{activity}") +        |> get("/api/v1/statuses/#{activity}")        assert %{                 "content" => "cofe is my copilot", @@ -441,7 +446,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do        result =          conn -        |> get("api/v1/statuses/#{activity}") +        |> get("/api/v1/statuses/#{activity}")        assert %{                 "content" => "club mate is my wingman", @@ -1644,7 +1649,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do        assert %{"id" => id} =                 conn                 |> put_req_header("content-type", "application/json") -               |> post("api/v1/statuses", %{ +               |> post("/api/v1/statuses", %{                   "status" => "oolong",                   "expires_in" => expires_in                 }) @@ -1894,7 +1899,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do        conn        |> assign(:user, user3)        |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) -      |> get("api/v1/timelines/home") +      |> get("/api/v1/timelines/home")      [reblogged_activity] = json_response_and_validate_schema(conn3, 200) diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index b13a8033b..c120dd53c 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -527,7 +527,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do          |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"]))        # Only direct should be visible here -      res_conn = get(conn_user_two, "api/v1/timelines/direct") +      res_conn = get(conn_user_two, "/api/v1/timelines/direct")        assert [status] = json_response_and_validate_schema(res_conn, :ok) @@ -539,14 +539,14 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do          build_conn()          |> assign(:user, user_one)          |> assign(:token, insert(:oauth_token, user: user_one, scopes: ["read:statuses"])) -        |> get("api/v1/timelines/direct") +        |> get("/api/v1/timelines/direct")        [status] = json_response_and_validate_schema(res_conn, :ok)        assert %{"visibility" => "direct"} = status        # Both should be visible here -      res_conn = get(conn_user_two, "api/v1/timelines/home") +      res_conn = get(conn_user_two, "/api/v1/timelines/home")        [_s1, _s2] = json_response_and_validate_schema(res_conn, :ok) @@ -559,14 +559,14 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do            })        end) -      res_conn = get(conn_user_two, "api/v1/timelines/direct") +      res_conn = get(conn_user_two, "/api/v1/timelines/direct")        statuses = json_response_and_validate_schema(res_conn, :ok)        assert length(statuses) == 20        max_id = List.last(statuses)["id"] -      res_conn = get(conn_user_two, "api/v1/timelines/direct?max_id=#{max_id}") +      res_conn = get(conn_user_two, "/api/v1/timelines/direct?max_id=#{max_id}")        assert [status] = json_response_and_validate_schema(res_conn, :ok) @@ -591,7 +591,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do            visibility: "direct"          }) -      res_conn = get(conn, "api/v1/timelines/direct") +      res_conn = get(conn, "/api/v1/timelines/direct")        [status] = json_response_and_validate_schema(res_conn, :ok)        assert status["id"] == direct.id diff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs index ddbe4557f..47425d2a9 100644 --- a/test/pleroma/web/mastodon_api/views/notification_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/notification_view_test.exs @@ -22,6 +22,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do    alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView    import Pleroma.Factory +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    defp test_notifications_rendering(notifications, user, expected_result) do      result = NotificationView.render("index.json", %{notifications: notifications, for: user}) diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index baa9b32f5..57b81798d 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -337,7 +337,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do          thread_muted: false,          emoji_reactions: [],          parent_visible: false, -        pinned_at: nil +        pinned_at: nil, +        quotes_count: 0        }      } diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs index f41d6a322..83a08d9fc 100644 --- a/test/pleroma/web/o_auth/o_auth_controller_test.exs +++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs @@ -186,7 +186,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do        assert html_response(conn, 302)        assert redirected_to(conn) == app.redirect_uris -      assert get_flash(conn, :error) == "Failed to authenticate: (error description)." +      assert conn.assigns.flash["error"] == "Failed to authenticate: (error description)."      end      test "GET /oauth/registration_details renders registration details form", %{ @@ -307,7 +307,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do            |> post("/oauth/register", bad_params)          assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/ -        assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken." +        assert conn.assigns.flash["error"] == "Error: #{bad_param} has already been taken."        end      end @@ -398,7 +398,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do          |> post("/oauth/register", params)        assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ -      assert get_flash(conn, :error) == "Invalid Username/Password" +      assert conn.assigns.flash["error"] == "Invalid Username/Password"      end    end diff --git a/test/pleroma/web/o_status/o_status_controller_test.exs b/test/pleroma/web/o_status/o_status_controller_test.exs index 36e581f5e..3e8fcd956 100644 --- a/test/pleroma/web/o_status/o_status_controller_test.exs +++ b/test/pleroma/web/o_status/o_status_controller_test.exs @@ -196,7 +196,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do          |> get("/notice/#{like_activity.id}")          |> response(200) -      assert resp =~ "<!--server-generated-meta-->" +      refute resp =~ ~r(<meta content="[^"]*" property="og:url")      end      test "404s a private notice", %{conn: conn} do diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs index 21e7d4839..8c2dcc1bb 100644 --- a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs @@ -13,6 +13,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do    import Pleroma.Factory +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do      user = insert(:user)      other_user = insert(:user) diff --git a/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs index 365d26ab1..02afeda67 100644 --- a/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs @@ -26,6 +26,8 @@ defmodule Pleroma.Web.PleromaApi.InstancesControllerTest do      constant_unreachable: constant_unreachable,      constant: constant    } do +    clear_config([:instance, :public], false) +      constant_host = URI.parse(constant).host      assert conn diff --git a/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs new file mode 100644 index 000000000..f942f0556 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.StatusControllerTest do +  use Pleroma.Web.ConnCase + +  alias Pleroma.Web.CommonAPI + +  import Pleroma.Factory + +  describe "getting quotes of a specified post" do +    setup do +      [current_user, user] = insert_pair(:user) +      %{user: current_user, conn: conn} = oauth_access(["read:statuses"], user: current_user) +      [current_user: current_user, user: user, conn: conn] +    end + +    test "shows quotes of a post", %{conn: conn} do +      user = insert(:user) +      activity = insert(:note_activity) + +      {:ok, quote_post} = CommonAPI.post(user, %{status: "quoat", quote_id: activity.id}) + +      response = +        conn +        |> get("/api/v1/pleroma/statuses/#{activity.id}/quotes") +        |> json_response_and_validate_schema(:ok) + +      [status] = response + +      assert length(response) == 1 +      assert status["id"] == quote_post.id +    end + +    test "returns 404 error when a post can't be seen", %{conn: conn} do +      activity = insert(:direct_note_activity) + +      response = +        conn +        |> get("/api/v1/pleroma/statuses/#{activity.id}/quotes") + +      assert json_response_and_validate_schema(response, 404) == %{"error" => "Record not found"} +    end + +    test "returns 404 error when a post does not exist", %{conn: conn} do +      response = +        conn +        |> get("/api/v1/pleroma/statuses/idontexist/quotes") + +      assert json_response_and_validate_schema(response, 404) == %{"error" => "Record not found"} +    end +  end +end diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/twitter_api/util_controller_test.exs index a4da23635..d06ae71aa 100644 --- a/test/pleroma/web/twitter_api/util_controller_test.exs +++ b/test/pleroma/web/twitter_api/util_controller_test.exs @@ -106,7 +106,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do          |> get("/api/pleroma/healthcheck")          |> json_response_and_validate_schema(503) -      assert response == %{} +      assert response == %{"error" => "Healthcheck disabled"}      end      test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do diff --git a/test/pleroma/workers/cron/digest_emails_worker_test.exs b/test/pleroma/workers/cron/digest_emails_worker_test.exs index 851f4d63a..e0bdf303e 100644 --- a/test/pleroma/workers/cron/digest_emails_worker_test.exs +++ b/test/pleroma/workers/cron/digest_emails_worker_test.exs @@ -14,6 +14,11 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorkerTest do    setup do: clear_config([:email_notifications, :digest])    setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end + +  setup do      clear_config([:email_notifications, :digest], %{        active: true,        inactivity_threshold: 7, diff --git a/test/pleroma/workers/cron/new_users_digest_worker_test.exs b/test/pleroma/workers/cron/new_users_digest_worker_test.exs index 84914876c..0e4234cc8 100644 --- a/test/pleroma/workers/cron/new_users_digest_worker_test.exs +++ b/test/pleroma/workers/cron/new_users_digest_worker_test.exs @@ -10,6 +10,11 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorkerTest do    alias Pleroma.Web.CommonAPI    alias Pleroma.Workers.Cron.NewUsersDigestWorker +  setup do +    Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) +    :ok +  end +    test "it sends new users digest emails" do      yesterday = NaiveDateTime.utc_now() |> Timex.shift(days: -1)      admin = insert(:user, %{is_admin: true}) diff --git a/test/support/mocks.ex b/test/support/mocks.ex index d167996bd..9693095ba 100644 --- a/test/support/mocks.ex +++ b/test/support/mocks.ex @@ -26,5 +26,6 @@ Mox.defmock(Pleroma.Web.ActivityPub.SideEffectsMock,  Mox.defmock(Pleroma.Web.FederatorMock, for: Pleroma.Web.Federator.Publishing)  Mox.defmock(Pleroma.ConfigMock, for: Pleroma.Config.Getting) +Mox.defmock(Pleroma.UnstubbedConfigMock, for: Pleroma.Config.Getting)  Mox.defmock(Pleroma.LoggerMock, for: Pleroma.Logging) | 
