Merge branch 'develop' into arm
This commit is contained in:
		
						commit
						fa23098093
					
				
					 32 changed files with 566 additions and 87 deletions
				
			
		| 
						 | 
				
			
			@ -17,6 +17,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
			
		|||
- Deactivated users can no longer show up in the emoji reaction list
 | 
			
		||||
- Embedded posts can no longer bypass `:restrict\_unauthenticated`
 | 
			
		||||
 | 
			
		||||
## Security
 | 
			
		||||
 | 
			
		||||
- Add `no_new_privs` hardening to OpenRC and systemd service files
 | 
			
		||||
 | 
			
		||||
## 2023.05
 | 
			
		||||
 | 
			
		||||
## Added
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,7 @@ Home, public, hashtag & list timelines accept these parameters:
 | 
			
		|||
## Statuses
 | 
			
		||||
 | 
			
		||||
- `visibility`: has additional possible values `list` and `local` (for local-only statuses)
 | 
			
		||||
- `emoji_reactions`: additional field since Akkoma 3.2.0; identical to `pleroma/emoji_reactions`
 | 
			
		||||
 | 
			
		||||
Has these additional fields under the `pleroma` object:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +37,9 @@ Has these additional fields under the `pleroma` object:
 | 
			
		|||
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being its mimetype. Currently, the only alternate representation supported is `text/plain`
 | 
			
		||||
- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
 | 
			
		||||
- `thread_muted`: true if the thread the post belongs to is muted
 | 
			
		||||
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.
 | 
			
		||||
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 2, me: true, account_ids: ["UserID1", "UserID2"]}`.
 | 
			
		||||
  The `account_ids` property was added in Akkoma 3.2.0.
 | 
			
		||||
  Further info about all reacting users at once, can be found using the `/statuses/:id/reactions` endpoint.
 | 
			
		||||
- `parent_visible`: If the parent of this post is visible to the user or not.
 | 
			
		||||
- `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -214,6 +217,11 @@ Returns: array of Status.
 | 
			
		|||
 | 
			
		||||
The maximum number of statuses is limited to 100 per request.
 | 
			
		||||
 | 
			
		||||
## PUT `/api/v1/statuses/:id/emoji_reactions/:emoji`
 | 
			
		||||
 | 
			
		||||
This endpoint is an extension of the Fedibird Mastodon fork.
 | 
			
		||||
It behaves identical to PUT `/api/v1/pleroma/statuses/:id/reactions/:emoji`.
 | 
			
		||||
 | 
			
		||||
## PATCH `/api/v1/accounts/update_credentials`
 | 
			
		||||
 | 
			
		||||
Additional parameters can be added to the JSON body/Form data:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,6 +38,8 @@ ProtectHome=true
 | 
			
		|||
ProtectSystem=full
 | 
			
		||||
; Sets up a new /dev mount for the process and only adds API pseudo devices like /dev/null, /dev/zero or /dev/random but not physical devices. Disabled by default because it may not work on devices like the Raspberry Pi.
 | 
			
		||||
PrivateDevices=false
 | 
			
		||||
; Ensures that the service process and all its children can never gain new privileges through execve(). 
 | 
			
		||||
NoNewPrivileges=true
 | 
			
		||||
; Drops the sysadmin capability from the daemon.
 | 
			
		||||
CapabilityBoundingSet=~CAP_SYS_ADMIN
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ pidfile="/var/run/akkoma.pid"
 | 
			
		|||
directory=/opt/akkoma
 | 
			
		||||
healthcheck_delay=60
 | 
			
		||||
healthcheck_timer=30
 | 
			
		||||
no_new_privs="yes"
 | 
			
		||||
 | 
			
		||||
: ${akkoma_port:-4000}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ defp generate_topics(object, activity) 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 +31,10 @@ defp visibility_tags(object, activity) do
 | 
			
		|||
        end
 | 
			
		||||
        |> item_creation_tags(object, activity)
 | 
			
		||||
 | 
			
		||||
      "local" ->
 | 
			
		||||
        ["public:local"]
 | 
			
		||||
        |> item_creation_tags(object, activity)
 | 
			
		||||
 | 
			
		||||
      "direct" ->
 | 
			
		||||
        ["direct"]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +43,10 @@ defp visibility_tags(object, activity) 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 +71,18 @@ defp remote_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]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -195,6 +195,7 @@ defp exclude_filtered(query, user) 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
 | 
			
		||||
