diff options
Diffstat (limited to 'lib')
152 files changed, 3431 insertions, 932 deletions
| diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 33d147d36..3a2ea44f8 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -304,13 +304,8 @@ defmodule Mix.Tasks.Pleroma.Config do      System.cmd("mix", ["format", path])    end -  if Code.ensure_loaded?(Config.Reader) do -    defp config_header, do: "import Config\r\n\r\n" -    defp read_file(config_file), do: Config.Reader.read_imports!(config_file) -  else -    defp config_header, do: "use Mix.Config\r\n\r\n" -    defp read_file(config_file), do: Mix.Config.eval!(config_file) -  end +  defp config_header, do: "import Config\r\n\r\n" +  defp read_file(config_file), do: Config.Reader.read_imports!(config_file)    defp write_and_delete(config, file, delete?) do      config diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 6b8f0ef68..ed560c177 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -154,9 +154,8 @@ defmodule Mix.Tasks.Pleroma.Database do      |> join(:inner, [a], o in Object,        on:          fragment( -          "(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')", +          "(?->>'id') = associated_object_id((?))",            o.data, -          a.data,            a.data          )      ) diff --git a/lib/mix/tasks/pleroma/openapi_spec.ex b/lib/mix/tasks/pleroma/openapi_spec.ex index 884f931f8..1ea468476 100644 --- a/lib/mix/tasks/pleroma/openapi_spec.ex +++ b/lib/mix/tasks/pleroma/openapi_spec.ex @@ -6,7 +6,70 @@ defmodule Mix.Tasks.Pleroma.OpenapiSpec do    def run([path]) do      # Load Pleroma application to get version info      Application.load(:pleroma) -    spec = Pleroma.Web.ApiSpec.spec(server_specific: false) |> Jason.encode!() -    File.write(path, spec) + +    spec_json = Pleroma.Web.ApiSpec.spec(server_specific: false) |> Jason.encode!() +    # to get rid of the structs +    spec_regened = spec_json |> Jason.decode!() + +    check_specs!(spec_regened) + +    File.write(path, spec_json) +  end + +  defp check_specs!(spec) do +    with :ok <- check_specs(spec) do +      :ok +    else +      {_, errors} -> +        IO.puts(IO.ANSI.format([:red, :bright, "Spec check failed, errors:"])) +        Enum.map(errors, &IO.puts/1) + +        raise "Spec check failed" +    end +  end + +  def check_specs(spec) do +    errors = +      spec["paths"] +      |> Enum.flat_map(fn {path, %{} = endpoints} -> +        Enum.map( +          endpoints, +          fn {method, endpoint} -> +            with :ok <- check_endpoint(spec, endpoint) do +              :ok +            else +              error -> +                "#{endpoint["operationId"]} (#{method} #{path}): #{error}" +            end +          end +        ) +        |> Enum.reject(fn res -> res == :ok end) +      end) + +    if errors == [] do +      :ok +    else +      {:error, errors} +    end +  end + +  defp check_endpoint(spec, endpoint) do +    valid_tags = available_tags(spec) + +    with {_, [_ | _] = tags} <- {:tags, endpoint["tags"]}, +         {_, []} <- {:unavailable, Enum.reject(tags, &(&1 in valid_tags))} do +      :ok +    else +      {:tags, _} -> +        "No tags specified" + +      {:unavailable, tags} -> +        "Tags #{inspect(tags)} not available. Please add it in \"x-tagGroups\" in Pleroma.Web.ApiSpec" +    end +  end + +  defp available_tags(spec) do +    spec["x-tagGroups"] +    |> Enum.flat_map(fn %{"tags" => tags} -> tags end)    end  end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 50ffb7f27..929fa1717 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -112,9 +112,10 @@ defmodule Mix.Tasks.Pleroma.User do           {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do        shell_info("Generated password reset token for #{user.nickname}") -      IO.puts("URL: #{Pleroma.Web.Router.Helpers.reset_password_url(Pleroma.Web.Endpoint, -      :reset, -      token.token)}") +      url = +        Pleroma.Web.Router.Helpers.reset_password_url(Pleroma.Web.Endpoint, :reset, token.token) + +      IO.puts("URL: #{url}")      else        _ ->          shell_error("No local user #{nickname}") diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 12c1a3b2e..3556aaf9e 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -53,7 +53,7 @@ defmodule Pleroma.Activity do      #      # ```      # |> join(:inner, [activity], o in Object, -    #      on: fragment("(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')", +    #      on: fragment("(?->>'id') = associated_object_id((?))",      #        o.data, activity.data, activity.data))      # |> preload([activity, object], [object: object])      # ``` @@ -69,9 +69,8 @@ defmodule Pleroma.Activity do      join(query, join_type, [activity], o in Object,        on:          fragment( -          "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", +          "(?->>'id') = associated_object_id(?)",            o.data, -          activity.data,            activity.data          ),        as: :object @@ -362,9 +361,11 @@ defmodule Pleroma.Activity do    end    def restrict_deactivated_users(query) do -    deactivated_users_query = from(u in User.Query.build(%{deactivated: true}), select: u.ap_id) - -    from(activity in query, where: activity.actor not in subquery(deactivated_users_query)) +    query +    |> join(:inner, [activity], user in User, +      as: :user, +      on: activity.actor == user.ap_id and user.is_active == true +    )    end    defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search diff --git a/lib/pleroma/activity/html.ex b/lib/pleroma/activity/html.ex index 071a89c8d..706b2d36c 100644 --- a/lib/pleroma/activity/html.ex +++ b/lib/pleroma/activity/html.ex @@ -8,6 +8,40 @@ defmodule Pleroma.Activity.HTML do    @cachex Pleroma.Config.get([:cachex, :provider], Cachex) +  # We store a list of cache keys related to an activity in a +  # separate cache, scrubber_management_cache. It has the same +  # size as scrubber_cache (see application.ex). Every time we add +  # a cache to scrubber_cache, we update scrubber_management_cache. +  # +  # The most recent write of a certain key in the management cache +  # is the same as the most recent write of any record related to that +  # key in the main cache. +  # Assuming LRW ( https://hexdocs.pm/cachex/Cachex.Policy.LRW.html ), +  # this means when the management cache is evicted by cachex, all +  # related records in the main cache will also have been evicted. + +  defp get_cache_keys_for(activity_id) do +    with {:ok, list} when is_list(list) <- @cachex.get(:scrubber_management_cache, activity_id) do +      list +    else +      _ -> [] +    end +  end + +  defp add_cache_key_for(activity_id, additional_key) do +    current = get_cache_keys_for(activity_id) + +    unless additional_key in current do +      @cachex.put(:scrubber_management_cache, activity_id, [additional_key | current]) +    end +  end + +  def invalidate_cache_for(activity_id) do +    keys = get_cache_keys_for(activity_id) +    Enum.map(keys, &@cachex.del(:scrubber_cache, &1)) +    @cachex.del(:scrubber_management_cache, activity_id) +  end +    def get_cached_scrubbed_html_for_activity(          content,          scrubbers, @@ -19,6 +53,8 @@ defmodule Pleroma.Activity.HTML do      @cachex.fetch!(:scrubber_cache, key, fn _key ->        object = Object.normalize(activity, fetch: false) + +      add_cache_key_for(activity.id, key)        HTML.ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)      end)    end diff --git a/lib/pleroma/activity/ir/topics.ex b/lib/pleroma/activity/ir/topics.ex index 56c52e9d1..8249cbe27 100644 --- a/lib/pleroma/activity/ir/topics.ex +++ b/lib/pleroma/activity/ir/topics.ex @@ -13,6 +13,14 @@ defmodule Pleroma.Activity.Ir.Topics do      |> List.flatten()    end +  defp generate_topics(%{data: %{"type" => "ChatMessage"}}, %{data: %{"type" => "Delete"}}) do +    ["user", "user:pleroma_chat"] +  end + +  defp generate_topics(%{data: %{"type" => "ChatMessage"}}, %{data: %{"type" => "Create"}}) do +    [] +  end +    defp generate_topics(%{data: %{"type" => "Answer"}}, _) do      []    end @@ -21,7 +29,7 @@ defmodule Pleroma.Activity.Ir.Topics do      ["user", "list"] ++ visibility_tags(object, activity)    end -  defp visibility_tags(object, activity) do +  defp visibility_tags(object, %{data: %{"type" => type}} = activity) when type != "Announce" do      case Visibility.get_visibility(activity) do        "public" ->          if activity.local do @@ -31,6 +39,10 @@ defmodule Pleroma.Activity.Ir.Topics do          end          |> item_creation_tags(object, activity) +      "local" -> +        ["public:local"] +        |> item_creation_tags(object, activity) +        "direct" ->          ["direct"] @@ -39,6 +51,10 @@ defmodule Pleroma.Activity.Ir.Topics do      end    end +  defp visibility_tags(_object, _activity) do +    [] +  end +    defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do      tags ++        remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity) @@ -63,7 +79,18 @@ defmodule Pleroma.Activity.Ir.Topics do    defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: [] -  defp attachment_topics(_object, %{local: true}), do: ["public:media", "public:local:media"] +  defp attachment_topics(_object, %{local: true} = activity) do +    case Visibility.get_visibility(activity) do +      "public" -> +        ["public:media", "public:local:media"] + +      "local" -> +        ["public:local:media"] + +      _ -> +        [] +    end +  end    defp attachment_topics(_object, %{actor: actor}) when is_binary(actor),      do: ["public:media", "public:remote:media:" <> URI.parse(actor).host] diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex index a898b2ea7..81c44ac05 100644 --- a/lib/pleroma/activity/queries.ex +++ b/lib/pleroma/activity/queries.ex @@ -52,8 +52,7 @@ defmodule Pleroma.Activity.Queries do        activity in query,        where:          fragment( -          "coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)", -          activity.data, +          "associated_object_id((?)) = ANY(?)",            activity.data,            ^object_ids          ) @@ -64,8 +63,7 @@ defmodule Pleroma.Activity.Queries do      from(activity in query,        where:          fragment( -          "coalesce((?)->'object'->>'id', (?)->>'object') = ?", -          activity.data, +          "associated_object_id((?)) = ?",            activity.data,            ^object_id          ) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index ae3ef9738..e68a3c57e 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -94,7 +94,8 @@ defmodule Pleroma.Application do          Pleroma.Repo,          Config.TransferTask,          Pleroma.Emoji, -        Pleroma.Web.Plugs.RateLimiter.Supervisor +        Pleroma.Web.Plugs.RateLimiter.Supervisor, +        {Task.Supervisor, name: Pleroma.TaskSupervisor}        ] ++          cachex_children() ++          http_children(adapter, @mix_env) ++ @@ -199,6 +200,7 @@ defmodule Pleroma.Application do        build_cachex("object", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),        build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),        build_cachex("scrubber", limit: 2500), +      build_cachex("scrubber_management", limit: 2500),        build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),        build_cachex("web_resp", limit: 2500),        build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10), @@ -207,7 +209,8 @@ defmodule Pleroma.Application do        build_cachex("chat_message_id_idempotency_key",          expiration: chat_message_id_idempotency_key_expiration(),          limit: 500_000 -      ) +      ), +      build_cachex("rel_me", limit: 2500)      ]    end @@ -248,7 +251,8 @@ defmodule Pleroma.Application do    defp background_migrators do      [ -      Pleroma.Migrators.HashtagsTableMigrator +      Pleroma.Migrators.HashtagsTableMigrator, +      Pleroma.Migrators.ContextObjectsDeletionMigrator      ]    end diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex index a3b623bdf..27799338f 100644 --- a/lib/pleroma/bbs/handler.ex +++ b/lib/pleroma/bbs/handler.ex @@ -42,8 +42,45 @@ defmodule Pleroma.BBS.Handler do    def puts_activity(activity) do      status = Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{activity: activity}) +      IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})") -    IO.puts(HTML.strip_tags(status.content)) + +    status.content +    |> String.split("<br/>") +    |> Enum.map(&HTML.strip_tags/1) +    |> Enum.map(&HtmlEntities.decode/1) +    |> Enum.map(&IO.puts/1) +  end + +  def puts_notification(activity, user) do +    notification = +      Pleroma.Web.MastodonAPI.NotificationView.render("show.json", %{ +        notification: activity, +        for: user +      }) + +    IO.puts( +      "== (#{notification.type}) #{notification.status.id} by #{notification.account.display_name} (#{notification.account.acct})" +    ) + +    notification.status.content +    |> String.split("<br/>") +    |> Enum.map(&HTML.strip_tags/1) +    |> Enum.map(&HtmlEntities.decode/1) +    |> (fn x -> +          case x do +            [content] -> +              "> " <> content + +            [head | _tail] -> +              # "> " <> hd <> "..." +              head +              |> String.slice(1, 80) +              |> (fn x -> "> " <> x <> "..." end).() +          end +        end).() +    |> IO.puts() +      IO.puts("")    end @@ -53,6 +90,11 @@ defmodule Pleroma.BBS.Handler do      IO.puts("home - Show the home timeline")      IO.puts("p <text> - Post the given text")      IO.puts("r <id> <text> - Reply to the post with the given id") +    IO.puts("t <id> - Show a thread from the given id") +    IO.puts("n - Show notifications") +    IO.puts("n read - Mark all notifactions as read") +    IO.puts("f <id> - Favourites the post with the given id") +    IO.puts("R <id> - Repeat the post with the given id")      IO.puts("quit - Quit")      state @@ -73,11 +115,53 @@ defmodule Pleroma.BBS.Handler do      state    end +  def handle_command(%{user: user} = state, "t " <> activity_id) do +    with %Activity{} = activity <- Activity.get_by_id(activity_id) do +      activities = +        ActivityPub.fetch_activities_for_context(activity.data["context"], %{ +          blocking_user: user, +          user: user, +          exclude_id: activity.id +        }) + +      case activities do +        [] -> +          activity_id +          |> Activity.get_by_id() +          |> puts_activity() + +        _ -> +          activities +          |> Enum.reverse() +          |> Enum.each(&puts_activity/1) +      end +    else +      _e -> IO.puts("Could not show this thread...") +    end + +    state +  end + +  def handle_command(%{user: user} = state, "n read") do +    Pleroma.Notification.clear(user) +    IO.puts("All notifications were marked as read") + +    state +  end + +  def handle_command(%{user: user} = state, "n") do +    user +    |> Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(%{}) +    |> Enum.each(&puts_notification(&1, user)) + +    state +  end +    def handle_command(%{user: user} = state, "p " <> text) do      text = String.trim(text) -    with {:ok, _activity} <- CommonAPI.post(user, %{status: text}) do -      IO.puts("Posted!") +    with {:ok, activity} <- CommonAPI.post(user, %{status: text}) do +      IO.puts("Posted! ID: #{activity.id}")      else        _e -> IO.puts("Could not post...")      end @@ -85,6 +169,19 @@ defmodule Pleroma.BBS.Handler do      state    end +  def handle_command(%{user: user} = state, "f " <> id) do +    id = String.trim(id) + +    with %Activity{} = activity <- Activity.get_by_id(id), +         {:ok, _activity} <- CommonAPI.favorite(user, activity) do +      IO.puts("Favourited!") +    else +      _e -> IO.puts("Could not Favourite...") +    end + +    state +  end +    def handle_command(state, "home") do      user = state.user @@ -123,7 +220,7 @@ defmodule Pleroma.BBS.Handler do          loop(%{state | counter: state.counter + 1}) -      {:error, :interrupted} -> +      {:input, ^input, {:error, :interrupted}} ->          IO.puts("Caught Ctrl+C...")          loop(%{state | counter: state.counter + 1}) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 599f1d3cf..b53b15d95 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -311,7 +311,7 @@ defmodule Pleroma.Config.DeprecationWarnings do      warning_preface = """      !!!DEPRECATION WARNING!!! -    Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later. +    Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings. The setting will not take effect until updated.      """      updated_config = diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex index 015be3d8e..bd85eccab 100644 --- a/lib/pleroma/config/loader.ex +++ b/lib/pleroma/config/loader.ex @@ -19,21 +19,10 @@ defmodule Pleroma.Config.Loader do      :tesla    ] -  if Code.ensure_loaded?(Config.Reader) do -    @reader Config.Reader - -    def read(path), do: @reader.read!(path) -  else -    # support for Elixir less than 1.9 -    @reader Mix.Config -    def read(path) do -      path -      |> @reader.eval!() -      |> elem(0) -    end -  end +  @reader Config.Reader    @spec read(Path.t()) :: keyword() +  def read(path), do: @reader.read!(path)    @spec merge(keyword(), keyword()) :: keyword()    def merge(c1, c2), do: @reader.merge(c1, c2) diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index 4199630af..44a984019 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -47,7 +47,7 @@ defmodule Pleroma.Config.TransferTask do        {logger, other} =          (Repo.all(ConfigDB) ++ deleted_settings)          |> Enum.map(&merge_with_default/1) -        |> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end) +        |> Enum.split_with(fn {group, _, _, _} -> group in [:logger] end)        logger        |> Enum.sort() @@ -104,11 +104,6 @@ defmodule Pleroma.Config.TransferTask do    end    # change logger configuration in runtime, without restart -  defp configure({:quack, key, _, merged}) do -    Logger.configure_backend(Quack.Logger, [{key, merged}]) -    :ok = update_env(:quack, key, merged) -  end -    defp configure({_, :backends, _, merged}) do      # removing current backends      Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1) diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex index 6befbbe19..846cede04 100644 --- a/lib/pleroma/config_db.ex +++ b/lib/pleroma/config_db.ex @@ -163,7 +163,6 @@ defmodule Pleroma.ConfigDB do    defp only_full_update?(%ConfigDB{group: group, key: key}) do      full_key_update = [        {:pleroma, :ecto_repos}, -      {:quack, :meta},        {:mime, :types},        {:cors_plug, [:max_age, :methods, :expose, :headers]},        {:swarm, :node_blacklist}, @@ -386,7 +385,7 @@ defmodule Pleroma.ConfigDB do    @spec module_name?(String.t()) :: boolean()    def module_name?(string) do -    Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or +    Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Ueberauth|Swoosh)\./, string) or        string in ["Oban", "Ueberauth", "ExSyslogger", "ConcurrentLimiter"]    end  end diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 7b63ab06e..cfb405218 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -28,6 +28,42 @@ defmodule Pleroma.Constants do        ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)    ) +  const(status_updatable_fields, +    do: [ +      "source", +      "tag", +      "updated", +      "emoji", +      "content", +      "summary", +      "sensitive", +      "attachment", +      "generator" +    ] +  ) + +  const(updatable_object_types, +    do: [ +      "Note", +      "Question", +      "Audio", +      "Video", +      "Event", +      "Article", +      "Page" +    ] +  ) + +  const(actor_types, +    do: [ +      "Application", +      "Group", +      "Organization", +      "Person", +      "Service" +    ] +  ) +    # basic regex, just there to weed out potential mistakes    # https://datatracker.ietf.org/doc/html/rfc2045#section-5.1    const(mime_regex, diff --git a/lib/pleroma/data_migration.ex b/lib/pleroma/data_migration.ex index 59d891d8d..8451678fc 100644 --- a/lib/pleroma/data_migration.ex +++ b/lib/pleroma/data_migration.ex @@ -42,4 +42,5 @@ defmodule Pleroma.DataMigration do    end    def populate_hashtags_table, do: get_by_name("populate_hashtags_table") +  def delete_context_objects, do: get_by_name("delete_context_objects")  end diff --git a/lib/pleroma/emoji-test.txt b/lib/pleroma/emoji-test.txt index dd5493366..87d093d64 100644 --- a/lib/pleroma/emoji-test.txt +++ b/lib/pleroma/emoji-test.txt @@ -1,13 +1,13 @@  # emoji-test.txt -# Date: 2021-08-26, 17:22:23 GMT -# © 2021 Unicode®, Inc. +# Date: 2022-08-12, 20:24:39 GMT +# © 2022 Unicode®, Inc.  # Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. -# For terms of use, see http://www.unicode.org/terms_of_use.html +# For terms of use, see https://www.unicode.org/terms_of_use.html  #  # Emoji Keyboard/Display Test Data for UTS #51 -# Version: 14.0 +# Version: 15.0  # -# For documentation and usage, see http://www.unicode.org/reports/tr51 +# For documentation and usage, see https://www.unicode.org/reports/tr51  #  # This file provides data for testing which emoji forms should be in keyboards and which should also be displayed/processed.  # Format: code points; status # emoji name @@ -92,6 +92,7 @@  1F62C                                                  ; fully-qualified     # 😬 E1.0 grimacing face  1F62E 200D 1F4A8                                       ; fully-qualified     # 😮💨 E13.1 face exhaling  1F925                                                  ; fully-qualified     # 🤥 E3.0 lying face +1FAE8                                                  ; fully-qualified     # 🫨 E15.0 shaking face  # subgroup: face-sleepy  1F60C                                                  ; fully-qualified     # 😌 E0.6 relieved face @@ -155,7 +156,7 @@  # subgroup: face-negative  1F624                                                  ; fully-qualified     # 😤 E0.6 face with steam from nose -1F621                                                  ; fully-qualified     # 😡 E0.6 pouting face +1F621                                                  ; fully-qualified     # 😡 E0.6 enraged face  1F620                                                  ; fully-qualified     # 😠 E0.6 angry face  1F92C                                                  ; fully-qualified     # 🤬 E5.0 face with symbols on mouth  1F608                                                  ; fully-qualified     # 😈 E1.0 smiling face with horns @@ -190,8 +191,7 @@  1F649                                                  ; fully-qualified     # 🙉 E0.6 hear-no-evil monkey  1F64A                                                  ; fully-qualified     # 🙊 E0.6 speak-no-evil monkey -# subgroup: emotion -1F48B                                                  ; fully-qualified     # 💋 E0.6 kiss mark +# subgroup: heart  1F48C                                                  ; fully-qualified     # 💌 E0.6 love letter  1F498                                                  ; fully-qualified     # 💘 E0.6 heart with arrow  1F49D                                                  ; fully-qualified     # 💝 E0.6 heart with ribbon @@ -210,14 +210,20 @@  2764 200D 1FA79                                        ; unqualified         # ❤🩹 E13.1 mending heart  2764 FE0F                                              ; fully-qualified     # ❤️ E0.6 red heart  2764                                                   ; unqualified         # ❤ E0.6 red heart +1FA77                                                  ; fully-qualified     # 🩷 E15.0 pink heart  1F9E1                                                  ; fully-qualified     # 🧡 E5.0 orange heart  1F49B                                                  ; fully-qualified     # 💛 E0.6 yellow heart  1F49A                                                  ; fully-qualified     # 💚 E0.6 green heart  1F499                                                  ; fully-qualified     # 💙 E0.6 blue heart +1FA75                                                  ; fully-qualified     # 🩵 E15.0 light blue heart  1F49C                                                  ; fully-qualified     # 💜 E0.6 purple heart  1F90E                                                  ; fully-qualified     # 🤎 E12.0 brown heart  1F5A4                                                  ; fully-qualified     # 🖤 E3.0 black heart +1FA76                                                  ; fully-qualified     # 🩶 E15.0 grey heart  1F90D                                                  ; fully-qualified     # 🤍 E12.0 white heart + +# subgroup: emotion +1F48B                                                  ; fully-qualified     # 💋 E0.6 kiss mark  1F4AF                                                  ; fully-qualified     # 💯 E0.6 hundred points  1F4A2                                                  ; fully-qualified     # 💢 E0.6 anger symbol  1F4A5                                                  ; fully-qualified     # 💥 E0.6 collision @@ -226,21 +232,20 @@  1F4A8                                                  ; fully-qualified     # 💨 E0.6 dashing away  1F573 FE0F                                             ; fully-qualified     # 🕳️ E0.7 hole  1F573                                                  ; unqualified         # 🕳 E0.7 hole -1F4A3                                                  ; fully-qualified     # 💣 E0.6 bomb  1F4AC                                                  ; fully-qualified     # 💬 E0.6 speech balloon  1F441 FE0F 200D 1F5E8 FE0F                             ; fully-qualified     # 👁️🗨️ E2.0 eye in speech bubble  1F441 200D 1F5E8 FE0F                                  ; unqualified         # 👁🗨️ E2.0 eye in speech bubble -1F441 FE0F 200D 1F5E8                                  ; unqualified         # 👁️🗨 E2.0 eye in speech bubble +1F441 FE0F 200D 1F5E8                                  ; minimally-qualified # 👁️🗨 E2.0 eye in speech bubble  1F441 200D 1F5E8                                       ; unqualified         # 👁🗨 E2.0 eye in speech bubble  1F5E8 FE0F                                             ; fully-qualified     # 🗨️ E2.0 left speech bubble  1F5E8                                                  ; unqualified         # 🗨 E2.0 left speech bubble  1F5EF FE0F                                             ; fully-qualified     # 🗯️ E0.7 right anger bubble  1F5EF                                                  ; unqualified         # 🗯 E0.7 right anger bubble  1F4AD                                                  ; fully-qualified     # 💭 E1.0 thought balloon -1F4A4                                                  ; fully-qualified     # 💤 E0.6 zzz +1F4A4                                                  ; fully-qualified     # 💤 E0.6 ZZZ -# Smileys & Emotion subtotal:		177 -# Smileys & Emotion subtotal:		177	w/o modifiers +# Smileys & Emotion subtotal:		180 +# Smileys & Emotion subtotal:		180	w/o modifiers  # group: People & Body @@ -300,6 +305,18 @@  1FAF4 1F3FD                                            ; fully-qualified     # 🫴🏽 E14.0 palm up hand: medium skin tone  1FAF4 1F3FE                                            ; fully-qualified     # 🫴🏾 E14.0 palm up hand: medium-dark skin tone  1FAF4 1F3FF                                            ; fully-qualified     # 🫴🏿 E14.0 palm up hand: dark skin tone +1FAF7                                                  ; fully-qualified     # 🫷 E15.0 leftwards pushing hand +1FAF7 1F3FB                                            ; fully-qualified     # 🫷🏻 E15.0 leftwards pushing hand: light skin tone +1FAF7 1F3FC                                            ; fully-qualified     # 🫷🏼 E15.0 leftwards pushing hand: medium-light skin tone +1FAF7 1F3FD                                            ; fully-qualified     # 🫷🏽 E15.0 leftwards pushing hand: medium skin tone +1FAF7 1F3FE                                            ; fully-qualified     # 🫷🏾 E15.0 leftwards pushing hand: medium-dark skin tone +1FAF7 1F3FF                                            ; fully-qualified     # 🫷🏿 E15.0 leftwards pushing hand: dark skin tone +1FAF8                                                  ; fully-qualified     # 🫸 E15.0 rightwards pushing hand +1FAF8 1F3FB                                            ; fully-qualified     # 🫸🏻 E15.0 rightwards pushing hand: light skin tone +1FAF8 1F3FC                                            ; fully-qualified     # 🫸🏼 E15.0 rightwards pushing hand: medium-light skin tone +1FAF8 1F3FD                                            ; fully-qualified     # 🫸🏽 E15.0 rightwards pushing hand: medium skin tone +1FAF8 1F3FE                                            ; fully-qualified     # 🫸🏾 E15.0 rightwards pushing hand: medium-dark skin tone +1FAF8 1F3FF                                            ; fully-qualified     # 🫸🏿 E15.0 rightwards pushing hand: dark skin tone  # subgroup: hand-fingers-partial  1F44C                                                  ; fully-qualified     # 👌 E0.6 OK hand @@ -473,11 +490,11 @@  1F932 1F3FE                                            ; fully-qualified     # 🤲🏾 E5.0 palms up together: medium-dark skin tone  1F932 1F3FF                                            ; fully-qualified     # 🤲🏿 E5.0 palms up together: dark skin tone  1F91D                                                  ; fully-qualified     # 🤝 E3.0 handshake -1F91D 1F3FB                                            ; fully-qualified     # 🤝🏻 E3.0 handshake: light skin tone -1F91D 1F3FC                                            ; fully-qualified     # 🤝🏼 E3.0 handshake: medium-light skin tone -1F91D 1F3FD                                            ; fully-qualified     # 🤝🏽 E3.0 handshake: medium skin tone -1F91D 1F3FE                                            ; fully-qualified     # 🤝🏾 E3.0 handshake: medium-dark skin tone -1F91D 1F3FF                                            ; fully-qualified     # 🤝🏿 E3.0 handshake: dark skin tone +1F91D 1F3FB                                            ; fully-qualified     # 🤝🏻 E14.0 handshake: light skin tone +1F91D 1F3FC                                            ; fully-qualified     # 🤝🏼 E14.0 handshake: medium-light skin tone +1F91D 1F3FD                                            ; fully-qualified     # 🤝🏽 E14.0 handshake: medium skin tone +1F91D 1F3FE                                            ; fully-qualified     # 🤝🏾 E14.0 handshake: medium-dark skin tone +1F91D 1F3FF                                            ; fully-qualified     # 🤝🏿 E14.0 handshake: dark skin tone  1FAF1 1F3FB 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏻🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone  1FAF1 1F3FB 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏻🫲🏽 E14.0 handshake: light skin tone, medium skin tone  1FAF1 1F3FB 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏻🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone @@ -1455,7 +1472,7 @@  1F575 1F3FF                                            ; fully-qualified     # 🕵🏿 E2.0 detective: dark skin tone  1F575 FE0F 200D 2642 FE0F                              ; fully-qualified     # 🕵️♂️ E4.0 man detective  1F575 200D 2642 FE0F                                   ; unqualified         # 🕵♂️ E4.0 man detective -1F575 FE0F 200D 2642                                   ; unqualified         # 🕵️♂ E4.0 man detective +1F575 FE0F 200D 2642                                   ; minimally-qualified # 🕵️♂ E4.0 man detective  1F575 200D 2642                                        ; unqualified         # 🕵♂ E4.0 man detective  1F575 1F3FB 200D 2642 FE0F                             ; fully-qualified     # 🕵🏻♂️ E4.0 man detective: light skin tone  1F575 1F3FB 200D 2642                                  ; minimally-qualified # 🕵🏻♂ E4.0 man detective: light skin tone @@ -1469,7 +1486,7 @@  1F575 1F3FF 200D 2642                                  ; minimally-qualified # 🕵🏿♂ E4.0 man detective: dark skin tone  1F575 FE0F 200D 2640 FE0F                              ; fully-qualified     # 🕵️♀️ E4.0 woman detective  1F575 200D 2640 FE0F                                   ; unqualified         # 🕵♀️ E4.0 woman detective -1F575 FE0F 200D 2640                                   ; unqualified         # 🕵️♀ E4.0 woman detective +1F575 FE0F 200D 2640                                   ; minimally-qualified # 🕵️♀ E4.0 woman detective  1F575 200D 2640                                        ; unqualified         # 🕵♀ E4.0 woman detective  1F575 1F3FB 200D 2640 FE0F                             ; fully-qualified     # 🕵🏻♀️ E4.0 woman detective: light skin tone  1F575 1F3FB 200D 2640                                  ; minimally-qualified # 🕵🏻♀ E4.0 woman detective: light skin tone @@ -2302,7 +2319,7 @@  1F3CC 1F3FF                                            ; fully-qualified     # 🏌🏿 E4.0 person golfing: dark skin tone  1F3CC FE0F 200D 2642 FE0F                              ; fully-qualified     # 🏌️♂️ E4.0 man golfing  1F3CC 200D 2642 FE0F                                   ; unqualified         # 🏌♂️ E4.0 man golfing -1F3CC FE0F 200D 2642                                   ; unqualified         # 🏌️♂ E4.0 man golfing +1F3CC FE0F 200D 2642                                   ; minimally-qualified # 🏌️♂ E4.0 man golfing  1F3CC 200D 2642                                        ; unqualified         # 🏌♂ E4.0 man golfing  1F3CC 1F3FB 200D 2642 FE0F                             ; fully-qualified     # 🏌🏻♂️ E4.0 man golfing: light skin tone  1F3CC 1F3FB 200D 2642                                  ; minimally-qualified # 🏌🏻♂ E4.0 man golfing: light skin tone @@ -2316,7 +2333,7 @@  1F3CC 1F3FF 200D 2642                                  ; minimally-qualified # 🏌🏿♂ E4.0 man golfing: dark skin tone  1F3CC FE0F 200D 2640 FE0F                              ; fully-qualified     # 🏌️♀️ E4.0 woman golfing  1F3CC 200D 2640 FE0F                                   ; unqualified         # 🏌♀️ E4.0 woman golfing -1F3CC FE0F 200D 2640                                   ; unqualified         # 🏌️♀ E4.0 woman golfing +1F3CC FE0F 200D 2640                                   ; minimally-qualified # 🏌️♀ E4.0 woman golfing  1F3CC 200D 2640                                        ; unqualified         # 🏌♀ E4.0 woman golfing  1F3CC 1F3FB 200D 2640 FE0F                             ; fully-qualified     # 🏌🏻♀️ E4.0 woman golfing: light skin tone  1F3CC 1F3FB 200D 2640                                  ; minimally-qualified # 🏌🏻♀ E4.0 woman golfing: light skin tone @@ -2427,7 +2444,7 @@  26F9 1F3FF                                             ; fully-qualified     # ⛹🏿 E2.0 person bouncing ball: dark skin tone  26F9 FE0F 200D 2642 FE0F                               ; fully-qualified     # ⛹️♂️ E4.0 man bouncing ball  26F9 200D 2642 FE0F                                    ; unqualified         # ⛹♂️ E4.0 man bouncing ball -26F9 FE0F 200D 2642                                    ; unqualified         # ⛹️♂ E4.0 man bouncing ball +26F9 FE0F 200D 2642                                    ; minimally-qualified # ⛹️♂ E4.0 man bouncing ball  26F9 200D 2642                                         ; unqualified         # ⛹♂ E4.0 man bouncing ball  26F9 1F3FB 200D 2642 FE0F                              ; fully-qualified     # ⛹🏻♂️ E4.0 man bouncing ball: light skin tone  26F9 1F3FB 200D 2642                                   ; minimally-qualified # ⛹🏻♂ E4.0 man bouncing ball: light skin tone @@ -2441,7 +2458,7 @@  26F9 1F3FF 200D 2642                                   ; minimally-qualified # ⛹🏿♂ E4.0 man bouncing ball: dark skin tone  26F9 FE0F 200D 2640 FE0F                               ; fully-qualified     # ⛹️♀️ E4.0 woman bouncing ball  26F9 200D 2640 FE0F                                    ; unqualified         # ⛹♀️ E4.0 woman bouncing ball -26F9 FE0F 200D 2640                                    ; unqualified         # ⛹️♀ E4.0 woman bouncing ball +26F9 FE0F 200D 2640                                    ; minimally-qualified # ⛹️♀ E4.0 woman bouncing ball  26F9 200D 2640                                         ; unqualified         # ⛹♀ E4.0 woman bouncing ball  26F9 1F3FB 200D 2640 FE0F                              ; fully-qualified     # ⛹🏻♀️ E4.0 woman bouncing ball: light skin tone  26F9 1F3FB 200D 2640                                   ; minimally-qualified # ⛹🏻♀ E4.0 woman bouncing ball: light skin tone @@ -2462,7 +2479,7 @@  1F3CB 1F3FF                                            ; fully-qualified     # 🏋🏿 E2.0 person lifting weights: dark skin tone  1F3CB FE0F 200D 2642 FE0F                              ; fully-qualified     # 🏋️♂️ E4.0 man lifting weights  1F3CB 200D 2642 FE0F                                   ; unqualified         # 🏋♂️ E4.0 man lifting weights -1F3CB FE0F 200D 2642                                   ; unqualified         # 🏋️♂ E4.0 man lifting weights +1F3CB FE0F 200D 2642                                   ; minimally-qualified # 🏋️♂ E4.0 man lifting weights  1F3CB 200D 2642                                        ; unqualified         # 🏋♂ E4.0 man lifting weights  1F3CB 1F3FB 200D 2642 FE0F                             ; fully-qualified     # 🏋🏻♂️ E4.0 man lifting weights: light skin tone  1F3CB 1F3FB 200D 2642                                  ; minimally-qualified # 🏋🏻♂ E4.0 man lifting weights: light skin tone @@ -2476,7 +2493,7 @@  1F3CB 1F3FF 200D 2642                                  ; minimally-qualified # 🏋🏿♂ E4.0 man lifting weights: dark skin tone  1F3CB FE0F 200D 2640 FE0F                              ; fully-qualified     # 🏋️♀️ E4.0 woman lifting weights  1F3CB 200D 2640 FE0F                                   ; unqualified         # 🏋♀️ E4.0 woman lifting weights -1F3CB FE0F 200D 2640                                   ; unqualified         # 🏋️♀ E4.0 woman lifting weights +1F3CB FE0F 200D 2640                                   ; minimally-qualified # 🏋️♀ E4.0 woman lifting weights  1F3CB 200D 2640                                        ; unqualified         # 🏋♀ E4.0 woman lifting weights  1F3CB 1F3FB 200D 2640 FE0F                             ; fully-qualified     # 🏋🏻♀️ E4.0 woman lifting weights: light skin tone  1F3CB 1F3FB 200D 2640                                  ; minimally-qualified # 🏋🏻♀ E4.0 woman lifting weights: light skin tone @@ -3262,8 +3279,8 @@  1FAC2                                                  ; fully-qualified     # 🫂 E13.0 people hugging  1F463                                                  ; fully-qualified     # 👣 E0.6 footprints -# People & Body subtotal:		2986 -# People & Body subtotal:		506	w/o modifiers +# People & Body subtotal:		2998 +# People & Body subtotal:		508	w/o modifiers  # group: Component @@ -3306,6 +3323,8 @@  1F405                                                  ; fully-qualified     # 🐅 E1.0 tiger  1F406                                                  ; fully-qualified     # 🐆 E1.0 leopard  1F434                                                  ; fully-qualified     # 🐴 E0.6 horse face +1FACE                                                  ; fully-qualified     # 🫎 E15.0 moose +1FACF                                                  ; fully-qualified     # 🫏 E15.0 donkey  1F40E                                                  ; fully-qualified     # 🐎 E0.6 horse  1F984                                                  ; fully-qualified     # 🦄 E1.0 unicorn  1F993                                                  ; fully-qualified     # 🦓 E5.0 zebra @@ -3373,6 +3392,9 @@  1F9A9                                                  ; fully-qualified     # 🦩 E12.0 flamingo  1F99A                                                  ; fully-qualified     # 🦚 E11.0 peacock  1F99C                                                  ; fully-qualified     # 🦜 E11.0 parrot +1FABD                                                  ; fully-qualified     # 🪽 E15.0 wing +1F426 200D 2B1B                                        ; fully-qualified     # 🐦⬛ E15.0 black bird +1FABF                                                  ; fully-qualified     # 🪿 E15.0 goose  # subgroup: animal-amphibian  1F438                                                  ; fully-qualified     # 🐸 E0.6 frog @@ -3399,6 +3421,7 @@  1F419                                                  ; fully-qualified     # 🐙 E0.6 octopus  1F41A                                                  ; fully-qualified     # 🐚 E0.6 spiral shell  1FAB8                                                  ; fully-qualified     # 🪸 E14.0 coral +1FABC                                                  ; fully-qualified     # 🪼 E15.0 jellyfish  # subgroup: animal-bug  1F40C                                                  ; fully-qualified     # 🐌 E0.6 snail @@ -3433,6 +3456,7 @@  1F33B                                                  ; fully-qualified     # 🌻 E0.6 sunflower  1F33C                                                  ; fully-qualified     # 🌼 E0.6 blossom  1F337                                                  ; fully-qualified     # 🌷 E0.6 tulip +1FABB                                                  ; fully-qualified     # 🪻 E15.0 hyacinth  # subgroup: plant-other  1F331                                                  ; fully-qualified     # 🌱 E0.6 seedling @@ -3451,9 +3475,10 @@  1F343                                                  ; fully-qualified     # 🍃 E0.6 leaf fluttering in wind  1FAB9                                                  ; fully-qualified     # 🪹 E14.0 empty nest  1FABA                                                  ; fully-qualified     # 🪺 E14.0 nest with eggs +1F344                                                  ; fully-qualified     # 🍄 E0.6 mushroom -# Animals & Nature subtotal:		151 -# Animals & Nature subtotal:		151	w/o modifiers +# Animals & Nature subtotal:		159 +# Animals & Nature subtotal:		159	w/o modifiers  # group: Food & Drink @@ -3492,10 +3517,11 @@  1F966                                                  ; fully-qualified     # 🥦 E5.0 broccoli  1F9C4                                                  ; fully-qualified     # 🧄 E12.0 garlic  1F9C5                                                  ; fully-qualified     # 🧅 E12.0 onion -1F344                                                  ; fully-qualified     # 🍄 E0.6 mushroom  1F95C                                                  ; fully-qualified     # 🥜 E3.0 peanuts  1FAD8                                                  ; fully-qualified     # 🫘 E14.0 beans  1F330                                                  ; fully-qualified     # 🌰 E0.6 chestnut +1FADA                                                  ; fully-qualified     # 🫚 E15.0 ginger root +1FADB                                                  ; fully-qualified     # 🫛 E15.0 pea pod  # subgroup: food-prepared  1F35E                                                  ; fully-qualified     # 🍞 E0.6 bread @@ -3607,8 +3633,8 @@  1FAD9                                                  ; fully-qualified     # 🫙 E14.0 jar  1F3FA                                                  ; fully-qualified     # 🏺 E1.0 amphora -# Food & Drink subtotal:		134 -# Food & Drink subtotal:		134	w/o modifiers +# Food & Drink subtotal:		135 +# Food & Drink subtotal:		135	w/o modifiers  # group: Travel & Places @@ -3974,11 +4000,10 @@  1F3AF                                                  ; fully-qualified     # 🎯 E0.6 bullseye  1FA80                                                  ; fully-qualified     # 🪀 E12.0 yo-yo  1FA81                                                  ; fully-qualified     # 🪁 E12.0 kite +1F52B                                                  ; fully-qualified     # 🔫 E0.6 water pistol  1F3B1                                                  ; fully-qualified     # 🎱 E0.6 pool 8 ball  1F52E                                                  ; fully-qualified     # 🔮 E0.6 crystal ball  1FA84                                                  ; fully-qualified     # 🪄 E13.0 magic wand -1F9FF                                                  ; fully-qualified     # 🧿 E11.0 nazar amulet -1FAAC                                                  ; fully-qualified     # 🪬 E14.0 hamsa  1F3AE                                                  ; fully-qualified     # 🎮 E0.6 video game  1F579 FE0F                                             ; fully-qualified     # 🕹️ E0.7 joystick  1F579                                                  ; unqualified         # 🕹 E0.7 joystick @@ -4013,8 +4038,8 @@  1F9F6                                                  ; fully-qualified     # 🧶 E11.0 yarn  1FAA2                                                  ; fully-qualified     # 🪢 E13.0 knot -# Activities subtotal:		97 -# Activities subtotal:		97	w/o modifiers +# Activities subtotal:		96 +# Activities subtotal:		96	w/o modifiers  # group: Objects @@ -4040,6 +4065,7 @@  1FA73                                                  ; fully-qualified     # 🩳 E12.0 shorts  1F459                                                  ; fully-qualified     # 👙 E0.6 bikini  1F45A                                                  ; fully-qualified     # 👚 E0.6 woman’s clothes +1FAAD                                                  ; fully-qualified     # 🪭 E15.0 folding hand fan  1F45B                                                  ; fully-qualified     # 👛 E0.6 purse  1F45C                                                  ; fully-qualified     # 👜 E0.6 handbag  1F45D                                                  ; fully-qualified     # 👝 E0.6 clutch bag @@ -4055,6 +4081,7 @@  1F461                                                  ; fully-qualified     # 👡 E0.6 woman’s sandal  1FA70                                                  ; fully-qualified     # 🩰 E12.0 ballet shoes  1F462                                                  ; fully-qualified     # 👢 E0.6 woman’s boot +1FAAE                                                  ; fully-qualified     # 🪮 E15.0 hair pick  1F451                                                  ; fully-qualified     # 👑 E0.6 crown  1F452                                                  ; fully-qualified     # 👒 E0.6 woman’s hat  1F3A9                                                  ; fully-qualified     # 🎩 E0.6 top hat @@ -4103,6 +4130,8 @@  1FA95                                                  ; fully-qualified     # 🪕 E12.0 banjo  1F941                                                  ; fully-qualified     # 🥁 E3.0 drum  1FA98                                                  ; fully-qualified     # 🪘 E13.0 long drum +1FA87                                                  ; fully-qualified     # 🪇 E15.0 maracas +1FA88                                                  ; fully-qualified     # 🪈 E15.0 flute  # subgroup: phone  1F4F1                                                  ; fully-qualified     # 📱 E0.6 mobile phone @@ -4275,7 +4304,7 @@  1F5E1                                                  ; unqualified         # 🗡 E0.7 dagger  2694 FE0F                                              ; fully-qualified     # ⚔️ E1.0 crossed swords  2694                                                   ; unqualified         # ⚔ E1.0 crossed swords -1F52B                                                  ; fully-qualified     # 🔫 E0.6 water pistol +1F4A3                                                  ; fully-qualified     # 💣 E0.6 bomb  1FA83                                                  ; fully-qualified     # 🪃 E13.0 boomerang  1F3F9                                                  ; fully-qualified     # 🏹 E1.0 bow and arrow  1F6E1 FE0F                                             ; fully-qualified     # 🛡️ E0.7 shield @@ -4354,12 +4383,14 @@  1FAA6                                                  ; fully-qualified     # 🪦 E13.0 headstone  26B1 FE0F                                              ; fully-qualified     # ⚱️ E1.0 funeral urn  26B1                                                   ; unqualified         # ⚱ E1.0 funeral urn +1F9FF                                                  ; fully-qualified     # 🧿 E11.0 nazar amulet +1FAAC                                                  ; fully-qualified     # 🪬 E14.0 hamsa  1F5FF                                                  ; fully-qualified     # 🗿 E0.6 moai  1FAA7                                                  ; fully-qualified     # 🪧 E13.0 placard  1FAAA                                                  ; fully-qualified     # 🪪 E14.0 identification card -# Objects subtotal:		304 -# Objects subtotal:		304	w/o modifiers +# Objects subtotal:		310 +# Objects subtotal:		310	w/o modifiers  # group: Symbols @@ -4455,6 +4486,7 @@  262E                                                   ; unqualified         # ☮ E1.0 peace symbol  1F54E                                                  ; fully-qualified     # 🕎 E1.0 menorah  1F52F                                                  ; fully-qualified     # 🔯 E0.6 dotted six-pointed star +1FAAF                                                  ; fully-qualified     # 🪯 E15.0 khanda  # subgroup: zodiac  2648                                                   ; fully-qualified     # ♈ E0.6 Aries @@ -4503,6 +4535,7 @@  1F505                                                  ; fully-qualified     # 🔅 E1.0 dim button  1F506                                                  ; fully-qualified     # 🔆 E1.0 bright button  1F4F6                                                  ; fully-qualified     # 📶 E0.6 antenna bars +1F6DC                                                  ; fully-qualified     # 🛜 E15.0 wireless  1F4F3                                                  ; fully-qualified     # 📳 E0.6 vibration mode  1F4F4                                                  ; fully-qualified     # 📴 E0.6 mobile phone off @@ -4693,8 +4726,8 @@  1F533                                                  ; fully-qualified     # 🔳 E0.6 white square button  1F532                                                  ; fully-qualified     # 🔲 E0.6 black square button -# Symbols subtotal:		302 -# Symbols subtotal:		302	w/o modifiers +# Symbols subtotal:		304 +# Symbols subtotal:		304	w/o modifiers  # group: Flags @@ -4709,7 +4742,7 @@  1F3F3 200D 1F308                                       ; unqualified         # 🏳🌈 E4.0 rainbow flag  1F3F3 FE0F 200D 26A7 FE0F                              ; fully-qualified     # 🏳️⚧️ E13.0 transgender flag  1F3F3 200D 26A7 FE0F                                   ; unqualified         # 🏳⚧️ E13.0 transgender flag -1F3F3 FE0F 200D 26A7                                   ; unqualified         # 🏳️⚧ E13.0 transgender flag +1F3F3 FE0F 200D 26A7                                   ; minimally-qualified # 🏳️⚧ E13.0 transgender flag  1F3F3 200D 26A7                                        ; unqualified         # 🏳⚧ E13.0 transgender flag  1F3F4 200D 2620 FE0F                                   ; fully-qualified     # 🏴☠️ E11.0 pirate flag  1F3F4 200D 2620                                        ; minimally-qualified # 🏴☠ E11.0 pirate flag @@ -4983,9 +5016,9 @@  # Flags subtotal:		275	w/o modifiers  # Status Counts -# fully-qualified : 3624 -# minimally-qualified : 817 -# unqualified : 252 +# fully-qualified : 3655 +# minimally-qualified : 827 +# unqualified : 242  # component : 9  #EOF diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index dd65d56ae..43a3447c3 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -51,6 +51,8 @@ defmodule Pleroma.Emoji do    @doc "Returns the path of the emoji `name`."    @spec get(String.t()) :: String.t() | nil    def get(name) do +    name = maybe_strip_name(name) +      case :ets.lookup(@ets, name) do        [{_, path}] -> path        _ -> nil @@ -139,6 +141,57 @@ defmodule Pleroma.Emoji do    def is_unicode_emoji?(_), do: false +  @emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/ + +  def is_custom_emoji?(s) when is_binary(s), do: Regex.match?(@emoji_regex, s) + +  def is_custom_emoji?(_), do: false + +  def maybe_strip_name(name) when is_binary(name), do: String.trim(name, ":") + +  def maybe_strip_name(name), do: name + +  def maybe_quote(name) when is_binary(name) do +    if is_unicode_emoji?(name) do +      name +    else +      if String.starts_with?(name, ":") do +        name +      else +        ":#{name}:" +      end +    end +  end + +  def maybe_quote(name), do: name + +  def emoji_url(%{"type" => "EmojiReact", "content" => _, "tag" => []}), do: nil + +  def emoji_url(%{"type" => "EmojiReact", "content" => emoji, "tag" => tags}) do +    emoji = maybe_strip_name(emoji) + +    tag = +      tags +      |> Enum.find(fn tag -> +        tag["type"] == "Emoji" && !is_nil(tag["name"]) && tag["name"] == emoji +      end) + +    if is_nil(tag) do +      nil +    else +      tag +      |> Map.get("icon") +      |> Map.get("url") +    end +  end + +  def emoji_url(_), do: nil + +  def emoji_name_with_instance(name, url) do +    url = url |> URI.parse() |> Map.get(:host) +    "#{name}@#{url}" +  end +    emoji_qualification_map =      emojis      |> Enum.filter(&String.contains?(&1, "\uFE0F")) diff --git a/lib/pleroma/http.ex b/lib/pleroma/http.ex index 2e82ceff2..d41061538 100644 --- a/lib/pleroma/http.ex +++ b/lib/pleroma/http.ex @@ -106,5 +106,12 @@ defmodule Pleroma.HTTP do      [Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.ConnectionPool]    end -  defp adapter_middlewares(_), do: [] +  defp adapter_middlewares(_) do +    if Pleroma.Config.get(:env) == :test do +      # Emulate redirects in test env, which are handled by adapters in other environments +      [Tesla.Middleware.FollowRedirects] +    else +      [] +    end +  end  end diff --git a/lib/pleroma/migrators/context_objects_deletion_migrator.ex b/lib/pleroma/migrators/context_objects_deletion_migrator.ex new file mode 100644 index 000000000..fb224795a --- /dev/null +++ b/lib/pleroma/migrators/context_objects_deletion_migrator.ex @@ -0,0 +1,139 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Migrators.ContextObjectsDeletionMigrator do +  defmodule State do +    use Pleroma.Migrators.Support.BaseMigratorState + +    @impl Pleroma.Migrators.Support.BaseMigratorState +    defdelegate data_migration(), to: Pleroma.DataMigration, as: :delete_context_objects +  end + +  use Pleroma.Migrators.Support.BaseMigrator + +  alias Pleroma.Migrators.Support.BaseMigrator +  alias Pleroma.Object + +  @doc "This migration removes objects created exclusively for contexts, containing only an `id` field." + +  @impl BaseMigrator +  def feature_config_path, do: [:features, :delete_context_objects] + +  @impl BaseMigrator +  def fault_rate_allowance, do: Config.get([:delete_context_objects, :fault_rate_allowance], 0) + +  @impl BaseMigrator +  def perform do +    data_migration_id = data_migration_id() +    max_processed_id = get_stat(:max_processed_id, 0) + +    Logger.info("Deleting context objects from `objects` (from oid: #{max_processed_id})...") + +    query() +    |> where([object], object.id > ^max_processed_id) +    |> Repo.chunk_stream(100, :batches, timeout: :infinity) +    |> Stream.each(fn objects -> +      object_ids = Enum.map(objects, & &1.id) + +      results = Enum.map(object_ids, &delete_context_object(&1)) + +      failed_ids = +        results +        |> Enum.filter(&(elem(&1, 0) == :error)) +        |> Enum.map(&elem(&1, 1)) + +      chunk_affected_count = +        results +        |> Enum.filter(&(elem(&1, 0) == :ok)) +        |> length() + +      for failed_id <- failed_ids do +        _ = +          Repo.query( +            "INSERT INTO data_migration_failed_ids(data_migration_id, record_id) " <> +              "VALUES ($1, $2) ON CONFLICT DO NOTHING;", +            [data_migration_id, failed_id] +          ) +      end + +      _ = +        Repo.query( +          "DELETE FROM data_migration_failed_ids " <> +            "WHERE data_migration_id = $1 AND record_id = ANY($2)", +          [data_migration_id, object_ids -- failed_ids] +        ) + +      max_object_id = Enum.at(object_ids, -1) + +      put_stat(:max_processed_id, max_object_id) +      increment_stat(:iteration_processed_count, length(object_ids)) +      increment_stat(:processed_count, length(object_ids)) +      increment_stat(:failed_count, length(failed_ids)) +      increment_stat(:affected_count, chunk_affected_count) +      put_stat(:records_per_second, records_per_second()) +      persist_state() + +      # A quick and dirty approach to controlling the load this background migration imposes +      sleep_interval = Config.get([:delete_context_objects, :sleep_interval_ms], 0) +      Process.sleep(sleep_interval) +    end) +    |> Stream.run() +  end + +  @impl BaseMigrator +  def query do +    # Context objects have no activity type, and only one field, `id`. +    # Only those context objects are without types. +    from( +      object in Object, +      where: fragment("(?)->'type' IS NULL", object.data), +      select: %{ +        id: object.id +      } +    ) +  end + +  @spec delete_context_object(integer()) :: {:ok | :error, integer()} +  defp delete_context_object(id) do +    result = +      %Object{id: id} +      |> Repo.delete() +      |> elem(0) + +    {result, id} +  end + +  @impl BaseMigrator +  def retry_failed do +    data_migration_id = data_migration_id() + +    failed_objects_query() +    |> Repo.chunk_stream(100, :one) +    |> Stream.each(fn object -> +      with {res, _} when res != :error <- delete_context_object(object.id) do +        _ = +          Repo.query( +            "DELETE FROM data_migration_failed_ids " <> +              "WHERE data_migration_id = $1 AND record_id = $2", +            [data_migration_id, object.id] +          ) +      end +    end) +    |> Stream.run() + +    put_stat(:failed_count, failures_count()) +    persist_state() + +    force_continue() +  end + +  defp failed_objects_query do +    from(o in Object) +    |> join(:inner, [o], dmf in fragment("SELECT * FROM data_migration_failed_ids"), +      on: dmf.record_id == o.id +    ) +    |> where([_o, dmf], dmf.data_migration_id == ^data_migration_id()) +    |> order_by([o], asc: o.id) +  end +end diff --git a/lib/pleroma/migrators/hashtags_table_migrator.ex b/lib/pleroma/migrators/hashtags_table_migrator.ex index fa1190b7d..dca4bfa6f 100644 --- a/lib/pleroma/migrators/hashtags_table_migrator.ex +++ b/lib/pleroma/migrators/hashtags_table_migrator.ex @@ -183,7 +183,7 @@ defmodule Pleroma.Migrators.HashtagsTableMigrator do      DELETE FROM hashtags_objects WHERE object_id IN        (SELECT DISTINCT objects.id FROM objects          JOIN hashtags_objects ON hashtags_objects.object_id = objects.id LEFT JOIN activities -          ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = +          ON associated_object_id(activities) =              (objects.data->>'id')            AND activities.data->>'type' = 'Create'          WHERE activities.id IS NULL); diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 52fd2656b..48d467c59 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -117,9 +117,8 @@ defmodule Pleroma.Notification do      |> join(:left, [n, a], object in Object,        on:          fragment( -          "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", +          "(?->>'id') = associated_object_id(?)",            object.data, -          a.data,            a.data          )      ) @@ -179,6 +178,7 @@ defmodule Pleroma.Notification do          from([_n, a, o] in query,            where:              fragment("not(?->>'content' ~* ?)", o.data, ^regex) or +              fragment("?->>'content' is null", o.data) or                fragment("?->>'actor' = ?", o.data, ^user.ap_id)          )      end @@ -193,13 +193,11 @@ defmodule Pleroma.Notification do        |> join(:left, [n, a], mutated_activity in Pleroma.Activity,          on:            fragment( -            "COALESCE((?->'object')->>'id', ?->>'object')", -            a.data, +            "associated_object_id(?)",              a.data            ) ==              fragment( -              "COALESCE((?->'object')->>'id', ?->>'object')", -              mutated_activity.data, +              "associated_object_id(?)",                mutated_activity.data              ) and              fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and @@ -341,14 +339,6 @@ defmodule Pleroma.Notification do      |> Repo.delete_all()    end -  def destroy_multiple_from_types(%{id: user_id}, types) do -    from(n in Notification, -      where: n.user_id == ^user_id, -      where: n.type in ^types -    ) -    |> Repo.delete_all() -  end -    def dismiss(%Pleroma.Activity{} = activity) do      Notification      |> where([n], n.activity_id == ^activity.id) @@ -385,7 +375,7 @@ defmodule Pleroma.Notification do    end    def create_notifications(%Activity{data: %{"type" => type}} = activity, options) -      when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag"] do +      when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do      do_create_notifications(activity, options)    end @@ -439,6 +429,9 @@ defmodule Pleroma.Notification do          activity          |> type_from_activity_object() +      "Update" -> +        "update" +        t ->          raise "No notification type for activity type #{t}"      end @@ -513,7 +506,16 @@ defmodule Pleroma.Notification do    def get_notified_from_activity(activity, local_only \\ true)    def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only) -      when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact", "Flag"] do +      when type in [ +             "Create", +             "Like", +             "Announce", +             "Follow", +             "Move", +             "EmojiReact", +             "Flag", +             "Update" +           ] do      potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)      potential_receivers = @@ -550,7 +552,24 @@ defmodule Pleroma.Notification do    end    def get_potential_receiver_ap_ids(%{data: %{"type" => "Flag", "actor" => actor}}) do -    (User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor] +    (User.all_users_with_privilege(:reports_manage_reports) +     |> Enum.map(fn user -> user.ap_id end)) -- +      [actor] +  end + +  # Update activity: notify all who repeated this +  def get_potential_receiver_ap_ids(%{data: %{"type" => "Update", "actor" => actor}} = activity) do +    with %Object{data: %{"id" => object_id}} <- Object.normalize(activity, fetch: false) do +      repeaters = +        Activity.Queries.by_type("Announce") +        |> Activity.Queries.by_object_id(object_id) +        |> Activity.with_joined_user_actor() +        |> where([a, u], u.local) +        |> select([a, u], u.ap_id) +        |> Repo.all() + +      repeaters -- [actor] +    end    end    def get_potential_receiver_ap_ids(activity) do @@ -661,7 +680,7 @@ defmodule Pleroma.Notification do      cond do        opts[:type] == "poll" -> false        user.ap_id == actor -> false -      !User.following?(follower, user) -> true +      !User.following?(user, follower) -> true        true -> false      end    end diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index fe264b5e0..aa137d250 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -40,8 +40,7 @@ defmodule Pleroma.Object do      join(query, join_type, [{object, object_position}], a in Activity,        on:          fragment( -          "COALESCE(?->'object'->>'id', ?->>'object') = (? ->> 'id') AND (?->>'type' = ?) ", -          a.data, +          "associated_object_id(?) = (? ->> 'id') AND (?->>'type' = ?) ",            a.data,            object.data,            a.data, @@ -145,7 +144,7 @@ defmodule Pleroma.Object do      Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")    end -  def normalize(_, options \\ [fetch: false]) +  def normalize(_, options \\ [fetch: false, id_only: false])    # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.    # Use this whenever possible, especially when walking graphs in an O(N) loop! @@ -173,10 +172,15 @@ defmodule Pleroma.Object do    def normalize(%{"id" => ap_id}, options), do: normalize(ap_id, options)    def normalize(ap_id, options) when is_binary(ap_id) do -    if Keyword.get(options, :fetch) do -      Fetcher.fetch_object_from_id!(ap_id, options) -    else -      get_cached_by_ap_id(ap_id) +    cond do +      Keyword.get(options, :id_only) -> +        ap_id + +      Keyword.get(options, :fetch) -> +        Fetcher.fetch_object_from_id!(ap_id, options) + +      true -> +        get_cached_by_ap_id(ap_id)      end    end @@ -208,10 +212,6 @@ defmodule Pleroma.Object do      end    end -  def context_mapping(context) do -    Object.change(%Object{}, %{data: %{"id" => context}}) -  end -    def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do      %ObjectTombstone{        id: id, @@ -425,4 +425,30 @@ defmodule Pleroma.Object do    end    def object_data_hashtags(_), do: [] + +  def get_emoji_reactions(object) do +    reactions = object.data["reactions"] + +    if is_list(reactions) or is_map(reactions) do +      reactions +      |> Enum.map(fn +        [_emoji, users, _maybe_url] = item when is_list(users) -> +          item + +        [emoji, users] when is_list(users) -> +          [emoji, users, nil] + +        # This case is here to process the Map situation, which will happen +        # only with the legacy two-value format. +        {emoji, users} when is_list(users) -> +          [emoji, users, nil] + +        _ -> +          nil +      end) +      |> Enum.reject(&is_nil/1) +    else +      [] +    end +  end  end diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index deb3dc711..a9a9eeeed 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Object.Fetcher do    alias Pleroma.HTTP +  alias Pleroma.Instances    alias Pleroma.Maps    alias Pleroma.Object    alias Pleroma.Object.Containment @@ -26,8 +27,42 @@ defmodule Pleroma.Object.Fetcher do    end    defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do +    has_history? = fn +      %{"formerRepresentations" => %{"orderedItems" => list}} when is_list(list) -> true +      _ -> false +    end +      internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields()) +    remote_history_exists? = has_history?.(new_data) + +    # If the remote history exists, we treat that as the only source of truth. +    new_data = +      if has_history?.(old_data) and not remote_history_exists? do +        Map.put(new_data, "formerRepresentations", old_data["formerRepresentations"]) +      else +        new_data +      end + +    # If the remote does not have history information, we need to manage it ourselves +    new_data = +      if not remote_history_exists? do +        changed? = +          Pleroma.Constants.status_updatable_fields() +          |> Enum.any?(fn field -> Map.get(old_data, field) != Map.get(new_data, field) end) + +        %{updated_object: updated_object} = +          new_data +          |> Object.Updater.maybe_update_history(old_data, +            updated: changed?, +            use_history_in_new_object?: false +          ) + +        updated_object +      else +        new_data +      end +      Map.merge(new_data, internal_fields)    end @@ -200,6 +235,10 @@ defmodule Pleroma.Object.Fetcher do           {:ok, body} <- get_object(id),           {:ok, data} <- safe_json_decode(body),           :ok <- Containment.contain_origin_from_id(id, data) do +      if not Instances.reachable?(id) do +        Instances.set_reachable(id) +      end +        {:ok, data}      else        {:scheme, _} -> diff --git a/lib/pleroma/object/updater.ex b/lib/pleroma/object/updater.ex new file mode 100644 index 000000000..ab38d3ed2 --- /dev/null +++ b/lib/pleroma/object/updater.ex @@ -0,0 +1,240 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Object.Updater do +  require Pleroma.Constants + +  def update_content_fields(orig_object_data, updated_object) do +    Pleroma.Constants.status_updatable_fields() +    |> Enum.reduce( +      %{data: orig_object_data, updated: false}, +      fn field, %{data: data, updated: updated} -> +        updated = +          updated or +            (field != "updated" and +               Map.get(updated_object, field) != Map.get(orig_object_data, field)) + +        data = +          if Map.has_key?(updated_object, field) do +            Map.put(data, field, updated_object[field]) +          else +            Map.drop(data, [field]) +          end + +        %{data: data, updated: updated} +      end +    ) +  end + +  def maybe_history(object) do +    with history <- Map.get(object, "formerRepresentations"), +         true <- is_map(history), +         "OrderedCollection" <- Map.get(history, "type"), +         true <- is_list(Map.get(history, "orderedItems")), +         true <- is_integer(Map.get(history, "totalItems")) do +      history +    else +      _ -> nil +    end +  end + +  def history_for(object) do +    with history when not is_nil(history) <- maybe_history(object) do +      history +    else +      _ -> history_skeleton() +    end +  end + +  defp history_skeleton do +    %{ +      "type" => "OrderedCollection", +      "totalItems" => 0, +      "orderedItems" => [] +    } +  end + +  def maybe_update_history( +        updated_object, +        orig_object_data, +        opts +      ) do +    updated = opts[:updated] +    use_history_in_new_object? = opts[:use_history_in_new_object?] + +    if not updated do +      %{updated_object: updated_object, used_history_in_new_object?: false} +    else +      # Put edit history +      # Note that we may have got the edit history by first fetching the object +      {new_history, used_history_in_new_object?} = +        with true <- use_history_in_new_object?, +             updated_history when not is_nil(updated_history) <- maybe_history(opts[:new_data]) do +          {updated_history, true} +        else +          _ -> +            history = history_for(orig_object_data) + +            latest_history_item = +              orig_object_data +              |> Map.drop(["id", "formerRepresentations"]) + +            updated_history = +              history +              |> Map.put("orderedItems", [latest_history_item | history["orderedItems"]]) +              |> Map.put("totalItems", history["totalItems"] + 1) + +            {updated_history, false} +        end + +      updated_object = +        updated_object +        |> Map.put("formerRepresentations", new_history) + +      %{updated_object: updated_object, used_history_in_new_object?: used_history_in_new_object?} +    end +  end + +  defp maybe_update_poll(to_be_updated, updated_object) do +    choice_key = fn data -> +      if Map.has_key?(data, "anyOf"), do: "anyOf", else: "oneOf" +    end + +    with true <- to_be_updated["type"] == "Question", +         key <- choice_key.(updated_object), +         true <- key == choice_key.(to_be_updated), +         orig_choices <- to_be_updated[key] |> Enum.map(&Map.drop(&1, ["replies"])), +         new_choices <- updated_object[key] |> Enum.map(&Map.drop(&1, ["replies"])), +         true <- orig_choices == new_choices do +      # Choices are the same, but counts are different +      to_be_updated +      |> Map.put(key, updated_object[key]) +    else +      # Choices (or vote type) have changed, do not allow this +      _ -> to_be_updated +    end +  end + +  # This calculates the data to be sent as the object of an Update. +  # new_data's formerRepresentations is not considered. +  # formerRepresentations is added to the returned data. +  def make_update_object_data(original_data, new_data, date) do +    %{data: updated_data, updated: updated} = +      original_data +      |> update_content_fields(new_data) + +    if not updated do +      updated_data +    else +      %{updated_object: updated_data} = +        updated_data +        |> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false) + +      updated_data +      |> Map.put("updated", date) +    end +  end + +  # This calculates the data of the new Object from an Update. +  # new_data's formerRepresentations is considered. +  def make_new_object_data_from_update_object(original_data, new_data) do +    update_is_reasonable = +      with {_, updated} when not is_nil(updated) <- {:cur_updated, new_data["updated"]}, +           {_, {:ok, updated_time, _}} <- {:cur_updated, DateTime.from_iso8601(updated)}, +           {_, last_updated} when not is_nil(last_updated) <- +             {:last_updated, original_data["updated"] || original_data["published"]}, +           {_, {:ok, last_updated_time, _}} <- +             {:last_updated, DateTime.from_iso8601(last_updated)}, +           :gt <- DateTime.compare(updated_time, last_updated_time) do +        :update_everything +      else +        # only allow poll updates +        {:cur_updated, _} -> :no_content_update +        :eq -> :no_content_update +        # allow all updates +        {:last_updated, _} -> :update_everything +        # allow no updates +        _ -> false +      end + +    %{ +      updated_object: updated_data, +      used_history_in_new_object?: used_history_in_new_object?, +      updated: updated +    } = +      if update_is_reasonable == :update_everything do +        %{data: updated_data, updated: updated} = +          original_data +          |> update_content_fields(new_data) + +        updated_data +        |> maybe_update_history(original_data, +          updated: updated, +          use_history_in_new_object?: true, +          new_data: new_data +        ) +        |> Map.put(:updated, updated) +      else +        %{ +          updated_object: original_data, +          used_history_in_new_object?: false, +          updated: false +        } +      end + +    updated_data = +      if update_is_reasonable != false do +        updated_data +        |> maybe_update_poll(new_data) +      else +        updated_data +      end + +    %{ +      updated_data: updated_data, +      updated: updated, +      used_history_in_new_object?: used_history_in_new_object? +    } +  end + +  def for_each_history_item(%{"orderedItems" => items} = history, _object, fun) do +    new_items = +      Enum.map(items, fun) +      |> Enum.reduce_while( +        {:ok, []}, +        fn +          {:ok, item}, {:ok, acc} -> {:cont, {:ok, acc ++ [item]}} +          e, _acc -> {:halt, e} +        end +      ) + +    case new_items do +      {:ok, items} -> {:ok, Map.put(history, "orderedItems", items)} +      e -> e +    end +  end + +  def for_each_history_item(history, _, _) do +    {:ok, history} +  end + +  def do_with_history(object, fun) do +    with history <- object["formerRepresentations"], +         object <- Map.drop(object, ["formerRepresentations"]), +         {_, {:ok, object}} <- {:main_body, fun.(object)}, +         {_, {:ok, history}} <- {:history_items, for_each_history_item(history, object, fun)} do +      object = +        if history do +          Map.put(object, "formerRepresentations", history) +        else +          object +        end + +      {:ok, object} +    else +      {:main_body, e} -> e +      {:history_items, e} -> e +    end +  end +end diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index dbe6fd209..5cfdae051 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -10,17 +10,14 @@ defmodule Pleroma.Signature do    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub +  @known_suffixes ["/publickey", "/main-key"] +    def key_id_to_actor_id(key_id) do      uri = -      URI.parse(key_id) +      key_id +      |> URI.parse()        |> Map.put(:fragment, nil) - -    uri = -      if not is_nil(uri.path) and String.ends_with?(uri.path, "/publickey") do -        Map.put(uri, :path, String.replace(uri.path, "/publickey", "")) -      else -        uri -      end +      |> remove_suffix(@known_suffixes)      maybe_ap_id = URI.to_string(uri) @@ -36,6 +33,16 @@ defmodule Pleroma.Signature do      end    end +  defp remove_suffix(uri, [test | rest]) do +    if not is_nil(uri.path) and String.ends_with?(uri.path, test) do +      Map.put(uri, :path, String.replace(uri.path, test, "")) +    else +      remove_suffix(uri, rest) +    end +  end + +  defp remove_suffix(uri, []), do: uri +    def fetch_public_key(conn) do      with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),           {:ok, actor_id} <- key_id_to_actor_id(kid), @@ -59,9 +66,8 @@ defmodule Pleroma.Signature do      end    end -  def sign(%User{} = user, headers) do -    with {:ok, %{keys: keys}} <- User.ensure_keys_present(user), -         {:ok, private_key, _} <- Keys.keys_from_pem(keys) do +  def sign(%User{keys: keys} = user, headers) do +    with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do        HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)      end    end diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index db2909276..4aee9326f 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -36,6 +36,7 @@ defmodule Pleroma.Upload do    alias Ecto.UUID    alias Pleroma.Config    alias Pleroma.Maps +  alias Pleroma.Web.ActivityPub.Utils    require Logger    @type source :: @@ -99,6 +100,7 @@ defmodule Pleroma.Upload do           {:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do        {:ok,         %{ +         "id" => Utils.generate_object_id(),           "type" => opts.activity_type,           "mediaType" => upload.content_type,           "url" => [ diff --git a/lib/pleroma/upload/filter/exiftool/read_description.ex b/lib/pleroma/upload/filter/exiftool/read_description.ex index 03d698a81..543b22031 100644 --- a/lib/pleroma/upload/filter/exiftool/read_description.ex +++ b/lib/pleroma/upload/filter/exiftool/read_description.ex @@ -33,7 +33,10 @@ defmodule Pleroma.Upload.Filter.Exiftool.ReadDescription do    defp read_when_empty(_, file, tag) do      try do        {tag_content, 0} = -        System.cmd("exiftool", ["-b", "-s3", tag, file], stderr_to_stdout: true, parallelism: true) +        System.cmd("exiftool", ["-b", "-s3", tag, file], +          stderr_to_stdout: false, +          parallelism: true +        )        tag_content = String.trim(tag_content) diff --git a/lib/pleroma/upload/filter/exiftool/strip_location.ex b/lib/pleroma/upload/filter/exiftool/strip_location.ex index 6100527d3..f2bcc4622 100644 --- a/lib/pleroma/upload/filter/exiftool/strip_location.ex +++ b/lib/pleroma/upload/filter/exiftool/strip_location.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripLocation do    # Formats not compatible with exiftool at this time    def filter(%Pleroma.Upload{content_type: "image/heic"}), do: {:ok, :noop}    def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop} +  def filter(%Pleroma.Upload{content_type: "image/svg" <> _}), do: {:ok, :noop}    def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do      try do diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index a57295891..f6e30555c 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -326,7 +326,7 @@ defmodule Pleroma.User do    end    def visible_for(%User{} = user, for_user) do -    if superuser?(for_user) do +    if privileged?(for_user, :users_manage_activation_state) do        :visible      else        visible_account_status(user) @@ -353,10 +353,45 @@ defmodule Pleroma.User do      end    end -  @spec superuser?(User.t()) :: boolean() -  def superuser?(%User{local: true, is_admin: true}), do: true -  def superuser?(%User{local: true, is_moderator: true}), do: true -  def superuser?(_), do: false +  @spec privileged?(User.t(), atom()) :: boolean() +  def privileged?(%User{is_admin: false, is_moderator: false}, _), do: false + +  def privileged?( +        %User{local: true, is_admin: is_admin, is_moderator: is_moderator}, +        privilege_tag +      ), +      do: +        privileged_for?(privilege_tag, is_admin, :admin_privileges) or +          privileged_for?(privilege_tag, is_moderator, :moderator_privileges) + +  def privileged?(_, _), do: false + +  defp privileged_for?(privilege_tag, true, config_role_key), +    do: privilege_tag in Config.get([:instance, config_role_key]) + +  defp privileged_for?(_, _, _), do: false + +  @spec privileges(User.t()) :: [atom()] +  def privileges(%User{local: false}) do +    [] +  end + +  def privileges(%User{is_moderator: false, is_admin: false}) do +    [] +  end + +  def privileges(%User{local: true, is_moderator: true, is_admin: true}) do +    (Config.get([:instance, :moderator_privileges]) ++ Config.get([:instance, :admin_privileges])) +    |> Enum.uniq() +  end + +  def privileges(%User{local: true, is_moderator: true, is_admin: false}) do +    Config.get([:instance, :moderator_privileges]) +  end + +  def privileges(%User{local: true, is_moderator: false, is_admin: true}) do +    Config.get([:instance, :admin_privileges]) +  end    @spec invisible?(User.t()) :: boolean()    def invisible?(%User{invisible: true}), do: true @@ -611,7 +646,13 @@ defmodule Pleroma.User do           {:ok, new_value} <- value_function.(value) do        put_change(changeset, map_field, new_value)      else -      _ -> changeset +      {:error, :file_too_large} -> +        Ecto.Changeset.validate_change(changeset, map_field, fn map_field, _value -> +          [{map_field, "file is too large"}] +        end) + +      _ -> +        changeset      end    end @@ -711,6 +752,7 @@ defmodule Pleroma.User do      |> put_ap_id()      |> unique_constraint(:ap_id)      |> put_following_and_follower_and_featured_address() +    |> put_private_key()    end    def register_changeset(struct, params \\ %{}, opts \\ []) do @@ -768,6 +810,7 @@ defmodule Pleroma.User do      |> put_ap_id()      |> unique_constraint(:ap_id)      |> put_following_and_follower_and_featured_address() +    |> put_private_key()    end    def validate_not_restricted_nickname(changeset, field) do @@ -846,6 +889,11 @@ defmodule Pleroma.User do      |> put_change(:featured_address, featured)    end +  defp put_private_key(changeset) do +    {:ok, pem} = Keys.generate_rsa_pem() +    put_change(changeset, :keys, pem) +  end +    defp autofollow_users(user) do      candidates = Config.get([:instance, :autofollowed_nicknames]) @@ -898,7 +946,7 @@ defmodule Pleroma.User do      end    end -  defp send_user_approval_email(user) do +  defp send_user_approval_email(%User{email: email} = user) when is_binary(email) do      user      |> Pleroma.Emails.UserEmail.approval_pending_email()      |> Pleroma.Emails.Mailer.deliver_async() @@ -906,6 +954,10 @@ defmodule Pleroma.User do      {:ok, :enqueued}    end +  defp send_user_approval_email(_user) do +    {:ok, :skipped} +  end +    defp send_admin_approval_emails(user) do      all_superusers()      |> Enum.filter(fn user -> not is_nil(user.email) end) @@ -1150,24 +1202,10 @@ defmodule Pleroma.User do      |> update_and_set_cache()    end -  def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do -    was_superuser_before_update = User.superuser?(user) - +  def update_and_set_cache(changeset) do      with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do        set_cache(user)      end -    |> maybe_remove_report_notifications(was_superuser_before_update) -  end - -  defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do -    if not User.superuser?(user), -      do: user |> Notification.destroy_multiple_from_types(["pleroma:report"]) - -    result -  end - -  defp maybe_remove_report_notifications(result, _) do -    result    end    def get_user_friends_ap_ids(user) do @@ -2086,6 +2124,7 @@ defmodule Pleroma.User do        follower_address: uri <> "/followers"      }      |> change +    |> put_private_key()      |> unique_constraint(:nickname)      |> Repo.insert()      |> set_cache() @@ -2118,7 +2157,8 @@ defmodule Pleroma.User do    @doc "Gets or fetch a user by uri or nickname."    @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()} -  def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri) +  def get_or_fetch("http://" <> _host = uri), do: get_or_fetch_by_ap_id(uri) +  def get_or_fetch("https://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)    def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)    # wait a period of time and return newest version of the User structs @@ -2246,6 +2286,11 @@ defmodule Pleroma.User do      |> Repo.all()    end +  @spec all_users_with_privilege(atom()) :: [User.t()] +  def all_users_with_privilege(privilege) do +    User.Query.build(%{is_privileged: privilege}) |> Repo.all() +  end +    def muting_reblogs?(%User{} = user, %User{} = target) do      UserRelationship.reblog_mute_exists?(user, target)    end @@ -2351,17 +2396,6 @@ defmodule Pleroma.User do      }    end -  def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user} - -  def ensure_keys_present(%User{} = user) do -    with {:ok, pem} <- Keys.generate_rsa_pem() do -      user -      |> cast(%{keys: pem}, [:keys]) -      |> validate_required([:keys]) -      |> update_and_set_cache() -    end -  end -    def get_ap_ids_by_nicknames(nicknames) do      from(u in User,        where: u.nickname in ^nicknames, diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index 20bc1ea61..3e090cac0 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -29,6 +29,7 @@ defmodule Pleroma.User.Query do    import Ecto.Query    import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] +  alias Pleroma.Config    alias Pleroma.FollowingRelationship    alias Pleroma.User @@ -49,6 +50,7 @@ defmodule Pleroma.User.Query do              is_suggested: boolean(),              is_discoverable: boolean(),              super_users: boolean(), +            is_privileged: atom(),              invisible: boolean(),              internal: boolean(),              followers: User.t(), @@ -136,6 +138,43 @@ defmodule Pleroma.User.Query do      )    end +  defp compose_query({:is_privileged, privilege}, query) do +    moderator_privileged = privilege in Config.get([:instance, :moderator_privileges]) +    admin_privileged = privilege in Config.get([:instance, :admin_privileges]) + +    query = compose_query({:active, true}, query) +    query = compose_query({:local, true}, query) + +    case {admin_privileged, moderator_privileged} do +      {false, false} -> +        where( +          query, +          false +        ) + +      {true, true} -> +        where( +          query, +          [u], +          u.is_admin or u.is_moderator +        ) + +      {true, false} -> +        where( +          query, +          [u], +          u.is_admin +        ) + +      {false, true} -> +        where( +          query, +          [u], +          u.is_moderator +        ) +    end +  end +    defp compose_query({:local, _}, query), do: location_query(query, true)    defp compose_query({:external, _}, query), do: location_query(query, false) diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index cd6f69f56..a7fb8fb83 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -94,6 +94,7 @@ defmodule Pleroma.User.Search do      |> subquery()      |> order_by(desc: :search_rank)      |> maybe_restrict_local(for_user) +    |> filter_deactivated_users()    end    defp select_top_users(query, top_user_ids) do @@ -166,6 +167,10 @@ defmodule Pleroma.User.Search do      from(q in query, where: q.actor_type != "Application")    end +  defp filter_deactivated_users(query) do +    from(q in query, where: q.is_active == true) +  end +    defp filter_blocked_user(query, %User{} = blocker) do      query      |> join(:left, [u], b in Pleroma.UserRelationship, diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 5b3e593d3..fbecf3129 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -91,8 +91,9 @@ defmodule Pleroma.UserRelationship do        expires_at: expires_at      })      |> Repo.insert( -      on_conflict: {:replace_all_except, [:id]}, -      conflict_target: [:source_id, :relationship_type, :target_id] +      on_conflict: {:replace_all_except, [:id, :inserted_at]}, +      conflict_target: [:source_id, :relationship_type, :target_id], +      returning: true      )    end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index bded254c6..f22756015 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -96,7 +96,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp increase_replies_count_if_reply(_create_data), do: :noop -  @object_types ~w[ChatMessage Question Answer Audio Video Event Article Note Page] +  @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      with {:ok, object} <- Object.create(object) do @@ -190,7 +190,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    def notify_and_stream(activity) do      Notification.create_notifications(activity) -    conversation = create_or_bump_conversation(activity, activity.actor) +    original_activity = +      case activity do +        %{data: %{"type" => "Update"}, object: %{data: %{"id" => id}}} -> +          Activity.get_create_by_object_ap_id_with_object(id) + +        _ -> +          activity +      end + +    conversation = create_or_bump_conversation(original_activity, original_activity.actor)      participations = get_participations(conversation)      stream_out(activity)      stream_out_participations(participations) @@ -256,7 +265,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    @impl true    def stream_out(%Activity{data: %{"type" => data_type}} = activity) -      when data_type in ["Create", "Announce", "Delete"] do +      when data_type in ["Create", "Announce", "Delete", "Update"] do      activity      |> Topics.get_activity_topics()      |> Streamer.stream(activity) @@ -392,11 +401,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do           _ <- notify_and_stream(activity),           :ok <-             maybe_federate(stripped_activity) do -      User.all_superusers() +      User.all_users_with_privilege(:reports_manage_reports)        |> Enum.filter(fn user -> user.ap_id != actor end)        |> Enum.filter(fn user -> not is_nil(user.email) end) -      |> Enum.each(fn superuser -> -        superuser +      |> Enum.each(fn privileged_user -> +        privileged_user          |> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)          |> Pleroma.Emails.Mailer.deliver_async()        end) @@ -1150,8 +1159,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        [activity, object: o] in query,        where:          fragment( -          "(?)->>'type' = 'Create' and coalesce((?)->'object'->>'id', (?)->>'object') = any (?)", -          activity.data, +          "(?)->>'type' = 'Create' and associated_object_id((?)) = any (?)",            activity.data,            activity.data,            ^ids @@ -1231,15 +1239,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end +  defp exclude_invisible_actors(query, %{type: "Flag"}), do: query    defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query    defp exclude_invisible_actors(query, _opts) do -    invisible_ap_ids = -      User.Query.build(%{invisible: true, select: [:ap_id]}) -      |> Repo.all() -      |> Enum.map(fn %{ap_id: ap_id} -> ap_id end) - -    from([activity] in query, where: activity.actor not in ^invisible_ap_ids) +    query +    |> join(:inner, [activity], u in User, +      as: :u, +      on: activity.actor == u.ap_id and u.invisible == false +    )    end    defp exclude_id(query, %{exclude_id: id}) when is_binary(id) do @@ -1369,7 +1377,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        |> restrict_instance(opts)        |> restrict_announce_object_actor(opts)        |> restrict_filtered(opts) -      |> Activity.restrict_deactivated_users() +      |> maybe_restrict_deactivated_users(opts)        |> exclude_poll_votes(opts)        |> exclude_chat_messages(opts)        |> exclude_invisible_actors(opts) @@ -1445,13 +1453,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}    def upload(file, opts \\ []) do -    with {:ok, data} <- Upload.store(file, opts) do +    with {:ok, data} <- Upload.store(sanitize_upload_file(file), opts) do        obj_data = Maps.put_if_present(data, "actor", opts[:actor])        Repo.insert(%Object{data: obj_data})      end    end +  defp sanitize_upload_file(%Plug.Upload{filename: filename} = upload) when is_binary(filename) do +    %Plug.Upload{ +      upload +      | filename: Path.basename(filename) +    } +  end + +  defp sanitize_upload_file(upload), do: upload +    @spec get_actor_url(any()) :: binary() | nil    defp get_actor_url(url) when is_binary(url), do: url    defp get_actor_url(%{"href" => href}) when is_binary(href), do: href @@ -1474,7 +1491,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()    defp normalize_image(_), do: nil -  defp object_to_user_data(data) do +  defp object_to_user_data(data, additional) do      fields =        data        |> Map.get("attachment", []) @@ -1506,15 +1523,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      public_key =        if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do          data["publicKey"]["publicKeyPem"] -      else -        nil        end      shared_inbox =        if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do          data["endpoints"]["sharedInbox"] -      else -        nil        end      birthday = @@ -1523,13 +1536,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do            {:ok, date} -> date            {:error, _} -> nil          end -      else -        nil        end      show_birthday = !!birthday -    user_data = %{ +    # if WebFinger request was already done, we probably have acct, otherwise +    # we request WebFinger here +    nickname = additional[:nickname_from_acct] || generate_nickname(data) + +    %{        ap_id: data["id"],        uri: get_actor_url(data["url"]),        ap_enabled: true, @@ -1551,23 +1566,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        inbox: data["inbox"],        shared_inbox: shared_inbox,        accepts_chat_messages: accepts_chat_messages, -      pinned_objects: pinned_objects,        birthday: birthday, -      show_birthday: show_birthday +      show_birthday: show_birthday, +      pinned_objects: pinned_objects, +      nickname: nickname      } +  end -    # nickname can be nil because of virtual actors -    if data["preferredUsername"] do -      Map.put( -        user_data, -        :nickname, -        "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}" -      ) +  defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do +    generated = "#{username}@#{URI.parse(data["id"]).host}" + +    if Config.get([WebFinger, :update_nickname_on_user_fetch]) do +      case WebFinger.finger(generated) do +        {:ok, %{"subject" => "acct:" <> acct}} -> acct +        _ -> generated +      end      else -      Map.put(user_data, :nickname, nil) +      generated      end    end +  # nickname can be nil because of virtual actors +  defp generate_nickname(_), do: nil +    def fetch_follow_information_for_user(user) do      with {:ok, following_data} <-             Fetcher.fetch_and_contain_remote_object_from_id(user.following_address), @@ -1639,17 +1660,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp collection_private(_data), do: {:ok, true} -  def user_data_from_user_object(data) do +  def user_data_from_user_object(data, additional \\ []) do      with {:ok, data} <- MRF.filter(data) do -      {:ok, object_to_user_data(data)} +      {:ok, object_to_user_data(data, additional)}      else        e -> {:error, e}      end    end -  def fetch_and_prepare_user_from_ap_id(ap_id) do +  def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do      with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id), -         {:ok, data} <- user_data_from_user_object(data) do +         {:ok, data} <- user_data_from_user_object(data, additional) do        {:ok, maybe_update_follow_information(data)}      else        # If this has been deleted, only log a debug and not an error @@ -1727,13 +1748,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  def make_user_from_ap_id(ap_id) do +  def make_user_from_ap_id(ap_id, additional \\ []) do      user = User.get_cached_by_ap_id(ap_id)      if user && !User.ap_enabled?(user) do        Transmogrifier.upgrade_user_from_ap_id(ap_id)      else -      with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do +      with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do          {:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)          if user do @@ -1753,8 +1774,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    end    def make_user_from_nickname(nickname) do -    with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do -      make_user_from_ap_id(ap_id) +    with {:ok, %{"ap_id" => ap_id, "subject" => "acct:" <> acct}} when not is_nil(ap_id) <- +           WebFinger.finger(nickname) do +      make_user_from_ap_id(ap_id, nickname_from_acct: acct)      else        _e -> {:error, "No AP id in WebFinger"}      end @@ -1776,4 +1798,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> restrict_visibility(%{visibility: "direct"})      |> order_by([activity], asc: activity.id)    end + +  defp maybe_restrict_deactivated_users(activity, %{type: "Flag"}), do: activity + +  defp maybe_restrict_deactivated_users(activity, _opts), +    do: Activity.restrict_deactivated_users(activity)  end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index b8f63d69d..1357c379c 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -66,8 +66,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    end    def user(conn, %{"nickname" => nickname}) do -    with %User{local: true} = user <- User.get_cached_by_nickname(nickname), -         {:ok, user} <- User.ensure_keys_present(user) do +    with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do        conn        |> put_resp_content_type("application/activity+json")        |> put_view(UserView) @@ -174,7 +173,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do      with %User{} = user <- User.get_cached_by_nickname(nickname), -         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),           {:show_follows, true} <-             {:show_follows, (for_user && for_user == user) || !user.hide_follows} do        {page, _} = Integer.parse(page) @@ -192,8 +190,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    end    def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do -    with %User{} = user <- User.get_cached_by_nickname(nickname), -         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        conn        |> put_resp_content_type("application/activity+json")        |> put_view(UserView) @@ -213,7 +210,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do      with %User{} = user <- User.get_cached_by_nickname(nickname), -         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),           {:show_followers, true} <-             {:show_followers, (for_user && for_user == user) || !user.hide_followers} do        {page, _} = Integer.parse(page) @@ -231,8 +227,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    end    def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do -    with %User{} = user <- User.get_cached_by_nickname(nickname), -         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        conn        |> put_resp_content_type("application/activity+json")        |> put_view(UserView) @@ -245,8 +240,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do          %{"nickname" => nickname, "page" => page?} = params        )        when page? in [true, "true"] do -    with %User{} = user <- User.get_cached_by_nickname(nickname), -         {:ok, user} <- User.ensure_keys_present(user) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        # "include_poll_votes" is a hack because postgres generates inefficient        # queries when filtering by 'Answer', poll votes will be hidden by the        # visibility filter in this case anyway @@ -270,8 +264,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    end    def outbox(conn, %{"nickname" => nickname}) do -    with %User{} = user <- User.get_cached_by_nickname(nickname), -         {:ok, user} <- User.ensure_keys_present(user) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        conn        |> put_resp_content_type("application/activity+json")        |> put_view(UserView) @@ -328,14 +321,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    end    defp represent_service_actor(%User{} = user, conn) do -    with {:ok, user} <- User.ensure_keys_present(user) do -      conn -      |> put_resp_content_type("application/activity+json") -      |> put_view(UserView) -      |> render("user.json", %{user: user}) -    else -      nil -> {:error, :not_found} -    end +    conn +    |> put_resp_content_type("application/activity+json") +    |> put_view(UserView) +    |> render("user.json", %{user: user})    end    defp represent_service_actor(nil, _), do: {:error, :not_found} @@ -388,12 +377,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{          "nickname" => nickname        }) do -    with {:ok, user} <- User.ensure_keys_present(user) do -      conn -      |> put_resp_content_type("application/activity+json") -      |> put_view(UserView) -      |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"}) -    end +    conn +    |> put_resp_content_type("application/activity+json") +    |> put_view(UserView) +    |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})    end    def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{ @@ -530,19 +517,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do      conn    end -  defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do -    {:ok, new_user} = User.ensure_keys_present(user) - -    for_user = -      if new_user != user and match?(%User{}, for_user) do -        User.get_cached_by_nickname(for_user.nickname) -      else -        for_user -      end - -    {new_user, for_user} -  end -    def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do      with {:ok, object} <-             ActivityPub.upload( diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 5b25138a4..8eab3a241 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -16,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.ActivityPub.Visibility    alias Pleroma.Web.CommonAPI.ActivityDraft +  alias Pleroma.Web.Endpoint    require Pleroma.Constants @@ -54,13 +55,87 @@ defmodule Pleroma.Web.ActivityPub.Builder do      {:ok, data, []}    end +  defp unicode_emoji_react(_object, data, emoji) do +    data +    |> Map.put("content", emoji) +    |> Map.put("type", "EmojiReact") +  end + +  defp add_emoji_content(data, emoji, url) do +    tag = [ +      %{ +        "id" => url, +        "type" => "Emoji", +        "name" => Emoji.maybe_quote(emoji), +        "icon" => %{ +          "type" => "Image", +          "url" => url +        } +      } +    ] + +    data +    |> Map.put("content", Emoji.maybe_quote(emoji)) +    |> Map.put("type", "EmojiReact") +    |> Map.put("tag", tag) +  end + +  defp remote_custom_emoji_react( +         %{data: %{"reactions" => existing_reactions}}, +         data, +         emoji +       ) do +    [emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@") + +    matching_reaction = +      Enum.find( +        existing_reactions, +        fn [name, _, url] -> +          if url != nil do +            url = URI.parse(url) +            url.host == instance && name == emoji_code +          end +        end +      ) + +    if matching_reaction do +      [name, _, url] = matching_reaction +      add_emoji_content(data, name, url) +    else +      {:error, "Could not react"} +    end +  end + +  defp remote_custom_emoji_react(_object, _data, _emoji) do +    {:error, "Could not react"} +  end + +  defp local_custom_emoji_react(data, emoji) do +    with %{file: path} = emojo <- Emoji.get(emoji) do +      url = "#{Endpoint.url()}#{path}" +      add_emoji_content(data, emojo.code, url) +    else +      _ -> {:error, "Emoji does not exist"} +    end +  end + +  defp custom_emoji_react(object, data, emoji) do +    if String.contains?(emoji, "@") do +      remote_custom_emoji_react(object, data, emoji) +    else +      local_custom_emoji_react(data, emoji) +    end +  end +    @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}    def emoji_react(actor, object, emoji) do      with {:ok, data, meta} <- object_action(actor, object) do        data = -        data -        |> Map.put("content", emoji) -        |> Map.put("type", "EmojiReact") +        if Emoji.is_unicode_emoji?(emoji) do +          unicode_emoji_react(object, data, emoji) +        else +          custom_emoji_react(object, data, emoji) +        end        {:ok, data, meta}      end @@ -218,10 +293,16 @@ defmodule Pleroma.Web.ActivityPub.Builder do      end    end -  # Retricted to user updates for now, always public    @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}    def update(actor, object) do -    to = [Pleroma.Constants.as_public(), actor.follower_address] +    {to, cc} = +      if object["type"] in Pleroma.Constants.actor_types() do +        # User updates, always public +        {[Pleroma.Constants.as_public(), actor.follower_address], []} +      else +        # Status updates, follow the recipients in the object +        {object["to"] || [], object["cc"] || []} +      end      {:ok,       %{ @@ -229,7 +310,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do         "type" => "Update",         "actor" => actor.ap_id,         "object" => object, -       "to" => to +       "to" => to, +       "cc" => cc       }, []}    end diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index 323ecdbf1..ff9f84497 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -53,10 +53,53 @@ defmodule Pleroma.Web.ActivityPub.MRF do    @required_description_keys [:key, :related_policy] +  def filter_one(policy, message) do +    should_plug_history? = +      if function_exported?(policy, :history_awareness, 0) do +        policy.history_awareness() +      else +        :manual +      end +      |> Kernel.==(:auto) + +    if not should_plug_history? do +      policy.filter(message) +    else +      main_result = policy.filter(message) + +      with {_, {:ok, main_message}} <- {:main, main_result}, +           {_, +            %{ +              "formerRepresentations" => %{ +                "orderedItems" => [_ | _] +              } +            }} = {_, object} <- {:object, message["object"]}, +           {_, {:ok, new_history}} <- +             {:history, +              Pleroma.Object.Updater.for_each_history_item( +                object["formerRepresentations"], +                object, +                fn item -> +                  with {:ok, filtered} <- policy.filter(Map.put(message, "object", item)) do +                    {:ok, filtered["object"]} +                  else +                    e -> e +                  end +                end +              )} do +        {:ok, put_in(main_message, ["object", "formerRepresentations"], new_history)} +      else +        {:main, _} -> main_result +        {:object, _} -> main_result +        {:history, e} -> e +      end +    end +  end +    def filter(policies, %{} = message) do      policies      |> Enum.reduce({:ok, message}, fn -      policy, {:ok, message} -> policy.filter(message) +      policy, {:ok, message} -> filter_one(policy, message)        _, error -> error      end)    end diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex index f0504ead4..3ec9c52ee 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex @@ -9,6 +9,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do    require Logger +  @impl true +  def history_awareness, do: :auto +    # has the user successfully posted before?    defp old_user?(%User{} = u) do      u.note_count > 0 || u.follower_count > 0 diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex index 51596c09f..a148cc1e7 100644 --- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex +++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do    @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) +  def history_awareness, do: :auto +    def filter_by_summary(          %{data: %{"summary" => parent_summary}} = _in_reply_to,          %{"summary" => child_summary} = child @@ -27,8 +29,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do    def filter_by_summary(_in_reply_to, child), do: child -  def filter(%{"type" => "Create", "object" => child_object} = object) -      when is_map(child_object) do +  def filter(%{"type" => type, "object" => child_object} = object) +      when type in ["Create", "Update"] and is_map(child_object) do      child =        child_object["inReplyTo"]        |> Object.normalize(fetch: false) diff --git a/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex b/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex index 255910b2f..70224561c 100644 --- a/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex +++ b/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex @@ -11,6 +11,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do    @behaviour Pleroma.Web.ActivityPub.MRF.Policy +  @impl true +  def history_awareness, do: :auto +    defp do_extract({:a, attrs, _}, acc) do      if Enum.find(attrs, fn {name, value} ->           name == "class" && value in ["mention", "u-url mention", "mention u-url"] @@ -74,11 +77,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do    @impl true    def filter(          %{ -          "type" => "Create", +          "type" => type,            "object" => %{"type" => "Note", "to" => to, "inReplyTo" => in_reply_to}          } = object        ) -      when is_list(to) and is_binary(in_reply_to) do +      when type in ["Create", "Update"] and is_list(to) and is_binary(in_reply_to) do      # image-only posts from pleroma apparently reach this MRF without the content field      content = object["object"]["content"] || "" diff --git a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex index 2142b7add..b73fd974c 100644 --- a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex @@ -16,6 +16,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do    @behaviour Pleroma.Web.ActivityPub.MRF.Policy +  @impl true +  def history_awareness, do: :manual +    defp check_reject(message, hashtags) do      if Enum.any?(Config.get([:mrf_hashtag, :reject]), fn match -> match in hashtags end) do        {:reject, "[HashtagPolicy] Matches with rejected keyword"} @@ -47,22 +50,46 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do    defp check_ftl_removal(message, _hashtags), do: {:ok, message} -  defp check_sensitive(message, hashtags) do -    if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do -      {:ok, Kernel.put_in(message, ["object", "sensitive"], true)} -    else -      {:ok, message} -    end +  defp check_sensitive(message) do +    {:ok, new_object} = +      Object.Updater.do_with_history(message["object"], fn object -> +        hashtags = Object.hashtags(%Object{data: object}) + +        if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do +          {:ok, Map.put(object, "sensitive", true)} +        else +          {:ok, object} +        end +      end) + +    {:ok, Map.put(message, "object", new_object)}    end    @impl true -  def filter(%{"type" => "Create", "object" => object} = message) do -    hashtags = Object.hashtags(%Object{data: object}) +  def filter(%{"type" => type, "object" => object} = message) when type in ["Create", "Update"] do +    history_items = +      with %{"formerRepresentations" => %{"orderedItems" => items}} <- object do +        items +      else +        _ -> [] +      end + +    historical_hashtags = +      Enum.reduce(history_items, [], fn item, acc -> +        acc ++ Object.hashtags(%Object{data: item}) +      end) + +    hashtags = Object.hashtags(%Object{data: object}) ++ historical_hashtags      if hashtags != [] do        with {:ok, message} <- check_reject(message, hashtags), -           {:ok, message} <- check_ftl_removal(message, hashtags), -           {:ok, message} <- check_sensitive(message, hashtags) do +           {:ok, message} <- +             (if "type" == "Create" do +                check_ftl_removal(message, hashtags) +              else +                {:ok, message} +              end), +           {:ok, message} <- check_sensitive(message) do          {:ok, message}        end      else diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index 00b64744f..687ec6c2f 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -27,24 +27,46 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do    end    defp check_reject(%{"object" => %{} = object} = message) do -    payload = object_payload(object) - -    if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern -> -         string_matches?(payload, pattern) -       end) do -      {:reject, "[KeywordPolicy] Matches with rejected keyword"} -    else +    with {:ok, _new_object} <- +           Pleroma.Object.Updater.do_with_history(object, fn object -> +             payload = object_payload(object) + +             if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern -> +                  string_matches?(payload, pattern) +                end) do +               {:reject, "[KeywordPolicy] Matches with rejected keyword"} +             else +               {:ok, message} +             end +           end) do        {:ok, message} +    else +      e -> e      end    end -  defp check_ftl_removal(%{"to" => to, "object" => %{} = object} = message) do -    payload = object_payload(object) +  defp check_ftl_removal(%{"type" => "Create", "to" => to, "object" => %{} = object} = message) do +    check_keyword = fn object -> +      payload = object_payload(object) -    if Pleroma.Constants.as_public() in to and -         Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern -> +      if Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->             string_matches?(payload, pattern)           end) do +        {:should_delist, nil} +      else +        {:ok, %{}} +      end +    end + +    should_delist? = fn object -> +      with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check_keyword) do +        false +      else +        _ -> true +      end +    end + +    if Pleroma.Constants.as_public() in to and should_delist?.(object) do        to = List.delete(to, Pleroma.Constants.as_public())        cc = [Pleroma.Constants.as_public() | message["cc"] || []] @@ -59,8 +81,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do      end    end +  defp check_ftl_removal(message) do +    {:ok, message} +  end +    defp check_replace(%{"object" => %{} = object} = message) do -    object = +    replace_kw = fn object ->        ["content", "name", "summary"]        |> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end)        |> Enum.reduce(object, fn field, object -> @@ -73,6 +99,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do          Map.put(object, field, data)        end) +      |> (fn object -> {:ok, object} end).() +    end + +    {:ok, object} = Pleroma.Object.Updater.do_with_history(object, replace_kw)      message = Map.put(message, "object", object) @@ -80,7 +110,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do    end    @impl true -  def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do +  def filter(%{"type" => type, "object" => %{"content" => _content}} = message) +      when type in ["Create", "Update"] do      with {:ok, message} <- check_reject(message),           {:ok, message} <- check_ftl_removal(message),           {:ok, message} <- check_replace(message) do diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index 0eac8f021..c95d35bb9 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -16,6 +16,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do      recv_timeout: 10_000    ] +  @impl true +  def history_awareness, do: :auto +    defp prefetch(url) do      # Fetching only proxiable resources      if MediaProxy.enabled?() and MediaProxy.url_proxiable?(url) do @@ -54,10 +57,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do    end    @impl true -  def filter( -        %{"type" => "Create", "object" => %{"attachment" => attachments} = _object} = message -      ) -      when is_list(attachments) and length(attachments) > 0 do +  def filter(%{"type" => type, "object" => %{"attachment" => attachments} = _object} = message) +      when type in ["Create", "Update"] and is_list(attachments) and length(attachments) > 0 do      preload(message)      {:ok, message} diff --git a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex index 4dc96e068..855cda3b9 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do    @impl true    def filter(%{"actor" => actor} = object) do      with true <- is_local?(actor), +         true <- is_eligible_type?(object),           true <- is_note?(object),           false <- has_attachment?(object),           true <- only_mentions?(object) do @@ -32,7 +33,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do    end    defp has_attachment?(%{ -         "type" => "Create",           "object" => %{"type" => "Note", "attachment" => attachments}         })         when length(attachments) > 0, @@ -40,7 +40,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do    defp has_attachment?(_), do: false -  defp only_mentions?(%{"type" => "Create", "object" => %{"type" => "Note", "source" => source}}) do +  defp only_mentions?(%{"object" => %{"type" => "Note", "source" => source}}) do +    source = +      case source do +        %{"content" => text} -> text +        _ -> source +      end +      non_mentions =        source |> String.split() |> Enum.filter(&(not String.starts_with?(&1, "@"))) |> length @@ -53,9 +59,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do    defp only_mentions?(_), do: false -  defp is_note?(%{"type" => "Create", "object" => %{"type" => "Note"}}), do: true +  defp is_note?(%{"object" => %{"type" => "Note"}}), do: true    defp is_note?(_), do: false +  defp is_eligible_type?(%{"type" => type}) when type in ["Create", "Update"], do: true +  defp is_eligible_type?(_), do: false +    @impl true    def describe, do: {:ok, %{}}  end diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex index aab647d8e..f81e9e52a 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex @@ -7,13 +7,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do    @behaviour Pleroma.Web.ActivityPub.MRF.Policy    @impl true +  def history_awareness, do: :auto + +  @impl true    def filter(          %{ -          "type" => "Create", +          "type" => type,            "object" => %{"content" => content, "attachment" => _} = _child_object          } = object        ) -      when content in [".", "<p>.</p>"] do +      when type in ["Create", "Update"] and content in [".", "<p>.</p>"] do      {:ok, put_in(object, ["object", "content"], "")}    end diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex index dc2c19d49..2dfc9a901 100644 --- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex +++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex @@ -9,7 +9,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do    @behaviour Pleroma.Web.ActivityPub.MRF.Policy    @impl true -  def filter(%{"type" => "Create", "object" => child_object} = object) do +  def history_awareness, do: :auto + +  @impl true +  def filter(%{"type" => type, "object" => child_object} = object) +      when type in ["Create", "Update"] do      scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy])      content = diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex index 0e9d25a0a..df1a6dcbb 100644 --- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex @@ -131,7 +131,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do            type: {:list, :atom},            description:              "A list of actions to apply to the post. `:delist` removes the post from public timelines; " <> -              "`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines; " <> +              "`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines, additionally for followers-only it degrades to a direct message; " <>                "`:reject` rejects the message entirely",            suggestions: [:delist, :strip_followers, :reject]          } diff --git a/lib/pleroma/web/activity_pub/mrf/policy.ex b/lib/pleroma/web/activity_pub/mrf/policy.ex index 0ac250c3d..0234de4d5 100644 --- a/lib/pleroma/web/activity_pub/mrf/policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/policy.ex @@ -12,5 +12,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do                label: String.t(),                description: String.t()              } -  @optional_callbacks config_description: 0 +  @callback history_awareness() :: :auto | :manual +  @optional_callbacks config_description: 0, history_awareness: 0  end diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index c0c7f3806..829ddeaea 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -40,9 +40,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do    defp check_media_removal(           %{host: actor_host} = _actor_info, -         %{"type" => "Create", "object" => %{"attachment" => child_attachment}} = object +         %{"type" => type, "object" => %{"attachment" => child_attachment}} = object         ) -       when length(child_attachment) > 0 do +       when length(child_attachment) > 0 and type in ["Create", "Update"] do      media_removal =        instance_list(:media_removal)        |> MRF.subdomains_regex() @@ -63,10 +63,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do    defp check_media_nsfw(           %{host: actor_host} = _actor_info,           %{ -           "type" => "Create", +           "type" => type,             "object" => %{} = _child_object           } = object -       ) do +       ) +       when type in ["Create", "Update"] do      media_nsfw =        instance_list(:media_nsfw)        |> MRF.subdomains_regex() diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index 10072b693..73760ca8f 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -27,22 +27,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do    defp process_tag(           "mrf_tag:media-force-nsfw",           %{ -           "type" => "Create", +           "type" => type,             "object" => %{"attachment" => child_attachment}           } = message         ) -       when length(child_attachment) > 0 do +       when length(child_attachment) > 0 and type in ["Create", "Update"] do      {:ok, Kernel.put_in(message, ["object", "sensitive"], true)}    end    defp process_tag(           "mrf_tag:media-strip",           %{ -           "type" => "Create", +           "type" => type,             "object" => %{"attachment" => child_attachment} = object           } = message         ) -       when length(child_attachment) > 0 do +       when length(child_attachment) > 0 and type in ["Create", "Update"] do      object = Map.delete(object, "attachment")      message = Map.put(message, "object", object) @@ -152,7 +152,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do      do: filter_message(target_actor, message)    @impl true -  def filter(%{"actor" => actor, "type" => "Create"} = message), +  def filter(%{"actor" => actor, "type" => type} = message) when type in ["Create", "Update"],      do: filter_message(actor, message)    @impl true diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index f3e31c931..5e0d1aa8e 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator -  alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator +  alias Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator @@ -102,9 +102,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do          %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,          meta        ) -      when objtype in ~w[Question Answer Audio Video Event Article Note Page] do -    with {:ok, object_data} <- cast_and_apply(object), -         meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), +      when objtype in ~w[Question Answer Audio Video Image Event Article Note Page] do +    with {:ok, object_data} <- cast_and_apply_and_stringify_with_history(object), +         meta = Keyword.put(meta, :object_data, object_data),           {:ok, create_activity} <-             create_activity             |> CreateGenericValidator.cast_and_validate(meta) @@ -115,29 +115,64 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    end    def validate(%{"type" => type} = object, meta) -      when type in ~w[Event Question Audio Video Article Note Page] do +      when type in ~w[Event Question Audio Video Image Article Note Page] do      validator =        case type do          "Event" -> EventValidator          "Question" -> QuestionValidator -        "Audio" -> AudioVideoValidator -        "Video" -> AudioVideoValidator +        "Audio" -> AudioImageVideoValidator +        "Video" -> AudioImageVideoValidator +        "Image" -> AudioImageVideoValidator          "Article" -> ArticleNotePageValidator          "Note" -> ArticleNotePageValidator          "Page" -> ArticleNotePageValidator        end      with {:ok, object} <- -           object -           |> validator.cast_and_validate() -           |> Ecto.Changeset.apply_action(:insert) do -      object = stringify_keys(object) +           do_separate_with_history(object, fn object -> +             with {:ok, object} <- +                    object +                    |> validator.cast_and_validate() +                    |> Ecto.Changeset.apply_action(:insert) do +               object = stringify_keys(object) + +               # Insert copy of hashtags as strings for the non-hashtag table indexing +               tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object}) +               object = Map.put(object, "tag", tag) + +               {:ok, object} +             end +           end) do +      {:ok, object, meta} +    end +  end -      # Insert copy of hashtags as strings for the non-hashtag table indexing -      tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object}) -      object = Map.put(object, "tag", tag) +  def validate( +        %{"type" => "Update", "object" => %{"type" => objtype} = object} = update_activity, +        meta +      ) +      when objtype in ~w[Question Answer Audio Video Event Article Note Page] do +    with {_, false} <- {:local, Access.get(meta, :local, false)}, +         {_, {:ok, object_data, _}} <- {:object_validation, validate(object, meta)}, +         meta = Keyword.put(meta, :object_data, object_data), +         {:ok, update_activity} <- +           update_activity +           |> UpdateValidator.cast_and_validate() +           |> Ecto.Changeset.apply_action(:insert) do +      update_activity = stringify_keys(update_activity) +      {:ok, update_activity, meta} +    else +      {:local, _} -> +        with {:ok, object} <- +               update_activity +               |> UpdateValidator.cast_and_validate() +               |> Ecto.Changeset.apply_action(:insert) do +          object = stringify_keys(object) +          {:ok, object, meta} +        end -      {:ok, object, meta} +      {:object_validation, e} -> +        e      end    end @@ -178,6 +213,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    def validate(o, m), do: {:error, {:validator_not_set, {o, m}}} +  def cast_and_apply_and_stringify_with_history(object) do +    do_separate_with_history(object, fn object -> +      with {:ok, object_data} <- cast_and_apply(object), +           object_data <- object_data |> stringify_keys() do +        {:ok, object_data} +      end +    end) +  end +    def cast_and_apply(%{"type" => "ChatMessage"} = object) do      ChatMessageValidator.cast_and_apply(object)    end @@ -190,8 +234,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do      AnswerValidator.cast_and_apply(object)    end -  def cast_and_apply(%{"type" => type} = object) when type in ~w[Audio Video] do -    AudioVideoValidator.cast_and_apply(object) +  def cast_and_apply(%{"type" => type} = object) when type in ~w[Audio Image Video] do +    AudioImageVideoValidator.cast_and_apply(object)    end    def cast_and_apply(%{"type" => "Event"} = object) do @@ -204,8 +248,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    def cast_and_apply(o), do: {:error, {:validator_not_set, o}} -  # is_struct/1 appears in Elixir 1.11 -  def stringify_keys(%{__struct__: _} = object) do +  def stringify_keys(object) when is_struct(object) do      object      |> Map.from_struct()      |> stringify_keys @@ -236,4 +279,54 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do      Object.normalize(object["object"], fetch: true)      :ok    end + +  defp for_each_history_item( +         %{"type" => "OrderedCollection", "orderedItems" => items} = history, +         object, +         fun +       ) do +    processed_items = +      Enum.map(items, fn item -> +        with item <- Map.put(item, "id", object["id"]), +             {:ok, item} <- fun.(item) do +          item +        else +          _ -> nil +        end +      end) + +    if Enum.all?(processed_items, &(not is_nil(&1))) do +      {:ok, Map.put(history, "orderedItems", processed_items)} +    else +      {:error, :invalid_history} +    end +  end + +  defp for_each_history_item(nil, _object, _fun) do +    {:ok, nil} +  end + +  defp for_each_history_item(_, _object, _fun) do +    {:error, :invalid_history} +  end + +  # fun is (object -> {:ok, validated_object_with_string_keys}) +  defp do_separate_with_history(object, fun) do +    with history <- object["formerRepresentations"], +         object <- Map.drop(object, ["formerRepresentations"]), +         {_, {:ok, object}} <- {:main_body, fun.(object)}, +         {_, {:ok, history}} <- {:history_items, for_each_history_item(history, object, fun)} do +      object = +        if history do +          Map.put(object, "formerRepresentations", history) +        else +          object +        end + +      {:ok, object} +    else +      {:main_body, e} -> e +      {:history_items, e} -> e +    end +  end  end diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index 57c8d1dc0..2670e3f17 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -49,7 +49,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do    defp fix_url(%{"url" => url} = data) when is_map(url), do: Map.put(data, "url", url["href"])    defp fix_url(data), do: data -  defp fix_tag(%{"tag" => tag} = data) when is_list(tag), do: data +  defp fix_tag(%{"tag" => tag} = data) when is_list(tag) do +    Map.put(data, "tag", Enum.filter(tag, &is_map/1)) +  end +    defp fix_tag(%{"tag" => tag} = data) when is_map(tag), do: Map.put(data, "tag", [tag])    defp fix_tag(data), do: Map.drop(data, ["tag"]) @@ -60,7 +63,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do    defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies),      do: Map.put(data, "replies", replies) -  defp fix_replies(%{"replies" => replies} = data) when is_bitstring(replies), +  # TODO: Pleroma does not have any support for Collections at the moment. +  # If the `replies` field is not something the ObjectID validator can handle, +  # the activity/object would be rejected, which is bad behavior. +  defp fix_replies(%{"replies" => replies} = data) when not is_list(replies),      do: Map.drop(data, ["replies"])    defp fix_replies(data), do: data @@ -94,7 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do    defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Article", "Note", "Page"]) -    |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) +    |> validate_required([:id, :actor, :attributedTo, :type, :context])      |> CommonValidations.validate_any_presence([:cc, :to])      |> CommonValidations.validate_fields_match([:actor, :attributedTo])      |> CommonValidations.validate_actor_presence() diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex index cf6e407e0..398020bff 100644 --- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do    @primary_key false    embedded_schema do +    field(:id, :string)      field(:type, :string)      field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")      field(:name, :string) @@ -43,10 +44,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do        |> fix_url()      struct -    |> cast(data, [:type, :mediaType, :name, :blurhash]) -    |> cast_embed(:url, with: &url_changeset/2) +    |> cast(data, [:id, :type, :mediaType, :name, :blurhash]) +    |> cast_embed(:url, with: &url_changeset/2, required: true)      |> validate_inclusion(:type, ~w[Link Document Audio Image Video]) -    |> validate_required([:type, :mediaType, :url]) +    |> validate_required([:type, :mediaType])    end    def url_changeset(struct, data) do @@ -90,6 +91,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do    defp validate_data(cng) do      cng      |> validate_inclusion(:type, ~w[Document Audio Image Video]) -    |> validate_required([:mediaType, :url, :type]) +    |> validate_required([:mediaType, :type])    end  end diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex index 432bd9039..79ff76104 100644 --- a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex @@ -2,7 +2,7 @@  # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>  # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator do    use Ecto.Schema    alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes @@ -55,9 +55,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do      url      |> Enum.concat(mpeg_url["tag"] || [])      |> Enum.find(fn -      %{"mediaType" => mime_type} -> String.starts_with?(mime_type, ["video/", "audio/"]) -      %{"mimeType" => mime_type} -> String.starts_with?(mime_type, ["video/", "audio/"]) -      _ -> false +      %{"mediaType" => mime_type} -> +        String.starts_with?(mime_type, ["video/", "audio/", "image/"]) + +      %{"mimeType" => mime_type} -> +        String.starts_with?(mime_type, ["video/", "audio/", "image/"]) + +      _ -> +        false      end)    end @@ -104,14 +109,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do      struct      |> cast(data, __schema__(:fields) -- [:attachment, :tag]) -    |> cast_embed(:attachment) +    |> cast_embed(:attachment, required: true)      |> cast_embed(:tag)    end    defp validate_data(data_cng) do      data_cng -    |> validate_inclusion(:type, ["Audio", "Video"]) -    |> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment]) +    |> validate_inclusion(:type, ~w[Audio Image Video]) +    |> validate_required([:id, :actor, :attributedTo, :type, :context])      |> CommonValidations.validate_any_presence([:cc, :to])      |> CommonValidations.validate_fields_match([:actor, :attributedTo])      |> CommonValidations.validate_actor_presence() 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 8e768ffbf..7b60c139a 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -33,6 +33,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do        field(:content, :string)        field(:published, ObjectValidators.DateTime) +      field(:updated, ObjectValidators.DateTime)        field(:emoji, ObjectValidators.Emoji, default: %{})        embeds_many(:attachment, AttachmentValidator)      end @@ -51,8 +52,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do        field(:summary, :string)        field(:context, :string) -      # short identifier for PleromaFE to group statuses by context -      field(:context_id, :integer)        field(:sensitive, :boolean, default: false)        field(:replies_count, :integer, default: 0) diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex index 4f8c083eb..add46d561 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -22,14 +22,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do    end    def fix_object_defaults(data) do -    %{data: %{"id" => context}, id: context_id} = -      Utils.create_context(data["context"] || data["conversation"]) +    context = +      Utils.maybe_create_context( +        data["context"] || data["conversation"] || data["inReplyTo"] || data["id"] +      )      %User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"])      data      |> Map.put("context", context) -    |> Map.put("context_id", context_id)      |> cast_and_filter_recipients("to", follower_collection)      |> cast_and_filter_recipients("cc", follower_collection)      |> cast_and_filter_recipients("bto", follower_collection) diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex index 704b3abc9..1c5b1a059 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex @@ -136,11 +136,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do    # This figures out if a user is able to create, delete or modify something    # based on the domain and superuser status -  @spec validate_modification_rights(Ecto.Changeset.t()) :: Ecto.Changeset.t() -  def validate_modification_rights(cng) do +  @spec validate_modification_rights(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t() +  def validate_modification_rights(cng, privilege) do      actor = User.get_cached_by_ap_id(get_field(cng, :actor)) -    if User.superuser?(actor) || same_domain?(cng) do +    if User.privileged?(actor, privilege) || same_domain?(cng) do        cng      else        cng diff --git a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex index c9a621cb1..2395abfd4 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex @@ -75,7 +75,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do      data      |> CommonFixes.fix_actor() -    |> Map.put_new("context", object["context"]) +    |> Map.put("context", object["context"])      |> fix_addressing(object)    end diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex index 035fd5bc9..4d8502ada 100644 --- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex @@ -61,7 +61,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do      |> validate_required([:id, :type, :actor, :to, :cc, :object])      |> validate_inclusion(:type, ["Delete"])      |> validate_delete_actor(:actor) -    |> validate_modification_rights() +    |> validate_modification_rights(:messages_delete)      |> validate_object_or_user_presence(allowed_types: @deletable_types)      |> add_deleted_activity_id()    end diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex index 0858281e5..a0b82b325 100644 --- a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex @@ -5,8 +5,10 @@  defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do    use Ecto.Schema +  alias Pleroma.Emoji    alias Pleroma.Object    alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes +  alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator    import Ecto.Changeset    import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -19,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do          import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields          message_fields()          activity_fields() +        embeds_many(:tag, TagValidator)        end      end @@ -43,7 +46,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do    def changeset(struct, data) do      struct -    |> cast(data, __schema__(:fields)) +    |> cast(data, __schema__(:fields) -- [:tag]) +    |> cast_embed(:tag)    end    defp fix(data) do @@ -53,12 +57,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do        |> CommonFixes.fix_actor()        |> CommonFixes.fix_activity_addressing() -    with %Object{} = object <- Object.normalize(data["object"]) do -      data -      |> CommonFixes.fix_activity_context(object) -      |> CommonFixes.fix_object_action_recipients(object) -    else -      _ -> data +    data = Map.put_new(data, "tag", []) + +    case Object.normalize(data["object"]) do +      %Object{} = object -> +        data +        |> CommonFixes.fix_activity_context(object) +        |> CommonFixes.fix_object_action_recipients(object) + +      _ -> +        data      end    end @@ -82,11 +90,31 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do    defp validate_emoji(cng) do      content = get_field(cng, :content) -    if Pleroma.Emoji.is_unicode_emoji?(content) do +    if Emoji.is_unicode_emoji?(content) || Emoji.is_custom_emoji?(content) do        cng      else        cng -      |> add_error(:content, "must be a single character emoji") +      |> add_error(:content, "is not a valid emoji") +    end +  end + +  defp maybe_validate_tag_presence(cng) do +    content = get_field(cng, :content) + +    if Emoji.is_unicode_emoji?(content) do +      cng +    else +      tag = get_field(cng, :tag) +      emoji_name = Emoji.maybe_strip_name(content) + +      case tag do +        [%{name: ^emoji_name, type: "Emoji", icon: %{url: _}}] -> +          cng + +        _ -> +          cng +          |> add_error(:tag, "does not contain an Emoji tag") +      end      end    end @@ -97,5 +125,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do      |> validate_actor_presence()      |> validate_object_presence()      |> validate_emoji() +    |> maybe_validate_tag_presence()    end  end diff --git a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex index 0e99f2037..ab204f69a 100644 --- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex @@ -62,7 +62,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do    defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Event"]) -    |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) +    |> validate_required([:id, :actor, :attributedTo, :type, :context])      |> CommonValidations.validate_any_presence([:cc, :to])      |> CommonValidations.validate_fields_match([:actor, :attributedTo])      |> CommonValidations.validate_actor_presence() diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex index 9412be4bc..ce3305142 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -80,7 +80,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do    defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Question"]) -    |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) +    |> validate_required([:id, :actor, :attributedTo, :type, :context])      |> CommonValidations.validate_any_presence([:cc, :to])      |> CommonValidations.validate_fields_match([:actor, :attributedTo])      |> CommonValidations.validate_actor_presence() diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index a5def312e..1e940a400 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -51,7 +51,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do      with actor = get_field(cng, :actor),           object = get_field(cng, :object),           {:ok, object_id} <- ObjectValidators.ObjectID.cast(object), -         true <- actor == object_id do +         actor_uri <- URI.parse(actor), +         object_uri <- URI.parse(object_id), +         true <- actor_uri.host == object_uri.host do        cng      else        _e -> diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index b997c15db..e19642d50 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -25,6 +25,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    alias Pleroma.Web.Streamer    alias Pleroma.Workers.PollWorker +  require Pleroma.Constants    require Logger    @cachex Pleroma.Config.get([:cachex, :provider], Cachex) @@ -153,23 +154,26 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    # Tasks this handles:    # - Update the user +  # - Update a non-user object (Note, Question, etc.)    #    # For a local user, we also get a changeset with the full information, so we    # can update non-federating, non-activitypub settings as well.    @impl true    def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do -    if changeset = Keyword.get(meta, :user_update_changeset) do -      changeset -      |> User.update_and_set_cache() +    updated_object_id = updated_object["id"] + +    with {_, true} <- {:has_id, is_binary(updated_object_id)}, +         %{"type" => type} <- updated_object, +         {_, is_user} <- {:is_user, type in Pleroma.Constants.actor_types()} do +      if is_user do +        handle_update_user(object, meta) +      else +        handle_update_object(object, meta) +      end      else -      {:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object) - -      User.get_by_ap_id(updated_object["id"]) -      |> User.remote_user_changeset(new_user_data) -      |> User.update_and_set_cache() +      _ -> +        {:ok, object, meta}      end - -    {:ok, object, meta}    end    # Tasks this handles: @@ -278,7 +282,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    # Tasks this handles:    # - Delete and unpins the create activity    # - Replace object with Tombstone -  # - Set up notification    # - Reduce the user note count    # - Reduce the reply count    # - Stream out the activity @@ -320,7 +323,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do        end      if result == :ok do -      Notification.create_notifications(object)        {:ok, object, meta}      else        {:error, result} @@ -390,6 +392,79 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do      {:ok, object, meta}    end +  defp handle_update_user( +         %{data: %{"type" => "Update", "object" => updated_object}} = object, +         meta +       ) do +    if changeset = Keyword.get(meta, :user_update_changeset) do +      changeset +      |> User.update_and_set_cache() +    else +      {:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object) + +      User.get_by_ap_id(updated_object["id"]) +      |> User.remote_user_changeset(new_user_data) +      |> User.update_and_set_cache() +    end + +    {:ok, object, meta} +  end + +  defp handle_update_object( +         %{data: %{"type" => "Update", "object" => updated_object}} = object, +         meta +       ) do +    orig_object_ap_id = updated_object["id"] +    orig_object = Object.get_by_ap_id(orig_object_ap_id) +    orig_object_data = orig_object.data + +    updated_object = +      if meta[:local] do +        # If this is a local Update, we don't process it by transmogrifier, +        # so we use the embedded object as-is. +        updated_object +      else +        meta[:object_data] +      end + +    if orig_object_data["type"] in Pleroma.Constants.updatable_object_types() do +      %{ +        updated_data: updated_object_data, +        updated: updated, +        used_history_in_new_object?: used_history_in_new_object? +      } = Object.Updater.make_new_object_data_from_update_object(orig_object_data, updated_object) + +      changeset = +        orig_object +        |> Repo.preload(:hashtags) +        |> Object.change(%{data: updated_object_data}) + +      with {:ok, new_object} <- Repo.update(changeset), +           {:ok, _} <- Object.invalid_object_cache(new_object), +           {:ok, _} <- Object.set_cache(new_object), +           # The metadata/utils.ex uses the object id for the cache. +           {:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(new_object.id) do +        if used_history_in_new_object? do +          with create_activity when not is_nil(create_activity) <- +                 Pleroma.Activity.get_create_by_object_ap_id(orig_object_ap_id), +               {:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(create_activity.id) do +            nil +          else +            _ -> nil +          end +        end + +        if updated do +          object +          |> Activity.normalize() +          |> ActivityPub.notify_and_stream() +        end +      end +    end + +    {:ok, object, meta} +  end +    def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, meta) do      with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do        actor = User.get_cached_by_ap_id(object.data["actor"]) @@ -445,7 +520,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    end    def handle_object_creation(%{"type" => objtype} = object, _activity, meta) -      when objtype in ~w[Audio Video Event Article Note Page] do +      when objtype in ~w[Audio Video Image Event Article Note Page] do      with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do        {:ok, object, meta}      end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index d6622df86..3141f8437 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -447,7 +447,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,          options        ) -      when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page} do +      when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do      fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)      object = @@ -687,6 +687,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> strip_internal_fields      |> strip_internal_tags      |> set_type +    |> maybe_process_history +  end + +  defp maybe_process_history(%{"formerRepresentations" => %{"orderedItems" => history}} = object) do +    processed_history = +      Enum.map( +        history, +        fn +          item when is_map(item) -> prepare_object(item) +          item -> item +        end +      ) + +    put_in(object, ["formerRepresentations", "orderedItems"], processed_history) +  end + +  defp maybe_process_history(object) do +    object    end    #  @doc @@ -711,6 +729,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      {:ok, data}    end +  def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) +      when objtype in Pleroma.Constants.updatable_object_types() do +    object = +      object +      |> prepare_object + +    data = +      data +      |> Map.put("object", object) +      |> Map.merge(Utils.make_json_ld_header()) +      |> Map.delete("bcc") + +    {:ok, data} +  end +    def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do      object =        object_id diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 9cde7805c..437220077 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -31,7 +31,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do      "Page",      "Question",      "Answer", -    "Audio" +    "Audio", +    "Image"    ]    @strip_status_report_states ~w(closed resolved)    @supported_report_states ~w(open closed resolved) @@ -154,22 +155,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do      Notification.get_notified_from_activity(%Activity{data: object}, false)    end -  def create_context(context) do -    context = context || generate_id("contexts") - -    # Ecto has problems accessing the constraint inside the jsonb, -    # so we explicitly check for the existed object before insert -    object = Object.get_cached_by_ap_id(context) - -    with true <- is_nil(object), -         changeset <- Object.context_mapping(context), -         {:ok, inserted_object} <- Repo.insert(changeset) do -      inserted_object -    else -      _ -> -        object -    end -  end +  def maybe_create_context(context), do: context || generate_id("contexts")    @doc """    Enqueues an activity for federation if it's local @@ -201,18 +187,16 @@ defmodule Pleroma.Web.ActivityPub.Utils do      |> Map.put_new("id", "pleroma:fakeid")      |> Map.put_new_lazy("published", &make_date/0)      |> Map.put_new("context", "pleroma:fakecontext") -    |> Map.put_new("context_id", -1)      |> lazy_put_object_defaults(true)    end    def lazy_put_activity_defaults(map, _fake?) do -    %{data: %{"id" => context}, id: context_id} = create_context(map["context"]) +    context = maybe_create_context(map["context"])      map      |> Map.put_new_lazy("id", &generate_activity_id/0)      |> Map.put_new_lazy("published", &make_date/0)      |> Map.put_new("context", context) -    |> Map.put_new("context_id", context_id)      |> lazy_put_object_defaults(false)    end @@ -226,7 +210,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do        |> Map.put_new("id", "pleroma:fake_object_id")        |> Map.put_new_lazy("published", &make_date/0)        |> Map.put_new("context", activity["context"]) -      |> Map.put_new("context_id", activity["context_id"])        |> Map.put_new("fake", true)      %{activity | "object" => object} @@ -239,7 +222,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do        |> Map.put_new_lazy("id", &generate_object_id/0)        |> Map.put_new_lazy("published", &make_date/0)        |> Map.put_new("context", activity["context"]) -      |> Map.put_new("context_id", activity["context_id"])      %{activity | "object" => object}    end @@ -344,21 +326,29 @@ defmodule Pleroma.Web.ActivityPub.Utils do            {:ok, Object.t()} | {:error, Ecto.Changeset.t()}    def add_emoji_reaction_to_object( -        %Activity{data: %{"content" => emoji, "actor" => actor}}, +        %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,          object        ) do      reactions = get_cached_emoji_reactions(object) +    emoji = Pleroma.Emoji.maybe_strip_name(emoji) +    url = maybe_emoji_url(emoji, activity)      new_reactions = -      case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do +      case Enum.find_index(reactions, fn [candidate, _, candidate_url] -> +             if is_nil(candidate_url) do +               emoji == candidate +             else +               url == candidate_url +             end +           end) do          nil -> -          reactions ++ [[emoji, [actor]]] +          reactions ++ [[emoji, [actor], url]]          index ->            List.update_at(              reactions,              index, -            fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end +            fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end            )        end @@ -367,18 +357,40 @@ defmodule Pleroma.Web.ActivityPub.Utils do      update_element_in_object("reaction", new_reactions, object, count)    end +  defp maybe_emoji_url( +         name, +         %Activity{ +           data: %{ +             "tag" => [ +               %{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}} +             ] +           } +         } +       ), +       do: url + +  defp maybe_emoji_url(_, _), do: nil +    def emoji_count(reactions_list) do -    Enum.reduce(reactions_list, 0, fn [_, users], acc -> acc + length(users) end) +    Enum.reduce(reactions_list, 0, fn [_, users, _], acc -> acc + length(users) end)    end    def remove_emoji_reaction_from_object( -        %Activity{data: %{"content" => emoji, "actor" => actor}}, +        %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,          object        ) do +    emoji = Pleroma.Emoji.maybe_strip_name(emoji)      reactions = get_cached_emoji_reactions(object) +    url = maybe_emoji_url(emoji, activity)      new_reactions = -      case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do +      case Enum.find_index(reactions, fn [candidate, _, candidate_url] -> +             if is_nil(candidate_url) do +               emoji == candidate +             else +               url == candidate_url +             end +           end) do          nil ->            reactions @@ -386,9 +398,9 @@ defmodule Pleroma.Web.ActivityPub.Utils do            List.update_at(              reactions,              index, -            fn [emoji, users] -> [emoji, List.delete(users, actor)] end +            fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end            ) -          |> Enum.reject(fn [_, users] -> Enum.empty?(users) end) +          |> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)        end      count = emoji_count(new_reactions) @@ -396,11 +408,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do    end    def get_cached_emoji_reactions(object) do -    if is_list(object.data["reactions"]) do -      object.data["reactions"] -    else -      [] -    end +    Object.get_emoji_reactions(object)    end    @spec add_like_to_object(Activity.t(), Object.t()) :: @@ -508,17 +516,37 @@ defmodule Pleroma.Web.ActivityPub.Utils do    def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do      %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id) +    emoji = Pleroma.Emoji.maybe_quote(emoji)      "EmojiReact"      |> Activity.Queries.by_type()      |> where(actor: ^ap_id) -    |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji)) +    |> custom_emoji_discriminator(emoji)      |> Activity.Queries.by_object_id(object_ap_id)      |> order_by([activity], fragment("? desc nulls last", activity.id))      |> limit(1)      |> Repo.one()    end +  defp custom_emoji_discriminator(query, emoji) do +    if String.contains?(emoji, "@") do +      stripped = Pleroma.Emoji.maybe_strip_name(emoji) +      [name, domain] = String.split(stripped, "@") +      domain_pattern = "%/" <> domain <> "/%" +      emoji_pattern = Pleroma.Emoji.maybe_quote(name) + +      query +      |> where([activity], fragment("?->>'content' = ? +        AND EXISTS ( +          SELECT FROM jsonb_array_elements(?->'tag') elem +          WHERE elem->>'id' ILIKE ? +        )", activity.data, ^emoji_pattern, activity.data, ^domain_pattern)) +    else +      query +      |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji)) +    end +  end +    #### Announce-related helpers    @doc """ @@ -714,20 +742,24 @@ defmodule Pleroma.Web.ActivityPub.Utils do      Enum.map(statuses || [], &build_flag_object/1)    end -  defp build_flag_object(%Activity{data: %{"id" => id}, object: %{data: data}}) do -    activity_actor = User.get_by_ap_id(data["actor"]) +  defp build_flag_object(%Activity{} = activity) do +    object = Object.normalize(activity, fetch: false) -    %{ -      "type" => "Note", -      "id" => id, -      "content" => data["content"], -      "published" => data["published"], -      "actor" => -        AccountView.render( -          "show.json", -          %{user: activity_actor, skip_visibility_check: true} -        ) -    } +    # Do not allow people to report Creates. Instead, report the Object that is Created. +    if activity.data["type"] != "Create" do +      build_flag_object_with_actor_and_id( +        object, +        User.get_by_ap_id(activity.data["actor"]), +        activity.data["id"] +      ) +    else +      build_flag_object(object) +    end +  end + +  defp build_flag_object(%Object{} = object) do +    actor = User.get_by_ap_id(object.data["actor"]) +    build_flag_object_with_actor_and_id(object, actor, object.data["id"])    end    defp build_flag_object(act) when is_map(act) or is_binary(act) do @@ -739,12 +771,12 @@ defmodule Pleroma.Web.ActivityPub.Utils do        end      case Activity.get_by_ap_id_with_object(id) do -      %Activity{} = activity -> -        build_flag_object(activity) +      %Activity{object: object} = _ -> +        build_flag_object(object)        nil -> -        if activity = Activity.get_by_object_ap_id_with_object(id) do -          build_flag_object(activity) +        if %Object{} = object = Object.get_by_ap_id(id) do +          build_flag_object(object)          else            %{"id" => id, "deleted" => true}          end @@ -753,6 +785,20 @@ defmodule Pleroma.Web.ActivityPub.Utils do    defp build_flag_object(_), do: [] +  defp build_flag_object_with_actor_and_id(%Object{data: data}, actor, id) do +    %{ +      "type" => "Note", +      "id" => id, +      "content" => data["content"], +      "published" => data["published"], +      "actor" => +        AccountView.render( +          "show.json", +          %{user: actor, skip_visibility_check: true} +        ) +    } +  end +    #### Report-related helpers    def get_reports(params, page, page_size) do      params = @@ -767,22 +813,21 @@ defmodule Pleroma.Web.ActivityPub.Utils do      ActivityPub.fetch_activities([], params, :offset)    end -  def update_report_state(%Activity{} = activity, state) -      when state in @strip_status_report_states do -    {:ok, stripped_activity} = strip_report_status_data(activity) +  defp maybe_strip_report_status(data, state) do +    with true <- Config.get([:instance, :report_strip_status]), +         true <- state in @strip_status_report_states, +         {:ok, stripped_activity} = strip_report_status_data(%Activity{data: data}) do +      data |> Map.put("object", stripped_activity.data["object"]) +    else +      _ -> data +    end +  end +  def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do      new_data =        activity.data        |> Map.put("state", state) -      |> Map.put("object", stripped_activity.data["object"]) - -    activity -    |> Changeset.change(data: new_data) -    |> Repo.update() -  end - -  def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do -    new_data = Map.put(activity.data, "state", state) +      |> maybe_strip_report_status(state)      activity      |> Changeset.change(data: new_data) diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex index f848aba3a..63caa915c 100644 --- a/lib/pleroma/web/activity_pub/views/object_view.ex +++ b/lib/pleroma/web/activity_pub/views/object_view.ex @@ -29,11 +29,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do    def render("object.json", %{object: %Activity{} = activity}) do      base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header() -    object = Object.normalize(activity, fetch: false) +    object_id = Object.normalize(activity, id_only: true)      additional =        Transmogrifier.prepare_object(activity.data) -      |> Map.put("object", object.data["id"]) +      |> Map.put("object", object_id)      Map.merge(base, additional)    end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 52f6bb56d..f69fca075 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -34,7 +34,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do    def render("endpoints.json", _), do: %{}    def render("service.json", %{user: user}) do -    {:ok, user} = User.ensure_keys_present(user)      {:ok, _, public_key} = Keys.keys_from_pem(user.keys)      public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)      public_key = :public_key.pem_encode([public_key]) @@ -71,7 +70,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do      do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)    def render("user.json", %{user: user}) do -    {:ok, user} = User.ensure_keys_present(user)      {:ok, _, public_key} = Keys.keys_from_pem(user.keys)      public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)      public_key = :public_key.pem_encode([public_key]) diff --git a/lib/pleroma/web/admin_api/controllers/chat_controller.ex b/lib/pleroma/web/admin_api/controllers/chat_controller.ex index c3e9e12ce..298543fcf 100644 --- a/lib/pleroma/web/admin_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/chat_controller.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.AdminAPI.ChatController do    alias Pleroma.Activity    alias Pleroma.Chat    alias Pleroma.Chat.MessageReference -  alias Pleroma.ModerationLog    alias Pleroma.Pagination    alias Pleroma.Web.AdminAPI    alias Pleroma.Web.CommonAPI @@ -42,12 +41,6 @@ defmodule Pleroma.Web.AdminAPI.ChatController do           ^chat_id <- to_string(cm_ref.chat_id),           %Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object_ap_id),           {:ok, _} <- CommonAPI.delete(activity_id, user) do -      ModerationLog.insert_log(%{ -        action: "chat_message_delete", -        actor: user, -        subject_id: message_id -      }) -        conn        |> put_view(MessageReferenceView)        |> render("show.json", chat_message_reference: cm_ref) diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex index c9a4bfde9..9a3d49b57 100644 --- a/lib/pleroma/web/admin_api/controllers/status_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/status_controller.ex @@ -65,12 +65,6 @@ defmodule Pleroma.Web.AdminAPI.StatusController do    def delete(%{assigns: %{user: user}} = conn, %{id: id}) do      with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do -      ModerationLog.insert_log(%{ -        action: "status_delete", -        actor: user, -        subject_id: id -      }) -        json(conn, %{})      end    end diff --git a/lib/pleroma/web/admin_api/report.ex b/lib/pleroma/web/admin_api/report.ex index 8d1abfa56..fa89e3405 100644 --- a/lib/pleroma/web/admin_api/report.ex +++ b/lib/pleroma/web/admin_api/report.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Web.AdminAPI.Report do    alias Pleroma.Activity +  alias Pleroma.Object    alias Pleroma.User    def extract_report_info( @@ -16,10 +17,44 @@ defmodule Pleroma.Web.AdminAPI.Report do        status_ap_ids        |> Enum.reject(&is_nil(&1))        |> Enum.map(fn -        act when is_map(act) -> Activity.get_by_ap_id_with_object(act["id"]) -        act when is_binary(act) -> Activity.get_by_ap_id_with_object(act) +        act when is_map(act) -> +          Activity.get_create_by_object_ap_id_with_object(act["id"]) || +            Activity.get_by_ap_id_with_object(act["id"]) || make_fake_activity(act, user) + +        act when is_binary(act) -> +          Activity.get_create_by_object_ap_id_with_object(act) || +            Activity.get_by_ap_id_with_object(act)        end)      %{report: report, user: user, account: account, statuses: statuses}    end + +  defp make_fake_activity(act, user) do +    %Activity{ +      id: "pleroma:fake:#{act["id"]}", +      data: %{ +        "actor" => user.ap_id, +        "type" => "Create", +        "to" => [], +        "cc" => [], +        "object" => act["id"], +        "published" => act["published"], +        "id" => act["id"], +        "context" => "pleroma:fake" +      }, +      recipients: [user.ap_id], +      object: %Object{ +        data: %{ +          "actor" => user.ap_id, +          "type" => "Note", +          "content" => act["content"], +          "published" => act["published"], +          "to" => [], +          "cc" => [], +          "id" => act["id"], +          "context" => "pleroma:fake" +        } +      } +    } +  end  end diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex index cae4241ff..2d56dc643 100644 --- a/lib/pleroma/web/api_spec.ex +++ b/lib/pleroma/web/api_spec.ex @@ -95,7 +95,8 @@ defmodule Pleroma.Web.ApiSpec do                "Relays",                "Report managment",                "Status administration", -              "User administration" +              "User administration", +              "Announcement management"              ]            },            %{"name" => "Applications", "tags" => ["Applications", "Push subscriptions"]}, @@ -110,10 +111,12 @@ defmodule Pleroma.Web.ApiSpec do                "Follow requests",                "Mascot",                "Markers", -              "Notifications" +              "Notifications", +              "Filters", +              "Settings"              ]            }, -          %{"name" => "Instance", "tags" => ["Custom emojis"]}, +          %{"name" => "Instance", "tags" => ["Custom emojis", "Instance misc"]},            %{"name" => "Messaging", "tags" => ["Chats", "Conversations"]},            %{              "name" => "Statuses", @@ -125,10 +128,21 @@ defmodule Pleroma.Web.ApiSpec do                "Retrieve status information",                "Scheduled statuses",                "Search", -              "Status actions" +              "Status actions", +              "Media attachments"              ]            }, -          %{"name" => "Miscellaneous", "tags" => ["Emoji packs", "Reports", "Suggestions"]} +          %{ +            "name" => "Miscellaneous", +            "tags" => [ +              "Emoji packs", +              "Reports", +              "Suggestions", +              "Announcements", +              "Remote interaction", +              "Others" +            ] +          }          ]        }      } diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 97616f5e7..f2897a3a3 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -64,7 +64,8 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do        requestBody: request_body("Parameters", update_credentials_request(), required: true),        responses: %{          200 => Operation.response("Account", "application/json", Account), -        403 => Operation.response("Error", "application/json", ApiError) +        403 => Operation.response("Error", "application/json", ApiError), +        413 => Operation.response("Error", "application/json", ApiError)        }      }    end @@ -223,12 +224,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do              type: :object,              properties: %{                reblogs: %Schema{ -                type: :boolean, +                allOf: [BooleanLike],                  description: "Receive this account's reblogs in home timeline? Defaults to true.",                  default: true                },                notify: %Schema{ -                type: :boolean, +                allOf: [BooleanLike],                  description:                    "Receive notifications for all statuses posted by the account? Defaults to false.",                  default: false @@ -376,6 +377,22 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do      }    end +  def remove_from_followers_operation do +    %Operation{ +      tags: ["Account actions"], +      summary: "Remove from followers", +      operationId: "AccountController.remove_from_followers", +      security: [%{"oAuth" => ["follow", "write:follows"]}], +      description: "Remove the given account from followers", +      parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], +      responses: %{ +        200 => Operation.response("Relationship", "application/json", AccountRelationship), +        400 => Operation.response("Error", "application/json", ApiError), +        404 => Operation.response("Error", "application/json", ApiError) +      } +    } +  end +    def note_operation do      %Operation{        tags: ["Account actions"], @@ -435,7 +452,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do        operationId: "AccountController.blocks",        description: "View your blocks. See also accounts/:id/{block,unblock}",        security: [%{"oAuth" => ["read:blocks"]}], -      parameters: pagination_params(), +      parameters: [with_relationships_param() | pagination_params()],        responses: %{          200 => Operation.response("Accounts", "application/json", array_of_accounts())        } @@ -444,7 +461,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do    def lookup_operation do      %Operation{ -      tags: ["Account lookup"], +      tags: ["Retrieve account information"],        summary: "Find a user by nickname",        operationId: "AccountController.lookup",        parameters: [ diff --git a/lib/pleroma/web/api_spec/operations/admin/announcement_operation.ex b/lib/pleroma/web/api_spec/operations/admin/announcement_operation.ex index 58a039e72..49850e5d2 100644 --- a/lib/pleroma/web/api_spec/operations/admin/announcement_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/announcement_operation.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.AnnouncementOperation do    def index_operation do      %Operation{ -      tags: ["Announcement managment"], +      tags: ["Announcement management"],        summary: "Retrieve a list of announcements",        operationId: "AdminAPI.AnnouncementController.index",        security: [%{"oAuth" => ["admin:read"]}], @@ -46,7 +46,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.AnnouncementOperation do    def show_operation do      %Operation{ -      tags: ["Announcement managment"], +      tags: ["Announcement management"],        summary: "Display one announcement",        operationId: "AdminAPI.AnnouncementController.show",        security: [%{"oAuth" => ["admin:read"]}], @@ -69,7 +69,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.AnnouncementOperation do    def delete_operation do      %Operation{ -      tags: ["Announcement managment"], +      tags: ["Announcement management"],        summary: "Delete one announcement",        operationId: "AdminAPI.AnnouncementController.delete",        security: [%{"oAuth" => ["admin:write"]}], @@ -92,7 +92,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.AnnouncementOperation do    def create_operation do      %Operation{ -      tags: ["Announcement managment"], +      tags: ["Announcement management"],        summary: "Create one announcement",        operationId: "AdminAPI.AnnouncementController.create",        security: [%{"oAuth" => ["admin:write"]}], @@ -107,7 +107,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.AnnouncementOperation do    def change_operation do      %Operation{ -      tags: ["Announcement managment"], +      tags: ["Announcement management"],        summary: "Change one announcement",        operationId: "AdminAPI.AnnouncementController.change",        security: [%{"oAuth" => ["admin:write"]}], diff --git a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex index 229912dd7..17383f1d0 100644 --- a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex @@ -70,7 +70,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do    def show_operation do      %Operation{ -      tags: ["Status adminitration)"], +      tags: ["Status administration"],        summary: "Get status",        operationId: "AdminAPI.StatusController.show",        parameters: [id_param() | admin_api_params()], @@ -84,7 +84,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do    def update_operation do      %Operation{ -      tags: ["Status adminitration)"], +      tags: ["Status administration"],        summary: "Change the scope of a status",        operationId: "AdminAPI.StatusController.update",        parameters: [id_param() | admin_api_params()], @@ -99,7 +99,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do    def delete_operation do      %Operation{ -      tags: ["Status adminitration)"], +      tags: ["Status administration"],        summary: "Delete status",        operationId: "AdminAPI.StatusController.delete",        parameters: [id_param() | admin_api_params()], @@ -143,7 +143,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do            }          },          tags: %Schema{type: :string}, -        is_confirmed: %Schema{type: :string} +        is_confirmed: %Schema{type: :boolean}        }      }    end diff --git a/lib/pleroma/web/api_spec/operations/announcement_operation.ex b/lib/pleroma/web/api_spec/operations/announcement_operation.ex index 71be0002a..6f7031962 100644 --- a/lib/pleroma/web/api_spec/operations/announcement_operation.ex +++ b/lib/pleroma/web/api_spec/operations/announcement_operation.ex @@ -15,7 +15,7 @@ defmodule Pleroma.Web.ApiSpec.AnnouncementOperation do    def index_operation do      %Operation{ -      tags: ["Announcement"], +      tags: ["Announcements"],        summary: "Retrieve a list of announcements",        operationId: "MastodonAPI.AnnouncementController.index",        security: [%{"oAuth" => []}], @@ -28,7 +28,7 @@ defmodule Pleroma.Web.ApiSpec.AnnouncementOperation do    def mark_read_operation do      %Operation{ -      tags: ["Announcement"], +      tags: ["Announcements"],        summary: "Mark one announcement as read",        operationId: "MastodonAPI.AnnouncementController.mark_read",        security: [%{"oAuth" => ["write:accounts"]}], diff --git a/lib/pleroma/web/api_spec/operations/directory_operation.ex b/lib/pleroma/web/api_spec/operations/directory_operation.ex index 55752fa62..23fa84dff 100644 --- a/lib/pleroma/web/api_spec/operations/directory_operation.ex +++ b/lib/pleroma/web/api_spec/operations/directory_operation.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.ApiSpec.DirectoryOperation do    def index_operation do      %Operation{ -      tags: ["Directory"], +      tags: ["Others"],        summary: "Profile directory",        operationId: "DirectoryController.index",        parameters: diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index 3c4b504fe..a07be7e40 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do    def show_operation do      %Operation{ -      tags: ["Instance"], +      tags: ["Instance misc"],        summary: "Retrieve instance information",        description: "Information about the server",        operationId: "InstanceController.show", @@ -25,7 +25,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do    def peers_operation do      %Operation{ -      tags: ["Instance"], +      tags: ["Instance misc"],        summary: "Retrieve list of known instances",        operationId: "InstanceController.peers",        responses: %{ diff --git a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex index 82ec1e7bb..45fa2b058 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do      %Operation{        tags: ["Backups"],        summary: "List backups", -      security: [%{"oAuth" => ["read:account"]}], +      security: [%{"oAuth" => ["read:backups"]}],        operationId: "PleromaAPI.BackupController.index",        responses: %{          200 => @@ -37,7 +37,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do      %Operation{        tags: ["Backups"],        summary: "Create a backup", -      security: [%{"oAuth" => ["read:account"]}], +      security: [%{"oAuth" => ["read:backups"]}],        operationId: "PleromaAPI.BackupController.create",        responses: %{          200 => diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex index d09c1c10e..b05bad197 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex @@ -133,7 +133,11 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiFileOperation do    defp files_object do      %Schema{        type: :object, -      additionalProperties: %Schema{type: :string}, +      additionalProperties: %Schema{ +        type: :string, +        description: "Filename of the emoji", +        extensions: %{"x-additionalPropertiesName": "Emoji name"} +      },        description: "Object with emoji names as keys and filenames as values"      }    end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex index 6add3ff33..efa36ffdc 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -227,13 +227,29 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do    defp emoji_packs_response do      Operation.response( -      "Object with pack names as keys and pack contents as values", +      "Emoji packs and the count",        "application/json",        %Schema{          type: :object, -        additionalProperties: emoji_pack(), +        properties: %{ +          packs: %Schema{ +            type: :object, +            description: "Object with pack names as keys and pack contents as values", +            additionalProperties: %Schema{ +              emoji_pack() +              | extensions: %{"x-additionalPropertiesName": "Pack name"} +            } +          }, +          count: %Schema{ +            type: :integer, +            description: "Number of emoji packs" +          } +        },          example: %{ -          "emojos" => emoji_pack().example +          "packs" => %{ +            "emojos" => emoji_pack().example +          }, +          "count" => 1          }        }      ) @@ -274,7 +290,11 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do    defp files_object do      %Schema{        type: :object, -      additionalProperties: %Schema{type: :string}, +      additionalProperties: %Schema{ +        type: :string, +        description: "Filename", +        extensions: %{"x-additionalPropertiesName": "Emoji name"} +      },        description: "Object with emoji names as keys and filenames as values"      }    end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_instances_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_instances_operation.ex index 82db4e1a8..e9319f3fb 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_instances_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_instances_operation.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaInstancesOperation do    def show_operation do      %Operation{ -      tags: ["Instance"], +      tags: ["Instance misc"],        summary: "Retrieve federation status",        description: "Information about instances deemed unreachable by the server",        operationId: "PleromaInstances.show", diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 639f24d49..5d6e82f3c 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -6,9 +6,13 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do    alias OpenApiSpex.Operation    alias OpenApiSpex.Schema    alias Pleroma.Web.ApiSpec.AccountOperation +  alias Pleroma.Web.ApiSpec.Schemas.Account    alias Pleroma.Web.ApiSpec.Schemas.ApiError +  alias Pleroma.Web.ApiSpec.Schemas.Attachment    alias Pleroma.Web.ApiSpec.Schemas.BooleanLike +  alias Pleroma.Web.ApiSpec.Schemas.Emoji    alias Pleroma.Web.ApiSpec.Schemas.FlakeID +  alias Pleroma.Web.ApiSpec.Schemas.Poll    alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus    alias Pleroma.Web.ApiSpec.Schemas.Status    alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope @@ -434,6 +438,59 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do      }    end +  def show_history_operation do +    %Operation{ +      tags: ["Retrieve status information"], +      summary: "Status history", +      description: "View history of a status", +      operationId: "StatusController.show_history", +      security: [%{"oAuth" => ["read:statuses"]}], +      parameters: [ +        id_param() +      ], +      responses: %{ +        200 => status_history_response(), +        404 => Operation.response("Not Found", "application/json", ApiError) +      } +    } +  end + +  def show_source_operation do +    %Operation{ +      tags: ["Retrieve status information"], +      summary: "Status source", +      description: "View source of a status", +      operationId: "StatusController.show_source", +      security: [%{"oAuth" => ["read:statuses"]}], +      parameters: [ +        id_param() +      ], +      responses: %{ +        200 => status_source_response(), +        404 => Operation.response("Not Found", "application/json", ApiError) +      } +    } +  end + +  def update_operation do +    %Operation{ +      tags: ["Status actions"], +      summary: "Update status", +      description: "Change the content of a status", +      operationId: "StatusController.update", +      security: [%{"oAuth" => ["write:statuses"]}], +      parameters: [ +        id_param() +      ], +      requestBody: request_body("Parameters", update_request(), required: true), +      responses: %{ +        200 => status_response(), +        403 => Operation.response("Forbidden", "application/json", ApiError), +        404 => Operation.response("Not Found", "application/json", ApiError) +      } +    } +  end +    def array_of_statuses do      %Schema{type: :array, items: Status, example: [Status.schema().example]}    end @@ -537,6 +594,60 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do      }    end +  defp update_request do +    %Schema{ +      title: "StatusUpdateRequest", +      type: :object, +      properties: %{ +        status: %Schema{ +          type: :string, +          nullable: true, +          description: +            "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided." +        }, +        media_ids: %Schema{ +          nullable: true, +          type: :array, +          items: %Schema{type: :string}, +          description: "Array of Attachment ids to be attached as media." +        }, +        poll: poll_params(), +        sensitive: %Schema{ +          allOf: [BooleanLike], +          nullable: true, +          description: "Mark status and attached media as sensitive?" +        }, +        spoiler_text: %Schema{ +          type: :string, +          nullable: true, +          description: +            "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field." +        }, +        content_type: %Schema{ +          type: :string, +          nullable: true, +          description: +            "The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint." +        }, +        to: %Schema{ +          type: :array, +          nullable: true, +          items: %Schema{type: :string}, +          description: +            "A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply" +        } +      }, +      example: %{ +        "status" => "What time is it?", +        "sensitive" => "false", +        "poll" => %{ +          "options" => ["Cofe", "Adventure"], +          "expires_in" => 420 +        } +      } +    } +  end +    def poll_params do      %Schema{        nullable: true, @@ -579,6 +690,87 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do      Operation.response("Status", "application/json", Status)    end +  defp status_history_response do +    Operation.response( +      "Status History", +      "application/json", +      %Schema{ +        title: "Status history", +        description: "Response schema for history of a status", +        type: :array, +        items: %Schema{ +          type: :object, +          properties: %{ +            account: %Schema{ +              allOf: [Account], +              description: "The account that authored this status" +            }, +            content: %Schema{ +              type: :string, +              format: :html, +              description: "HTML-encoded status content" +            }, +            sensitive: %Schema{ +              type: :boolean, +              description: "Is this status marked as sensitive content?" +            }, +            spoiler_text: %Schema{ +              type: :string, +              description: +                "Subject or summary line, below which status content is collapsed until expanded" +            }, +            created_at: %Schema{ +              type: :string, +              format: "date-time", +              description: "The date when this status was created" +            }, +            media_attachments: %Schema{ +              type: :array, +              items: Attachment, +              description: "Media that is attached to this status" +            }, +            emojis: %Schema{ +              type: :array, +              items: Emoji, +              description: "Custom emoji to be used when rendering status content" +            }, +            poll: %Schema{ +              allOf: [Poll], +              nullable: true, +              description: "The poll attached to the status" +            } +          } +        } +      } +    ) +  end + +  defp status_source_response do +    Operation.response( +      "Status Source", +      "application/json", +      %Schema{ +        type: :object, +        properties: %{ +          id: FlakeID, +          text: %Schema{ +            type: :string, +            description: "Raw source of status content" +          }, +          spoiler_text: %Schema{ +            type: :string, +            description: +              "Subject or summary line, below which status content is collapsed until expanded" +          }, +          content_type: %Schema{ +            type: :string, +            description: "The content type of the source" +          } +        } +      } +    ) +  end +    defp context do      %Schema{        title: "StatusContext", diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex index 1cc90990f..084329ad7 100644 --- a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex +++ b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do    def emoji_operation do      %Operation{ -      tags: ["Emojis"], +      tags: ["Custom emojis"],        summary: "List all custom emojis",        operationId: "UtilController.emoji",        parameters: [], @@ -30,7 +30,8 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do                properties: %{                  image_url: %Schema{type: :string},                  tags: %Schema{type: :array, items: %Schema{type: :string}} -              } +              }, +              extensions: %{"x-additionalPropertiesName": "Emoji name"}              },              example: %{                "firefox" => %{ @@ -45,7 +46,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do    def frontend_configurations_operation do      %Operation{ -      tags: ["Configuration"], +      tags: ["Others"],        summary: "Dump frontend configurations",        operationId: "UtilController.frontend_configurations",        parameters: [], @@ -53,7 +54,12 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do          200 =>            Operation.response("List", "application/json", %Schema{              type: :object, -            additionalProperties: %Schema{type: :object} +            additionalProperties: %Schema{ +              type: :object, +              description: +                "Opaque object representing the instance-wide configuration for the frontend", +              extensions: %{"x-additionalPropertiesName": "Frontend name"} +            }            })        }      } @@ -132,7 +138,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do    def update_notificaton_settings_operation do      %Operation{ -      tags: ["Accounts"], +      tags: ["Settings"],        summary: "Update Notification Settings",        security: [%{"oAuth" => ["write:accounts"]}],        operationId: "UtilController.update_notificaton_settings", @@ -207,6 +213,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do      %Operation{        summary: "Get a captcha",        operationId: "UtilController.captcha", +      tags: ["Others"],        parameters: [],        responses: %{          200 => Operation.response("Success", "application/json", %Schema{type: :object}) @@ -356,7 +363,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do    def healthcheck_operation do      %Operation{ -      tags: ["Accounts"], +      tags: ["Others"],        summary: "Quick status check on the instance",        security: [%{"oAuth" => ["write:accounts"]}],        operationId: "UtilController.healthcheck", @@ -371,7 +378,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do    def remote_subscribe_operation do      %Operation{ -      tags: ["Accounts"], +      tags: ["Remote interaction"],        summary: "Remote Subscribe",        operationId: "UtilController.remote_subscribe",        parameters: [], @@ -381,7 +388,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do    def remote_interaction_operation do      %Operation{ -      tags: ["Accounts"], +      tags: ["Remote interaction"],        summary: "Remote interaction",        operationId: "UtilController.remote_interaction",        requestBody: request_body("Parameters", remote_interaction_request(), required: true), @@ -405,6 +412,16 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do      }    end +  def show_subscribe_form_operation do +    %Operation{ +      tags: ["Remote interaction"], +      summary: "Show remote subscribe form", +      operationId: "UtilController.show_subscribe_form", +      parameters: [], +      responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})} +    } +  end +    defp delete_account_request do      %Schema{        title: "AccountDeleteRequest", diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 6e6e30315..bc29cf4a6 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -73,6 +73,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do          format: "date-time",          description: "The date when this status was created"        }, +      edited_at: %Schema{ +        type: :string, +        format: "date-time", +        nullable: true, +        description: "The date when this status was last edited" +      },        emojis: %Schema{          type: :array,          items: Emoji, @@ -138,13 +144,23 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do          properties: %{            content: %Schema{              type: :object, -            additionalProperties: %Schema{type: :string}, +            additionalProperties: %Schema{ +              type: :string, +              description: "Alternate representation in the MIME type specified", +              extensions: %{"x-additionalPropertiesName": "MIME type"} +            },              description:                "A map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`"            }, +          context: %Schema{ +            type: :string, +            description: "The thread identifier the status is associated with" +          },            conversation_id: %Schema{              type: :integer, -            description: "The ID of the AP context the status is associated with (if any)" +            deprecated: true, +            description: +              "The ID of the AP context the status is associated with (if any); deprecated, please use `context` instead"            },            direct_conversation_id: %Schema{              type: :integer, @@ -183,7 +199,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do            },            spoiler_text: %Schema{              type: :object, -            additionalProperties: %Schema{type: :string}, +            additionalProperties: %Schema{ +              type: :string, +              description: "Alternate representation in the MIME type specified", +              extensions: %{"x-additionalPropertiesName": "MIME type"} +            },              description:                "A map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`."            }, @@ -319,6 +339,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do        "pinned" => false,        "pleroma" => %{          "content" => %{"text/plain" => "foobar"}, +        "context" => "http://localhost:4001/objects/8b4c0c80-6a37-4d2a-b1b9-05a19e3875aa",          "conversation_id" => 345_972,          "direct_conversation_id" => nil,          "emoji_reactions" => [], diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 1b95ee89c..89cc0d6fe 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.CommonAPI do    alias Pleroma.Activity    alias Pleroma.Conversation.Participation    alias Pleroma.Formatter +  alias Pleroma.ModerationLog    alias Pleroma.Object    alias Pleroma.ThreadMute    alias Pleroma.User @@ -144,9 +145,24 @@ defmodule Pleroma.Web.CommonAPI do             {:find_activity, Activity.get_by_id(activity_id)},           {_, %Object{} = object, _} <-             {:find_object, Object.normalize(activity, fetch: false), activity}, -         true <- User.superuser?(user) || user.ap_id == object.data["actor"], +         true <- User.privileged?(user, :messages_delete) || user.ap_id == object.data["actor"],           {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),           {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do +      if User.privileged?(user, :messages_delete) and user.ap_id != object.data["actor"] do +        action = +          if object.data["type"] == "ChatMessage" do +            "chat_message_delete" +          else +            "status_delete" +          end + +        ModerationLog.insert_log(%{ +          action: action, +          actor: user, +          subject_id: activity_id +        }) +      end +        {:ok, delete}      else        {:find_activity, _} -> @@ -402,6 +418,41 @@ defmodule Pleroma.Web.CommonAPI do      end    end +  def update(user, orig_activity, changes) do +    with orig_object <- Object.normalize(orig_activity), +         {:ok, new_object} <- make_update_data(user, orig_object, changes), +         {:ok, update_data, _} <- Builder.update(user, new_object), +         {:ok, update, _} <- Pipeline.common_pipeline(update_data, local: true) do +      {:ok, update} +    else +      _ -> {:error, nil} +    end +  end + +  defp make_update_data(user, orig_object, changes) do +    kept_params = %{ +      visibility: Visibility.get_visibility(orig_object), +      in_reply_to_id: +        with replied_id when is_binary(replied_id) <- orig_object.data["inReplyTo"], +             %Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(replied_id) do +          activity_id +        else +          _ -> nil +        end +    } + +    params = Map.merge(changes, kept_params) + +    with {:ok, draft} <- ActivityDraft.create(user, params) do +      change = +        Object.Updater.make_update_object_data(orig_object.data, draft.object, Utils.make_date()) + +      {:ok, change} +    else +      _ -> {:error, nil} +    end +  end +    @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}    def pin(id, %User{} = user) do      with %Activity{} = activity <- create_activity_by_id(id), diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 7c21c8c3a..9af635da8 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -224,7 +224,10 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do      object =        note_data        |> Map.put("emoji", emoji) -      |> Map.put("source", draft.status) +      |> Map.put("source", %{ +        "content" => draft.status, +        "mediaType" => Utils.get_content_type(draft.params[:content_type]) +      })        |> Map.put("generator", draft.params[:generator])      %__MODULE__{draft | object: object} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index ce850b038..ff0814329 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -37,7 +37,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def attachments_from_ids_no_descs(ids) do      Enum.map(ids, fn media_id -> -      case Repo.get(Object, media_id) do +      case get_attachment(media_id) do          %Object{data: data} -> data          _ -> nil        end @@ -51,13 +51,17 @@ defmodule Pleroma.Web.CommonAPI.Utils do      {_, descs} = Jason.decode(descs_str)      Enum.map(ids, fn media_id -> -      with %Object{data: data} <- Repo.get(Object, media_id) do +      with %Object{data: data} <- get_attachment(media_id) do          Map.put(data, "name", descs[media_id])        end      end)      |> Enum.reject(&is_nil/1)    end +  defp get_attachment(media_id) do +    Repo.get(Object, media_id) +  end +    @spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}    def get_to_and_cc(%{in_reply_to_conversation: %Participation{} = participation}) do @@ -219,7 +223,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do      |> maybe_add_attachments(draft.attachments, attachment_links)    end -  defp get_content_type(content_type) do +  def get_content_type(content_type) do      if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do        content_type      else @@ -449,35 +453,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def get_report_statuses(_, _), do: {:ok, nil} -  # DEPRECATED mostly, context objects are now created at insertion time. -  def context_to_conversation_id(context) do -    with %Object{id: id} <- Object.get_cached_by_ap_id(context) do -      id -    else -      _e -> -        changeset = Object.context_mapping(context) - -        case Repo.insert(changeset) do -          {:ok, %{id: id}} -> -            id - -          # This should be solved by an upsert, but it seems ecto -          # has problems accessing the constraint inside the jsonb. -          {:error, _} -> -            Object.get_cached_by_ap_id(context).id -        end -    end -  end - -  def conversation_id_to_context(id) do -    with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do -      context -    else -      _e -> -        {:error, dgettext("errors", "No such conversation")} -    end -  end -    def validate_character_limit("" = _full_payload, [] = _attachments) do      {:error, dgettext("errors", "Cannot post an empty status without attachments")}    end diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex index e7feefc07..318b6cb11 100644 --- a/lib/pleroma/web/federator.ex +++ b/lib/pleroma/web/federator.ex @@ -47,10 +47,15 @@ defmodule Pleroma.Web.Federator do    end    @impl true -  def publish(activity) do -    PublisherWorker.enqueue("publish", %{"activity_id" => activity.id}) +  def publish(%Pleroma.Activity{data: %{"type" => type}} = activity) do +    PublisherWorker.enqueue("publish", %{"activity_id" => activity.id}, +      priority: publish_priority(type) +    )    end +  defp publish_priority("Delete"), do: 3 +  defp publish_priority(_), do: 0 +    # Job Worker Callbacks    @spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()} @@ -61,10 +66,8 @@ defmodule Pleroma.Web.Federator do    def perform(:publish, activity) do      Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end) -    with %User{} = actor <- User.get_cached_by_ap_id(activity.data["actor"]), -         {:ok, actor} <- User.ensure_keys_present(actor) do -      Publisher.publish(actor, activity) -    end +    %User{} = actor = User.get_cached_by_ap_id(activity.data["actor"]) +    Publisher.publish(actor, activity)    end    def perform(:incoming_ap_doc, params) do diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex index 35a5f9482..034722eb2 100644 --- a/lib/pleroma/web/feed/feed_view.ex +++ b/lib/pleroma/web/feed/feed_view.ex @@ -6,7 +6,6 @@ defmodule Pleroma.Web.Feed.FeedView do    use Phoenix.HTML    use Pleroma.Web, :view -  alias Pleroma.Formatter    alias Pleroma.Object    alias Pleroma.User    alias Pleroma.Web.Gettext @@ -14,14 +13,8 @@ defmodule Pleroma.Web.Feed.FeedView do    require Pleroma.Constants -  @spec pub_date(String.t() | DateTime.t()) :: String.t() -  def pub_date(date) when is_binary(date) do -    date -    |> Timex.parse!("{ISO:Extended}") -    |> pub_date -  end - -  def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}") +  @days ~w(Mon Tue Wed Thu Fri Sat Sun) +  @months ~w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)    def prepare_activity(activity, opts \\ []) do      object = Object.normalize(activity, fetch: false) @@ -41,13 +34,18 @@ defmodule Pleroma.Web.Feed.FeedView do    def most_recent_update(activities) do      with %{updated_at: updated_at} <- List.first(activities) do -      NaiveDateTime.to_iso8601(updated_at) +      to_rfc3339(updated_at)      end    end -  def most_recent_update(activities, user) do +  def most_recent_update(activities, user, :atom) do +    (List.first(activities) || user).updated_at +    |> to_rfc3339() +  end + +  def most_recent_update(activities, user, :rss) do      (List.first(activities) || user).updated_at -    |> NaiveDateTime.to_iso8601() +    |> to_rfc2822()    end    def feed_logo do @@ -61,6 +59,10 @@ defmodule Pleroma.Web.Feed.FeedView do      |> MediaProxy.url()    end +  def email(user) do +    user.nickname <> "@" <> Pleroma.Web.Endpoint.host() +  end +    def logo(user) do      user      |> User.avatar_url() @@ -69,18 +71,35 @@ defmodule Pleroma.Web.Feed.FeedView do    def last_activity(activities), do: List.last(activities) -  def activity_title(%{"content" => content}, opts \\ %{}) do -    content -    |> Pleroma.Web.Metadata.Utils.scrub_html() -    |> Pleroma.Emoji.Formatter.demojify() -    |> Formatter.truncate(opts[:max_length], opts[:omission]) -    |> escape() +  def activity_title(%{"content" => content} = data, opts \\ %{}) do +    summary = Map.get(data, "summary", "") + +    title = +      cond do +        summary != "" -> summary +        content != "" -> activity_content(data) +        true -> "a post" +      end + +    title +    |> Pleroma.Web.Metadata.Utils.scrub_html_and_truncate(opts[:max_length], opts[:omission]) +    |> HtmlEntities.encode() +  end + +  def activity_description(data) do +    content = activity_content(data) +    summary = data["summary"] + +    cond do +      content != "" -> escape(content) +      summary != "" -> escape(summary) +      true -> escape(data["type"]) +    end    end    def activity_content(%{"content" => content}) do      content      |> String.replace(~r/[\n\r]/, "") -    |> escape()    end    def activity_content(_), do: "" @@ -112,4 +131,60 @@ defmodule Pleroma.Web.Feed.FeedView do      |> html_escape()      |> safe_to_string()    end + +  @spec to_rfc3339(String.t() | NativeDateTime.t()) :: String.t() +  def to_rfc3339(date) when is_binary(date) do +    date +    |> Timex.parse!("{ISO:Extended}") +    |> to_rfc3339() +  end + +  def to_rfc3339(nd) do +    nd +    |> Timex.to_datetime() +    |> Timex.format!("{RFC3339}") +  end + +  @spec to_rfc2822(String.t() | DateTime.t() | NativeDateTime.t()) :: String.t() +  def to_rfc2822(datestr) when is_binary(datestr) do +    datestr +    |> Timex.parse!("{ISO:Extended}") +    |> to_rfc2822() +  end + +  def to_rfc2822(%DateTime{} = date) do +    date +    |> DateTime.to_naive() +    |> NaiveDateTime.to_erl() +    |> rfc2822_from_erl() +  end + +  def to_rfc2822(nd) do +    nd +    |> Timex.to_datetime() +    |> DateTime.to_naive() +    |> NaiveDateTime.to_erl() +    |> rfc2822_from_erl() +  end + +  @doc """ +  Builds a RFC2822 timestamp from an Erlang timestamp +  [RFC2822 3.3 - Date and Time Specification](https://tools.ietf.org/html/rfc2822#section-3.3) +  This function always assumes the Erlang timestamp is in Universal time, not Local time +  """ +  def rfc2822_from_erl({{year, month, day} = date, {hour, minute, second}}) do +    day_name = Enum.at(@days, :calendar.day_of_the_week(date) - 1) +    month_name = Enum.at(@months, month - 1) + +    date_part = "#{day_name}, #{day} #{month_name} #{year}" +    time_part = "#{pad(hour)}:#{pad(minute)}:#{pad(second)}" + +    date_part <> " " <> time_part <> " +0000" +  end + +  defp pad(num) do +    num +    |> Integer.to_string() +    |> String.pad_leading(2, "0") +  end  end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index bf931dc6b..c313a0e97 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -76,16 +76,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    plug(      OAuthScopesPlug, -    %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow] +    %{scopes: ["follow", "write:follows"]} +    when action in [:follow_by_uri, :follow, :unfollow, :remove_from_followers]    )    plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)    plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute]) -  @relationship_actions [:follow, :unfollow] +  @relationship_actions [:follow, :unfollow, :remove_from_followers]    @needs_account ~W( -    followers following lists follow unfollow mute unmute block unblock note endorse unendorse +    followers following lists follow unfollow mute unmute block unblock +    note endorse unendorse remove_from_followers    )a    plug( @@ -252,19 +254,26 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do          with_pleroma_settings: true        )      else -      _e -> render_error(conn, :forbidden, "Invalid request") +      {:error, %Ecto.Changeset{errors: [avatar: {"file is too large", _}]}} -> +        render_error(conn, :request_entity_too_large, "File is too large") + +      {:error, %Ecto.Changeset{errors: [banner: {"file is too large", _}]}} -> +        render_error(conn, :request_entity_too_large, "File is too large") + +      {:error, %Ecto.Changeset{errors: [background: {"file is too large", _}]}} -> +        render_error(conn, :request_entity_too_large, "File is too large") + +      _e -> +        render_error(conn, :forbidden, "Invalid request")      end    end    defp normalize_fields_attributes(fields) do -    if Enum.all?(fields, &is_tuple/1) do -      Enum.map(fields, fn {_, v} -> v end) -    else -      Enum.map(fields, fn -        %{} = field -> %{"name" => field.name, "value" => field.value} -        field -> field -      end) -    end +    if(Enum.all?(fields, &is_tuple/1), do: Enum.map(fields, fn {_, v} -> v end), else: fields) +    |> Enum.map(fn +      %{} = field -> %{"name" => field.name, "value" => field.value} +      field -> field +    end)    end    @doc "GET /api/v1/accounts/relationships" @@ -477,6 +486,20 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do      end    end +  @doc "POST /api/v1/accounts/:id/remove_from_followers" +  def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do +    {:error, "Can not unfollow yourself"} +  end + +  def remove_from_followers(%{assigns: %{user: followed, account: follower}} = conn, _params) do +    with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do +      render(conn, "relationship.json", user: followed, target: follower) +    else +      nil -> +        render_error(conn, :not_found, "Record not found") +    end +  end +    @doc "POST /api/v1/follows"    def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do      case User.get_cached_by_nickname(uri) do @@ -517,7 +540,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do      conn      |> add_link_headers(users) -    |> render("index.json", users: users, for: user, as: :user) +    |> render("index.json", +      users: users, +      for: user, +      as: :user, +      embed_relationships: embed_relationships?(params) +    )    end    @doc "GET /api/v1/accounts/lookup" diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 740cf58e7..a490e8319 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -51,6 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do      move      pleroma:emoji_reaction      poll +    update    }    def index(%{assigns: %{user: user}} = conn, params) do      params = diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 42a95bdc5..e594ea491 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -38,7 +38,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do             :index,             :show,             :card, -           :context +           :context, +           :show_history, +           :show_source           ]    ) @@ -49,7 +51,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do             :create,             :delete,             :reblog, -           :unreblog +           :unreblog, +           :update           ]    ) @@ -191,6 +194,59 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do      create(%Plug.Conn{conn | body_params: params}, %{})    end +  @doc "GET /api/v1/statuses/:id/history" +  def show_history(%{assigns: assigns} = conn, %{id: id} = params) do +    with user = assigns[:user], +         %Activity{} = activity <- Activity.get_by_id_with_object(id), +         true <- Visibility.visible_for_user?(activity, user) do +      try_render(conn, "history.json", +        activity: activity, +        for: user, +        with_direct_conversation_id: true, +        with_muted: Map.get(params, :with_muted, false) +      ) +    else +      _ -> {:error, :not_found} +    end +  end + +  @doc "GET /api/v1/statuses/:id/source" +  def show_source(%{assigns: assigns} = conn, %{id: id} = _params) do +    with user = assigns[:user], +         %Activity{} = activity <- Activity.get_by_id_with_object(id), +         true <- Visibility.visible_for_user?(activity, user) do +      try_render(conn, "source.json", +        activity: activity, +        for: user +      ) +    else +      _ -> {:error, :not_found} +    end +  end + +  @doc "PUT /api/v1/statuses/:id" +  def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do +    with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)}, +         {_, true} <- {:visible, Visibility.visible_for_user?(activity, user)}, +         {_, true} <- {:is_create, activity.data["type"] == "Create"}, +         actor <- Activity.user_actor(activity), +         {_, true} <- {:own_status, actor.id == user.id}, +         changes <- body_params |> put_application(conn), +         {_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)}, +         {_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do +      try_render(conn, "show.json", +        activity: activity, +        for: user, +        with_direct_conversation_id: true, +        with_muted: Map.get(params, :with_muted, false) +      ) +    else +      {:own_status, _} -> {:error, :forbidden} +      {:pipeline, _} -> {:error, :internal_server_error} +      _ -> {:error, :not_found} +    end +  end +    @doc "GET /api/v1/statuses/:id"    def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do      with %Activity{} = activity <- Activity.get_by_id_with_object(id), diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index b4d092eed..467dc2fac 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -61,7 +61,20 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do    end    def get_notifications(user, params \\ %{}) do -    options = cast_params(params) +    options = +      cast_params(params) |> Map.update(:include_types, [], fn include_types -> include_types end) + +    options = +      if ("pleroma:report" not in options.include_types and +            User.privileged?(user, :reports_manage_reports)) or +           User.privileged?(user, :reports_manage_reports) do +        options +      else +        options +        |> Map.update(:exclude_types, ["pleroma:report"], fn current_exclude_types -> +          current_exclude_types ++ ["pleroma:report"] +        end) +      end      user      |> Notification.for_user_query(options) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 2260bf5da..cc3e3582f 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -370,19 +370,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    defp maybe_put_chat_token(data, _, _, _), do: data    defp maybe_put_role(data, %User{show_role: true} = user, _) do -    data -    |> Kernel.put_in([:pleroma, :is_admin], user.is_admin) -    |> Kernel.put_in([:pleroma, :is_moderator], user.is_moderator) +    put_role(data, user)    end    defp maybe_put_role(data, %User{id: user_id} = user, %User{id: user_id}) do +    put_role(data, user) +  end + +  defp maybe_put_role(data, _, _), do: data + +  defp put_role(data, user) do      data      |> Kernel.put_in([:pleroma, :is_admin], user.is_admin)      |> Kernel.put_in([:pleroma, :is_moderator], user.is_moderator) +    |> Kernel.put_in([:pleroma, :privileges], User.privileges(user))    end -  defp maybe_put_role(data, _, _), do: data -    defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do      Kernel.put_in(        data, @@ -399,12 +402,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    defp maybe_put_allow_following_move(data, _, _), do: data -  defp maybe_put_activation_status(data, user, %User{is_admin: true}) do -    Kernel.put_in(data, [:pleroma, :deactivated], !user.is_active) +  defp maybe_put_activation_status(data, user, user_for) do +    if User.privileged?(user_for, :users_manage_activation_state), +      do: Kernel.put_in(data, [:pleroma, :deactivated], !user.is_active), +      else: data    end -  defp maybe_put_activation_status(data, _, _), do: data -    defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{id: user_id}) do      data      |> Kernel.put_in( diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 62931bd41..efd2a0af6 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do        thumbnail:          URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail))          |> to_string, -      languages: ["en"], +      languages: Keyword.get(instance, :languages, ["en"]),        registrations: Keyword.get(instance, :registrations_open),        approval_required: Keyword.get(instance, :account_approval_required),        # Extra (not present in Mastodon): @@ -48,7 +48,6 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do            federation: federation(),            fields_limits: fields_limits(),            post_formats: Config.get([:instance, :allowed_post_formats]), -          privileged_staff: Config.get([:instance, :privileged_staff]),            birthday_required: Config.get([:instance, :birthday_required]),            birthday_min_age: Config.get([:instance, :birthday_min_age])          }, @@ -69,6 +68,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do        "shareable_emoji_packs",        "multifetch",        "pleroma:api/v1/notifications:include_types_filter", +      "editing",        if Config.get([:activitypub, :blockers_visible]) do          "blockers_visible"        end, @@ -92,13 +92,15 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do          "safe_dm_mentions"        end,        "pleroma_emoji_reactions", +      "pleroma_custom_emoji_reactions",        "pleroma_chat_messages",        if Config.get([:instance, :show_reactions]) do          "exposable_reactions"        end,        if Config.get([:instance, :profile_directory]) do          "profile_directory" -      end +      end, +      "pleroma:get:main/ostatus"      ]      |> Enum.filter(& &1)    end diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 0dc7f3beb..2a51f3755 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -17,9 +17,14 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do    alias Pleroma.Web.MastodonAPI.AccountView    alias Pleroma.Web.MastodonAPI.NotificationView    alias Pleroma.Web.MastodonAPI.StatusView +  alias Pleroma.Web.MediaProxy    alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView -  @parent_types ~w{Like Announce EmojiReact} +  defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id + +  defp object_id_for(%{data: %{"object" => id}}) when is_binary(id), do: id + +  @parent_types ~w{Like Announce EmojiReact Update}    def render("index.json", %{notifications: notifications, for: reading_user} = opts) do      activities = Enum.map(notifications, & &1.activity) @@ -30,7 +35,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do          %{data: %{"type" => type}} ->            type in @parent_types        end) -      |> Enum.map(& &1.data["object"]) +      |> Enum.map(&object_id_for/1)        |> Activity.create_by_object_ap_id()        |> Activity.with_preloaded_object(:left)        |> Pleroma.Repo.all() @@ -78,9 +83,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do      parent_activity_fn = fn ->        if opts[:parent_activities] do -        Activity.Queries.find_by_object_ap_id(opts[:parent_activities], activity.data["object"]) +        Activity.Queries.find_by_object_ap_id(opts[:parent_activities], object_id_for(activity))        else -        Activity.get_create_by_object_ap_id(activity.data["object"]) +        Activity.get_create_by_object_ap_id(object_id_for(activity))        end      end @@ -109,6 +114,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do        "reblog" ->          put_status(response, parent_activity_fn.(), reading_user, status_render_opts) +      "update" -> +        put_status(response, parent_activity_fn.(), reading_user, status_render_opts) +        "move" ->          put_target(response, activity, reading_user, %{}) @@ -138,7 +146,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do    end    defp put_emoji(response, activity) do -    Map.put(response, :emoji, activity.data["content"]) +    response +    |> Map.put(:emoji, activity.data["content"]) +    |> Map.put(:emoji_url, MediaProxy.url(Pleroma.Emoji.emoji_url(activity.data)))    end    defp put_chat_message(response, activity, reading_user, opts) do diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 1ebfd6740..dea22f9c2 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -57,11 +57,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      end)    end -  defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id), -    do: context_id - -  defp get_context_id(%{data: %{"context" => context}}) when is_binary(context), -    do: Utils.context_to_conversation_id(context) +  # DEPRECATED This field seems to be a left-over from the StatusNet era. +  # If your application uses `pleroma.conversation_id`: this field is deprecated. +  # It is currently stubbed instead by doing a CRC32 of the context, and +  # clearing the MSB to avoid overflow exceptions with signed integers on the +  # different clients using this field (Java/Kotlin code, mostly; see Husky.) +  # This should be removed in a future version of Pleroma. Pleroma-FE currently +  # depends on this field, as well. +  defp get_context_id(%{data: %{"context" => context}}) when is_binary(context) do +    import Bitwise + +    :erlang.crc32(context) +    |> band(bnot(0x8000_0000)) +  end    defp get_context_id(_), do: nil @@ -258,10 +266,30 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      created_at = Utils.to_masto_date(object.data["published"]) +    edited_at = +      with %{"updated" => updated} <- object.data, +           date <- Utils.to_masto_date(updated), +           true <- date != "" do +        date +      else +        _ -> +          nil +      end +      reply_to = get_reply_to(activity, opts)      reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"]) +    history_len = +      1 + +        (Object.Updater.history_for(object.data) +         |> Map.get("orderedItems") +         |> length()) + +    # See render("history.json", ...) for more details +    # Here the implicit index of the current content is 0 +    chrono_order = history_len - 1 +      content =        object        |> render_content() @@ -271,14 +299,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        |> Activity.HTML.get_cached_scrubbed_html_for_activity(          User.html_filter_policy(opts[:for]),          activity, -        "mastoapi:content" +        "mastoapi:content:#{chrono_order}"        )      content_plaintext =        content        |> Activity.HTML.get_cached_stripped_html_for_activity(          activity, -        "mastoapi:content" +        "mastoapi:content:#{chrono_order}"        )      summary = object.data["summary"] || "" @@ -306,14 +334,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        end      emoji_reactions = -      object.data -      |> Map.get("reactions", []) +      object +      |> Object.get_emoji_reactions()        |> EmojiReactionController.filter_allowed_users(          opts[:for],          Map.get(opts, :with_muted, false)        ) -      |> Stream.map(fn {emoji, users} -> -        build_emoji_map(emoji, users, opts[:for]) +      |> Stream.map(fn {emoji, users, url} -> +        build_emoji_map(emoji, users, url, opts[:for])        end)        |> Enum.to_list() @@ -344,8 +372,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        reblog: nil,        card: card,        content: content_html, -      text: opts[:with_source] && object.data["source"], +      text: opts[:with_source] && get_source_text(object.data["source"]),        created_at: created_at, +      edited_at: edited_at,        reblogs_count: announcement_count,        replies_count: object.data["repliesCount"] || 0,        favourites_count: like_count, @@ -367,6 +396,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        pleroma: %{          local: activity.local,          conversation_id: get_context_id(activity), +        context: object.data["context"],          in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,          content: %{"text/plain" => content_plaintext},          spoiler_text: %{"text/plain" => summary}, @@ -384,6 +414,100 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      nil    end +  def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do +    object = Object.normalize(activity, fetch: false) + +    hashtags = Object.hashtags(object) + +    user = CommonAPI.get_user(activity.data["actor"]) + +    past_history = +      Object.Updater.history_for(object.data) +      |> Map.get("orderedItems") +      |> Enum.map(&Map.put(&1, "id", object.data["id"])) +      |> Enum.map(&%Object{data: &1, id: object.id}) + +    history = +      [object | past_history] +      # Mastodon expects the original to be at the first +      |> Enum.reverse() +      |> Enum.with_index() +      |> Enum.map(fn {object, chrono_order} -> +        %{ +          # The history is prepended every time there is a new edit. +          # In chrono_order, the oldest item is always at 0, and so on. +          # The chrono_order is an invariant kept between edits. +          chrono_order: chrono_order, +          object: object +        } +      end) + +    individual_opts = +      opts +      |> Map.put(:as, :item) +      |> Map.put(:user, user) +      |> Map.put(:hashtags, hashtags) + +    render_many(history, StatusView, "history_item.json", individual_opts) +  end + +  def render( +        "history_item.json", +        %{ +          activity: activity, +          user: user, +          item: %{object: object, chrono_order: chrono_order}, +          hashtags: hashtags +        } = opts +      ) do +    sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw") + +    attachment_data = object.data["attachment"] || [] +    attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment) + +    created_at = Utils.to_masto_date(object.data["updated"] || object.data["published"]) + +    content = +      object +      |> render_content() + +    content_html = +      content +      |> Activity.HTML.get_cached_scrubbed_html_for_activity( +        User.html_filter_policy(opts[:for]), +        activity, +        "mastoapi:content:#{chrono_order}" +      ) + +    summary = object.data["summary"] || "" + +    %{ +      account: +        AccountView.render("show.json", %{ +          user: user, +          for: opts[:for] +        }), +      content: content_html, +      sensitive: sensitive, +      spoiler_text: summary, +      created_at: created_at, +      media_attachments: attachments, +      emojis: build_emojis(object.data["emoji"]), +      poll: render(PollView, "show.json", object: object, for: opts[:for]) +    } +  end + +  def render("source.json", %{activity: %{data: %{"object" => _object}} = activity} = _opts) do +    object = Object.normalize(activity, fetch: false) + +    %{ +      id: activity.id, +      text: get_source_text(Map.get(object.data, "source", "")), +      spoiler_text: Map.get(object.data, "summary", ""), +      content_type: get_source_content_type(object.data["source"]) +    } +  end +    def render("card.json", %{rich_media: rich_media, page_url: page_url}) do      page_url_data = URI.parse(page_url) @@ -436,10 +560,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          true -> "unknown"        end -    <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href) +    attachment_id = +      with {_, ap_id} when is_binary(ap_id) <- {:ap_id, attachment["id"]}, +           {_, %Object{data: _object_data, id: object_id}} <- +             {:object, Object.get_by_ap_id(ap_id)} do +        to_string(object_id) +      else +        _ -> +          <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href) +          to_string(attachment["id"] || hash_id) +      end      %{ -      id: to_string(attachment["id"] || hash_id), +      id: attachment_id,        url: href,        remote_url: href,        preview_url: href_preview, @@ -569,11 +702,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      end    end -  defp build_emoji_map(emoji, users, current_user) do +  defp build_emoji_map(emoji, users, url, current_user) do      %{ -      name: emoji, +      name: Pleroma.Web.PleromaAPI.EmojiReactionView.emoji_name(emoji, url),        count: length(users), -      me: !!(current_user && current_user.ap_id in users) +      url: MediaProxy.url(url), +      me: !!(current_user && current_user.ap_id in users), +      account_ids: Enum.map(users, fn user -> User.get_cached_by_ap_id(user).id end)      }    end @@ -601,4 +736,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    end    defp build_image_url(_, _), do: nil + +  defp get_source_text(%{"content" => content} = _source) do +    content +  end + +  defp get_source_text(source) when is_binary(source) do +    source +  end + +  defp get_source_text(_) do +    "" +  end + +  defp get_source_content_type(%{"mediaType" => type} = _source) do +    type +  end + +  defp get_source_content_type(_source) do +    Utils.get_content_type(nil) +  end  end diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index e62b8a135..88444106d 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -32,7 +32,8 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do            req          end -      {:cowboy_websocket, req, %{user: user, topic: topic, count: 0, timer: nil}, +      {:cowboy_websocket, req, +       %{user: user, topic: topic, oauth_token: oauth_token, count: 0, timer: nil},         %{idle_timeout: @timeout}}      else        {:error, :bad_topic} -> @@ -52,7 +53,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do        "#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topic #{state.topic}"      ) -    Streamer.add_socket(state.topic, state.user) +    Streamer.add_socket(state.topic, state.oauth_token)      {:ok, %{state | timer: timer()}}    end @@ -98,6 +99,10 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do      {:reply, :ping, %{state | timer: nil, count: 0}, :hibernate}    end +  def websocket_info(:close, state) do +    {:stop, state} +  end +    # State can be `[]` only in case we terminate before switching to websocket,    # we already log errors for these cases in `init/1`, so just do nothing here    def terminate(_reason, _req, []), do: :ok diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex index f0bee85c8..eabd8cb00 100644 --- a/lib/pleroma/web/metadata/providers/rel_me.ex +++ b/lib/pleroma/web/metadata/providers/rel_me.ex @@ -8,12 +8,20 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do    @impl Provider    def build_tags(%{user: user}) do -    bio_tree = Floki.parse_fragment!(user.bio) +    profile_tree = +      user.bio +      |> append_fields_tag(user.fields) +      |> Floki.parse_fragment!() -    (Floki.attribute(bio_tree, "link[rel~=me]", "href") ++ -       Floki.attribute(bio_tree, "a[rel~=me]", "href")) +    (Floki.attribute(profile_tree, "link[rel~=me]", "href") ++ +       Floki.attribute(profile_tree, "a[rel~=me]", "href"))      |> Enum.map(fn link ->        {:link, [rel: "me", href: link], []}      end)    end + +  defp append_fields_tag(bio, fields) do +    fields +    |> Enum.reduce(bio, fn %{"value" => v}, res -> res <> v end) +  end  end diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex index bf0a12212..2dac22ee2 100644 --- a/lib/pleroma/web/metadata/providers/twitter_card.ex +++ b/lib/pleroma/web/metadata/providers/twitter_card.ex @@ -20,12 +20,12 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do      [        title_tag(user), -      {:meta, [property: "twitter:description", content: scrubbed_content], []} +      {:meta, [name: "twitter:description", content: scrubbed_content], []}      ] ++        if attachments == [] or Metadata.activity_nsfw?(object) do          [            image_tag(user), -          {:meta, [property: "twitter:card", content: "summary"], []} +          {:meta, [name: "twitter:card", content: "summary"], []}          ]        else          attachments @@ -37,20 +37,19 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do      with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do        [          title_tag(user), -        {:meta, [property: "twitter:description", content: truncated_bio], []}, +        {:meta, [name: "twitter:description", content: truncated_bio], []},          image_tag(user), -        {:meta, [property: "twitter:card", content: "summary"], []} +        {:meta, [name: "twitter:card", content: "summary"], []}        ]      end    end    defp title_tag(user) do -    {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} +    {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}    end    def image_tag(user) do -    {:meta, [property: "twitter:image", content: MediaProxy.preview_url(User.avatar_url(user))], -     []} +    {:meta, [name: "twitter:image", content: MediaProxy.preview_url(User.avatar_url(user))], []}    end    defp build_attachments(id, %{data: %{"attachment" => attachments}}) do @@ -60,10 +59,10 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do            case Utils.fetch_media_type(@media_types, url["mediaType"]) do              "audio" ->                [ -                {:meta, [property: "twitter:card", content: "player"], []}, -                {:meta, [property: "twitter:player:width", content: "480"], []}, -                {:meta, [property: "twitter:player:height", content: "80"], []}, -                {:meta, [property: "twitter:player", content: player_url(id)], []} +                {:meta, [name: "twitter:card", content: "player"], []}, +                {:meta, [name: "twitter:player:width", content: "480"], []}, +                {:meta, [name: "twitter:player:height", content: "80"], []}, +                {:meta, [name: "twitter:player", content: player_url(id)], []}                  | acc                ] @@ -74,10 +73,10 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do              # workaround.              "image" ->                [ -                {:meta, [property: "twitter:card", content: "summary_large_image"], []}, +                {:meta, [name: "twitter:card", content: "summary_large_image"], []},                  {:meta,                   [ -                   property: "twitter:player", +                   name: "twitter:player",                     content: MediaProxy.url(url["href"])                   ], []}                  | acc @@ -90,14 +89,14 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do                width = url["width"] || 480                [ -                {:meta, [property: "twitter:card", content: "player"], []}, -                {:meta, [property: "twitter:player", content: player_url(id)], []}, -                {:meta, [property: "twitter:player:width", content: "#{width}"], []}, -                {:meta, [property: "twitter:player:height", content: "#{height}"], []}, -                {:meta, [property: "twitter:player:stream", content: MediaProxy.url(url["href"])], +                {:meta, [name: "twitter:card", content: "player"], []}, +                {:meta, [name: "twitter:player", content: player_url(id)], []}, +                {:meta, [name: "twitter:player:width", content: "#{width}"], []}, +                {:meta, [name: "twitter:player:height", content: "#{height}"], []}, +                {:meta, [name: "twitter:player:stream", content: MediaProxy.url(url["href"])],                   []}, -                {:meta, -                 [property: "twitter:player:stream:content_type", content: url["mediaType"]], []} +                {:meta, [name: "twitter:player:stream:content_type", content: url["mediaType"]], +                 []}                  | acc                ] @@ -123,8 +122,8 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do        !is_nil(url["height"]) && !is_nil(url["width"]) ->          metadata ++            [ -            {:meta, [property: "twitter:player:width", content: "#{url["width"]}"], []}, -            {:meta, [property: "twitter:player:height", content: "#{url["height"]}"], []} +            {:meta, [name: "twitter:player:width", content: "#{url["width"]}"], []}, +            {:meta, [name: "twitter:player:height", content: "#{url["height"]}"], []}            ]        true -> diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex index 8052eaa44..80a8be9a2 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -8,8 +8,8 @@ defmodule Pleroma.Web.Metadata.Utils do    alias Pleroma.Formatter    alias Pleroma.HTML -  def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do -    content +  defp scrub_html_and_truncate_object_field(field, object) do +    field      # html content comes from DB already encoded, decode first and scrub after      |> HtmlEntities.decode()      |> String.replace(~r/<br\s?\/?>/, " ") @@ -19,12 +19,24 @@ defmodule Pleroma.Web.Metadata.Utils do      |> Formatter.truncate()    end -  def scrub_html_and_truncate(content, max_length \\ 200) when is_binary(content) do +  def scrub_html_and_truncate(%{data: %{"summary" => summary}} = object) +      when is_binary(summary) and summary != "" do +    summary +    |> scrub_html_and_truncate_object_field(object) +  end + +  def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do +    content +    |> scrub_html_and_truncate_object_field(object) +  end + +  def scrub_html_and_truncate(content, max_length \\ 200, omission \\ "...") +      when is_binary(content) do      content      |> scrub_html      |> Emoji.Formatter.demojify()      |> HtmlEntities.decode() -    |> Formatter.truncate(max_length) +    |> Formatter.truncate(max_length, omission)    end    def scrub_html(content) when is_binary(content) do diff --git a/lib/pleroma/web/nodeinfo/nodeinfo.ex b/lib/pleroma/web/nodeinfo/nodeinfo.ex index 62d445f34..9e27ac26c 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo.ex @@ -49,6 +49,10 @@ defmodule Pleroma.Web.Nodeinfo.Nodeinfo do            enabled: false          },          staffAccounts: staff_accounts, +        roles: %{ +          admin: Config.get([:instance, :admin_privileges]), +          moderator: Config.get([:instance, :moderator_privileges]) +        },          federation: federation,          pollLimits: Config.get([:instance, :poll_limits]),          postFormats: Config.get([:instance, :allowed_post_formats]), @@ -69,8 +73,7 @@ defmodule Pleroma.Web.Nodeinfo.Nodeinfo do          mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false),          features: features,          restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]), -        skipThreadContainment: Config.get([:instance, :skip_thread_containment], false), -        privilegedStaff: Config.get([:instance, :privileged_staff]) +        skipThreadContainment: Config.get([:instance, :skip_thread_containment], false)        }      }    end diff --git a/lib/pleroma/web/o_auth/token/strategy/revoke.ex b/lib/pleroma/web/o_auth/token/strategy/revoke.ex index 752efca89..3b265b339 100644 --- a/lib/pleroma/web/o_auth/token/strategy/revoke.ex +++ b/lib/pleroma/web/o_auth/token/strategy/revoke.ex @@ -21,6 +21,18 @@ defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do    @doc "Revokes access token"    @spec revoke(Token.t()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()}    def revoke(%Token{} = token) do -    Repo.delete(token) +    with {:ok, token} <- Repo.delete(token) do +      Task.Supervisor.start_child( +        Pleroma.TaskSupervisor, +        Pleroma.Web.Streamer, +        :close_streams_by_oauth_token, +        [token], +        restart: :transient +      ) + +      {:ok, token} +    else +      result -> result +    end    end  end diff --git a/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex index 1a0548295..b9daed22b 100644 --- a/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.BackupController do    alias Pleroma.Web.Plugs.OAuthScopesPlug    action_fallback(Pleroma.Web.MastodonAPI.FallbackController) -  plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action in [:index, :create]) +  plug(OAuthScopesPlug, %{scopes: ["read:backups"]} when action in [:index, :create])    plug(Pleroma.Web.ApiSpec.CastAndValidate)    defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaBackupOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex index 78fd0b219..662cc15d6 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex @@ -28,8 +28,8 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do    def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do      with true <- Pleroma.Config.get([:instance, :show_reactions]),           %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), -         %Object{data: %{"reactions" => reactions}} when is_list(reactions) <- -           Object.normalize(activity, fetch: false) do +         %Object{} = object <- Object.normalize(activity, fetch: false), +         reactions <- Object.get_emoji_reactions(object) do        reactions =          reactions          |> filter(params) @@ -50,29 +50,32 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do            if not with_muted, do: User.cached_muted_users_ap_ids(user), else: []        end -    filter_emoji = fn emoji, users -> +    filter_emoji = fn emoji, users, url ->        case Enum.reject(users, &(&1 in exclude_ap_ids)) do          [] -> nil -        users -> {emoji, users} +        users -> {emoji, users, url}        end      end      reactions      |> Stream.map(fn -      [emoji, users] when is_list(users) -> filter_emoji.(emoji, users) -      {emoji, users} when is_list(users) -> filter_emoji.(emoji, users) -      _ -> nil +      [emoji, users, url] when is_list(users) -> filter_emoji.(emoji, users, url)      end)      |> Stream.reject(&is_nil/1)    end    defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do -    Enum.filter(reactions, fn [e, _] -> e == emoji end) +    Enum.filter(reactions, fn [e, _, _] -> e == emoji end)    end    defp filter(reactions, _), do: reactions    def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do +    emoji = +      emoji +      |> Pleroma.Emoji.fully_qualify_emoji() +      |> Pleroma.Emoji.maybe_quote() +      with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do        activity = Activity.get_by_id(activity_id) @@ -83,6 +86,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do    end    def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do +    emoji = +      emoji +      |> Pleroma.Emoji.fully_qualify_emoji() +      |> Pleroma.Emoji.maybe_quote() +      with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do        activity = Activity.get_by_id(activity_id) diff --git a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex index 68ebd8292..6df4ab9d0 100644 --- a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex +++ b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex @@ -7,17 +7,30 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do    alias Pleroma.Web.MastodonAPI.AccountView +  def emoji_name(emoji, nil), do: emoji + +  def emoji_name(emoji, url) do +    url = URI.parse(url) + +    if url.host == Pleroma.Web.Endpoint.host() do +      emoji +    else +      "#{emoji}@#{url.host}" +    end +  end +    def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do      render_many(emoji_reactions, __MODULE__, "show.json", opts)    end -  def render("show.json", %{emoji_reaction: {emoji, user_ap_ids}, user: user}) do +  def render("show.json", %{emoji_reaction: {emoji, user_ap_ids, url}, user: user}) do      users = fetch_users(user_ap_ids)      %{ -      name: emoji, +      name: emoji_name(emoji, url),        count: length(users),        accounts: render(AccountView, "index.json", users: users, for: user), +      url: Pleroma.Web.MediaProxy.url(url),        me: !!(user && user.ap_id in user_ap_ids)      }    end diff --git a/lib/pleroma/web/plugs/authentication_plug.ex b/lib/pleroma/web/plugs/authentication_plug.ex index a7fd697b5..f912a1542 100644 --- a/lib/pleroma/web/plugs/authentication_plug.ex +++ b/lib/pleroma/web/plugs/authentication_plug.ex @@ -38,10 +38,6 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do    def call(conn, _), do: conn -  def checkpw(password, "$6" <> _ = password_hash) do -    :crypt.crypt(password, password_hash) == password_hash -  end -    def checkpw(password, "$2" <> _ = password_hash) do      # Handle bcrypt passwords for Mastodon migration      Bcrypt.verify_pass(password, password_hash) @@ -60,10 +56,6 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do      do_update_password(user, password)    end -  def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do -    do_update_password(user, password) -  end -    def maybe_update_password(user, _), do: {:ok, user}    defp do_update_password(user, password) do diff --git a/lib/pleroma/web/plugs/ensure_privileged_plug.ex b/lib/pleroma/web/plugs/ensure_privileged_plug.ex new file mode 100644 index 000000000..f886c87ea --- /dev/null +++ b/lib/pleroma/web/plugs/ensure_privileged_plug.ex @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsurePrivilegedPlug do +  @moduledoc """ +  Ensures staff are privileged enough to do certain tasks. +  """ +  import Pleroma.Web.TranslationHelpers +  import Plug.Conn + +  alias Pleroma.Config +  alias Pleroma.User + +  def init(options) do +    options +  end + +  def call(%{assigns: %{user: %User{is_admin: false, is_moderator: false}}} = conn, _) do +    conn +    |> render_error(:forbidden, "User isn't privileged.") +    |> halt() +  end + +  def call( +        %{assigns: %{user: %User{is_admin: is_admin, is_moderator: is_moderator}}} = conn, +        privilege +      ) do +    if (is_admin and privilege in Config.get([:instance, :admin_privileges])) or +         (is_moderator and privilege in Config.get([:instance, :moderator_privileges])) do +      conn +    else +      conn +      |> render_error(:forbidden, "User isn't privileged.") +      |> halt() +    end +  end + +  def call(conn, _) do +    conn +    |> render_error(:forbidden, "User isn't privileged.") +    |> halt() +  end +end diff --git a/lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex b/lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex deleted file mode 100644 index 3c2109496..000000000 --- a/lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex +++ /dev/null @@ -1,36 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plugs.EnsureStaffPrivilegedPlug do -  @moduledoc """ -  Ensures staff are privileged enough to do certain tasks. -  """ -  import Pleroma.Web.TranslationHelpers -  import Plug.Conn - -  alias Pleroma.Config -  alias Pleroma.User - -  def init(options) do -    options -  end - -  def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _), do: conn - -  def call(%{assigns: %{user: %User{is_moderator: true}}} = conn, _) do -    if Config.get!([:instance, :privileged_staff]) do -      conn -    else -      conn -      |> render_error(:forbidden, "User is not an admin.") -      |> halt() -    end -  end - -  def call(conn, _) do -    conn -    |> render_error(:forbidden, "User is not a staff member.") -    |> halt() -  end -end diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index b89948cec..34895c8d5 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -68,7 +68,7 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do          ]        } -      [{"reply-to", Jason.encode!(report_group)} | headers] +      [{"report-to", Jason.encode!(report_group)} | headers]      else        headers      end @@ -117,7 +117,7 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do        if Config.get(:env) == :dev do          "script-src 'self' 'unsafe-eval'"        else -        "script-src 'self'" +        "script-src 'self' 'wasm-unsafe-eval'"        end      report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"] diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex index d023754a6..4bf325218 100644 --- a/lib/pleroma/web/plugs/http_signature_plug.ex +++ b/lib/pleroma/web/plugs/http_signature_plug.ex @@ -25,21 +25,58 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do      end    end +  defp validate_signature(conn, request_target) do +    # Newer drafts for HTTP signatures now use @request-target instead of the +    # old (request-target). We'll now support both for incoming signatures. +    conn = +      conn +      |> put_req_header("(request-target)", request_target) +      |> put_req_header("@request-target", request_target) + +    HTTPSignatures.validate_conn(conn) +  end + +  defp validate_signature(conn) do +    # This (request-target) is non-standard, but many implementations do it +    # this way due to a misinterpretation of +    # https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-06 +    # "path" was interpreted as not having the query, though later examples +    # show that it must be the absolute path + query. This behavior is kept to +    # make sure most software (Pleroma itself, Mastodon, and probably others) +    # do not break. +    request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}" + +    # This is the proper way to build the @request-target, as expected by +    # many HTTP signature libraries, clarified in the following draft: +    # https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html#section-2.2.6 +    # It is the same as before, but containing the query part as well. +    proper_target = request_target <> "?#{conn.query_string}" + +    cond do +      # Normal, non-standard behavior but expected by Pleroma and more. +      validate_signature(conn, request_target) -> +        true + +      # Has query string and the previous one failed: let's try the standard. +      conn.query_string != "" -> +        validate_signature(conn, proper_target) + +      # If there's no query string and signature fails, it's rotten. +      true -> +        false +    end +  end +    defp maybe_assign_valid_signature(conn) do      if has_signature_header?(conn) do -      # set (request-target) header to the appropriate value -      # we also replace the digest header with the one we computed -      request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}" - +      # we replace the digest header with the one we computed in DigestPlug        conn = -        conn -        |> put_req_header("(request-target)", request_target) -        |> case do +        case conn do            %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest)            conn -> conn          end -      assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn)) +      assign(conn, :valid_signature, validate_signature(conn))      else        Logger.debug("No signature header!")        conn diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex index 0f74d626b..ba04ddb72 100644 --- a/lib/pleroma/web/plugs/o_auth_plug.ex +++ b/lib/pleroma/web/plugs/o_auth_plug.ex @@ -47,15 +47,17 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do    #    @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil    defp fetch_user_and_token(token) do -    query = +    token_query =        from(t in Token, -        where: t.token == ^token, -        join: user in assoc(t, :user), -        preload: [user: user] +        where: t.token == ^token        ) -    with %Token{user: user} = token_record <- Repo.one(query) do +    with %Token{user_id: user_id} = token_record <- Repo.one(token_query), +         false <- is_nil(user_id), +         %User{} = user <- User.get_cached_by_id(user_id) do        {:ok, user, token_record} +    else +      _ -> nil      end    end diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index daf3eeb9e..3c5f00764 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Web.Push.Impl do    require Logger    import Ecto.Query -  @types ["Create", "Follow", "Announce", "Like", "Move", "EmojiReact"] +  @types ["Create", "Follow", "Announce", "Like", "Move", "EmojiReact", "Update"]    @doc "Performs sending notifications for user subscriptions"    @spec perform(Notification.t()) :: list(any) | :error | {:error, :unknown_type} @@ -174,6 +174,15 @@ defmodule Pleroma.Web.Push.Impl do      end    end +  def format_body( +        %{activity: %{data: %{"type" => "Update"}}}, +        actor, +        _object, +        _mastodon_type +      ) do +    "@#{actor.nickname} edited a status" +  end +    def format_title(activity, mastodon_type \\ nil)    def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_type) do @@ -187,6 +196,7 @@ defmodule Pleroma.Web.Push.Impl do        "follow_request" -> "New Follow Request"        "reblog" -> "New Repeat"        "favourite" -> "New Favorite" +      "update" -> "New Update"        "pleroma:chat_mention" -> "New Chat Message"        "pleroma:emoji_reaction" -> "New Reaction"        type -> "New #{String.capitalize(type || "event")}" diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex index 98fbc1c59..ceb6a05f0 100644 --- a/lib/pleroma/web/rel_me.ex +++ b/lib/pleroma/web/rel_me.ex @@ -9,17 +9,13 @@ defmodule Pleroma.Web.RelMe do      recv_timeout: 2_000    ] -  if Pleroma.Config.get(:env) == :test do -    def parse(url) when is_binary(url), do: parse_url(url) -  else -    @cachex Pleroma.Config.get([:cachex, :provider], Cachex) -    def parse(url) when is_binary(url) do -      @cachex.fetch!(:rel_me_cache, url, fn _ -> -        {:commit, parse_url(url)} -      end) -    rescue -      e -> {:error, "Cachex error: #{inspect(e)}"} -    end +  @cachex Pleroma.Config.get([:cachex, :provider], Cachex) +  def parse(url) when is_binary(url) do +    @cachex.fetch!(:rel_me_cache, url, fn _ -> +      {:commit, parse_url(url)} +    end) +  rescue +    e -> {:error, "Cachex error: #{inspect(e)}"}    end    def parse(_), do: {:error, "No URL provided"} diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 842596e97..c1a690e28 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -101,14 +101,80 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Web.Plugs.IdempotencyPlug)    end -  pipeline :require_privileged_staff do -    plug(Pleroma.Web.Plugs.EnsureStaffPrivilegedPlug) -  end -    pipeline :require_admin do      plug(Pleroma.Web.Plugs.UserIsAdminPlug)    end +  pipeline :require_privileged_role_users_delete do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_delete) +  end + +  pipeline :require_privileged_role_users_manage_credentials do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_credentials) +  end + +  pipeline :require_privileged_role_messages_read do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :messages_read) +  end + +  pipeline :require_privileged_role_users_manage_tags do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_tags) +  end + +  pipeline :require_privileged_role_users_manage_activation_state do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_activation_state) +  end + +  pipeline :require_privileged_role_users_manage_invites do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_invites) +  end + +  pipeline :require_privileged_role_reports_manage_reports do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :reports_manage_reports) +  end + +  pipeline :require_privileged_role_users_read do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_read) +  end + +  pipeline :require_privileged_role_messages_delete do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :messages_delete) +  end + +  pipeline :require_privileged_role_emoji_manage_emoji do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :emoji_manage_emoji) +  end + +  pipeline :require_privileged_role_instances_delete do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :instances_delete) +  end + +  pipeline :require_privileged_role_moderation_log_read do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :moderation_log_read) +  end + +  pipeline :require_privileged_role_statistics_read do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :statistics_read) +  end + +  pipeline :require_privileged_role_announcements_manage_announcements do +    plug(:admin_api) +    plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :announcements_manage_announcements) +  end +    pipeline :pleroma_html do      plug(:browser)      plug(:authenticate) @@ -167,8 +233,6 @@ defmodule Pleroma.Web.Router do    scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do      pipe_through([:admin_api, :require_admin]) -    put("/users/disable_mfa", AdminAPIController, :disable_mfa) -      get("/users/:nickname/permission_group", AdminAPIController, :right_get)      get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get) @@ -199,17 +263,10 @@ defmodule Pleroma.Web.Router do      post("/relay", RelayController, :follow)      delete("/relay", RelayController, :unfollow) -    patch("/users/force_password_reset", AdminAPIController, :force_password_reset) -    get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials) -    patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials) -      get("/instance_document/:name", InstanceDocumentController, :show)      patch("/instance_document/:name", InstanceDocumentController, :update)      delete("/instance_document/:name", InstanceDocumentController, :delete) -    patch("/users/confirm_email", AdminAPIController, :confirm_email) -    patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email) -      get("/config", ConfigController, :show)      post("/config", ConfigController, :update)      get("/config/descriptions", ConfigController, :descriptions) @@ -229,6 +286,11 @@ defmodule Pleroma.Web.Router do      post("/frontends/install", FrontendController, :install)      post("/backups", AdminAPIController, :create_backup) +  end + +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do +    pipe_through(:require_privileged_role_announcements_manage_announcements)      get("/announcements", AnnouncementController, :index)      post("/announcements", AnnouncementController, :create) @@ -237,14 +299,29 @@ defmodule Pleroma.Web.Router do      delete("/announcements/:id", AnnouncementController, :delete)    end -  # AdminAPI: admins and mods (staff) can perform these actions (if enabled by config) +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)    scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do -    pipe_through([:admin_api, :require_privileged_staff]) +    pipe_through(:require_privileged_role_users_delete)      delete("/users", UserController, :delete) +  end + +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do +    pipe_through(:require_privileged_role_users_manage_credentials)      get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) +    get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)      patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials) +    put("/users/disable_mfa", AdminAPIController, :disable_mfa) +    patch("/users/force_password_reset", AdminAPIController, :force_password_reset) +    patch("/users/confirm_email", AdminAPIController, :confirm_email) +    patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email) +  end + +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do +    pipe_through(:require_privileged_role_messages_read)      get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)      get("/users/:nickname/chats", AdminAPIController, :list_user_chats) @@ -253,52 +330,100 @@ defmodule Pleroma.Web.Router do      get("/chats/:id", ChatController, :show)      get("/chats/:id/messages", ChatController, :messages) + +    get("/instances/:instance/statuses", InstanceController, :list_statuses) + +    get("/statuses/:id", StatusController, :show)    end -  # AdminAPI: admins and mods (staff) can perform these actions +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)    scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do -    pipe_through(:admin_api) +    pipe_through(:require_privileged_role_users_manage_tags)      put("/users/tag", AdminAPIController, :tag_users)      delete("/users/tag", AdminAPIController, :untag_users) +  end + +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do +    pipe_through(:require_privileged_role_users_manage_activation_state)      patch("/users/:nickname/toggle_activation", UserController, :toggle_activation)      patch("/users/activate", UserController, :activate)      patch("/users/deactivate", UserController, :deactivate) -    patch("/users/approve", UserController, :approve) +  end + +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do +    pipe_through(:require_privileged_role_users_manage_invites) +    patch("/users/approve", UserController, :approve)      post("/users/invite_token", InviteController, :create)      get("/users/invites", InviteController, :index)      post("/users/revoke_invite", InviteController, :revoke)      post("/users/email_invite", InviteController, :email) +  end -    get("/users", UserController, :index) -    get("/users/:nickname", UserController, :show) - -    get("/instances/:instance/statuses", InstanceController, :list_statuses) -    delete("/instances/:instance", InstanceController, :delete) +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do +    pipe_through(:require_privileged_role_reports_manage_reports)      get("/reports", ReportController, :index)      get("/reports/:id", ReportController, :show)      patch("/reports", ReportController, :update)      post("/reports/:id/notes", ReportController, :notes_create)      delete("/reports/:report_id/notes/:id", ReportController, :notes_delete) +  end + +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do +    pipe_through(:require_privileged_role_users_read) + +    get("/users", UserController, :index) +    get("/users/:nickname", UserController, :show) +  end + +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do +    pipe_through(:require_privileged_role_messages_delete) -    get("/statuses/:id", StatusController, :show)      put("/statuses/:id", StatusController, :update)      delete("/statuses/:id", StatusController, :delete) -    get("/moderation_log", AdminAPIController, :list_log) +    delete("/chats/:id/messages/:message_id", ChatController, :delete_message) +  end + +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do +    pipe_through(:require_privileged_role_emoji_manage_emoji)      post("/reload_emoji", AdminAPIController, :reload_emoji) -    get("/stats", AdminAPIController, :stats) +  end -    delete("/chats/:id/messages/:message_id", ChatController, :delete_message) +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do +    pipe_through(:require_privileged_role_instances_delete) + +    delete("/instances/:instance", InstanceController, :delete) +  end + +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do +    pipe_through(:require_privileged_role_moderation_log_read) + +    get("/moderation_log", AdminAPIController, :list_log) +  end + +  # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) +  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do +    pipe_through(:require_privileged_role_statistics_read) + +    get("/stats", AdminAPIController, :stats)    end    scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do      scope "/pack" do -      pipe_through(:admin_api) +      pipe_through(:require_privileged_role_emoji_manage_emoji)        post("/", EmojiPackController, :create)        patch("/", EmojiPackController, :update) @@ -313,7 +438,7 @@ defmodule Pleroma.Web.Router do      # Modifying packs      scope "/packs" do -      pipe_through(:admin_api) +      pipe_through(:require_privileged_role_emoji_manage_emoji)        get("/import", EmojiPackController, :import_from_filesystem)        get("/remote", EmojiPackController, :remote) @@ -337,6 +462,7 @@ defmodule Pleroma.Web.Router do      pipe_through(:pleroma_html)      post("/main/ostatus", UtilController, :remote_subscribe) +    get("/main/ostatus", UtilController, :show_subscribe_form)      get("/ostatus_subscribe", RemoteFollowController, :follow)      post("/ostatus_subscribe", RemoteFollowController, :do_follow)    end @@ -509,6 +635,7 @@ defmodule Pleroma.Web.Router do      post("/accounts/:id/note", AccountController, :note)      post("/accounts/:id/pin", AccountController, :endorse)      post("/accounts/:id/unpin", AccountController, :unendorse) +    post("/accounts/:id/remove_from_followers", AccountController, :remove_from_followers)      get("/conversations", ConversationController, :index)      post("/conversations/:id/read", ConversationController, :mark_as_read) @@ -570,6 +697,7 @@ defmodule Pleroma.Web.Router do      get("/bookmarks", StatusController, :bookmarks)      post("/statuses", StatusController, :create) +    put("/statuses/:id", StatusController, :update)      delete("/statuses/:id", StatusController, :delete)      post("/statuses/:id/reblog", StatusController, :reblog)      post("/statuses/:id/unreblog", StatusController, :unreblog) @@ -629,6 +757,8 @@ defmodule Pleroma.Web.Router do      get("/statuses/:id/card", StatusController, :card)      get("/statuses/:id/favourited_by", StatusController, :favourited_by)      get("/statuses/:id/reblogged_by", StatusController, :reblogged_by) +    get("/statuses/:id/history", StatusController, :show_history) +    get("/statuses/:id/source", StatusController, :show_source)      get("/custom_emojis", CustomEmojiController, :index) @@ -705,8 +835,7 @@ defmodule Pleroma.Web.Router do    end    scope "/", Pleroma.Web do -    # Note: html format is supported only if static FE is enabled -    pipe_through([:accepts_html_xml, :static_fe]) +    pipe_through([:accepts_html_xml])      get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)    end diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index ff7f62a1e..3c0da5c27 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -37,7 +37,7 @@ defmodule Pleroma.Web.Streamer do            {:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized}    def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do      with {:ok, topic} <- get_topic(stream, user, oauth_token, params) do -      add_socket(topic, user) +      add_socket(topic, oauth_token)      end    end @@ -120,10 +120,10 @@ defmodule Pleroma.Web.Streamer do    end    @doc "Registers the process for streaming. Use `get_topic/3` to get the full authorized topic." -  def add_socket(topic, user) do +  def add_socket(topic, oauth_token) do      if should_env_send?() do -      auth? = if user, do: true -      Registry.register(@registry, topic, auth?) +      oauth_token_id = if oauth_token, do: oauth_token.id, else: false +      Registry.register(@registry, topic, oauth_token_id)      end      {:ok, topic} @@ -296,6 +296,24 @@ defmodule Pleroma.Web.Streamer do    defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop +  defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do +    create_activity = +      Pleroma.Activity.get_create_by_object_ap_id(item.object.data["id"]) +      |> Map.put(:object, item.object) + +    anon_render = StreamerView.render("status_update.json", create_activity) + +    Registry.dispatch(@registry, topic, fn list -> +      Enum.each(list, fn {pid, auth?} -> +        if auth? do +          send(pid, {:render_with_user, StreamerView, "status_update.json", create_activity}) +        else +          send(pid, {:text, anon_render}) +        end +      end) +    end) +  end +    defp push_to_socket(topic, item) do      anon_render = StreamerView.render("update.json", item) @@ -320,6 +338,22 @@ defmodule Pleroma.Web.Streamer do      end    end +  def close_streams_by_oauth_token(oauth_token) do +    if should_env_send?() do +      Registry.select( +        @registry, +        [ +          { +            {:"$1", :"$2", :"$3"}, +            [{:==, :"$3", oauth_token.id}], +            [:"$2"] +          } +        ] +      ) +      |> Enum.each(fn pid -> send(pid, :close) end) +    end +  end +    # In test environement, only return true if the registry is started.    # In benchmark environment, returns false.    # In any other environment, always returns true. diff --git a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex index 57bd92468..b774f7984 100644 --- a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex @@ -3,15 +3,15 @@    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>    <id><%= @data["id"] %></id>    <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title> -  <content type="html"><%= activity_content(@data) %></content> -  <published><%= @activity.data["published"] %></published> -  <updated><%= @activity.data["published"] %></updated> +  <content type="html"><%= activity_description(@data) %></content> +  <published><%= to_rfc3339(@data["published"]) %></published> +  <updated><%= to_rfc3339(@data["published"]) %></updated>    <ostatus:conversation ref="<%= activity_context(@activity) %>">      <%= activity_context(@activity) %>    </ostatus:conversation>    <link href="<%= activity_context(@activity) %>" rel="ostatus:conversation"/> -  <%= if @data["summary"] do %> +  <%= if @data["summary"] != "" do %>      <summary><%= escape(@data["summary"]) %></summary>    <% end %> diff --git a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex index 279f2171d..7de98f736 100644 --- a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex +++ b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex @@ -3,17 +3,12 @@    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>    <guid><%= @data["id"] %></guid>    <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title> -  <description><%= activity_content(@data) %></description> -  <pubDate><%= @activity.data["published"] %></pubDate> -  <updated><%= @activity.data["published"] %></updated> +  <description><%= activity_description(@data) %></description> +  <pubDate><%= to_rfc2822(@data["published"]) %></pubDate>    <ostatus:conversation ref="<%= activity_context(@activity) %>">      <%= activity_context(@activity) %>    </ostatus:conversation> -  <%= if @data["summary"] do %> -    <description><%= escape(@data["summary"]) %></description> -  <% end %> -    <%= if @activity.local do %>      <link><%= @data["id"] %></link>    <% else %> @@ -27,7 +22,7 @@    <% end %>    <%= for attachment <- @data["attachment"] || [] do %> -    <link type="<%= attachment_type(attachment) %>"><%= attachment_href(attachment) %></link> +    <enclosure url="<%= attachment_href(attachment) %>" type="<%= attachment_type(attachment) %>" />    <% end %>    <%= if @data["inReplyTo"] do %> diff --git a/lib/pleroma/web/templates/feed/feed/_author.atom.eex b/lib/pleroma/web/templates/feed/feed/_author.atom.eex index 25cbffada..90be8a559 100644 --- a/lib/pleroma/web/templates/feed/feed/_author.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/_author.atom.eex @@ -1,17 +1,14 @@  <author> -  <id><%= @user.ap_id %></id> -  <activity:object>http://activitystrea.ms/schema/1.0/person</activity:object>    <uri><%= @user.ap_id %></uri> +  <name><%= @user.nickname %></name> +  <activity:object>http://activitystrea.ms/schema/1.0/person</activity:object> +  <activity:displayName><%= @user.name %></activity:displayName> +  <activity:image><%= User.avatar_url(@user) %></activity:image> +  <activity:id><%= @user.ap_id %></activity:id> +  <activity:published><%= to_rfc3339(@user.inserted_at) %></activity:published> +  <activity:updated><%= to_rfc3339(@user.updated_at) %></activity:updated> +  <activity:url><%= @user.ap_id %></activity:url>    <poco:preferredUsername><%= @user.nickname %></poco:preferredUsername>    <poco:displayName><%= @user.name %></poco:displayName>    <poco:note><%= escape(@user.bio) %></poco:note> -  <summary><%= escape(@user.bio) %></summary> -  <name><%= @user.nickname %></name> -  <link rel="avatar" href="<%= User.avatar_url(@user) %>"/> -  <%= if User.banner_url(@user) do %> -    <link rel="header" href="<%= User.banner_url(@user) %>"/> -  <% end %> -  <%= if @user.local do %> -    <ap_enabled>true</ap_enabled> -  <% end %>  </author> diff --git a/lib/pleroma/web/templates/feed/feed/_author.rss.eex b/lib/pleroma/web/templates/feed/feed/_author.rss.eex index 526aeddcf..22477e6b1 100644 --- a/lib/pleroma/web/templates/feed/feed/_author.rss.eex +++ b/lib/pleroma/web/templates/feed/feed/_author.rss.eex @@ -1,17 +1,10 @@ -<managingEditor> -  <guid><%= @user.ap_id %></guid> -  <activity:object>http://activitystrea.ms/schema/1.0/person</activity:object> -  <uri><%= @user.ap_id %></uri> -  <poco:preferredUsername><%= @user.nickname %></poco:preferredUsername> -  <poco:displayName><%= @user.name %></poco:displayName> -  <poco:note><%= escape(@user.bio) %></poco:note> -  <description><%= escape(@user.bio) %></description> -  <name><%= @user.nickname %></name> -  <link rel="avatar"><%= User.avatar_url(@user) %></link> -  <%= if User.banner_url(@user) do %> -    <link rel="header"><%= User.banner_url(@user) %></link> -  <% end %> -  <%= if @user.local do %> -    <ap_enabled>true</ap_enabled> -  <% end %> -</managingEditor> +<managingEditor><%= "#{email(@user)} (#{escape(@user.name)})" %></managingEditor> +<activity:object>http://activitystrea.ms/schema/1.0/person</activity:object> +<activity:displayName><%= @user.name %></activity:displayName> +<activity:image><%= User.avatar_url(@user) %></activity:image> +<activity:id><%= @user.ap_id %></activity:id> +<activity:published><%= to_rfc3339(@user.inserted_at) %></activity:published> +<activity:updated><%= to_rfc3339(@user.updated_at) %></activity:updated> +<poco:preferredUsername><%= @user.nickname %></poco:preferredUsername> +<poco:displayName><%= @user.name %></poco:displayName> +<poco:note><%= escape(@user.bio) %></poco:note> diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex index aa3035bca..03c222975 100644 --- a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex @@ -1,12 +1,22 @@  <entry> -    <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> -    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> - -    <%= render @view_module, "_tag_author.atom", assigns %> - -    <id><%= @data["id"] %></id> -    <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title> -    <content type="html"><%= activity_content(@data) %></content> +  <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> +  <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + +    <%= render Phoenix.Controller.view_module(@conn), "_tag_author.atom", assigns %> + +  <id><%= @data["id"] %></id> +  <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title> +  <content type="html"><%= activity_description(@data) %></content> +  <published><%= to_rfc3339(@data["published"]) %></published> +  <updated><%= to_rfc3339(@data["published"]) %></updated> +  <ostatus:conversation ref="<%= activity_context(@activity) %>"> +    <%= activity_context(@activity) %> +  </ostatus:conversation> +  <link href="<%= activity_context(@activity) %>" rel="ostatus:conversation"/> + +  <%= if @data["summary"] != "" do %> +    <summary><%= @data["summary"] %></summary> +  <% end %>    <%= if @activity.local do %>      <link type="application/atom+xml" href='<%= @data["id"] %>' rel="self"/> @@ -15,37 +25,25 @@      <link type="text/html" href='<%= @data["external_url"] %>' rel="alternate"/>    <% end %> -    <published><%= @activity.data["published"] %></published> -    <updated><%= @activity.data["published"] %></updated> - -    <ostatus:conversation ref="<%= activity_context(@activity) %>"> -      <%= activity_context(@activity) %> -    </ostatus:conversation> -    <link href="<%= activity_context(@activity) %>" rel="ostatus:conversation"/> - -   <%= if @data["summary"] do %> -    <summary><%= @data["summary"] %></summary> -   <% end %> - -    <%= for id <- @activity.recipients do %> -      <%= if id == Pleroma.Constants.as_public() do %> +  <%= for id <- @activity.recipients do %> +    <%= if id == Pleroma.Constants.as_public() do %> +      <link rel="mentioned" +            ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" +            href="http://activityschema.org/collection/public"/> +    <% else %> +      <%= unless Regex.match?(~r/^#{Pleroma.Web.Endpoint.url()}.+followers$/, id) do %>          <link rel="mentioned" -          ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" -          href="http://activityschema.org/collection/public"/> -      <% else %> -        <%= unless Regex.match?(~r/^#{Pleroma.Web.Endpoint.url()}.+followers$/, id) do %> -          <link rel="mentioned" -            ostatus:object-type="http://activitystrea.ms/schema/1.0/person" -            href="<%= id %>" /> -        <% end %> +              ostatus:object-type="http://activitystrea.ms/schema/1.0/person" +              href="<%= id %>" />        <% end %>      <% end %> +  <% end %> -    <%= for tag <- Pleroma.Object.hashtags(@object) do %> -      <category term="<%= tag %>"></category> -    <% end %> +  <%= for tag <- Pleroma.Object.hashtags(@object) do %> +    <category term="<%= tag %>"></category> +  <% end %> -    <%= for {emoji, file} <- @data["emoji"] || %{} do %> -      <link name="<%= emoji %>" rel="emoji" href="<%= file %>"/> -    <% end %> +  <%= for {emoji, file} <- @data["emoji"] || %{} do %> +    <link name="<%= emoji %>" rel="emoji" href="<%= file %>"/> +  <% end %>  </entry> diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex index 2334e24a2..1b8c34b87 100644 --- a/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex +++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex @@ -4,9 +4,9 @@    <guid isPermalink="true"><%= activity_context(@activity) %></guid>    <link><%= activity_context(@activity) %></link> -  <pubDate><%= pub_date(@activity.data["published"]) %></pubDate> +  <pubDate><%= to_rfc2822(@data["published"]) %></pubDate> -  <description><%= activity_content(@data) %></description> +  <description><%= activity_description(@data) %></description>    <%= for attachment <- @data["attachment"] || [] do %>      <enclosure url="<%= attachment_href(attachment) %>" type="<%= attachment_type(attachment) %>"/>    <% end %> diff --git a/lib/pleroma/web/templates/feed/feed/_tag_author.atom.eex b/lib/pleroma/web/templates/feed/feed/_tag_author.atom.eex index 997c4936e..71c696832 100644 --- a/lib/pleroma/web/templates/feed/feed/_tag_author.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/_tag_author.atom.eex @@ -1,18 +1,14 @@  <author> -    <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type> -    <id><%= @actor.ap_id %></id> -    <uri><%= @actor.ap_id %></uri> -    <name><%= @actor.nickname %></name> -    <summary><%= escape(@actor.bio) %></summary> -    <link rel="avatar" href="<%= User.avatar_url(@actor) %>"/> -    <%= if User.banner_url(@actor) do %> -      <link rel="header" href="<%= User.banner_url(@actor) %>"/> -    <% end %> -    <%= if @actor.local do %> -      <ap_enabled>true</ap_enabled> -    <% end %> -   -    <poco:preferredUsername><%= @actor.nickname %></poco:preferredUsername>         -    <poco:displayName><%= @actor.name %></poco:displayName> -    <poco:note><%= escape(@actor.bio) %></poco:note>     +  <uri><%= @actor.ap_id %></uri> +  <name><%= @actor.nickname %></name> +  <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type> +  <activity:displayName><%= @actor.name %></activity:displayName> +  <activity:image><%= User.avatar_url(@actor) %></activity:image> +  <activity:id><%= @actor.ap_id %></activity:id> +  <activity:published><%= to_rfc3339(@actor.inserted_at) %></activity:published> +  <activity:updated><%= to_rfc3339(@actor.updated_at) %></activity:updated> +  <activity:url><%= @actor.ap_id %></activity:url> +  <poco:preferredUsername><%= @actor.nickname %></poco:preferredUsername> +  <poco:displayName><%= @actor.name %></poco:displayName> +  <poco:note><%= escape(@actor.bio) %></poco:note>  </author> diff --git a/lib/pleroma/web/templates/feed/feed/tag.atom.eex b/lib/pleroma/web/templates/feed/feed/tag.atom.eex index 6d497e84c..14b0ee594 100644 --- a/lib/pleroma/web/templates/feed/feed/tag.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/tag.atom.eex @@ -1,22 +1,20 @@  <?xml version="1.0" encoding="UTF-8"?> +<feed +  xmlns="http://www.w3.org/2005/Atom" +  xmlns:thr="http://purl.org/syndication/thread/1.0" +  xmlns:activity="http://activitystrea.ms/spec/1.0/" +  xmlns:poco="http://portablecontacts.net/spec/1.0" +  xmlns:ostatus="http://ostatus.org/schema/1.0" +  xmlns:statusnet="http://status.net/schema/api/1/"> -<feed xml:lang="<%= Gettext.language_tag() %>" xmlns="http://www.w3.org/2005/Atom" -      xmlns:thr="http://purl.org/syndication/thread/1.0" -      xmlns:georss="http://www.georss.org/georss" -      xmlns:activity="http://activitystrea.ms/spec/1.0/" -      xmlns:media="http://purl.org/syndication/atommedia" -      xmlns:poco="http://portablecontacts.net/spec/1.0" -      xmlns:ostatus="http://ostatus.org/schema/1.0" -      xmlns:statusnet="http://status.net/schema/api/1/"> +  <id><%= Routes.tag_feed_url(@conn, :feed, @tag) <> ".atom" %></id> +  <title>#<%= @tag %></title> +  <subtitle><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></subtitle> +  <logo><%= feed_logo() %></logo> +  <updated><%= most_recent_update(@activities) %></updated> +  <link rel="self" href="<%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.atom'  %>" type="application/atom+xml"/> -    <id><%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %></id> -    <title>#<%= @tag %></title> - -    <subtitle><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></subtitle> -    <logo><%= feed_logo() %></logo> -    <updated><%= most_recent_update(@activities) %></updated> -    <link rel="self" href="<%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.atom'  %>" type="application/atom+xml"/> -    <%= for activity <- @activities do %> -    <%= render @view_module, "_tag_activity.atom", Map.merge(assigns, prepare_activity(activity, actor: true)) %> -    <% end %> +  <%= for activity <- @activities do %> +  <%= render Phoenix.Controller.view_module(@conn), "_tag_activity.atom", Map.merge(assigns, prepare_activity(activity, actor: true)) %> +  <% end %>  </feed> diff --git a/lib/pleroma/web/templates/feed/feed/tag.rss.eex b/lib/pleroma/web/templates/feed/feed/tag.rss.eex index edcc3e436..27dde5627 100644 --- a/lib/pleroma/web/templates/feed/feed/tag.rss.eex +++ b/lib/pleroma/web/templates/feed/feed/tag.rss.eex @@ -1,15 +1,16 @@  <?xml version="1.0" encoding="UTF-8"?> -<rss version="2.0" xmlns:webfeeds="http://webfeeds.org/rss/1.0"> +<rss version="2.0" +     xmlns:webfeeds="http://webfeeds.org/rss/1.0" +     xmlns:thr="http://purl.org/syndication/thread/1.0">    <channel> -      <title>#<%= @tag %></title>      <description><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></description>      <link><%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %></link>      <webfeeds:logo><%= feed_logo() %></webfeeds:logo>      <webfeeds:accentColor>2b90d9</webfeeds:accentColor>      <%= for activity <- @activities do %> -    <%= render @view_module, "_tag_activity.xml", Map.merge(assigns, prepare_activity(activity)) %> +    <%= render Phoenix.Controller.view_module(@conn), "_tag_activity.xml", Map.merge(assigns, prepare_activity(activity)) %>      <% end %>    </channel>  </rss> diff --git a/lib/pleroma/web/templates/feed/feed/user.atom.eex b/lib/pleroma/web/templates/feed/feed/user.atom.eex index 5c1f0ecbc..e36bfc66c 100644 --- a/lib/pleroma/web/templates/feed/feed/user.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/user.atom.eex @@ -8,17 +8,18 @@    <id><%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".atom" %></id>    <title><%= @user.nickname <> "'s timeline" %></title> -  <updated><%= most_recent_update(@activities, @user) %></updated> +  <subtitle><%= escape(@user.bio) %></subtitle> +  <updated><%= most_recent_update(@activities, @user, :atom) %></updated>    <logo><%= logo(@user) %></logo>    <link rel="self" href="<%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/> -  <%= render @view_module, "_author.atom", assigns %> +  <%= render Phoenix.Controller.view_module(@conn), "_author.atom", assigns %>    <%= if last_activity(@activities) do %>      <link rel="next" href="<%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.atom?max_id=#{last_activity(@activities).id}' %>" type="application/atom+xml"/>    <% end %>    <%= for activity <- @activities do %> -  <%= render @view_module, "_activity.atom", Map.merge(assigns, prepare_activity(activity)) %> +  <%= render Phoenix.Controller.view_module(@conn), "_activity.atom", Map.merge(assigns, prepare_activity(activity)) %>    <% end %>  </feed> diff --git a/lib/pleroma/web/templates/feed/feed/user.rss.eex b/lib/pleroma/web/templates/feed/feed/user.rss.eex index 6b842a085..fae3fcf3d 100644 --- a/lib/pleroma/web/templates/feed/feed/user.rss.eex +++ b/lib/pleroma/web/templates/feed/feed/user.rss.eex @@ -1,20 +1,30 @@  <?xml version="1.0" encoding="UTF-8" ?> -<rss version="2.0"> +<rss version="2.0" +     xmlns:atom="http://www.w3.org/2005/Atom" +     xmlns:thr="http://purl.org/syndication/thread/1.0" +     xmlns:activity="http://activitystrea.ms/spec/1.0/" +     xmlns:ostatus="http://ostatus.org/schema/1.0" +     xmlns:poco="http://portablecontacts.net/spec/1.0">    <channel> -    <guid><%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".rss" %></guid>      <title><%= @user.nickname <> "'s timeline" %></title> -    <updated><%= most_recent_update(@activities, @user) %></updated> -    <image><%= logo(@user) %></image>      <link><%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss' %></link> +    <atom:link href="<%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".atom" %>" +               rel="self" type="application/rss+xml" /> +    <description><%= escape(@user.bio) %></description> +    <image> +      <url><%= logo(@user) %></url> +      <title><%= @user.nickname <> "'s timeline" %></title> +      <link><%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss' %></link> +    </image> -    <%= render @view_module, "_author.rss", assigns %> +    <%= render Phoenix.Controller.view_module(@conn), "_author.rss", assigns %>      <%= if last_activity(@activities) do %>        <link rel="next"><%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss?max_id=#{last_activity(@activities).id}' %></link>      <% end %>      <%= for activity <- @activities do %> -    <%= render @view_module, "_activity.rss", Map.merge(assigns, prepare_activity(activity)) %> +    <%= render Phoenix.Controller.view_module(@conn), "_activity.rss", Map.merge(assigns, prepare_activity(activity)) %>      <% end %>    </channel>  </rss> diff --git a/lib/pleroma/web/templates/layout/email.html.eex b/lib/pleroma/web/templates/layout/email.html.eex index 087aa4fc0..5858e48b4 100644 --- a/lib/pleroma/web/templates/layout/email.html.eex +++ b/lib/pleroma/web/templates/layout/email.html.eex @@ -5,6 +5,6 @@      <title><%= @email.subject %></title>    </head>    <body> -    <%= render @view_module, @view_template, assigns %> +    <%= render Phoenix.Controller.view_module(@conn), Phoenix.Controller.view_template(@conn), assigns %>    </body>  </html> diff --git a/lib/pleroma/web/templates/layout/embed.html.eex b/lib/pleroma/web/templates/layout/embed.html.eex index 8b905f070..1197288e5 100644 --- a/lib/pleroma/web/templates/layout/embed.html.eex +++ b/lib/pleroma/web/templates/layout/embed.html.eex @@ -10,6 +10,6 @@      <base target="_parent">    </head>    <body> -    <%= render @view_module, @view_template, assigns %> +    <%= render Phoenix.Controller.view_module(@conn), Phoenix.Controller.view_template(@conn), assigns %>    </body>  </html> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex index 8b894cd58..98904ad64 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex @@ -2,7 +2,7 @@  <%= form_for @conn, Routes.o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %>    <div style="display: none"> -    <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> +    <%= render Phoenix.Controller.view_module(@conn), "_scopes.html", Map.merge(assigns, %{form: f}) %>    </div>    <%= hidden_input f, :client_id, value: @client_id %> 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 a2f41618e..b3654f3eb 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 @@ -21,7 +21,7 @@  <div class="container__content">    <%= if @app do %>      <p><%= raw Gettext.dpgettext("static_pages", "oauth authorize message", "Application <strong>%{client_name}</strong> is requesting access to your account.", client_name: safe_to_string(html_escape(@app.client_name))) %></p> -    <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> +    <%= render Phoenix.Controller.view_module(@conn), "_scopes.html", Map.merge(assigns, %{form: f}) %>    <% end %>    <%= if @user do %> @@ -63,5 +63,5 @@  <% end %>  <%= if Pleroma.Config.oauth_consumer_enabled?() do %> -  <%= render @view_module, Pleroma.Web.Auth.WrapperAuthenticator.oauth_consumer_template(), assigns %> +  <%= render Phoenix.Controller.view_module(@conn), Pleroma.Web.Auth.WrapperAuthenticator.oauth_consumer_template(), assigns %>  <% end %> diff --git a/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex b/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex new file mode 100644 index 000000000..d77174967 --- /dev/null +++ b/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex @@ -0,0 +1,10 @@ +<%= if @error do %> +  <h2><%= Gettext.dpgettext("static_pages", "status interact error", "Error: %{error}", error: @error) %></h2> +<% else %> +  <h2><%= raw Gettext.dpgettext("static_pages", "status interact header", "Interacting with %{nickname}'s %{status_link}", nickname: safe_to_string(html_escape(@nickname)), status_link: safe_to_string(link(Gettext.dpgettext("static_pages", "status interact header - status link text", "status"), to: @status_link))) %></h2> +  <%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "status"], fn f -> %> +  <%= hidden_input f, :status_id, value: @status_id %> +  <%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %> +  <%= submit Gettext.dpgettext("static_pages", "status interact authorization button", "Interact") %> +  <% end %> +<% end %> diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 5731c78a8..d5a24ae6c 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    require Logger +  alias Pleroma.Activity    alias Pleroma.Config    alias Pleroma.Emoji    alias Pleroma.Healthcheck @@ -16,8 +17,16 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    alias Pleroma.Web.Plugs.OAuthScopesPlug    alias Pleroma.Web.WebFinger -  plug(Pleroma.Web.ApiSpec.CastAndValidate when action != :remote_subscribe) -  plug(Pleroma.Web.Plugs.FederatingPlug when action == :remote_subscribe) +  plug( +    Pleroma.Web.ApiSpec.CastAndValidate +    when action != :remote_subscribe and action != :show_subscribe_form +  ) + +  plug( +    Pleroma.Web.Plugs.FederatingPlug +    when action == :remote_subscribe +    when action == :show_subscribe_form +  )    plug(      OAuthScopesPlug, @@ -44,7 +53,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TwitterUtilOperation -  def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do +  def show_subscribe_form(conn, %{"nickname" => nick}) do      with %User{} = user <- User.get_cached_by_nickname(nick),           avatar = User.avatar_url(user) do        conn @@ -54,11 +63,52 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do          render(conn, "subscribe.html", %{            nickname: nick,            avatar: nil, -          error: "Could not find user" +          error: +            Pleroma.Web.Gettext.dpgettext( +              "static_pages", +              "remote follow error message - user not found", +              "Could not find user" +            )          })      end    end +  def show_subscribe_form(conn, %{"status_id" => id}) do +    with %Activity{} = activity <- Activity.get_by_id(id), +         {:ok, ap_id} <- get_ap_id(activity), +         %User{} = user <- User.get_cached_by_ap_id(activity.actor), +         avatar = User.avatar_url(user) do +      conn +      |> render("status_interact.html", %{ +        status_link: ap_id, +        status_id: id, +        nickname: user.nickname, +        avatar: avatar, +        error: false +      }) +    else +      _e -> +        render(conn, "status_interact.html", %{ +          status_id: id, +          avatar: nil, +          error: +            Pleroma.Web.Gettext.dpgettext( +              "static_pages", +              "status interact error message - status not found", +              "Could not find status" +            ) +        }) +    end +  end + +  def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do +    show_subscribe_form(conn, %{"nickname" => nick}) +  end + +  def remote_subscribe(conn, %{"status_id" => id, "profile" => _}) do +    show_subscribe_form(conn, %{"status_id" => id}) +  end +    def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do      with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),           %User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do @@ -69,7 +119,33 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do          render(conn, "subscribe.html", %{            nickname: nick,            avatar: nil, -          error: "Something went wrong." +          error: +            Pleroma.Web.Gettext.dpgettext( +              "static_pages", +              "remote follow error message - unknown error", +              "Something went wrong." +            ) +        }) +    end +  end + +  def remote_subscribe(conn, %{"status" => %{"status_id" => id, "profile" => profile}}) do +    with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile), +         %Activity{} = activity <- Activity.get_by_id(id), +         {:ok, ap_id} <- get_ap_id(activity) do +      conn +      |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id)) +    else +      _e -> +        render(conn, "status_interact.html", %{ +          status_id: id, +          avatar: nil, +          error: +            Pleroma.Web.Gettext.dpgettext( +              "static_pages", +              "status interact error message - unknown error", +              "Something went wrong." +            )          })      end    end @@ -83,6 +159,15 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do      end    end +  defp get_ap_id(activity) do +    object = Pleroma.Object.normalize(activity, fetch: false) + +    case object do +      %{data: %{"id" => ap_id}} -> {:ok, ap_id} +      _ -> {:no_ap_id, nil} +    end +  end +    def frontend_configurations(conn, _params) do      render(conn, "frontend_configurations.json")    end diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex index 69f243097..31b7c0c0c 100644 --- a/lib/pleroma/web/twitter_api/views/util_view.ex +++ b/lib/pleroma/web/twitter_api/views/util_view.ex @@ -4,7 +4,9 @@  defmodule Pleroma.Web.TwitterAPI.UtilView do    use Pleroma.Web, :view +  import Phoenix.HTML    import Phoenix.HTML.Form +  import Phoenix.HTML.Link    alias Pleroma.Config    alias Pleroma.Web.Endpoint    alias Pleroma.Web.Gettext diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index 16c2b7d61..6a55242b0 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -25,6 +25,20 @@ defmodule Pleroma.Web.StreamerView do      |> Jason.encode!()    end +  def render("status_update.json", %Activity{} = activity, %User{} = user) do +    %{ +      event: "status.update", +      payload: +        Pleroma.Web.MastodonAPI.StatusView.render( +          "show.json", +          activity: activity, +          for: user +        ) +        |> Jason.encode!() +    } +    |> Jason.encode!() +  end +    def render("notification.json", %Notification{} = notify, %User{} = user) do      %{        event: "notification", @@ -51,6 +65,19 @@ defmodule Pleroma.Web.StreamerView do      |> Jason.encode!()    end +  def render("status_update.json", %Activity{} = activity) do +    %{ +      event: "status.update", +      payload: +        Pleroma.Web.MastodonAPI.StatusView.render( +          "show.json", +          activity: activity +        ) +        |> Jason.encode!() +    } +    |> Jason.encode!() +  end +    def render("chat_update.json", %{chat_message_reference: cm_ref}) do      # Explicitly giving the cmr for the object here, so we don't accidentally      # send a later 'last_message' that was inserted between inserting this and diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex index 6cd9962ce..f95dc2458 100644 --- a/lib/pleroma/web/web_finger.ex +++ b/lib/pleroma/web/web_finger.ex @@ -32,7 +32,13 @@ defmodule Pleroma.Web.WebFinger do    def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do      host = Pleroma.Web.Endpoint.host() -    regex = ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/ + +    regex = +      if webfinger_domain = Pleroma.Config.get([__MODULE__, :domain]) do +        ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@(#{host}|#{webfinger_domain})/ +      else +        ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/ +      end      with %{"username" => username} <- Regex.named_captures(regex, resource),           %User{} = user <- User.get_cached_by_nickname(username) do @@ -63,18 +69,14 @@ defmodule Pleroma.Web.WebFinger do    end    def represent_user(user, "JSON") do -    {:ok, user} = User.ensure_keys_present(user) -      %{ -      "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", +      "subject" => "acct:#{user.nickname}@#{domain()}",        "aliases" => gather_aliases(user),        "links" => gather_links(user)      }    end    def represent_user(user, "XML") do -    {:ok, user} = User.ensure_keys_present(user) -      aliases =        user        |> gather_aliases() @@ -88,12 +90,16 @@ defmodule Pleroma.Web.WebFinger do        :XRD,        %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},        [ -        {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"} +        {:Subject, "acct:#{user.nickname}@#{domain()}"}        ] ++ aliases ++ links      }      |> XmlBuilder.to_doc()    end +  defp domain do +    Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host() +  end +    defp webfinger_from_xml(body) do      with {:ok, doc} <- XML.parse_document(body) do        subject = XML.string_from_xpath("//Subject", doc) @@ -150,17 +156,15 @@ defmodule Pleroma.Web.WebFinger do    end    def find_lrdd_template(domain) do -    with {:ok, %{status: status, body: body}} when status in 200..299 <- -           HTTP.get("http://#{domain}/.well-known/host-meta") do +    # WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1 +    meta_url = "https://#{domain}/.well-known/host-meta" + +    with {:ok, %{status: status, body: body}} when status in 200..299 <- HTTP.get(meta_url) do        get_template_from_xml(body)      else -      _ -> -        with {:ok, %{body: body, status: status}} when status in 200..299 <- -               HTTP.get("https://#{domain}/.well-known/host-meta") do -          get_template_from_xml(body) -        else -          e -> {:error, "Can't find LRDD template: #{inspect(e)}"} -        end +      error -> +        Logger.warn("Can't find LRDD template in #{inspect(meta_url)}: #{inspect(error)}") +        {:error, :lrdd_not_found}      end    end @@ -174,7 +178,7 @@ defmodule Pleroma.Web.WebFinger do      end    end -  defp get_address_from_domain(_, _), do: nil +  defp get_address_from_domain(_, _), do: {:error, :webfinger_no_domain}    @spec finger(String.t()) :: {:ok, map()} | {:error, any()}    def finger(account) do @@ -191,13 +195,11 @@ defmodule Pleroma.Web.WebFinger do      encoded_account = URI.encode("acct:#{account}")      with address when is_binary(address) <- get_address_from_domain(domain, encoded_account), -         response <- +         {:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <-             HTTP.get(               address,               [{"accept", "application/xrd+xml,application/jrd+json"}] -           ), -         {:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <- -           response do +           ) do        case List.keyfind(headers, "content-type", 0) do          {_, content_type} ->            case Plug.Conn.Utils.media_type(content_type) do @@ -215,10 +217,9 @@ defmodule Pleroma.Web.WebFinger do            {:error, {:content_type, nil}}        end      else -      e -> -        Logger.debug(fn -> "Couldn't finger #{account}" end) -        Logger.debug(fn -> inspect(e) end) -        {:error, e} +      error -> +        Logger.debug("Couldn't finger #{account}: #{inspect(error)}") +        error      end    end  end diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex index 0a397eae0..4c1764053 100644 --- a/lib/pleroma/workers/attachments_cleanup_worker.ex +++ b/lib/pleroma/workers/attachments_cleanup_worker.ex @@ -31,6 +31,9 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do    def perform(%Job{args: %{"op" => "cleanup_attachments", "object" => _object}}), do: {:ok, :skip} +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(900) +    defp do_clean({object_ids, attachment_urls}) do      uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex index 91440cbe6..794417612 100644 --- a/lib/pleroma/workers/background_worker.ex +++ b/lib/pleroma/workers/background_worker.ex @@ -43,4 +43,7 @@ defmodule Pleroma.Workers.BackgroundWorker do    def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do      Instance.perform(:delete_instance, host)    end + +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(900)  end diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex index 7657fa9ce..12ee70f00 100644 --- a/lib/pleroma/workers/backup_worker.ex +++ b/lib/pleroma/workers/backup_worker.ex @@ -30,6 +30,7 @@ defmodule Pleroma.Workers.BackupWorker do      |> Oban.insert()    end +  @impl Oban.Worker    def perform(%Job{          args: %{"op" => "process", "backup_id" => backup_id, "admin_user_id" => admin_user_id}        }) do @@ -49,6 +50,9 @@ defmodule Pleroma.Workers.BackupWorker do      end    end +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(900) +    defp has_email?(user) do      not is_nil(user.email) and user.email != ""    end diff --git a/lib/pleroma/workers/mailer_worker.ex b/lib/pleroma/workers/mailer_worker.ex index 81764ba72..940716558 100644 --- a/lib/pleroma/workers/mailer_worker.ex +++ b/lib/pleroma/workers/mailer_worker.ex @@ -12,4 +12,7 @@ defmodule Pleroma.Workers.MailerWorker do      |> :erlang.binary_to_term()      |> Pleroma.Emails.Mailer.deliver(config)    end + +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(5)  end diff --git a/lib/pleroma/workers/mute_expire_worker.ex b/lib/pleroma/workers/mute_expire_worker.ex index a7841d917..8ce458d48 100644 --- a/lib/pleroma/workers/mute_expire_worker.ex +++ b/lib/pleroma/workers/mute_expire_worker.ex @@ -17,4 +17,7 @@ defmodule Pleroma.Workers.MuteExpireWorker do      Pleroma.Web.CommonAPI.remove_mute(user_id, activity_id)      :ok    end + +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(5)  end diff --git a/lib/pleroma/workers/poll_worker.ex b/lib/pleroma/workers/poll_worker.ex index 4c7eab5c1..022d026f8 100644 --- a/lib/pleroma/workers/poll_worker.ex +++ b/lib/pleroma/workers/poll_worker.ex @@ -19,6 +19,9 @@ defmodule Pleroma.Workers.PollWorker do      end    end +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(5) +    defp find_poll_activity(activity_id) do      with nil <- Activity.get_by_id(activity_id) do        {:error, :poll_activity_not_found} diff --git a/lib/pleroma/workers/publisher_worker.ex b/lib/pleroma/workers/publisher_worker.ex index 528a06bb3..598ae3779 100644 --- a/lib/pleroma/workers/publisher_worker.ex +++ b/lib/pleroma/workers/publisher_worker.ex @@ -22,4 +22,7 @@ defmodule Pleroma.Workers.PublisherWorker do      params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end)      Federator.perform(:publish_one, String.to_atom(module_name), params)    end + +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(10)  end diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex index 0545d3ece..e554684fe 100644 --- a/lib/pleroma/workers/purge_expired_activity.ex +++ b/lib/pleroma/workers/purge_expired_activity.ex @@ -35,6 +35,9 @@ defmodule Pleroma.Workers.PurgeExpiredActivity do      end    end +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(5) +    defp enabled? do      with false <- Pleroma.Config.get([__MODULE__, :enabled], false) do        {:error, :expired_activities_disabled} diff --git a/lib/pleroma/workers/purge_expired_filter.ex b/lib/pleroma/workers/purge_expired_filter.ex index 933ecb3f6..9114aeb7f 100644 --- a/lib/pleroma/workers/purge_expired_filter.ex +++ b/lib/pleroma/workers/purge_expired_filter.ex @@ -31,6 +31,9 @@ defmodule Pleroma.Workers.PurgeExpiredFilter do      |> Repo.delete()    end +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(5) +    @spec get_expiration(pos_integer()) :: Job.t() | nil    def get_expiration(id) do      from(j in Job, diff --git a/lib/pleroma/workers/purge_expired_token.ex b/lib/pleroma/workers/purge_expired_token.ex index 1d322b6b6..2ccd9e80b 100644 --- a/lib/pleroma/workers/purge_expired_token.ex +++ b/lib/pleroma/workers/purge_expired_token.ex @@ -26,4 +26,7 @@ defmodule Pleroma.Workers.PurgeExpiredToken do      |> Pleroma.Repo.get(id)      |> Pleroma.Repo.delete()    end + +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(5)  end diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index c41b44e14..cf1bb62b4 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -13,8 +13,14 @@ defmodule Pleroma.Workers.ReceiverWorker do        {:ok, res}      else        {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} +      {:error, :already_present} -> {:cancel, :already_present} +      {:error, {:validate_object, reason}} -> {:cancel, reason} +      {:error, {:error, {:validate, reason}}} -> {:cancel, reason}        {:error, {:reject, reason}} -> {:cancel, reason}        e -> e      end    end + +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(5)  end diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex index c3158bbbe..d2a77aa17 100644 --- a/lib/pleroma/workers/remote_fetcher_worker.ex +++ b/lib/pleroma/workers/remote_fetcher_worker.ex @@ -11,4 +11,7 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do    def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do      {:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"])    end + +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(10)  end diff --git a/lib/pleroma/workers/scheduled_activity_worker.ex b/lib/pleroma/workers/scheduled_activity_worker.ex index 9a17330b6..4df84d00f 100644 --- a/lib/pleroma/workers/scheduled_activity_worker.ex +++ b/lib/pleroma/workers/scheduled_activity_worker.ex @@ -37,6 +37,9 @@ defmodule Pleroma.Workers.ScheduledActivityWorker do      end    end +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(5) +    defp find_scheduled_activity(id) do      with nil <- Repo.get(ScheduledActivity, id) do        {:error, :scheduled_activity_not_found} diff --git a/lib/pleroma/workers/transmogrifier_worker.ex b/lib/pleroma/workers/transmogrifier_worker.ex index ed319c585..1f3f5385e 100644 --- a/lib/pleroma/workers/transmogrifier_worker.ex +++ b/lib/pleroma/workers/transmogrifier_worker.ex @@ -12,4 +12,7 @@ defmodule Pleroma.Workers.TransmogrifierWorker do      user = User.get_cached_by_id(user_id)      Pleroma.Web.ActivityPub.Transmogrifier.perform(:user_upgrade, user)    end + +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(5)  end diff --git a/lib/pleroma/workers/web_pusher_worker.ex b/lib/pleroma/workers/web_pusher_worker.ex index 6447a5edc..67e84b0c9 100644 --- a/lib/pleroma/workers/web_pusher_worker.ex +++ b/lib/pleroma/workers/web_pusher_worker.ex @@ -17,4 +17,7 @@ defmodule Pleroma.Workers.WebPusherWorker do      Pleroma.Web.Push.Impl.perform(notification)    end + +  @impl Oban.Worker +  def timeout(_job), do: :timer.seconds(5)  end | 