| 
						 | 
				
			
			@ -695,7 +696,7 @@ def skip?(
 | 
			
		|||
    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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -876,7 +876,7 @@ def post_register_action(%User{is_approved: true, is_confirmed: true} = 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()
 | 
			
		||||
| 
						 | 
				
			
			@ -884,6 +884,10 @@ defp send_user_approval_email(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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,10 @@ def cast_and_filter_recipients(message, field, follower_collection, field_fallba
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def fix_object_defaults(data) do
 | 
			
		||||
    context = Utils.maybe_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"])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -410,7 +410,7 @@ def blocks_operation 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())
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -143,7 +143,7 @@ def admin_account do
 | 
			
		|||
          }
 | 
			
		||||
        },
 | 
			
		||||
        tags: %Schema{type: :string},
 | 
			
		||||
        is_confirmed: %Schema{type: :string}
 | 
			
		||||
        is_confirmed: %Schema{type: :boolean}
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -88,7 +88,7 @@ def reject_follow_request(follower, followed) do
 | 
			
		|||
 | 
			
		||||
  def delete(activity_id, user) do
 | 
			
		||||
    with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
 | 
			
		||||
           {:find_activity, Activity.get_by_id(activity_id)},
 | 
			
		||||
           {:find_activity, Activity.get_by_id(activity_id, filter: [])},
 | 
			
		||||
         {_, %Object{} = object, _} <-
 | 
			
		||||
           {:find_object, Object.normalize(activity, fetch: false), activity},
 | 
			
		||||
         true <- User.superuser?(user) || user.ap_id == object.data["actor"],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -144,6 +144,8 @@ def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data)
 | 
			
		|||
      when is_list(options) do
 | 
			
		||||
    limits = Config.get([:instance, :poll_limits])
 | 
			
		||||
 | 
			
		||||
    options = options |> Enum.uniq()
 | 
			
		||||
 | 
			
		||||
    with :ok <- validate_poll_expiration(expires_in, limits),
 | 
			
		||||
         :ok <- validate_poll_options_amount(options, limits),
 | 
			
		||||
         :ok <- validate_poll_options_length(options, limits) do
 | 
			
		||||
| 
						 | 
				
			
			@ -179,10 +181,15 @@ def make_poll_data(_data) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  defp validate_poll_options_amount(options, %{max_options: max_options}) do
 | 
			
		||||
    if Enum.count(options) > max_options do
 | 
			
		||||
      {:error, "Poll can't contain more than #{max_options} options"}
 | 
			
		||||
    else
 | 
			
		||||
      :ok
 | 
			
		||||
    cond do
 | 
			
		||||
      Enum.count(options) < 2 ->
 | 
			
		||||
        {:error, "Poll must contain at least 2 options"}
 | 
			
		||||
 | 
			
		||||
      Enum.count(options) > max_options ->
 | 
			
		||||
        {:error, "Poll can't contain more than #{max_options} options"}
 | 
			
		||||
 | 
			
		||||
      true ->
 | 
			
		||||
        :ok
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -518,7 +518,12 @@ def blocks(%{assigns: %{user: user}} = conn, params) 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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,12 +20,12 @@ def build_tags(%{activity_id: id, object: object, user: user}) 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 @@ def build_tags(%{user: user}) 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 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) 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 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) 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 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) 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 @@ defp maybe_add_dimensions(metadata, url) 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 ->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,7 +37,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
 | 
			
		|||
        %{query_params: %{"name" => name}} = conn ->
 | 
			
		||||
          name = escape_header_value(name)
 | 
			
		||||
 | 
			
		||||
          put_resp_header(conn, "content-disposition", "filename=\"#{name}\"")
 | 
			
		||||
          put_resp_header(conn, "content-disposition", ~s[inline; filename="#{name}"])
 | 
			
		||||
 | 
			
		||||
        conn ->
 | 
			
		||||
          conn
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,7 @@ defmodule Pleroma.Web.Streamer do
 | 
			
		|||
  def registry, do: @registry
 | 
			
		||||
 | 
			
		||||
  @public_streams ["public", "public:local", "public:media", "public:local:media"]
 | 
			
		||||
  @local_streams ["public:local", "public:local:media"]
 | 
			
		||||
  @user_streams ["user", "user:notification", "direct"]
 | 
			
		||||
 | 
			
		||||
  @doc "Expands and authorizes a stream, and registers the process for streaming."
 | 
			
		||||
| 
						 | 
				
			
			@ -41,14 +42,37 @@ def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp can_access_stream(user, oauth_token, kind) do
 | 
			
		||||
    with {_, true} <- {:restrict?, Config.restrict_unauthenticated_access?(:timelines, kind)},
 | 
			
		||||
         {_, %User{id: user_id}, %Token{user_id: user_id}} <- {:user, user, oauth_token},
 | 
			
		||||
         {_, true} <-
 | 
			
		||||
           {:scopes,
 | 
			
		||||
            OAuthScopesPlug.filter_descendants(["read:statuses"], oauth_token.scopes) != []} do
 | 
			
		||||
      true
 | 
			
		||||
    else
 | 
			
		||||
      {:restrict?, _} ->
 | 
			
		||||
        true
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        false
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc "Expand and authorizes a stream"
 | 
			
		||||
  @spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) ::
 | 
			
		||||
          {:ok, topic :: String.t()} | {:error, :bad_topic}
 | 
			
		||||
  def get_topic(stream, user, oauth_token, params \\ %{})
 | 
			
		||||
 | 
			
		||||
  # Allow all public steams.
 | 
			
		||||
  def get_topic(stream, _user, _oauth_token, _params) when stream in @public_streams do
 | 
			
		||||
    {:ok, stream}
 | 
			
		||||
  # Allow all public steams if the instance allows unauthenticated access.
 | 
			
		||||
  # Otherwise, only allow users with valid oauth tokens.
 | 
			
		||||
  def get_topic(stream, user, oauth_token, _params) when stream in @public_streams do
 | 
			
		||||
    kind = if stream in @local_streams, do: :local, else: :federated
 | 
			
		||||
 | 
			
		||||
    if can_access_stream(user, oauth_token, kind) do
 | 
			
		||||
      {:ok, stream}
 | 
			
		||||
    else
 | 
			
		||||
      {:error, :unauthorized}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Allow all hashtags streams.
 | 
			
		||||
| 
						 | 
				
			
			@ -57,12 +81,20 @@ def get_topic("hashtag", _user, _oauth_token, %{"tag" => tag} = _params) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  # Allow remote instance streams.
 | 
			
		||||
  def get_topic("public:remote", _user, _oauth_token, %{"instance" => instance} = _params) do
 | 
			
		||||
    {:ok, "public:remote:" <> instance}
 | 
			
		||||
  def get_topic("public:remote", user, oauth_token, %{"instance" => instance} = _params) do
 | 
			
		||||
    if can_access_stream(user, oauth_token, :federated) do
 | 
			
		||||
      {:ok, "public:remote:" <> instance}
 | 
			
		||||
    else
 | 
			
		||||
      {:error, :unauthorized}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_topic("public:remote:media", _user, _oauth_token, %{"instance" => instance} = _params) do
 | 
			
		||||
    {:ok, "public:remote:media:" <> instance}
 | 
			
		||||
  def get_topic("public:remote:media", user, oauth_token, %{"instance" => instance} = _params) do
 | 
			
		||||
    if can_access_stream(user, oauth_token, :federated) do
 | 
			
		||||
      {:ok, "public:remote:media:" <> instance}
 | 
			
		||||
    else
 | 
			
		||||
      {:error, :unauthorized}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Expand user streams.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ command=/opt/akkoma/bin/pleroma
 | 
			
		|||
command_args="start"
 | 
			
		||||
command_user=akkoma
 | 
			
		||||
command_background=1
 | 
			
		||||
no_new_privs="yes"
 | 
			
		||||
 | 
			
		||||
# Ask process to terminate within 30 seconds, otherwise kill it
 | 
			
		||||
retry="SIGTERM/30/SIGKILL/5"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","Hashtag":"as:Hashtag","quoteUrl":"as:quoteUrl","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","featured":"toot:featured","discoverable":"toot:discoverable","schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value","misskey":"https://misskey-hub.net/ns#","_misskey_content":"misskey:_misskey_content","_misskey_quote":"misskey:_misskey_quote","_misskey_reaction":"misskey:_misskey_reaction","_misskey_votes":"misskey:_misskey_votes","_misskey_talk":"misskey:_misskey_talk","isCat":"misskey:isCat","vcard":"http://www.w3.org/2006/vcard/ns#"}],"id":"https://mk.absturztau.be/notes/93e7nm8wqg/activity","actor":"https://mk.absturztau.be/users/8ozbzjs3o8","type":"Create","published":"2022-08-01T11:06:49.568Z","object":{"id":"https://mk.absturztau.be/notes/93e7nm8wqg","type":"Note","attributedTo":"https://mk.absturztau.be/users/8ozbzjs3o8","summary":null,"content":"<p><span>meow</span></p>","_misskey_content":"meow","published":"2022-08-01T11:06:49.568Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mk.absturztau.be/users/8ozbzjs3o8/followers"],"inReplyTo":null,"attachment":[],"sensitive":false,"tag":[]},"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mk.absturztau.be/users/8ozbzjs3o8/followers"]}
 | 
			
		||||
| 
						 | 
				
			
			@ -35,7 +35,7 @@ test "always add user and list topics" do
 | 
			
		|||
    setup do
 | 
			
		||||
      activity = %Activity{
 | 
			
		||||
        object: %Object{data: %{"type" => "Note"}},
 | 
			
		||||
        data: %{"to" => [Pleroma.Constants.as_public()]}
 | 
			
		||||
        data: %{"to" => [Pleroma.Constants.as_public()], "type" => "Create"}
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      {:ok, activity: activity}
 | 
			
		||||
| 
						 | 
				
			
			@ -114,6 +114,55 @@ test "local action doesn't produce public:remote topic", %{activity: activity} d
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "public visibility Announces" do
 | 
			
		||||
    setup do
 | 
			
		||||
      activity = %Activity{
 | 
			
		||||
        object: %Object{data: %{"attachment" => []}},
 | 
			
		||||
        data: %{"type" => "Announce", "to" => [Pleroma.Constants.as_public()]}
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      {:ok, activity: activity}
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "does not generate public topics", %{activity: activity} do
 | 
			
		||||
      topics = Topics.get_activity_topics(activity)
 | 
			
		||||
 | 
			
		||||
      refute "public" in topics
 | 
			
		||||
      refute "public:remote" in topics
 | 
			
		||||
      refute "public:local" in topics
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "local-public visibility create events" do
 | 
			
		||||
    setup do
 | 
			
		||||
      activity = %Activity{
 | 
			
		||||
        object: %Object{data: %{"attachment" => []}},
 | 
			
		||||
        data: %{"type" => "Create", "to" => [Pleroma.Web.ActivityPub.Utils.as_local_public()]}
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      {:ok, activity: activity}
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "doesn't produce public topics", %{activity: activity} do
 | 
			
		||||
      topics = Topics.get_activity_topics(activity)
 | 
			
		||||
 | 
			
		||||
      refute Enum.member?(topics, "public")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "produces public:local topics", %{activity: activity} do
 | 
			
		||||
      topics = Topics.get_activity_topics(activity)
 | 
			
		||||
 | 
			
		||||
      assert Enum.member?(topics, "public:local")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "with no attachments doesn't produce public:media topics", %{activity: activity} do
 | 
			
		||||
      topics = Topics.get_activity_topics(activity)
 | 
			
		||||
 | 
			
		||||
      refute Enum.member?(topics, "public:media")
 | 
			
		||||
      refute Enum.member?(topics, "public:local:media")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "public visibility create events with attachments" do
 | 
			
		||||
    setup do
 | 
			
		||||
      activity = %Activity{
 | 
			
		||||
| 
						 | 
				
			
			@ -152,9 +201,36 @@ test "non-local action produces public:remote:media topic", %{activity: activity
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "local-public visibility create events with attachments" do
 | 
			
		||||
    setup do
 | 
			
		||||
      activity = %Activity{
 | 
			
		||||
        object: %Object{data: %{"attachment" => ["foo"]}},
 | 
			
		||||
        data: %{"type" => "Create", "to" => [Pleroma.Web.ActivityPub.Utils.as_local_public()]}
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      {:ok, activity: activity}
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "do not produce public:media topics", %{activity: activity} do
 | 
			
		||||
      topics = Topics.get_activity_topics(activity)
 | 
			
		||||
 | 
			
		||||
      refute Enum.member?(topics, "public:media")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "produces public:local:media topics", %{activity: activity} do
 | 
			
		||||
      topics = Topics.get_activity_topics(activity)
 | 
			
		||||
 | 
			
		||||
      assert Enum.member?(topics, "public:local:media")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "non-public visibility" do
 | 
			
		||||
    test "produces direct topic" do
 | 
			
		||||
      activity = %Activity{object: %Object{data: %{"type" => "Note"}}, data: %{"to" => []}}
 | 
			
		||||
      activity = %Activity{
 | 
			
		||||
        object: %Object{data: %{"type" => "Note"}},
 | 
			
		||||
        data: %{"to" => [], "type" => "Create"}
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      topics = Topics.get_activity_topics(activity)
 | 
			
		||||
 | 
			
		||||
      assert Enum.member?(topics, "direct")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -328,6 +328,32 @@ test "it disables notifications from strangers" do
 | 
			
		|||
      refute Notification.create_notification(activity, followed)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it disables notifications from non-followees" do
 | 
			
		||||
      follower = insert(:user)
 | 
			
		||||
 | 
			
		||||
      followed =
 | 
			
		||||
        insert(:user,
 | 
			
		||||
          notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      CommonAPI.follow(follower, followed)
 | 
			
		||||
      {:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"})
 | 
			
		||||
      refute Notification.create_notification(activity, followed)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it allows notifications from followees" do
 | 
			
		||||
      poster = insert(:user)
 | 
			
		||||
 | 
			
		||||
      receiver =
 | 
			
		||||
        insert(:user,
 | 
			
		||||
          notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      CommonAPI.follow(receiver, poster)
 | 
			
		||||
      {:ok, activity} = CommonAPI.post(poster, %{status: "hey @#{receiver.nickname}"})
 | 
			
		||||
      assert Notification.create_notification(activity, receiver)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it doesn't create a notification for user if he is the activity author" do
 | 
			
		||||
      activity = insert(:note_activity)
 | 
			
		||||
      author = User.get_cached_by_ap_id(activity.data["actor"])
 | 
			
		||||
| 
						 | 
				
			
			@ -1225,5 +1251,32 @@ test "it returns notifications about favorites with filtered word", %{user: user
 | 
			
		|||
 | 
			
		||||
      assert length(Notification.for_user(user)) == 1
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it returns notifications when related object is without content and filters are defined",
 | 
			
		||||
         %{user: user} do
 | 
			
		||||
      followed_user = insert(:user, is_locked: true)
 | 
			
		||||
 | 
			
		||||
      insert(:filter, user: followed_user, phrase: "test", hide: true)
 | 
			
		||||
 | 
			
		||||
      {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
 | 
			
		||||
      refute FollowingRelationship.following?(user, followed_user)
 | 
			
		||||
      assert [notification] = Notification.for_user(followed_user)
 | 
			
		||||
 | 
			
		||||
      assert %{type: "follow_request"} =
 | 
			
		||||
               NotificationView.render("show.json", %{
 | 
			
		||||
                 notification: notification,
 | 
			
		||||
                 for: followed_user
 | 
			
		||||
               })
 | 
			
		||||
 | 
			
		||||
      assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
 | 
			
		||||
 | 
			
		||||
      assert [notification] = Notification.for_user(followed_user)
 | 
			
		||||
 | 
			
		||||
      assert %{type: "follow"} =
 | 
			
		||||
               NotificationView.render("show.json", %{
 | 
			
		||||
                 notification: notification,
 | 
			
		||||
                 for: followed_user
 | 
			
		||||
               })
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -557,6 +557,21 @@ test "it fails gracefully with invalid email config" do
 | 
			
		|||
      refute_email_sent()
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it works when the registering user does not provide an email" do
 | 
			
		||||
      clear_config([Pleroma.Emails.Mailer, :enabled], false)
 | 
			
		||||
      clear_config([:instance, :account_activation_required], false)
 | 
			
		||||
      clear_config([:instance, :account_approval_required], true)
 | 
			
		||||
 | 
			
		||||
      cng = User.register_changeset(%User{}, @full_user_data |> Map.put(:email, ""))
 | 
			
		||||
 | 
			
		||||
      # The user is still created
 | 
			
		||||
      assert {:ok, %User{nickname: "nick"}} = User.register(cng)
 | 
			
		||||
 | 
			
		||||
      # No emails are sent
 | 
			
		||||
      ObanHelpers.perform_all()
 | 
			
		||||
      refute_email_sent()
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do
 | 
			
		||||
      clear_config([:instance, :account_activation_required], true)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -732,9 +732,7 @@ test "it streams out the announce", %{announce: announce} do
 | 
			
		|||
      ]) do
 | 
			
		||||
        {:ok, announce, _} = SideEffects.handle(announce)
 | 
			
		||||
 | 
			
		||||
        assert called(
 | 
			
		||||
                 Pleroma.Web.Streamer.stream(["user", "list", "public", "public:local"], announce)
 | 
			
		||||
               )
 | 
			
		||||
        assert called(Pleroma.Web.Streamer.stream(["user", "list"], announce))
 | 
			
		||||
 | 
			
		||||
        assert called(Pleroma.Web.Push.send(:_))
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -783,4 +783,42 @@ test "quote fetching should stop after n levels", _ do
 | 
			
		|||
      } = Transmogrifier.fix_quote_url(note)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  test "the standalone note uses its own ID when context is missing" do
 | 
			
		||||
    insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
 | 
			
		||||
 | 
			
		||||
    activity =
 | 
			
		||||
      "test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json"
 | 
			
		||||
      |> File.read!()
 | 
			
		||||
      |> Jason.decode!()
 | 
			
		||||
 | 
			
		||||
    {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(activity)
 | 
			
		||||
    object = Object.normalize(modified, fetch: false)
 | 
			
		||||
 | 
			
		||||
    assert object.data["context"] == object.data["id"]
 | 
			
		||||
    assert modified.data["context"] == object.data["id"]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  test "the reply note uses its parent's ID when context is missing and reply is unreachable" do
 | 
			
		||||
    insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
 | 
			
		||||
 | 
			
		||||
    activity =
 | 
			
		||||
      "test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json"
 | 
			
		||||
      |> File.read!()
 | 
			
		||||
      |> Jason.decode!()
 | 
			
		||||
 | 
			
		||||
    object =
 | 
			
		||||
      activity["object"]
 | 
			
		||||
      |> Map.put("inReplyTo", "https://404.site/object/went-to-buy-milk")
 | 
			
		||||
 | 
			
		||||
    activity =
 | 
			
		||||
      activity
 | 
			
		||||
      |> Map.put("object", object)
 | 
			
		||||
 | 
			
		||||
    {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(activity)
 | 
			
		||||
    object = Object.normalize(modified, fetch: false)
 | 
			
		||||
 | 
			
		||||
    assert object.data["context"] == object.data["inReplyTo"]
 | 
			
		||||
    assert modified.data["context"] == object.data["inReplyTo"]
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -225,6 +225,20 @@ test "superusers deleting non-local posts won't federate the delete" do
 | 
			
		|||
 | 
			
		||||
      refute Activity.get_by_id(post.id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it allows privileged users to delete banned user's posts" do
 | 
			
		||||
      clear_config([:instance, :moderator_privileges], [:messages_delete])
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      moderator = insert(:user, is_moderator: true)
 | 
			
		||||
 | 
			
		||||
      {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
 | 
			
		||||
      User.set_activation(user, false)
 | 
			
		||||
 | 
			
		||||
      assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
 | 
			
		||||
      assert delete.local
 | 
			
		||||
 | 
			
		||||
      refute Activity.get_by_id(post.id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  test "favoriting race condition" do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1895,6 +1895,39 @@ test "getting a list of blocks" do
 | 
			
		|||
    assert [%{"id" => ^id2}] = result
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  test "list of blocks with with_relationships parameter" do
 | 
			
		||||
    %{user: user, conn: conn} = oauth_access(["read:blocks"])
 | 
			
		||||
    %{id: id1} = other_user1 = insert(:user)
 | 
			
		||||
    %{id: id2} = other_user2 = insert(:user)
 | 
			
		||||
    %{id: id3} = other_user3 = insert(:user)
 | 
			
		||||
 | 
			
		||||
    {:ok, _, _} = User.follow(other_user1, user)
 | 
			
		||||
    {:ok, _, _} = User.follow(other_user2, user)
 | 
			
		||||
    {:ok, _, _} = User.follow(other_user3, user)
 | 
			
		||||
 | 
			
		||||
    {:ok, _} = User.block(user, other_user1)
 | 
			
		||||
    {:ok, _} = User.block(user, other_user2)
 | 
			
		||||
    {:ok, _} = User.block(user, other_user3)
 | 
			
		||||
 | 
			
		||||
    assert [
 | 
			
		||||
             %{
 | 
			
		||||
               "id" => ^id1,
 | 
			
		||||
               "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}}
 | 
			
		||||
             },
 | 
			
		||||
             %{
 | 
			
		||||
               "id" => ^id2,
 | 
			
		||||
               "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}}
 | 
			
		||||
             },
 | 
			
		||||
             %{
 | 
			
		||||
               "id" => ^id3,
 | 
			
		||||
               "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}}
 | 
			
		||||
             }
 | 
			
		||||
           ] =
 | 
			
		||||
             conn
 | 
			
		||||
             |> get("/api/v1/blocks?with_relationships=true")
 | 
			
		||||
             |> json_response_and_validate_schema(200)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  test "account lookup", %{conn: conn} do
 | 
			
		||||
    %{nickname: acct} = insert(:user, %{nickname: "nickname"})
 | 
			
		||||
    %{nickname: acct_two} = insert(:user, %{nickname: "nickname@notlocaldoma.in"})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -674,7 +674,10 @@ test "option limit is enforced", %{conn: conn} do
 | 
			
		|||
        |> put_req_header("content-type", "application/json")
 | 
			
		||||
        |> post("/api/v1/statuses", %{
 | 
			
		||||
          "status" => "desu~",
 | 
			
		||||
          "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
 | 
			
		||||
          "poll" => %{
 | 
			
		||||
            "options" => Enum.map(0..limit, fn num -> "desu #{num}" end),
 | 
			
		||||
            "expires_in" => 1
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
      %{"error" => error} = json_response_and_validate_schema(conn, 422)
 | 
			
		||||
| 
						 | 
				
			
			@ -690,7 +693,7 @@ test "option character limit is enforced", %{conn: conn} do
 | 
			
		|||
        |> post("/api/v1/statuses", %{
 | 
			
		||||
          "status" => "...",
 | 
			
		||||
          "poll" => %{
 | 
			
		||||
            "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
 | 
			
		||||
            "options" => [String.duplicate(".", limit + 1), "lol"],
 | 
			
		||||
            "expires_in" => 1
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
| 
						 | 
				
			
			@ -772,6 +775,32 @@ test "scheduled poll", %{conn: conn} do
 | 
			
		|||
      assert object.data["type"] == "Question"
 | 
			
		||||
      assert length(object.data["oneOf"]) == 3
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "cannot have only one option", %{conn: conn} do
 | 
			
		||||
      conn =
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_req_header("content-type", "application/json")
 | 
			
		||||
        |> post("/api/v1/statuses", %{
 | 
			
		||||
          "status" => "desu~",
 | 
			
		||||
          "poll" => %{"options" => ["mew"], "expires_in" => 1}
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
      %{"error" => error} = json_response_and_validate_schema(conn, 422)
 | 
			
		||||
      assert error == "Poll must contain at least 2 options"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "cannot have only duplicated options", %{conn: conn} do
 | 
			
		||||
      conn =
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_req_header("content-type", "application/json")
 | 
			
		||||
        |> post("/api/v1/statuses", %{
 | 
			
		||||
          "status" => "desu~",
 | 
			
		||||
          "poll" => %{"options" => ["mew", "mew"], "expires_in" => 1}
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
      %{"error" => error} = json_response_and_validate_schema(conn, 422)
 | 
			
		||||
      assert error == "Poll must contain at least 2 options"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  test "get a status" do
 | 
			
		||||
| 
						 | 
				
			
			@ -1044,6 +1073,27 @@ test "when you're an admin or moderator", %{conn: conn} do
 | 
			
		|||
      refute Activity.get_by_id(activity1.id)
 | 
			
		||||
      refute Activity.get_by_id(activity2.id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "when you're privileged and the user is banned", %{conn: conn} do
 | 
			
		||||
      clear_config([:instance, :moderator_privileges], [:messages_delete])
 | 
			
		||||
      posting_user = insert(:user, is_active: false)
 | 
			
		||||
      refute posting_user.is_active
 | 
			
		||||
      activity = insert(:note_activity, user: posting_user)
 | 
			
		||||
      user = insert(:user, is_moderator: true)
 | 
			
		||||
 | 
			
		||||
      res_conn =
 | 
			
		||||
        conn
 | 
			
		||||
        |> assign(:user, user)
 | 
			
		||||
        |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"]))
 | 
			
		||||
        |> delete("/api/v1/statuses/#{activity.id}")
 | 
			
		||||
 | 
			
		||||
      assert %{} = json_response_and_validate_schema(res_conn, 200)
 | 
			
		||||
 | 
			
		||||
      # assert ModerationLog |> Repo.one() |> ModerationLog.get_log_entry_message() ==
 | 
			
		||||
      #          "@#{user.nickname} deleted status ##{activity.id}"
 | 
			
		||||
 | 
			
		||||
      refute Activity.get_by_id(activity.id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "reblogging" do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,11 +11,24 @@ test "it renders all links with rel='me' from user bio" do
 | 
			
		|||
    bio =
 | 
			
		||||
      ~s(<a href="https://some-link.com">https://some-link.com</a> <a rel="me" href="https://another-link.com">https://another-link.com</a> <link href="http://some.com"> <link rel="me" href="http://some3.com">)
 | 
			
		||||
 | 
			
		||||
    user = insert(:user, %{bio: bio})
 | 
			
		||||
    fields = [
 | 
			
		||||
      %{
 | 
			
		||||
        "name" => "profile",
 | 
			
		||||
        "value" => ~S(<a rel="me" href="http://profile.com">http://profile.com</a>)
 | 
			
		||||
      },
 | 
			
		||||
      %{
 | 
			
		||||
        "name" => "like",
 | 
			
		||||
        "value" => ~S(<a href="http://cofe.io">http://cofe.io</a>)
 | 
			
		||||
      },
 | 
			
		||||
      %{"name" => "foo", "value" => "bar"}
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    user = insert(:user, %{bio: bio, fields: fields})
 | 
			
		||||
 | 
			
		||||
    assert RelMe.build_tags(%{user: user}) == [
 | 
			
		||||
             {:link, [rel: "me", href: "http://some3.com"], []},
 | 
			
		||||
             {:link, [rel: "me", href: "https://another-link.com"], []}
 | 
			
		||||
             {:link, [rel: "me", href: "https://another-link.com"], []},
 | 
			
		||||
             {:link, [rel: "me", href: "http://profile.com"], []}
 | 
			
		||||
           ]
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,10 +22,10 @@ test "it renders twitter card for user info" do
 | 
			
		|||
    res = TwitterCard.build_tags(%{user: user})
 | 
			
		||||
 | 
			
		||||
    assert res == [
 | 
			
		||||
             {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []},
 | 
			
		||||
             {:meta, [property: "twitter:description", content: "born 19 March 1994"], []},
 | 
			
		||||
             {:meta, [property: "twitter:image", content: avatar_url], []},
 | 
			
		||||
             {:meta, [property: "twitter:card", content: "summary"], []}
 | 
			
		||||
             {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []},
 | 
			
		||||
             {:meta, [name: "twitter:description", content: "born 19 March 1994"], []},
 | 
			
		||||
             {:meta, [name: "twitter:image", content: avatar_url], []},
 | 
			
		||||
             {:meta, [name: "twitter:card", content: "summary"], []}
 | 
			
		||||
           ]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -47,11 +47,11 @@ test "it uses summary twittercard if post has no attachment" do
 | 
			
		|||
    result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id})
 | 
			
		||||
 | 
			
		||||
    assert [
 | 
			
		||||
             {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []},
 | 
			
		||||
             {:meta, [property: "twitter:description", content: "pleroma in a nutshell"], []},
 | 
			
		||||
             {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"],
 | 
			
		||||
             {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []},
 | 
			
		||||
             {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []},
 | 
			
		||||
             {:meta, [name: "twitter:image", content: "http://localhost:4001/images/avi.png"],
 | 
			
		||||
              []},
 | 
			
		||||
             {:meta, [property: "twitter:card", content: "summary"], []}
 | 
			
		||||
             {:meta, [name: "twitter:card", content: "summary"], []}
 | 
			
		||||
           ] == result
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -73,15 +73,15 @@ test "it uses summary as description if post has one" do
 | 
			
		|||
    result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id})
 | 
			
		||||
 | 
			
		||||
    assert [
 | 
			
		||||
             {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []},
 | 
			
		||||
             {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []},
 | 
			
		||||
             {:meta,
 | 
			
		||||
              [
 | 
			
		||||
                property: "twitter:description",
 | 
			
		||||
                name: "twitter:description",
 | 
			
		||||
                content: "Public service announcement on caffeine consumption"
 | 
			
		||||
              ], []},
 | 
			
		||||
             {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"],
 | 
			
		||||
             {:meta, [name: "twitter:image", content: "http://localhost:4001/images/avi.png"],
 | 
			
		||||
              []},
 | 
			
		||||
             {:meta, [property: "twitter:card", content: "summary"], []}
 | 
			
		||||
             {:meta, [name: "twitter:card", content: "summary"], []}
 | 
			
		||||
           ] == result
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -123,11 +123,11 @@ test "it renders avatar not attachment if post is nsfw and unfurl_nsfw is disabl
 | 
			
		|||
    result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id})
 | 
			
		||||
 | 
			
		||||
    assert [
 | 
			
		||||
             {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []},
 | 
			
		||||
             {:meta, [property: "twitter:description", content: "pleroma in a nutshell"], []},
 | 
			
		||||
             {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"],
 | 
			
		||||
             {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []},
 | 
			
		||||
             {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []},
 | 
			
		||||
             {:meta, [name: "twitter:image", content: "http://localhost:4001/images/avi.png"],
 | 
			
		||||
              []},
 | 
			
		||||
             {:meta, [property: "twitter:card", content: "summary"], []}
 | 
			
		||||
             {:meta, [name: "twitter:card", content: "summary"], []}
 | 
			
		||||
           ] == result
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -179,26 +179,26 @@ test "it renders supported types of attachments and skips unknown types" do
 | 
			
		|||
    result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id})
 | 
			
		||||
 | 
			
		||||
    assert [
 | 
			
		||||
             {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []},
 | 
			
		||||
             {:meta, [property: "twitter:description", content: "pleroma in a nutshell"], []},
 | 
			
		||||
             {:meta, [property: "twitter:card", content: "summary_large_image"], []},
 | 
			
		||||
             {:meta, [property: "twitter:player", content: "https://pleroma.gov/tenshi.png"], []},
 | 
			
		||||
             {:meta, [property: "twitter:player:width", content: "1280"], []},
 | 
			
		||||
             {:meta, [property: "twitter:player:height", content: "1024"], []},
 | 
			
		||||
             {:meta, [property: "twitter:card", content: "player"], []},
 | 
			
		||||
             {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []},
 | 
			
		||||
             {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []},
 | 
			
		||||
             {:meta, [name: "twitter:card", content: "summary_large_image"], []},
 | 
			
		||||
             {:meta, [name: "twitter:player", content: "https://pleroma.gov/tenshi.png"], []},
 | 
			
		||||
             {:meta, [name: "twitter:player:width", content: "1280"], []},
 | 
			
		||||
             {:meta, [name: "twitter:player:height", content: "1024"], []},
 | 
			
		||||
             {:meta, [name: "twitter:card", content: "player"], []},
 | 
			
		||||
             {:meta,
 | 
			
		||||
              [
 | 
			
		||||
                property: "twitter:player",
 | 
			
		||||
                name: "twitter:player",
 | 
			
		||||
                content: Router.Helpers.o_status_url(Endpoint, :notice_player, activity.id)
 | 
			
		||||
              ], []},
 | 
			
		||||
             {:meta, [property: "twitter:player:width", content: "800"], []},
 | 
			
		||||
             {:meta, [property: "twitter:player:height", content: "600"], []},
 | 
			
		||||
             {:meta, [name: "twitter:player:width", content: "800"], []},
 | 
			
		||||
             {:meta, [name: "twitter:player:height", content: "600"], []},
 | 
			
		||||
             {:meta,
 | 
			
		||||
              [
 | 
			
		||||
                property: "twitter:player:stream",
 | 
			
		||||
                name: "twitter:player:stream",
 | 
			
		||||
                content: "https://pleroma.gov/about/juche.webm"
 | 
			
		||||
              ], []},
 | 
			
		||||
             {:meta, [property: "twitter:player:stream:content_type", content: "video/webm"], []}
 | 
			
		||||
             {:meta, [name: "twitter:player:stream:content_type", content: "video/webm"], []}
 | 
			
		||||
           ] == result
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,11 +33,11 @@ test "does not send Content-Disposition header when name param is not set", %{
 | 
			
		|||
  test "sends Content-Disposition header when name param is set", %{
 | 
			
		||||
    attachment_url: attachment_url
 | 
			
		||||
  } do
 | 
			
		||||
    conn = get(build_conn(), attachment_url <> "?name=\"cofe\".gif")
 | 
			
		||||
    conn = get(build_conn(), attachment_url <> ~s[?name="cofe".gif])
 | 
			
		||||
 | 
			
		||||
    assert Enum.any?(
 | 
			
		||||
             conn.resp_headers,
 | 
			
		||||
             &(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""})
 | 
			
		||||
             &(&1 == {"content-disposition", ~s[inline; filename="\\"cofe\\".gif"]})
 | 
			
		||||
           )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +48,7 @@ test "removes control characters from the Content-Disposition header", %{
 | 
			
		|||
 | 
			
		||||
    assert Enum.any?(
 | 
			
		||||
             conn.resp_headers,
 | 
			
		||||
             &(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""})
 | 
			
		||||
             &(&1 == {"content-disposition", ~s[inline; filename="\\"cofe\\".gif"]})
 | 
			
		||||
           )
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,26 @@ test "allows public" do
 | 
			
		|||
      assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "rejects local public streams if restricted_unauthenticated is on" do
 | 
			
		||||
      clear_config([:restrict_unauthenticated, :timelines, :local], true)
 | 
			
		||||
 | 
			
		||||
      assert {:error, :unauthorized} = Streamer.get_topic("public:local", nil, nil)
 | 
			
		||||
      assert {:error, :unauthorized} = Streamer.get_topic("public:local:media", nil, nil)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "rejects remote public streams if restricted_unauthenticated is on" do
 | 
			
		||||
      clear_config([:restrict_unauthenticated, :timelines, :federated], true)
 | 
			
		||||
 | 
			
		||||
      assert {:error, :unauthorized} = Streamer.get_topic("public", nil, nil)
 | 
			
		||||
      assert {:error, :unauthorized} = Streamer.get_topic("public:media", nil, nil)
 | 
			
		||||
 | 
			
		||||
      assert {:error, :unauthorized} =
 | 
			
		||||
               Streamer.get_topic("public:remote", nil, nil, %{"instance" => "lain.com"})
 | 
			
		||||
 | 
			
		||||
      assert {:error, :unauthorized} =
 | 
			
		||||
               Streamer.get_topic("public:remote:media", nil, nil, %{"instance" => "lain.com"})
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "allows instance streams" do
 | 
			
		||||
      assert {:ok, "public:remote:lain.com"} =
 | 
			
		||||
               Streamer.get_topic("public:remote", nil, nil, %{"instance" => "lain.com"})
 | 
			
		||||
| 
						 | 
				
			
			@ -65,6 +85,63 @@ test "allows public streams (regardless of OAuth token scopes)", %{
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "allows local public streams if restricted_unauthenticated is on", %{
 | 
			
		||||
      user: user,
 | 
			
		||||
      token: oauth_token
 | 
			
		||||
    } do
 | 
			
		||||
      clear_config([:restrict_unauthenticated, :timelines, :local], true)
 | 
			
		||||
 | 
			
		||||
      %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user)
 | 
			
		||||
      %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user)
 | 
			
		||||
 | 
			
		||||
      assert {:ok, "public:local"} = Streamer.get_topic("public:local", user, oauth_token)
 | 
			
		||||
 | 
			
		||||
      assert {:ok, "public:local:media"} =
 | 
			
		||||
               Streamer.get_topic("public:local:media", user, oauth_token)
 | 
			
		||||
 | 
			
		||||
      for token <- [read_notifications_token, badly_scoped_token] do
 | 
			
		||||
        assert {:error, :unauthorized} = Streamer.get_topic("public:local", user, token)
 | 
			
		||||
 | 
			
		||||
        assert {:error, :unauthorized} = Streamer.get_topic("public:local:media", user, token)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "allows remote public streams if restricted_unauthenticated is on", %{
 | 
			
		||||
      user: user,
 | 
			
		||||
      token: oauth_token
 | 
			
		||||
    } do
 | 
			
		||||
      clear_config([:restrict_unauthenticated, :timelines, :federated], true)
 | 
			
		||||
 | 
			
		||||
      %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user)
 | 
			
		||||
      %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user)
 | 
			
		||||
 | 
			
		||||
      assert {:ok, "public"} = Streamer.get_topic("public", user, oauth_token)
 | 
			
		||||
      assert {:ok, "public:media"} = Streamer.get_topic("public:media", user, oauth_token)
 | 
			
		||||
 | 
			
		||||
      assert {:ok, "public:remote:lain.com"} =
 | 
			
		||||
               Streamer.get_topic("public:remote", user, oauth_token, %{"instance" => "lain.com"})
 | 
			
		||||
 | 
			
		||||
      assert {:ok, "public:remote:media:lain.com"} =
 | 
			
		||||
               Streamer.get_topic("public:remote:media", user, oauth_token, %{
 | 
			
		||||
                 "instance" => "lain.com"
 | 
			
		||||
               })
 | 
			
		||||
 | 
			
		||||
      for token <- [read_notifications_token, badly_scoped_token] do
 | 
			
		||||
        assert {:error, :unauthorized} = Streamer.get_topic("public", user, token)
 | 
			
		||||
        assert {:error, :unauthorized} = Streamer.get_topic("public:media", user, token)
 | 
			
		||||
 | 
			
		||||
        assert {:error, :unauthorized} =
 | 
			
		||||
                 Streamer.get_topic("public:remote", user, token, %{
 | 
			
		||||
                   "instance" => "lain.com"
 | 
			
		||||
                 })
 | 
			
		||||
 | 
			
		||||
        assert {:error, :unauthorized} =
 | 
			
		||||
                 Streamer.get_topic("public:remote:media", user, token, %{
 | 
			
		||||
                   "instance" => "lain.com"
 | 
			
		||||
                 })
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "allows user streams (with proper OAuth token scopes)", %{
 | 
			
		||||
      user: user,
 | 
			
		||||
      token: read_oauth_token
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1085,6 +1085,14 @@ def get("http://404.site" <> _, _, _, _) do
 | 
			
		|||
     }}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get("https://404.site" <> _, _, _, _) do
 | 
			
		||||
    {:ok,
 | 
			
		||||
     %Tesla.Env{
 | 
			
		||||
       status: 404,
 | 
			
		||||
       body: ""
 | 
			
		||||
     }}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get(
 | 
			
		||||
        "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=acct:lain@zetsubou.xn--q9jyb4c",
 | 
			
		||||
        _,
 | 
			
		||||
| 
						 | 
				
			
			@ -1399,6 +1407,15 @@ def get("https://mk.absturztau.be/notes/93e7nm8wqg", _, _, _) do
 | 
			
		|||
     }}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get("https://mk.absturztau.be/notes/93e7nm8wqg/activity", _, _, _) do
 | 
			
		||||
    {:ok,
 | 
			
		||||
     %Tesla.Env{
 | 
			
		||||
       status: 200,
 | 
			
		||||
       body: File.read!("test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json"),
 | 
			
		||||
       headers: activitypub_object_headers()
 | 
			
		||||
     }}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get("https://p.helene.moe/objects/fd5910ac-d9dc-412e-8d1d-914b203296c4", _, _, _) do
 | 
			
		||||
    {:ok,
 | 
			
		||||
     %Tesla.Env{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue